├── .env ├── .gitignore ├── .gitmodules ├── .travis.yml ├── CONTRIBUTING.md ├── Changes.md ├── Code Layout.md ├── LICENSE ├── Makefile ├── README.md ├── ast └── ast.go ├── astmap.go ├── brain.go ├── cmd └── rivescript │ └── main.go ├── config.go ├── debug.go ├── deprecated.go ├── doc.go ├── doc_test.go ├── eg ├── README.md ├── brain │ ├── admin.rive │ ├── begin.rive │ ├── clients.rive │ ├── eliza.rive │ ├── javascript.rive │ ├── myself.rive │ └── rpg.rive └── json-server │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── main.go │ └── main_test.go ├── errors.go ├── go.mod ├── go.sum ├── inheritance.go ├── lang └── javascript │ └── javascript.go ├── loading.go ├── macro └── macros.go ├── macro_test.go ├── parser.go ├── parser └── parser.go ├── regexp.go ├── rivescript.go ├── rsts_test.go ├── sessions ├── interface.go ├── memory │ └── memory.go ├── null │ └── null.go ├── redis │ ├── README.md │ ├── extra.go │ ├── integration_test.go │ ├── internal_test.go │ └── redis.go └── sqlite │ └── sqlite.go ├── sorting.go ├── tags.go ├── testsuite.rive └── utils.go /.env: -------------------------------------------------------------------------------- 1 | export GOPATH="$(pwd)/.gopath" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.gopath 2 | /bin 3 | /dist 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "rsts"] 2 | path = rsts 3 | url = https://github.com/aichaos/rsts 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "1.8" 4 | - "1.7" 5 | - "1.6" 6 | - "1.5" 7 | - tip 8 | script: make test 9 | notifications: 10 | webhooks: 11 | urls: 12 | - https://webhooks.gitter.im/e/f037a5b8287e8b7561fe 13 | on_success: always # options: [always|never|change] default: always 14 | on_failure: always # options: [always|never|change] default: always 15 | on_start: never # options: [always|never|change] default: always 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Interested in contributing to RiveScript? Great! 4 | 5 | First, check the general guidelines for RiveScript and its primary implementations 6 | found at - in particular, understand 7 | the goals and scope of the RiveScript language and the style guide for the 8 | Go implementation (briefly: use `gofmt`). 9 | 10 | ## Quick Start 11 | 12 | Fork, then clone the git repo: 13 | 14 | ```bash 15 | $ git clone git@github.com:your-username/rivescript-go 16 | ``` 17 | 18 | If you are an experienced Go developer, you can clone the repo into your 19 | standard `$GOPATH`. If you are new to Go or don't want to deal with the 20 | `$GOPATH`, you can use the commands in the Makefile; these create a "private" 21 | Go path inside the repo folder so you can simply clone the repo and get up and 22 | running in no time. 23 | 24 | After cloning, run these Make commands to get your dev environment set up: 25 | 26 | ```bash 27 | $ make setup 28 | $ make build 29 | ``` 30 | 31 | See the README.md for more Make commands. 32 | 33 | ## Submitting Code Changes 34 | 35 | Run `make fmt` or `gofmt` to clean up your source code before submitting a 36 | pull request. Also verify that `make test` works and that all the unit tests 37 | pass. 38 | 39 | Push to your fork and [submit a pull request](https://github.com/aichaos/rivescript-go/compare/). 40 | 41 | At this point you're waiting on me. I'm usually pretty quick to comment on pull 42 | requests (within a few days) and I may suggest some changes, improvements or 43 | alternatives. 44 | 45 | Some things that will increase the chance that your pull request is accepted: 46 | 47 | * Follow the style guide at 48 | * Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 49 | -------------------------------------------------------------------------------- /Changes.md: -------------------------------------------------------------------------------- 1 | # Change History 2 | 3 | This documents the history of significant changes to `rivescript-go`. 4 | 5 | ## v0.4.0 - Aug 15, 2023 6 | 7 | This update will modernize the Go port of RiveScript bringing some of the 8 | newer features that were available on the JavaScript or Python ports. 9 | 10 | ### The ?Keyword Command 11 | 12 | This update adds support for the newer `?Keyword` command in RiveScript. 13 | 14 | This command works around a Unicode matching bug that affected the 15 | Go, JavaScript and Python ports of RiveScript. For example: 16 | 17 | ```rivescript 18 | // You wanted this trigger to match if "你好" appears anywhere 19 | // in a user's message, but it wasn't working before. 20 | + [*] 你好 [*] 21 | - 你好! 22 | 23 | // Now: use the ?Keyword command in place of +Trigger 24 | ? 你好 25 | - 你好! 26 | ``` 27 | 28 | The optional wildcard `[*]` syntax didn't work when paired with Unicode 29 | symbols because the regular expression that `[*]` is turned into (which 30 | involves "word boundary" or `\b` characters) only worked with ASCII latin 31 | characters. The ?Keyword command works around this by translating into a 32 | +Trigger that tries _every_ combination of literal word, wildcard on either 33 | or both sides, and optional wildcard, to ensure that your keyword trigger 34 | will indeed match your keyword _anywhere_ in the user's message. 35 | 36 | ### CaseSensitive User Message Support 37 | 38 | By default RiveScript would always lowercase the user's message as it 39 | comes in. If this is undesirable and you'd like to preserve their _actual_ 40 | capitalization (when it gets captured by wildcards and comes out in their 41 | `` tags), provide the new CaseSensitive boolean to your RiveScript 42 | constructor: 43 | 44 | ```go 45 | bot := rivescript.New(&rivescript.Config{ 46 | CaseSensitive: true, 47 | }) 48 | ``` 49 | 50 | The built-in `rivescript` command line program adds a `-case` parameter 51 | to enable this option for testing: 52 | 53 | ```bash 54 | rivescript -case /path/to/brain 55 | ``` 56 | 57 | ### JavaScript Object Macros 58 | 59 | The official JavaScript object macro support library has a couple of 60 | exciting new updates. 61 | 62 | Firstly, the JavaScript engine has been replaced from 63 | [github.com/robertkrimen/otto](https://github.com/robertkrimen/otto) with 64 | [github.com/dop251/goja](https://github.com/dop251/goja) which should 65 | provide a better quality of life for writing JavaScript functions for 66 | your bot. Goja supports many of the modern ES6+ features including the 67 | let and const keyword and arrow functions. 68 | 69 | Additionally, the Goja runtime will be exposed at the `.VM` accessor 70 | for the JavaScriptHandler class, so you can directly play with it and 71 | set global variables or Go functions that can be called from your 72 | JavaScript macros in your bot, allowing for much greater extensibility. 73 | 74 | ### Other Changes 75 | 76 | * Add shellword parsing for `` tags: you can pass "quoted strings" 77 | in which will go in as one 'word' in the `args` array, instead of the 78 | arguments being literally split by space characters. This brings the 79 | Go port of RiveScript in line with the JavaScript port which has been 80 | doing this for a while. 81 | * Fix the rsts_test.go to properly load from the RSTS (RiveScript Test 82 | Suite) git submodule. 83 | 84 | ## v0.3.1 - Aug 20, 2021 85 | 86 | This release simply adds a `go.mod` to this project so that it gets along well 87 | with modern Go projects. 88 | 89 | ## v0.3.0 - Apr 30, 2017 90 | 91 | This update brings some long-needed restructuring to the source layout of 92 | `rivescript-go`. Briefly: it moves all source files from the `src/` subpackage 93 | into the root package namespace, and removes the wrapper shim functions (their 94 | documentation was then moved to the actual implementation functions). 95 | 96 | ### API Breaking Changes 97 | 98 | * The `github.com/aichaos/rivescript-go/src` subpackage has been removed, and 99 | all of the things you used to import from there can now be found in the root 100 | package instead. Most notably, the `RiveScript` struct needed to be imported 101 | (again) from the `src` subpackage for use with Go object macros. 102 | 103 | To update source code where you used Go object macros: 104 | 105 | ```diff 106 | import ( 107 | "github.com/aichaos/rivescript-go" 108 | - rss "github.com/aichaos/rivescript-go/src" 109 | ) 110 | 111 | func main() { 112 | bot = rivescript.New(nil) 113 | 114 | - subroutine := func(rs *rss.RiveScript, args []string) string { 115 | + subroutine := func(rs *rivescript.RiveScript, args []string) string { 116 | return "Hello world" 117 | } 118 | 119 | bot.SetSubroutine("hello", subroutine) 120 | } 121 | ``` 122 | * `rivescript.Version` is now a string constant (replacing `VERSION`). The 123 | instance method `Version()` has been removed. 124 | 125 | ## Changes 126 | 127 | * All RiveScript unit tests have been removed in favor of those from the 128 | [RiveScript Test Suite](https://github.com/aichaos/rsts). The test file 129 | `rsts_test.go` implements the Go test runner, and the `rsts` repo was added 130 | as a Git submodule. 131 | * The Git commit hash is now encoded into the front-end command line client, 132 | printed along with the version number in the welcome banner. 133 | 134 | ## v0.2.0 - Feb 7, 2017 135 | 136 | This update focuses on bug fixes and code reorganization. 137 | 138 | ### API Breaking Changes 139 | 140 | * `rivescript.New()` and the `Config` struct have been refactored. `Config` 141 | now comes from the `rivescript` package directly rather than needing to 142 | import from `rivescript/config`. 143 | 144 | For your code, this means you can remove the `aichaos/rivescript-go/config` 145 | import and change the `config.Config` name to `rivescript.Config`: 146 | 147 | ```go 148 | import "github.com/aichaos/rivescript-go" 149 | 150 | func main() { 151 | // Example defining the struct to override defaults. 152 | bot := rivescript.New(&rivescript.Config{Debug: true}) 153 | 154 | // For the old `config.Basic()` that provided default settings, just 155 | // pass in a nil Config object. 156 | bot = rivescript.New(nil) 157 | 158 | // For the old `config.UTF8()` helper function that provided a Config with 159 | // UTF-8 mode enabled, instead call rivescript.WithUTF8() 160 | bot = rivescript.New(rivescript.WithUTF8()) 161 | } 162 | ``` 163 | * `Reply()`, `SortReplies()` and `CurrentUser()` now return an `error` value 164 | in addition to what they already returned. 165 | 166 | ### Changes 167 | 168 | * Add ANSI colors to the RiveScript shell (`cmd/rivescript`); they can be 169 | disabled with the `-nocolor` command line option. 170 | * Add new commands to the RiveScript shell: 171 | * `/debug [true|false]` to toggle the debug mode (`/debug` will print 172 | the current setting of debug mode). 173 | * `/dump ` to print the internal data structures for the 174 | topics and sorted trigger sets, respectively. 175 | * Separate the unit tests into multiple files and put them in the `rivescript` 176 | package instead of `rivescript_test`; this enables test code coverage 177 | reporting (we're at 72.1% coverage!) 178 | * Handle module configuration at the root package instead of in the `src` 179 | package. This enabled getting rid of the `rivescript/config` package and 180 | making the public API more sane. 181 | * Code cleanup via `go vet` 182 | * Add more documentation and examples to the Go doc. 183 | * Fix `@Redirects` not working sometimes when tags like `` insert capital 184 | letters (bug #1) 185 | * Fix an incorrect regexp that makes wildcards inside of optionals, like `[_]`, 186 | not matchable in `` tags. For example, with `+ my favorite [_] is *` 187 | and a message of "my favorite color is red", `` would be "red" because 188 | the optional makes its wildcard non-capturing (bug #15) 189 | * Fix the `` tag handling to support star numbers greater than ``: 190 | you can use as many star numbers as will be captured by your trigger (bug #16) 191 | * Fix a probable bug within inheritance/includes: some parts of the code were 192 | looking in one location for them, another in the other, so they probably 193 | didn't work perfectly before. 194 | * Fix `RemoveHandler()` to make it remove all known object macros that used that 195 | handler, which protects against a possible null pointer exception. 196 | * Fix `LoadDirectory()` to return an error when doesn't find any RiveScript 197 | source files to load, which helps protect against the common error that you 198 | gave it the wrong directory. 199 | * New unit tests: object macros. 200 | * An internal optimization that allowed for cleaning up a redundant storage 201 | location for triggers that have `%Previous` commands (PR #20) 202 | 203 | ## v0.1.0 - Dec 11, 2016 204 | 205 | This update changes some function prototypes in the API which breaks backward 206 | compatibility with existing code. 207 | 208 | * **API Breaking Changes:** 209 | * `rivescript.New()` now takes a `*config.Config` struct to configure the 210 | instance. This is now the preferred way to configure debug mode, strict 211 | mode, UTF-8, etc. rather than functions like `SetUTF8()`. 212 | 213 | For RiveScript's default settings, you can do `rivescript.New(config.Basic())` 214 | or `rivescript.New(nil)`. For UTF-8 support, `rivescript.New(config.UTF8())` 215 | is a convenient config template to use. 216 | * `GetDepth()` and `SetDepth()` now use a `uint` instead of an `int`. But 217 | these functions are deprecated anyway. 218 | * `GetUservars()` and `GetAllUservars()` return `*sessions.UserData` objects 219 | instead of `map[string]string` for the user data. 220 | * `ThawUservars()` now takes a `sessions.ThawAction` instead of a string to 221 | specify the action. Valid values are `Thaw`, `Discard`, or `Keep` 222 | (constants from the `sessions` package). 223 | * **Deprecated Functions:** 224 | * Configuration functions (getters and setters). Use the `Config` struct 225 | when calling `rivescript.New(*config.Config)` instead: 226 | * `SetDebug()`, `SetUTF8()`, `SetDepth()`, `SetStrict()` 227 | * `GetDebug()`, `GetUTF8()`, `GetDepth()`, `GetStrict()` 228 | * **Changes:** 229 | * Add support for pluggable session stores for user variables. The default 230 | one still keeps user variables in memory, but you can specify your own 231 | implementation instead. 232 | 233 | The interface for a `SessionManager` is in the `sessions` package. The 234 | default in-memory manager is in `sessions/memory`. By implementing your own 235 | session manager, you can change where RiveScript keeps track of user 236 | variables, e.g. to put them in a database or cache. 237 | * Make the library thread-safe with regards to getting/setting user variables 238 | while answering a message. The default in-memory session manager implements 239 | a mutex for accessing user variables. 240 | 241 | ## v0.0.3 - Sept 28, 2016 242 | 243 | This update was all about restructuring the internal source code to make certain 244 | internal modules exportable for third party users (e.g. the parser) and to 245 | reduce clutter from the root of the git repo. 246 | 247 | * Massive restructuring of the internal source code: 248 | * Tidied up the root of the git repo by moving *all* of the implementation 249 | code into the `src/` subdirectory and making the root RiveScript module a 250 | very lightweight API wrapper around the code. *Note: do not import the 251 | src package directly. Only go through the public API at the root module.* 252 | * Added public facing Parser submodule: 253 | [rivescript-go/parser](https://github.com/aichaos/rivescript-go/tree/master/parser). 254 | It enables third party developers to write applications that simply parse 255 | RiveScript code and getting an abstract syntax tree from it. 256 | * Moved exported object macro helpers to 257 | [rivescript-go/macro](https://github.com/aichaos/rivescript-go/tree/master/macro). 258 | -------------------------------------------------------------------------------- /Code Layout.md: -------------------------------------------------------------------------------- 1 | # Code Layout 2 | 3 | This project has a handful of `.go` files and it might not always be clear where 4 | to look for each function call. 5 | 6 | Here is a walkthrough of the code layout in a more logical fashion. 7 | 8 | ## RiveScript Module 9 | 10 | | File Name | Purpose and Methods | 11 | |------------------|----------------------------------------------------------------------| 12 | | `astmap.go` | Private aliases for `rivescript/ast` structs. | 13 | | `brain.go` | `Reply()` and its implementation. | 14 | | `config.go` | Config struct and public config methods (e.g. `SetUservar()`). | 15 | | `debug.go` | Debugging functions. | 16 | | `deprecated.go` | Deprecated methods are moved to this file. | 17 | | `doc.go` | Main module documentation for Go Doc. | 18 | | `errors.go` | Error types used by the RiveScript module. | 19 | | `inheritance.go` | Functions related to topic inheritance. | 20 | | `loading.go` | File loading functions (`LoadFile()`, `LoadDirectory()`, `Stream()`) | 21 | | `parser.go` | Internal implementation of `rivescript/parser` | 22 | | `regexp.go` | Definitions for commonly used regular expressions. | 23 | | `rivescript.go` | `RiveScript` definition, constructor, and `Version()` methods. | 24 | | `sorting.go` | `SortReplies()` and its implementation. | 25 | | `tags.go` | Tag processing functions. | 26 | | `utils.go` | Misc utility functions. | 27 | 28 | ## Test Files 29 | 30 | | File Name | Purpose | 31 | |-----------------|--------------------------------------------| 32 | | `doc_test.go` | Example snippets. | 33 | | `macro_test.go` | Tests external object macros (JavaScript). | 34 | | `rsts_test.go` | The RiveScript Test Suite. | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Noah Petherbridge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=$(shell grep -e 'const Version' rivescript.go | head -n 1 | cut -d '"' -f 2) 2 | BUILD=$(shell git describe --always) 3 | CURDIR=$(shell pwd) 4 | 5 | # Inject the build version (commit hash) into the executable. 6 | LDFLAGS := -ldflags "-X main.Build=$(BUILD)" 7 | 8 | # `make setup` to set up git submodules 9 | .PHONY: setup 10 | setup: 11 | git submodule init 12 | git submodule update 13 | 14 | # `make build` to build the binary 15 | .PHONY: build 16 | build: 17 | go build $(LDFLAGS) -o bin/rivescript cmd/rivescript/main.go 18 | 19 | # `make run` to run the rivescript cmd 20 | .PHONY: run 21 | run: 22 | go run $(LDFLAGS) cmd/rivescript/main.go eg/brain 23 | 24 | # `make debug` to run the rivescript cmd in debug mode 25 | .PHONY: debug 26 | debug: 27 | go run $(LDFLAGS) cmd/rivescript/main.go -debug eg/brain 28 | 29 | # `make fmt` to run gofmt 30 | .PHONY: fmt 31 | fmt: 32 | gofmt -w . 33 | 34 | # `make test` to run unit tests 35 | .PHONY: test 36 | test: 37 | go test 38 | 39 | # `make clean` cleans up everything 40 | .PHONY: clean 41 | clean: 42 | rm -rf bin dist 43 | 44 | ################################################################################ 45 | ## Below are commands for shipping distributable binaries for each platfomr. ## 46 | ################################################################################ 47 | 48 | PLATFORMS := linux/amd64 linux/386 darwin/amd64 49 | WIN32 := windows/amd64 windows/386 50 | release: $(PLATFORMS) $(WIN32) 51 | .PHONY: release $(PLATFORMS) 52 | 53 | # Handy variables to pull OS and arch from $PLATFORMS. 54 | temp = $(subst /, ,$@) 55 | os = $(word 1, $(temp)) 56 | arch = $(word 2, $(temp)) 57 | 58 | $(PLATFORMS): 59 | mkdir -p dist/rivescript-$(VERSION)-$(os)-$(arch) 60 | cp -r README.md LICENSE Changes.md eg dist/rivescript-$(VERSION)-$(os)-$(arch)/ 61 | GOOS=$(os) GOARCH=$(arch) go build $(LDFLAGS) -v -i -o bin/rivescript cmd/rivescript/main.go 62 | cp bin/rivescript dist/rivescript-$(VERSION)-$(os)-$(arch)/ 63 | cd dist; tar -czvf ../rivescript-$(VERSION)-$(os)-$(arch).tar.gz rivescript-$(VERSION)-$(os)-$(arch) 64 | 65 | $(WIN32): 66 | mkdir -p dist/rivescript-$(VERSION)-$(os)-$(arch) 67 | cp -r README.md LICENSE Changes.md eg dist/rivescript-$(VERSION)-$(os)-$(arch)/ 68 | GOOS=$(os) GOARCH=$(arch) go build $(LDFLAGS) -v -i -o bin/rivescript.exe cmd/rivescript/main.go 69 | cp bin/rivescript.exe dist/rivescript-$(VERSION)-$(os)-$(arch)/ 70 | echo -e "@echo off\nrivescript eg/brain" > dist/rivescript-$(VERSION)-$(os)-$(arch)/example.bat 71 | cd dist; zip -r ../rivescript-$(VERSION)-$(os)-$(arch).zip rivescript-$(VERSION)-$(os)-$(arch) 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RiveScript-Go 2 | 3 | [![GoDoc](https://godoc.org/github.com/aichaos/rivescript-go?status.svg)](https://godoc.org/github.com/aichaos/rivescript-go) 4 | [![Gitter](https://badges.gitter.im/aichaos/rivescript-go.svg)](https://gitter.im/aichaos/rivescript-go?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 5 | [![Build Status](https://travis-ci.org/aichaos/rivescript-go.svg?branch=master)](https://travis-ci.org/aichaos/rivescript-go) 6 | 7 | ## Introduction 8 | 9 | This is a RiveScript interpreter library written for the Go programming 10 | language. RiveScript is a scripting language for chatterbots, making it easy 11 | to write trigger/response pairs for building up a bot's intelligence. 12 | 13 | **This project is currently in Beta status.** The API should be mostly stable 14 | but things might move around on you. 15 | 16 | ## About RiveScript 17 | 18 | RiveScript is a scripting language for authoring chatbots. It has a very 19 | simple syntax and is designed to be easy to read and fast to write. 20 | 21 | A simple example of what RiveScript looks like: 22 | 23 | ``` 24 | + hello bot 25 | - Hello human. 26 | ``` 27 | 28 | This matches a user's message of "hello bot" and would reply "Hello human." 29 | Or for a slightly more complicated example: 30 | 31 | ``` 32 | + my name is * 33 | * == => >Wow, we have the same name! 34 | * != undefined => >Did you change your name? 35 | - >Nice to meet you, ! 36 | ``` 37 | 38 | The official website for RiveScript is https://www.rivescript.com/ 39 | 40 | To test drive RiveScript in your web browser, try the 41 | [RiveScript Playground](https://play.rivescript.com/). 42 | 43 | ## Documentation 44 | 45 | * RiveScript Library: 46 | * RiveScript Stand-alone Interpreter: 47 | * JavaScript Object Macros: 48 | * RiveScript Parser: 49 | 50 | Also check out the [**RiveScript Community Wiki**](https://github.com/aichaos/rivescript/wiki) 51 | for common design patterns and tips & tricks for RiveScript. 52 | 53 | ## Installation 54 | 55 | For the development library: 56 | 57 | `go get github.com/aichaos/rivescript-go` 58 | 59 | For the stand-alone `rivescript` binary for testing a bot: 60 | 61 | `go get github.com/aichaos/rivescript-go/cmd/rivescript` 62 | 63 | ## Usage 64 | 65 | The distribution of RiveScript includes an interactive shell for testing your 66 | RiveScript bot. Run it with the path to a folder on disk that contains your 67 | RiveScript documents. Example: 68 | 69 | ```bash 70 | # (Linux) 71 | $ rivescript eg/brain 72 | 73 | # (Windows) 74 | > rivescript.exe eg/brain 75 | ``` 76 | 77 | See `rivescript -help` for options it accepts, including debug mode and UTF-8 78 | mode. 79 | 80 | When used as a library for writing your own chatbot, the synopsis is as follows: 81 | 82 | ```go 83 | package main 84 | 85 | import ( 86 | "fmt" 87 | "github.com/aichaos/rivescript-go" 88 | ) 89 | 90 | func main() { 91 | // Create a new bot with the default settings. 92 | bot := rivescript.New(nil) 93 | 94 | // To enable UTF-8 mode, you'd have initialized the bot like: 95 | bot = rivescript.New(rivescript.WithUTF8()) 96 | 97 | // Load a directory full of RiveScript documents (.rive files) 98 | err := bot.LoadDirectory("eg/brain") 99 | if err != nil { 100 | fmt.Printf("Error loading from directory: %s", err) 101 | } 102 | 103 | // Load an individual file. 104 | err = bot.LoadFile("./testsuite.rive") 105 | if err != nil { 106 | fmt.Printf("Error loading from file: %s", err) 107 | } 108 | 109 | // Sort the replies after loading them! 110 | bot.SortReplies() 111 | 112 | // Get a reply. 113 | reply, err := bot.Reply("local-user", "Hello, bot!") 114 | if err != nil { 115 | fmt.Printf("Error: %s\n", err) 116 | } else { 117 | fmt.Printf("The bot says: %s", reply) 118 | } 119 | } 120 | ``` 121 | 122 | ## Configuration 123 | 124 | The constructor takes an optional `Config` struct. Here is a full example with 125 | all the supported options. You only need to provide keys that are different to 126 | the defaults. 127 | 128 | ```go 129 | bot := rivescript.New(&rivescript.Config{ 130 | Debug: false, // Debug mode, off by default 131 | Strict: false, // No strict syntax checking 132 | UTF8: false, // No UTF-8 support enabled by default 133 | Depth: 50, // Becomes default 50 if Depth is <= 0 134 | Seed: time.Now().UnixNano(), // Random number seed (default is == 0) 135 | SessionManager: memory.New(), // Default in-memory session manager 136 | }) 137 | ``` 138 | 139 | For convenience, you can use a shortcut: 140 | 141 | ```go 142 | // A nil config uses all the defaults. 143 | bot = rivescript.New(nil) 144 | 145 | // WithUTF8 enables UTF-8 mode (other settings left as default). 146 | bot = rivescript.New(rivescript.WithUTF8()) 147 | ``` 148 | 149 | ## Object Macros 150 | 151 | A common feature in many RiveScript implementations is the object macro, which 152 | enables you to write dynamic program code (in your favorite programming 153 | language) to add extra capabilities to your bot. For example, your bot could 154 | answer a question of "what is the weather like in *$location*" by running some 155 | code to look up their answer via a web API. 156 | 157 | The Go version of RiveScript has support for object macros written in Go 158 | (at compile time of your application). It also has optional support for 159 | JavaScript object macros using the [goja](https://github.com/dop251/goja) library. 160 | 161 | Here is how to define a Go object macro: 162 | 163 | ```go 164 | bot.SetSubroutine(func(rs *rivescript.RiveScript, args []string) string { 165 | return "Hello world!" 166 | }) 167 | ``` 168 | 169 | ### JavaScript Object Macros 170 | 171 | Here is an example how to make JavaScript object macros available via 172 | the [goja](https://github.com/dop251/goja) module: 173 | 174 | ```go 175 | package main 176 | 177 | import ( 178 | "fmt" 179 | "github.com/aichaos/rivescript-go" 180 | "github.com/aichaos/rivescript-go/lang/javascript" 181 | ) 182 | 183 | func main() { 184 | // Initialize RiveScript first. 185 | bot := rivescript.New(rivescript.WithUTF8()) 186 | 187 | // Add the JavaScript object macro handler. 188 | js := javascript.New(bot) 189 | bot.SetHandler("javascript", js) 190 | 191 | // You can access the goja VM and set your own global 192 | // variable or function bindings to be called from your 193 | // object macros. 194 | js.VM.Set("helloFunc", func(name string) string { 195 | return fmt.Sprintf("Hello, %s!", name) 196 | }) 197 | 198 | // Load some RiveScript code. This example just tests the 199 | // JavaScript object macro support. 200 | err := bot.Stream(` 201 | > object add javascript 202 | let a = args[0]; 203 | let b = args[1]; 204 | return parseInt(a) + parseInt(b); 205 | < object 206 | 207 | > object fn javascript 208 | let result = helloFunc(args[0]) 209 | return result 210 | < object 211 | 212 | + add # and # 213 | - + = add 214 | 215 | + say hello * 216 | - fn 217 | `) 218 | if err != nil { 219 | fmt.Printf("Error loading RiveScript document: %s", err) 220 | } 221 | 222 | // Sort the replies after loading them! 223 | bot.SortReplies() 224 | 225 | // Get some replies! 226 | inputs := []string{"add 5 and 12", "say hello goja"} 227 | for _, message := range inputs { 228 | fmt.Printf("You said: %s\n", message) 229 | reply, err := bot.Reply("local-user", message) 230 | if err != nil { 231 | fmt.Printf("Error: %s\n", err) 232 | } else { 233 | fmt.Printf("The bot says: %s\n", reply) 234 | } 235 | } 236 | } 237 | ``` 238 | 239 | ## UTF-8 Support 240 | 241 | UTF-8 support in RiveScript is considered an experimental feature. It is 242 | disabled by default. 243 | 244 | By default (without UTF-8 mode on), triggers may only contain basic ASCII 245 | characters (no foreign characters), and the user's message is stripped of all 246 | characters except letters, numbers and spaces. This means that, for example, 247 | you can't capture a user's e-mail address in a RiveScript reply, because of 248 | the @ and . characters. 249 | 250 | When UTF-8 mode is enabled, these restrictions are lifted. Triggers are only 251 | limited to not contain certain metacharacters like the backslash, and the 252 | user's message is only stripped of backslashes and HTML angled brackets 253 | (to protect from obvious XSS if you use RiveScript in a web application). 254 | Additionally, common punctuation characters are stripped out, with the default 255 | set being `/[.,!?;:]/g`. This can be overridden by providing a new regexp 256 | string literal to the `RiveScript.SetUnicodePunctuation` function. Example: 257 | 258 | ```go 259 | // Make a new bot with UTF-8 mode enabled. 260 | bot := rivescript.New(rivescript.WithUTF8()) 261 | 262 | // Override the punctuation characters that get stripped 263 | // from the user's message. 264 | bot.SetUnicodePunctuation(`[.,!?;:]`); 265 | ``` 266 | 267 | The `` tags in RiveScript will capture the user's "raw" input, so you can 268 | write replies to get the user's e-mail address or store foreign characters in 269 | their name. 270 | 271 | ## Building 272 | 273 | I use a GNU Makefile to make building and running this module easier. The 274 | relevant commands are: 275 | 276 | * `make setup` - run this after freshly cloning this repo. It runs the 277 | `git submodule` commands to pull down vendored dependencies. 278 | * `make build` - this will build the front-end command from `cmd/rivescript` 279 | and place its binary into the `bin/` directory. It builds a binary relevant 280 | to your current system, so on Linux this will create a Linux binary. 281 | It's also recommended to run this one at least once, because it will cache 282 | dependency packages and speed up subsequent builds and runs. 283 | * `make run` - runs the front-end command and points it to the `eg/brain` folder 284 | as its RiveScript source. 285 | * `make fmt` - runs `gofmt -w` on all the source files. 286 | * `make test` - runs the unit tests. 287 | * `make clean` - cleans up the `.gopath`, `bin` and `dist` directories. 288 | 289 | ### Testing 290 | 291 | The rivescript-go repo submodules the RiveScript Test Suite (rsts) project. 292 | If you didn't do a `git clone --recursive` for rivescript-go you can pull the 293 | submodule via the following commands: 294 | 295 | ```bash 296 | git submodule init 297 | git submodule update 298 | ``` 299 | 300 | Then `make test` (or `go test`) should show results from the tests run 301 | out of the rsts/ folder. 302 | 303 | ### Releasing 304 | 305 | You can build a release for an individual platform by running a command like 306 | `make linux/amd64`. The valid build targets are currently as follows: 307 | 308 | * Linux: `linux/386` and `linux/amd64` 309 | * Windows: `windows/386` and `windows/amd64` 310 | * MacOS: `darwin/amd64` 311 | 312 | Run `make release` to automatically build releases for all supported platforms. 313 | 314 | A directory for the release is created in `dist/rivescript-$VERSION-$OS-$ARCH/` 315 | that contains the built binary, README.md, Changes.md and examples. You can 316 | inspect this directory afterwards; its contents are automatically tarred up 317 | (zip for Windows) and placed in the root of the git repo. 318 | 319 | If you are cross-compiling for a different system, you may need to mess with 320 | permissions so that Go can download the standard library for the new target. 321 | Example: 322 | 323 | ```bash 324 | % sudo mkdir /usr/lib/golang/pkg/windows_386 325 | % chown your_user:your_user /usr/lib/golang/pkg/windows_386 326 | ``` 327 | 328 | ## See Also 329 | 330 | * [rivescript-go/parser](./parser) - A standalone package for parsing RiveScript 331 | code and returning an "abstract syntax tree." 332 | * [rivescript-go/macro](./macro) - Contains an interface for creating your own 333 | object macro handlers for foreign programming languages. 334 | * [rivescript-go/sessions](./sessions) - Contains the interface for user 335 | variable session managers as well as the default in-memory manager and the 336 | `NullStore` for testing. Other official session managers (e.g. Redis) are in 337 | here as well. 338 | 339 | ## License 340 | 341 | ``` 342 | The MIT License (MIT) 343 | 344 | Copyright (c) 2017 Noah Petherbridge 345 | 346 | Permission is hereby granted, free of charge, to any person obtaining a copy 347 | of this software and associated documentation files (the "Software"), to deal 348 | in the Software without restriction, including without limitation the rights 349 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 350 | copies of the Software, and to permit persons to whom the Software is 351 | furnished to do so, subject to the following conditions: 352 | 353 | The above copyright notice and this permission notice shall be included in all 354 | copies or substantial portions of the Software. 355 | 356 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 357 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 358 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 359 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 360 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 361 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 362 | SOFTWARE. 363 | ``` 364 | 365 | ## See Also 366 | 367 | The official RiveScript website, http://www.rivescript.com/ 368 | -------------------------------------------------------------------------------- /ast/ast.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package ast defines the Abstract Syntax Tree for RiveScript. 3 | 4 | The tree looks like this (in JSON-style syntax): 5 | 6 | { 7 | "Begin": { 8 | "Global": {}, // Global vars 9 | "Var": {}, // Bot variables 10 | "Sub": {}, // Substitution map 11 | "Person": {}, // Person substitution map 12 | "Array": {}, // Arrays 13 | }, 14 | "Topics": {}, 15 | "Objects": [], 16 | } 17 | */ 18 | package ast 19 | 20 | // Root represents the root of the AST tree. 21 | type Root struct { 22 | Begin Begin `json:"begin"` 23 | Topics map[string]*Topic `json:"topics"` 24 | Objects []*Object `json:"objects"` 25 | } 26 | 27 | // Begin represents the "begin block" style data (configuration). 28 | type Begin struct { 29 | Global map[string]string `json:"global"` 30 | Var map[string]string `json:"var"` 31 | Sub map[string]string `json:"sub"` 32 | Person map[string]string `json:"person"` 33 | Array map[string][]string `json:"array"` // Map of string (names) to arrays-of-strings 34 | } 35 | 36 | // Topic represents a topic of conversation. 37 | type Topic struct { 38 | Triggers []*Trigger `json:"triggers"` 39 | Includes map[string]bool `json:"includes"` 40 | Inherits map[string]bool `json:"inherits"` 41 | } 42 | 43 | // Trigger has a trigger pattern and all the subsequent handlers for it. 44 | type Trigger struct { 45 | Trigger string `json:"trigger"` 46 | Reply []string `json:"reply"` 47 | Condition []string `json:"condition"` 48 | Redirect string `json:"redirect"` 49 | Previous string `json:"previous"` 50 | } 51 | 52 | // Object contains source code of dynamically parsed object macros. 53 | type Object struct { 54 | Name string `json:"name"` 55 | Language string `json:"language"` 56 | Code []string `json:"code"` 57 | } 58 | 59 | // New creates a new, empty, abstract syntax tree. 60 | func New() *Root { 61 | ast := &Root{ 62 | // Initialize all the structures. 63 | Begin: Begin{ 64 | Global: map[string]string{}, 65 | Var: map[string]string{}, 66 | Sub: map[string]string{}, 67 | Person: map[string]string{}, 68 | Array: map[string][]string{}, 69 | }, 70 | Topics: map[string]*Topic{}, 71 | Objects: []*Object{}, 72 | } 73 | 74 | // Initialize the 'random' topic. 75 | ast.AddTopic("random") 76 | 77 | return ast 78 | } 79 | 80 | // AddTopic sets up the AST tree for a new topic and gets it ready for 81 | // triggers to be added. 82 | func (ast *Root) AddTopic(name string) { 83 | ast.Topics[name] = new(Topic) 84 | ast.Topics[name].Triggers = []*Trigger{} 85 | ast.Topics[name].Includes = map[string]bool{} 86 | ast.Topics[name].Inherits = map[string]bool{} 87 | } 88 | -------------------------------------------------------------------------------- /astmap.go: -------------------------------------------------------------------------------- 1 | package rivescript 2 | 3 | /* 4 | For my own sanity while programming the code, these structs mirror the data 5 | in the 'ast' subpackage but uses non-exported fields for the bot's own use. 6 | 7 | The logic is as follows: 8 | 9 | - The parser subpackage becomes a stand-alone Go module that third party 10 | developers can use to make their own applications around the RiveScript 11 | scripting language itself. To that end, it exports a public AST tree. 12 | - In RiveScript's parse() function, it uses the public parser package and 13 | gets back an AST tree full of exported fields. It doesn't need these fields 14 | to be exported, and it copies them into internal fields of similar names. 15 | - I don't want to use the exported AST names directly because it makes the 16 | code become a Russian Roulette of capital or non-capital names. 17 | 18 | An example of how unwieldy the code would be if I use the direct AST types: 19 | 20 | rs.thats[topic].Triggers[trigger.Trigger].Previous[trigger.Previous] 21 | ^ ^ ^ ^ 22 | 23 | If the ast package structs are updated, update the mappings in this package too. 24 | */ 25 | 26 | type astTopic struct { 27 | triggers []*astTrigger 28 | } 29 | 30 | type astTrigger struct { 31 | trigger string 32 | reply []string 33 | condition []string 34 | redirect string 35 | previous string 36 | } 37 | -------------------------------------------------------------------------------- /brain.go: -------------------------------------------------------------------------------- 1 | package rivescript 2 | 3 | import ( 4 | "fmt" 5 | re "regexp" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | /* 11 | Reply fetches a reply from the bot for a user's message. 12 | 13 | Parameters 14 | 15 | username: The name of the user requesting a reply. 16 | message: The user's message. 17 | */ 18 | func (rs *RiveScript) Reply(username, message string) (string, error) { 19 | rs.say("Asked to reply to [%s] %s", username, message) 20 | var err error 21 | 22 | // Initialize a user profile for this user? 23 | rs.sessions.Init(username) 24 | 25 | // Store the current user's ID. 26 | rs.inReplyContext = true 27 | rs.currentUser = username 28 | 29 | // Format their message. 30 | message = rs.formatMessage(message, false) 31 | var reply string 32 | 33 | // If the BEGIN block exists, consult it first. 34 | if _, ok := rs.topics["__begin__"]; ok { 35 | var begin string 36 | begin, err = rs.getReply(username, "request", true, 0) 37 | if err != nil { 38 | return "", err 39 | } 40 | 41 | // OK to continue? 42 | if strings.Contains(begin, "{ok}") { 43 | reply, err = rs.getReply(username, message, false, 0) 44 | if err != nil { 45 | return "", err 46 | } 47 | begin = strings.NewReplacer("{ok}", reply).Replace(begin) 48 | } 49 | 50 | reply = begin 51 | reply = rs.processTags(username, message, reply, []string{}, []string{}, 0) 52 | } else { 53 | reply, err = rs.getReply(username, message, false, 0) 54 | if err != nil { 55 | return "", err 56 | } 57 | } 58 | 59 | // Save their message history. 60 | rs.sessions.AddHistory(username, message, reply) 61 | 62 | // Unset the current user's ID. 63 | rs.currentUser = "" 64 | rs.inReplyContext = false 65 | 66 | return reply, nil 67 | } 68 | 69 | /* 70 | getReply is the internal logic behind Reply(). 71 | 72 | Parameters 73 | 74 | username: The name of the user requesting a reply. 75 | message: The user's message. 76 | isBegin: Whether this reply is for the "BEGIN Block" context or not. 77 | step: Recursion depth counter. 78 | */ 79 | func (rs *RiveScript) getReply(username string, message string, isBegin bool, step uint) (string, error) { 80 | // Needed to sort replies? 81 | if len(rs.sorted.topics) == 0 { 82 | rs.warn("You forgot to call SortReplies()!") 83 | return "", ErrRepliesNotSorted 84 | } 85 | 86 | // Collect data on this user. 87 | topic, err := rs.sessions.Get(username, "topic") 88 | if err != nil { 89 | topic = "random" 90 | } 91 | stars := []string{} 92 | thatStars := []string{} // For %Previous 93 | var reply string 94 | 95 | // Avoid letting them fall into a missing topic. 96 | if _, ok := rs.topics[topic]; !ok { 97 | rs.warn("User %s was in an empty topic named '%s'", username, topic) 98 | rs.sessions.Set(username, map[string]string{"topic": "random"}) 99 | topic = "random" 100 | } 101 | 102 | // Avoid deep recursion. 103 | if step > rs.Depth { 104 | return "", ErrDeepRecursion 105 | } 106 | 107 | // Are we in the BEGIN block? 108 | if isBegin { 109 | topic = "__begin__" 110 | } 111 | 112 | // More topic sanity checking. 113 | if _, ok := rs.topics[topic]; !ok { 114 | // This was handled before, which would mean topic=random and it doesn't 115 | // exist. Serious issue! 116 | return "", ErrNoDefaultTopic 117 | } 118 | 119 | // Create a pointer for the matched data when we find it. 120 | var matched *astTrigger 121 | matchedTrigger := "" 122 | foundMatch := false 123 | 124 | // See if there were any %Previous's in this topic, or any topic related to 125 | // it. This should only be done the first time -- not during a recursive 126 | // redirection. This is because in a redirection, "lastReply" is still gonna 127 | // be the same as it was the first time, resulting in an infinite loop! 128 | if step == 0 { 129 | allTopics := []string{topic} 130 | if len(rs.includes[topic]) > 0 || len(rs.inherits[topic]) > 0 { 131 | // Get ALL the topics! 132 | allTopics = rs.getTopicTree(topic, 0) 133 | } 134 | 135 | // Scan them all. 136 | for _, top := range allTopics { 137 | rs.say("Checking topic %s for any %%Previous's.", top) 138 | 139 | if len(rs.sorted.thats[top]) > 0 { 140 | rs.say("There's a %%Previous in this topic!") 141 | 142 | // Get the bot's last reply to the user. 143 | history, _ := rs.sessions.GetHistory(username) 144 | lastReply := history.Reply[0] 145 | 146 | // Format the bot's reply the same way as the human's. 147 | lastReply = rs.formatMessage(lastReply, true) 148 | rs.say("Bot's last reply: %s", lastReply) 149 | 150 | // See if it's a match. 151 | for _, trig := range rs.sorted.thats[top] { 152 | pattern := trig.pointer.previous 153 | botside := rs.triggerRegexp(username, pattern) 154 | rs.say("Try to match lastReply (%s) to %s (%s)", lastReply, pattern, botside) 155 | 156 | // Match? 157 | matcher := re.MustCompile(fmt.Sprintf("^%s$", botside)) 158 | match := matcher.FindStringSubmatch(lastReply) 159 | if len(match) > 0 { 160 | // Huzzah! See if OUR message is right too... 161 | rs.say("Bot side matched!") 162 | 163 | // Collect the bot stars. 164 | thatStars = []string{} 165 | if len(match) > 1 { 166 | for i := range match[1:] { 167 | thatStars = append(thatStars, match[i+1]) 168 | } 169 | } 170 | 171 | // Compare the triggers to the user's message. 172 | userSide := trig.pointer 173 | regexp := rs.triggerRegexp(username, userSide.trigger) 174 | rs.say("Try to match %s against %s (%s)", message, userSide.trigger, regexp) 175 | 176 | // If the trigger is atomic, we don't need to deal with the regexp engine. 177 | isMatch := false 178 | if isAtomic(userSide.trigger) { 179 | if message == regexp { 180 | isMatch = true 181 | } 182 | } else { 183 | matcher := re.MustCompile(fmt.Sprintf("^%s$", regexp)) 184 | match := matcher.FindStringSubmatch(message) 185 | if len(match) > 0 { 186 | isMatch = true 187 | 188 | // Get the user's message stars. 189 | if len(match) > 1 { 190 | for i := range match[1:] { 191 | stars = append(stars, match[i+1]) 192 | } 193 | } 194 | } 195 | } 196 | 197 | // Was it a match? 198 | if isMatch { 199 | // Keep the trigger pointer. 200 | matched = userSide 201 | foundMatch = true 202 | matchedTrigger = userSide.trigger 203 | break 204 | } 205 | } 206 | } 207 | } 208 | } 209 | } 210 | 211 | // Search their topic for a match to their trigger. 212 | if !foundMatch { 213 | rs.say("Searching their topic for a match...") 214 | for _, trig := range rs.sorted.topics[topic] { 215 | pattern := trig.trigger 216 | regexp := rs.triggerRegexp(username, pattern) 217 | rs.say("Try to match \"%s\" against %s (%s)", message, pattern, regexp) 218 | 219 | // If the trigger is atomic, we don't need to bother with the regexp engine. 220 | isMatch := false 221 | if isAtomic(pattern) && message == regexp { 222 | isMatch = true 223 | } else { 224 | // Non-atomic triggers always need the regexp. 225 | matcher := re.MustCompile(fmt.Sprintf("^%s$", regexp)) 226 | match := matcher.FindStringSubmatch(message) 227 | if len(match) > 0 { 228 | // The regexp matched! 229 | isMatch = true 230 | 231 | // Collect the stars. 232 | if len(match) > 1 { 233 | for i := range match[1:] { 234 | stars = append(stars, match[i+1]) 235 | } 236 | } 237 | } 238 | } 239 | 240 | // A match somehow? 241 | if isMatch { 242 | rs.say("Found a match!") 243 | 244 | // Keep the pointer to this trigger's data. 245 | matched = trig.pointer 246 | foundMatch = true 247 | matchedTrigger = pattern 248 | break 249 | } 250 | } 251 | } 252 | 253 | // Store what trigger they matched on. 254 | rs.sessions.SetLastMatch(username, matchedTrigger) 255 | 256 | // Did we match? 257 | if foundMatch { 258 | for range []int{0} { // A single loop so we can break out early 259 | // See if there are any hard redirects. 260 | if len(matched.redirect) > 0 { 261 | rs.say("Redirecting us to %s", matched.redirect) 262 | redirect := matched.redirect 263 | redirect = rs.processTags(username, message, redirect, stars, thatStars, 0) 264 | redirect = strings.ToLower(redirect) 265 | rs.say("Pretend user said: %s", redirect) 266 | reply, err = rs.getReply(username, redirect, isBegin, step+1) 267 | if err != nil { 268 | return "", err 269 | } 270 | break 271 | } 272 | 273 | // Check the conditionals. 274 | for _, row := range matched.condition { 275 | halves := strings.Split(row, "=>") 276 | if len(halves) == 2 { 277 | condition := reCondition.FindStringSubmatch(strings.TrimSpace(halves[0])) 278 | if len(condition) > 0 { 279 | left := strings.TrimSpace(condition[1]) 280 | eq := condition[2] 281 | right := strings.TrimSpace(condition[3]) 282 | potreply := strings.TrimSpace(halves[1]) // Potential reply 283 | 284 | // Process tags all around 285 | left = rs.processTags(username, message, left, stars, thatStars, step) 286 | right = rs.processTags(username, message, right, stars, thatStars, step) 287 | 288 | // Defaults? 289 | if len(left) == 0 { 290 | left = UNDEFINED 291 | } 292 | if len(right) == 0 { 293 | right = UNDEFINED 294 | } 295 | 296 | rs.say("Check if %s %s %s", left, eq, right) 297 | 298 | // Validate it. 299 | passed := false 300 | if eq == "eq" || eq == "==" { 301 | if left == right { 302 | passed = true 303 | } 304 | } else if eq == "ne" || eq == "!=" || eq == "<>" { 305 | if left != right { 306 | passed = true 307 | } 308 | } else { 309 | // Dealing with numbers here. 310 | iLeft, errLeft := strconv.Atoi(left) 311 | iRight, errRight := strconv.Atoi(right) 312 | if errLeft == nil && errRight == nil { 313 | if eq == "<" && iLeft < iRight { 314 | passed = true 315 | } else if eq == "<=" && iLeft <= iRight { 316 | passed = true 317 | } else if eq == ">" && iLeft > iRight { 318 | passed = true 319 | } else if eq == ">=" && iLeft >= iRight { 320 | passed = true 321 | } 322 | } else { 323 | rs.warn("Failed to evaluate numeric condition!") 324 | } 325 | } 326 | 327 | if passed { 328 | reply = potreply 329 | break 330 | } 331 | } 332 | } 333 | } 334 | 335 | // Have our reply yet? 336 | if len(reply) > 0 { 337 | break 338 | } 339 | 340 | // Process weights in the replies. 341 | bucket := []string{} 342 | for _, rep := range matched.reply { 343 | match := reWeight.FindStringSubmatch(rep) 344 | if len(match) > 0 { 345 | weight, _ := strconv.Atoi(match[1]) 346 | if weight <= 0 { 347 | rs.warn("Can't have a weight <= 0!") 348 | weight = 1 349 | } 350 | 351 | for i := weight; i > 0; i-- { 352 | bucket = append(bucket, rep) 353 | } 354 | } else { 355 | bucket = append(bucket, rep) 356 | } 357 | } 358 | 359 | // Get a random reply. 360 | if len(bucket) > 0 { 361 | reply = bucket[rs.randomInt(len(bucket))] 362 | } 363 | } 364 | } 365 | 366 | // Still no reply?? Give up with the fallback error replies. 367 | if !foundMatch { 368 | return "", ErrNoTriggerMatched 369 | } else if len(reply) == 0 { 370 | return "", ErrNoReplyFound 371 | } 372 | 373 | rs.say("Reply: %s", reply) 374 | 375 | // Process tags for the BEGIN block. 376 | if isBegin { 377 | // The BEGIN block can set {topic} and user vars. 378 | 379 | // Topic setter 380 | match := reTopic.FindStringSubmatch(reply) 381 | var giveup uint 382 | for len(match) > 0 { 383 | giveup++ 384 | if giveup > rs.Depth { 385 | rs.warn("Infinite loop looking for topic tag!") 386 | break 387 | } 388 | name := match[1] 389 | rs.sessions.Set(username, map[string]string{"topic": name}) 390 | reply = strings.Replace(reply, fmt.Sprintf("{topic=%s}", name), "", -1) 391 | match = reTopic.FindStringSubmatch(reply) 392 | } 393 | 394 | // Set user vars 395 | match = reSet.FindStringSubmatch(reply) 396 | giveup = 0 397 | for len(match) > 0 { 398 | giveup++ 399 | if giveup > rs.Depth { 400 | rs.warn("Infinite loop looking for set tag!") 401 | break 402 | } 403 | name := match[1] 404 | value := match[2] 405 | rs.sessions.Set(username, map[string]string{name: value}) 406 | reply = strings.Replace(reply, fmt.Sprintf("", name, value), "", -1) 407 | match = reSet.FindStringSubmatch(reply) 408 | } 409 | } else { 410 | reply = rs.processTags(username, message, reply, stars, thatStars, 0) 411 | } 412 | 413 | return reply, nil 414 | } 415 | -------------------------------------------------------------------------------- /cmd/rivescript/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Stand-alone RiveScript Interpreter. 3 | 4 | This is an example program included with the RiveScript Go library. It serves as 5 | a way to quickly demo and test a RiveScript bot. 6 | 7 | Usage 8 | 9 | rivescript [options] /path/to/rive/files 10 | 11 | Options 12 | 13 | --debug Enable debug mode. 14 | --utf8 Enable UTF-8 support within RiveScript. 15 | --depth Override the recursion depth limit (default 50) 16 | */ 17 | package main 18 | 19 | import ( 20 | "bufio" 21 | "flag" 22 | "fmt" 23 | "os" 24 | "strings" 25 | 26 | "github.com/aichaos/rivescript-go" 27 | "github.com/aichaos/rivescript-go/lang/javascript" 28 | ) 29 | 30 | // Build is the git commit hash that the binary was built from. 31 | var Build = "-unknown-" 32 | 33 | var ( 34 | // Command line arguments. 35 | version bool 36 | debug bool 37 | utf8 bool 38 | depth uint 39 | caseSensitive bool 40 | nostrict bool 41 | nocolor bool 42 | ) 43 | 44 | func init() { 45 | flag.BoolVar(&version, "version", false, "Show the version number and exit.") 46 | flag.BoolVar(&debug, "debug", false, "Enable debug mode.") 47 | flag.BoolVar(&utf8, "utf8", false, "Enable UTF-8 mode.") 48 | flag.UintVar(&depth, "depth", 50, "Recursion depth limit") 49 | flag.BoolVar(&caseSensitive, "case", false, "Enable the CaseSensitive flag, preserving capitalization in user messages") 50 | flag.BoolVar(&nostrict, "nostrict", false, "Disable strict syntax checking") 51 | flag.BoolVar(&nocolor, "nocolor", false, "Disable ANSI colors") 52 | } 53 | 54 | func main() { 55 | // Collect command line arguments. 56 | flag.Parse() 57 | args := flag.Args() 58 | 59 | if version { 60 | fmt.Printf("RiveScript-Go version %s\n", rivescript.Version) 61 | os.Exit(0) 62 | } 63 | 64 | if len(args) == 0 { 65 | fmt.Fprintln(os.Stderr, "Usage: rivescript [options] ") 66 | os.Exit(1) 67 | } 68 | 69 | root := args[0] 70 | 71 | // Initialize the bot. 72 | bot := rivescript.New(&rivescript.Config{ 73 | Debug: debug, 74 | Strict: !nostrict, 75 | Depth: depth, 76 | UTF8: utf8, 77 | CaseSensitive: caseSensitive, 78 | }) 79 | 80 | // JavaScript object macro handler. 81 | bot.SetHandler("javascript", javascript.New(bot)) 82 | 83 | // Load the target directory. 84 | err := bot.LoadDirectory(root) 85 | if err != nil { 86 | fmt.Printf("Error loading directory: %s", err) 87 | os.Exit(1) 88 | } 89 | 90 | bot.SortReplies() 91 | 92 | fmt.Printf(` 93 | . . 94 | .:...:: RiveScript Interpreter (Go) 95 | .:: ::. Library Version: v%s (build %s) 96 | ..:;;. ' .;;:.. 97 | . ''' . Type '/quit' to quit. 98 | :;,:,;: Type '/help' for more options. 99 | : : 100 | 101 | Using the RiveScript bot found in: %s 102 | Type a message to the bot and press Return to send it. 103 | `, rivescript.Version, Build, root) 104 | 105 | // Drop into the interactive command shell. 106 | reader := bufio.NewReader(os.Stdin) 107 | for { 108 | color(yellow, "You>") 109 | text, _ := reader.ReadString('\n') 110 | text = strings.TrimSpace(text) 111 | if len(text) == 0 { 112 | continue 113 | } 114 | 115 | if strings.Contains(text, "/help") { 116 | help() 117 | } else if strings.Contains(text, "/quit") { 118 | os.Exit(0) 119 | } else if strings.Contains(text, "/debug t") { 120 | bot.SetGlobal("debug", "true") 121 | color(cyan, "Debug mode enabled.", "\n") 122 | } else if strings.Contains(text, "/debug f") { 123 | bot.SetGlobal("debug", "false") 124 | color(cyan, "Debug mode disabled.", "\n") 125 | } else if strings.Contains(text, "/debug") { 126 | debug, _ := bot.GetGlobal("debug") 127 | color(cyan, "Debug mode is currently:", debug, "\n") 128 | } else if strings.Contains(text, "/dump t") { 129 | bot.DumpTopics() 130 | } else if strings.Contains(text, "/dump s") { 131 | bot.DumpSorted() 132 | } else { 133 | reply, err := bot.Reply("localuser", text) 134 | if err != nil { 135 | color(red, "Error>", err.Error(), "\n") 136 | } else { 137 | color(green, "RiveScript>", reply, "\n") 138 | } 139 | } 140 | } 141 | } 142 | 143 | // Names for pretty ANSI colors. 144 | const ( 145 | red = `31;1` 146 | yellow = `33;1` 147 | green = `32;1` 148 | cyan = `36;1` 149 | ) 150 | 151 | func color(color string, text ...string) { 152 | if nocolor { 153 | fmt.Printf( 154 | "%s %s", 155 | text[0], 156 | strings.Join(text[1:], " "), 157 | ) 158 | } else { 159 | fmt.Printf( 160 | "\x1b[%sm%s\x1b[0m %s", 161 | color, 162 | text[0], 163 | strings.Join(text[1:], " "), 164 | ) 165 | } 166 | } 167 | 168 | func help() { 169 | fmt.Printf(`Supported commands: 170 | - /help 171 | Show this text. 172 | - /quit 173 | Exit the program. 174 | - /debug [true|false] 175 | Enable or disable debug mode. If no setting is given, it prints 176 | the current debug mode. 177 | - /dump 178 | For debugging purposes, dump the topic and sorted trigger trees. 179 | `) 180 | } 181 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package rivescript 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/aichaos/rivescript-go/macro" 10 | "github.com/aichaos/rivescript-go/sessions" 11 | ) 12 | 13 | /* 14 | Config provides options to configure the RiveScript bot. 15 | 16 | Create a pointer to this type and send it to the New() constructor to change 17 | the default settings. You only need to provide settings you want to override; 18 | the zero-values of all the options are handled appropriately by the RiveScript 19 | library. 20 | 21 | The default values are documented below. 22 | */ 23 | type Config struct { 24 | // Debug enables verbose logging to standard output. Default false. 25 | Debug bool 26 | 27 | // Strict enables strict syntax checking, where a syntax error in RiveScript 28 | // code is considered fatal at parse time. Default true. 29 | Strict bool 30 | 31 | // UTF8 enables UTF-8 mode within the bot. Default false. 32 | // 33 | // When UTF-8 mode is enabled, triggers in the RiveScript source files are 34 | // allowed to contain foreign characters. Additionally, the user's incoming 35 | // messages are left *mostly* intact, so that they send messages with 36 | // foreign characters to the bot. 37 | UTF8 bool 38 | 39 | // Depth controls the global limit for recursive functions within 40 | // RiveScript. Default 50. 41 | Depth uint 42 | 43 | // Random number seed, if you'd like to customize it. The default is for 44 | // RiveScript to choose its own seed, `time.Now().UnixNano()` 45 | Seed int64 46 | 47 | // Preserve the capitalization on a user's message instead of lowercasing it. 48 | // By default RiveScript will lowercase all messages coming in - set this and 49 | // the original casing will be preserved through wildcards and star tags. 50 | CaseSensitive bool 51 | 52 | // SessionManager is an implementation of the same name for managing user 53 | // variables for the bot. The default is the in-memory session handler. 54 | SessionManager sessions.SessionManager 55 | } 56 | 57 | // WithUTF8 provides a Config object that enables UTF-8 mode. 58 | func WithUTF8() *Config { 59 | return &Config{ 60 | UTF8: true, 61 | } 62 | } 63 | 64 | /* 65 | SetHandler sets a custom language handler for RiveScript object macros. 66 | 67 | Parameters 68 | 69 | lang: What your programming language is called, e.g. "javascript" 70 | handler: An implementation of macro.MacroInterface. 71 | */ 72 | func (rs *RiveScript) SetHandler(lang string, handler macro.MacroInterface) { 73 | rs.cLock.Lock() 74 | defer rs.cLock.Unlock() 75 | 76 | rs.handlers[lang] = handler 77 | } 78 | 79 | /* 80 | RemoveHandler removes an object macro language handler. 81 | 82 | If the handler has already loaded object macros, they will be deleted from 83 | the bot along with the handler. 84 | 85 | Parameters 86 | 87 | lang: The programming language for the handler to remove. 88 | */ 89 | func (rs *RiveScript) RemoveHandler(lang string) { 90 | rs.cLock.Lock() 91 | defer rs.cLock.Unlock() 92 | 93 | // Purge all loaded objects for this handler. 94 | for name, language := range rs.objlangs { 95 | if language == lang { 96 | delete(rs.objlangs, name) 97 | } 98 | } 99 | 100 | // And delete the handler itself. 101 | delete(rs.handlers, lang) 102 | } 103 | 104 | /* 105 | SetSubroutine defines a Go object macro from your program. 106 | 107 | Parameters 108 | 109 | name: The name of your subroutine for the `` tag in RiveScript. 110 | fn: A function with a prototype `func(*RiveScript, []string) string` 111 | */ 112 | func (rs *RiveScript) SetSubroutine(name string, fn Subroutine) { 113 | rs.cLock.Lock() 114 | defer rs.cLock.Unlock() 115 | 116 | rs.subroutines[name] = fn 117 | } 118 | 119 | /* 120 | DeleteSubroutine removes a Go object macro. 121 | 122 | Parameters 123 | 124 | name: The name of the object macro to be deleted. 125 | */ 126 | func (rs *RiveScript) DeleteSubroutine(name string) { 127 | rs.cLock.Lock() 128 | defer rs.cLock.Unlock() 129 | 130 | delete(rs.subroutines, name) 131 | } 132 | 133 | /* 134 | SetGlobal sets a global variable. 135 | 136 | This is equivalent to `! global` in RiveScript. Set the value to `undefined` 137 | to delete a global. 138 | */ 139 | func (rs *RiveScript) SetGlobal(name, value string) { 140 | rs.cLock.Lock() 141 | defer rs.cLock.Unlock() 142 | 143 | // Special globals that reconfigure the interpreter. 144 | if name == "debug" { 145 | switch strings.ToLower(value) { 146 | case "true", "t", "on", "yes": 147 | rs.Debug = true 148 | default: 149 | rs.Debug = false 150 | } 151 | } else if name == "depth" { 152 | depth, err := strconv.Atoi(value) 153 | if err != nil { 154 | rs.warn("Can't set global `depth` to `%s`: %s\n", value, err) 155 | } else { 156 | rs.Depth = uint(depth) 157 | } 158 | } 159 | 160 | if value == UNDEFINED { 161 | delete(rs.global, name) 162 | } else { 163 | rs.global[name] = value 164 | } 165 | } 166 | 167 | /* 168 | SetVariable sets a bot variable. 169 | 170 | This is equivalent to `! var` in RiveScript. Set the value to `undefined` 171 | to delete a bot variable. 172 | */ 173 | func (rs *RiveScript) SetVariable(name, value string) { 174 | rs.cLock.Lock() 175 | defer rs.cLock.Unlock() 176 | 177 | if value == UNDEFINED { 178 | delete(rs.vars, name) 179 | } else { 180 | rs.vars[name] = value 181 | } 182 | } 183 | 184 | /* 185 | SetSubstitution sets a substitution pattern. 186 | 187 | This is equivalent to `! sub` in RiveScript. Set the value to `undefined` 188 | to delete a substitution. 189 | */ 190 | func (rs *RiveScript) SetSubstitution(name, value string) { 191 | rs.cLock.Lock() 192 | defer rs.cLock.Unlock() 193 | 194 | if value == UNDEFINED { 195 | delete(rs.sub, name) 196 | } else { 197 | rs.sub[name] = value 198 | } 199 | } 200 | 201 | /* 202 | SetPerson sets a person substitution pattern. 203 | 204 | This is equivalent to `! person` in RiveScript. Set the value to `undefined` 205 | to delete a person substitution. 206 | */ 207 | func (rs *RiveScript) SetPerson(name, value string) { 208 | rs.cLock.Lock() 209 | defer rs.cLock.Unlock() 210 | 211 | if value == UNDEFINED { 212 | delete(rs.person, name) 213 | } else { 214 | rs.person[name] = value 215 | } 216 | } 217 | 218 | /* 219 | SetUservar sets a variable for a user. 220 | 221 | This is equivalent to `` in RiveScript. Set the value to `undefined` 222 | to delete a substitution. 223 | */ 224 | func (rs *RiveScript) SetUservar(username, name, value string) { 225 | rs.sessions.Set(username, map[string]string{ 226 | name: value, 227 | }) 228 | } 229 | 230 | /* 231 | SetUservars sets a map of variables for a user. 232 | 233 | Set multiple user variables by providing a map[string]string of key/value pairs. 234 | Equivalent to calling `SetUservar()` for each pair in the map. 235 | */ 236 | func (rs *RiveScript) SetUservars(username string, data map[string]string) { 237 | rs.sessions.Set(username, data) 238 | } 239 | 240 | /* 241 | GetGlobal gets a global variable. 242 | 243 | This is equivalent to `` in RiveScript. Returns `undefined` if the 244 | variable isn't defined. 245 | */ 246 | func (rs *RiveScript) GetGlobal(name string) (string, error) { 247 | rs.cLock.Lock() 248 | defer rs.cLock.Unlock() 249 | 250 | // Special globals. 251 | if name == "debug" { 252 | return fmt.Sprintf("%v", rs.Debug), nil 253 | } else if name == "depth" { 254 | return strconv.Itoa(int(rs.Depth)), nil 255 | } 256 | 257 | if _, ok := rs.global[name]; ok { 258 | return rs.global[name], nil 259 | } 260 | return UNDEFINED, fmt.Errorf("global variable %s not found", name) 261 | } 262 | 263 | /* 264 | GetVariable gets a bot variable. 265 | 266 | This is equivalent to `` in RiveScript. Returns `undefined` if the 267 | variable isn't defined. 268 | */ 269 | func (rs *RiveScript) GetVariable(name string) (string, error) { 270 | rs.cLock.Lock() 271 | defer rs.cLock.Unlock() 272 | 273 | if _, ok := rs.vars[name]; ok { 274 | return rs.vars[name], nil 275 | } 276 | return UNDEFINED, fmt.Errorf("bot variable %s not found", name) 277 | } 278 | 279 | /* 280 | GetUservar gets a user variable. 281 | 282 | This is equivalent to `` in RiveScript. Returns `undefined` if the 283 | variable isn't defined. 284 | */ 285 | func (rs *RiveScript) GetUservar(username, name string) (string, error) { 286 | return rs.sessions.Get(username, name) 287 | } 288 | 289 | /* 290 | GetUservars gets all the variables for a user. 291 | 292 | This returns a `map[string]string` containing all the user's variables. 293 | */ 294 | func (rs *RiveScript) GetUservars(username string) (*sessions.UserData, error) { 295 | return rs.sessions.GetAny(username) 296 | } 297 | 298 | /* 299 | GetAllUservars gets all the variables for all the users. 300 | 301 | This returns a map of username (strings) to `map[string]string` of their 302 | variables. 303 | */ 304 | func (rs *RiveScript) GetAllUservars() map[string]*sessions.UserData { 305 | return rs.sessions.GetAll() 306 | } 307 | 308 | // ClearUservars deletes all the variables that belong to a user. 309 | func (rs *RiveScript) ClearUservars(username string) { 310 | rs.sessions.Clear(username) 311 | } 312 | 313 | // ClearAllUservars deletes all variables for all users. 314 | func (rs *RiveScript) ClearAllUservars() { 315 | rs.sessions.ClearAll() 316 | } 317 | 318 | /* 319 | FreezeUservars freezes the variable state of a user. 320 | 321 | This will clone and preserve the user's entire variable state, so that it 322 | can be restored later with `ThawUservars()`. 323 | */ 324 | func (rs *RiveScript) FreezeUservars(username string) error { 325 | return rs.sessions.Freeze(username) 326 | } 327 | 328 | /* 329 | ThawUservars unfreezes a user's variables. 330 | 331 | The `action` can be one of the following: 332 | * thaw: Restore the variables and delete the frozen copy. 333 | * discard: Don't restore the variables, just delete the frozen copy. 334 | * keep: Keep the frozen copy after restoring. 335 | */ 336 | func (rs *RiveScript) ThawUservars(username string, action sessions.ThawAction) error { 337 | return rs.sessions.Thaw(username, action) 338 | } 339 | 340 | // LastMatch returns the user's last matched trigger. 341 | func (rs *RiveScript) LastMatch(username string) (string, error) { 342 | return rs.sessions.GetLastMatch(username) 343 | } 344 | 345 | /* 346 | CurrentUser returns the current user's ID. 347 | 348 | This is only useful from within an object macro, to get the ID of the user who 349 | invoked the macro. This value is set at the beginning of `Reply()` and unset 350 | at the end, so this function will return empty outside of a reply context. 351 | */ 352 | func (rs *RiveScript) CurrentUser() (string, error) { 353 | if rs.inReplyContext { 354 | return rs.currentUser, nil 355 | } 356 | return "", errors.New("CurrentUser() can only be called inside a reply context") 357 | } 358 | -------------------------------------------------------------------------------- /debug.go: -------------------------------------------------------------------------------- 1 | package rivescript 2 | 3 | // Debugging methods 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | // say prints a debugging message 10 | func (rs *RiveScript) say(message string, a ...interface{}) { 11 | if rs.Debug { 12 | fmt.Printf(message+"\n", a...) 13 | } 14 | } 15 | 16 | // warn prints a warning message for non-fatal errors 17 | func (rs *RiveScript) warn(message string, a ...interface{}) { 18 | if !rs.Quiet { 19 | fmt.Printf("[WARN] "+message+"\n", a...) 20 | } 21 | } 22 | 23 | // warnSyntax is like warn but takes a filename and line number. 24 | func (rs *RiveScript) warnSyntax(message string, filename string, lineno int, a ...interface{}) { 25 | message += fmt.Sprintf(" at %s line %d", filename, lineno) 26 | rs.warn(message, a...) 27 | } 28 | 29 | /* 30 | DumpTopics is a debug method which pretty-prints the topic tree structure from 31 | the bot's memory. 32 | */ 33 | func (rs *RiveScript) DumpTopics() { 34 | for topic, data := range rs.topics { 35 | fmt.Printf("Topic: %s\n", topic) 36 | for _, trigger := range data.triggers { 37 | fmt.Printf(" + %s\n", trigger.trigger) 38 | if trigger.previous != "" { 39 | fmt.Printf(" %% %s\n", trigger.previous) 40 | } 41 | for _, cond := range trigger.condition { 42 | fmt.Printf(" * %s\n", cond) 43 | } 44 | for _, reply := range trigger.reply { 45 | fmt.Printf(" - %s\n", reply) 46 | } 47 | if trigger.redirect != "" { 48 | fmt.Printf(" @ %s\n", trigger.redirect) 49 | } 50 | } 51 | } 52 | } 53 | 54 | /* 55 | DumpSorted is a debug method which pretty-prints the sort tree of topics from 56 | the bot's memory. 57 | */ 58 | func (rs *RiveScript) DumpSorted() { 59 | rs._dumpSorted(rs.sorted.topics, "Topics") 60 | rs._dumpSorted(rs.sorted.thats, "Thats") 61 | rs._dumpSortedList(rs.sorted.sub, "Substitutions") 62 | rs._dumpSortedList(rs.sorted.person, "Person Substitutions") 63 | } 64 | func (rs *RiveScript) _dumpSorted(tree map[string][]sortedTriggerEntry, label string) { 65 | fmt.Printf("Sort Buffer: %s\n", label) 66 | for topic, data := range tree { 67 | fmt.Printf(" Topic: %s\n", topic) 68 | for _, trigger := range data { 69 | fmt.Printf(" + %s\n", trigger.trigger) 70 | } 71 | } 72 | } 73 | func (rs *RiveScript) _dumpSortedList(list []string, label string) { 74 | fmt.Printf("Sort buffer: %s\n", label) 75 | for _, item := range list { 76 | fmt.Printf(" %s\n", item) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /deprecated.go: -------------------------------------------------------------------------------- 1 | package rivescript 2 | 3 | // deprecated.go is where functions that are deprecated move to. 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | ) 9 | 10 | // common function to put the deprecated note. 11 | func deprecated(name, since string) { 12 | fmt.Fprintf( 13 | os.Stderr, 14 | "Use of 'rivescript.%s()' is deprecated since v%s (this is v%s)\n", 15 | name, 16 | since, 17 | Version, 18 | ) 19 | } 20 | 21 | // SetDebug enables or disable debug mode. 22 | func (rs *RiveScript) SetDebug(value bool) { 23 | deprecated("SetDebug", "0.1.0") 24 | rs.Debug = value 25 | } 26 | 27 | // GetDebug tells you the current status of the debug mode. 28 | func (rs *RiveScript) GetDebug() bool { 29 | deprecated("GetDebug", "0.1.0") 30 | return rs.Debug 31 | } 32 | 33 | // SetUTF8 enables or disabled UTF-8 mode. 34 | func (rs *RiveScript) SetUTF8(value bool) { 35 | deprecated("SetUTF8", "0.1.0") 36 | rs.UTF8 = value 37 | } 38 | 39 | // GetUTF8 returns the current status of UTF-8 mode. 40 | func (rs *RiveScript) GetUTF8() bool { 41 | deprecated("GetUTF8", "0.1.0") 42 | return rs.UTF8 43 | } 44 | 45 | // SetDepth lets you override the recursion depth limit (default 50). 46 | func (rs *RiveScript) SetDepth(value uint) { 47 | deprecated("SetDepth", "0.1.0") 48 | rs.Depth = value 49 | } 50 | 51 | // GetDepth returns the current recursion depth limit. 52 | func (rs *RiveScript) GetDepth() uint { 53 | deprecated("GetDepth", "0.1.0") 54 | return rs.Depth 55 | } 56 | 57 | // SetStrict enables strict syntax checking when parsing RiveScript code. 58 | func (rs *RiveScript) SetStrict(value bool) { 59 | deprecated("SetStrict", "0.1.0") 60 | rs.Strict = value 61 | } 62 | 63 | // GetStrict returns the strict syntax check setting. 64 | func (rs *RiveScript) GetStrict() bool { 65 | deprecated("GetStrict", "0.1.0") 66 | return rs.Strict 67 | } 68 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package rivescript implements the RiveScript chatbot scripting language. 3 | 4 | About RiveScript 5 | 6 | RiveScript is a scripting language for authoring chatbots. It has a very 7 | simple syntax and is designed to be easy to read and fast to write. 8 | 9 | A simple example of what RiveScript looks like: 10 | 11 | + hello bot 12 | - Hello human. 13 | 14 | This matches a user's message of "hello bot" and would reply "Hello human." 15 | Or for a slightly more complicated example: 16 | 17 | + my name is * 18 | * == => >Wow, we have the same name! 19 | * != undefined => >Did you change your name? 20 | - >Nice to meet you, ! 21 | 22 | The official website for RiveScript is https://www.rivescript.com/ 23 | 24 | To test drive RiveScript in your web browser, try the 25 | [RiveScript Playground](https://play.rivescript.com/). 26 | 27 | Object Macros 28 | 29 | A common feature in many RiveScript implementations is the object macro, which 30 | enables you to write dynamic program code (in your favorite programming 31 | language) to add extra capabilities to your bot. For example, your bot could 32 | answer a question of `what is the weather like in _____` by running some 33 | code to look up their answer via a web API. 34 | 35 | The Go version of RiveScript has support for object macros written in Go 36 | (at compile time of your application). It also has optional support for 37 | JavaScript object macros using the Otto library. 38 | 39 | UTF-8 Support 40 | 41 | UTF-8 support in RiveScript is considered an experimental feature. It is 42 | disabled by default. Enable it by setting `RiveScript.SetUTF8(true)`. 43 | 44 | By default (without UTF-8 mode on), triggers may only contain basic ASCII 45 | characters (no foreign characters), and the user's message is stripped of all 46 | characters except letters, numbers and spaces. This means that, for example, 47 | you can't capture a user's e-mail address in a RiveScript reply, because of 48 | the @ and . characters. 49 | 50 | When UTF-8 mode is enabled, these restrictions are lifted. Triggers are only 51 | limited to not contain certain metacharacters like the backslash, and the 52 | user's message is only stripped of backslashes and HTML angled brackets 53 | (to protect from obvious XSS if you use RiveScript in a web application). 54 | Additionally, common punctuation characters are stripped out, with the default 55 | set being `/[.,!?;:]/g`. This can be overridden by providing a new regexp 56 | string literal to the `RiveScript.SetUnicodePunctuation` function. Example: 57 | 58 | // Make a new bot with UTF-8 mode enabled. 59 | bot := rivescript.New(config.UTF8()) 60 | 61 | // Override the punctuation characters that get stripped from the 62 | // user's message. 63 | bot.SetUnicodePunctuation(`[.,!?;:]`); 64 | 65 | The `` tags in RiveScript will capture the user's "raw" input, so you can 66 | write replies to get the user's e-mail address or store foreign characters in 67 | their name. 68 | 69 | See Also 70 | 71 | The official homepage of RiveScript, http://www.rivescript.com/ 72 | 73 | */ 74 | package rivescript 75 | -------------------------------------------------------------------------------- /doc_test.go: -------------------------------------------------------------------------------- 1 | package rivescript_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aichaos/rivescript-go" 7 | "github.com/aichaos/rivescript-go/lang/javascript" 8 | ) 9 | 10 | func Example() { 11 | // Create a new RiveScript instance, which represents an individual bot 12 | // with its own brain and memory of users. 13 | // 14 | // You can provide a rivescript.Config struct to configure the bot and 15 | // provide values that differ from the defaults: 16 | bot := rivescript.New(&rivescript.Config{ 17 | UTF8: true, // enable UTF-8 mode 18 | Debug: true, // enable debug mode 19 | }) 20 | 21 | // Or if you want the default configuration, provide a nil config. 22 | // See the documentation for the rivescript.Config type for information 23 | // on what the defaults are. 24 | bot = rivescript.New(nil) 25 | 26 | // Load a directory full of RiveScript documents (.rive files) 27 | bot.LoadDirectory("eg/brain") 28 | 29 | // Load an individual file. 30 | bot.LoadFile("testsuite.rive") 31 | 32 | // Stream in more RiveScript code dynamically from a string. 33 | bot.Stream(` 34 | + hello bot 35 | - Hello, human! 36 | `) 37 | 38 | // Sort the replies after loading them! 39 | bot.SortReplies() 40 | 41 | // Get a reply. 42 | reply, _ := bot.Reply("local-user", "Hello, bot!") 43 | fmt.Printf("The bot says: %s", reply) 44 | } 45 | 46 | func ExampleRiveScript_utf8() { 47 | // Examples of using UTF-8 mode in RiveScript. 48 | bot := rivescript.New(rivescript.WithUTF8()) 49 | 50 | bot.Stream(` 51 | // Without UTF-8 mode enabled, this trigger would be a syntax error 52 | // for containing non-ASCII characters; but in UTF-8 mode you can use it. 53 | + comment ça va 54 | - ça va bien. 55 | `) 56 | 57 | // Always call SortReplies when you're done loading replies. 58 | bot.SortReplies() 59 | 60 | // Without UTF-8 mode enabled, the user's message "comment ça va" would 61 | // have the ç symbol removed; but in UTF-8 mode it's preserved and can 62 | // match the trigger we defined. 63 | reply, _ := bot.Reply("local-user", "Comment ça va?") 64 | fmt.Println(reply) // "ça va bien." 65 | } 66 | 67 | func ExampleRiveScript_javascript() { 68 | // Example for configuring the JavaScript object macro handler via Otto. 69 | bot := rivescript.New(nil) 70 | 71 | // Create the JS handler. 72 | bot.SetHandler("javascript", javascript.New(bot)) 73 | 74 | // Now we can use object macros written in JS! 75 | bot.Stream(` 76 | > object add javascript 77 | var a = args[0]; 78 | var b = args[1]; 79 | return parseInt(a) + parseInt(b); 80 | < object 81 | 82 | > object setname javascript 83 | // Set the user's name via JavaScript 84 | var uid = rs.CurrentUser(); 85 | rs.SetUservar(uid, args[0], args[1]) 86 | < object 87 | 88 | + add # and # 89 | - + = add 90 | 91 | + my name is * 92 | - I will remember that.setname 93 | 94 | + what is my name 95 | - You are . 96 | `) 97 | bot.SortReplies() 98 | 99 | reply, _ := bot.Reply("local-user", "Add 5 and 7") 100 | fmt.Printf("Bot: %s\n", reply) 101 | } 102 | 103 | func ExampleRiveScript_subroutine() { 104 | // Example for defining a Go function as an object macro. 105 | bot := rivescript.New(nil) 106 | 107 | // Define an object macro named `setname` 108 | bot.SetSubroutine("setname", func(rs *rivescript.RiveScript, args []string) string { 109 | uid, _ := rs.CurrentUser() 110 | rs.SetUservar(uid, args[0], args[1]) 111 | return "" 112 | }) 113 | 114 | // Stream in some RiveScript code. 115 | bot.Stream(` 116 | + my name is * 117 | - I will remember that.setname 118 | 119 | + what is my name 120 | - You are . 121 | `) 122 | bot.SortReplies() 123 | 124 | bot.Reply("local-user", "my name is bob") 125 | reply, _ := bot.Reply("local-user", "What is my name?") 126 | fmt.Printf("Bot: %s\n", reply) 127 | } 128 | -------------------------------------------------------------------------------- /eg/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | These directories include example snippets for how to do various things with 4 | RiveScript-Go. 5 | 6 | ## RiveScript Example 7 | 8 | * [brain](brain/) - The standard default RiveScript brain (`.rive` files) that 9 | implements an Eliza-like bot with added triggers to demonstrate other 10 | features of RiveScript. 11 | 12 | ## Client Examples 13 | 14 | * [json-server](json-server/) - A minimal Go web server that makes a RiveScript 15 | bot available at a JSON POST endpoint. 16 | -------------------------------------------------------------------------------- /eg/brain/admin.rive: -------------------------------------------------------------------------------- 1 | // Administrative functions. 2 | 3 | + shutdown{weight=10000} 4 | * eq => Shutting down... (j/k, not supported) 5 | - {@botmaster only} 6 | 7 | + botmaster only 8 | - This command can only be used by my botmaster. != 9 | -------------------------------------------------------------------------------- /eg/brain/begin.rive: -------------------------------------------------------------------------------- 1 | ! version = 2.0 2 | 3 | > begin 4 | + request // This trigger is tested first. 5 | - {ok} // An {ok} in the response means it's okay to get a real reply 6 | < begin 7 | 8 | // The Botmaster's Name 9 | ! var master = localuser 10 | 11 | // Bot Variables 12 | ! var name = Aiden 13 | ! var fullname = Aiden Rive 14 | ! var age = 5 15 | ! var birthday = October 12 16 | ! var sex = male 17 | ! var location = Michigan 18 | ! var city = Detroit 19 | ! var eyes = blue 20 | ! var hair = light brown 21 | ! var hairlen = short 22 | ! var color = blue 23 | ! var band = Nickelback 24 | ! var book = Myst 25 | ! var author = Stephen King 26 | ! var job = robot 27 | ! var website = www.rivescript.com 28 | 29 | // Substitutions 30 | ! sub " = " 31 | ! sub ' = ' 32 | ! sub & = & 33 | ! sub < = < 34 | ! sub > = > 35 | ! sub + = plus 36 | ! sub - = minus 37 | ! sub / = divided 38 | ! sub * = times 39 | ! sub i'm = i am 40 | ! sub i'd = i would 41 | ! sub i've = i have 42 | ! sub i'll = i will 43 | ! sub don't = do not 44 | ! sub isn't = is not 45 | ! sub you'd = you would 46 | ! sub you're = you are 47 | ! sub you've = you have 48 | ! sub you'll = you will 49 | ! sub he'd = he would 50 | ! sub he's = he is 51 | ! sub he'll = he will 52 | ! sub she'd = she would 53 | ! sub she's = she is 54 | ! sub she'll = she will 55 | ! sub they'd = they would 56 | ! sub they're = they are 57 | ! sub they've = they have 58 | ! sub they'll = they will 59 | ! sub we'd = we would 60 | ! sub we're = we are 61 | ! sub we've = we have 62 | ! sub we'll = we will 63 | ! sub whats = what is 64 | ! sub what's = what is 65 | ! sub what're = what are 66 | ! sub what've = what have 67 | ! sub what'll = what will 68 | ! sub can't = can not 69 | ! sub whos = who is 70 | ! sub who's = who is 71 | ! sub who'd = who would 72 | ! sub who'll = who will 73 | ! sub don't = do not 74 | ! sub didn't = did not 75 | ! sub it's = it is 76 | ! sub could've = could have 77 | ! sub couldn't = could not 78 | ! sub should've = should have 79 | ! sub shouldn't = should not 80 | ! sub would've = would have 81 | ! sub wouldn't = would not 82 | ! sub when's = when is 83 | ! sub when're = when are 84 | ! sub when'd = when did 85 | ! sub y = why 86 | ! sub u = you 87 | ! sub ur = your 88 | ! sub r = are 89 | ! sub n = and 90 | ! sub im = i am 91 | ! sub wat = what 92 | ! sub wats = what is 93 | ! sub ohh = oh 94 | ! sub becuse = because 95 | ! sub becasue = because 96 | ! sub becuase = because 97 | ! sub practise = practice 98 | ! sub its a = it is a 99 | ! sub fav = favorite 100 | ! sub fave = favorite 101 | ! sub yesi = yes i 102 | ! sub yetit = yet it 103 | ! sub iam = i am 104 | ! sub welli = well i 105 | ! sub wellit = well it 106 | ! sub amfine = am fine 107 | ! sub aman = am an 108 | ! sub amon = am on 109 | ! sub amnot = am not 110 | ! sub realy = really 111 | ! sub iamusing = i am using 112 | ! sub amleaving = am leaving 113 | ! sub yuo = you 114 | ! sub youre = you are 115 | ! sub didnt = did not 116 | ! sub ain't = is not 117 | ! sub aint = is not 118 | ! sub wanna = want to 119 | ! sub brb = be right back 120 | ! sub bbl = be back later 121 | ! sub gtg = got to go 122 | ! sub g2g = got to go 123 | ! sub lyl = love you lots 124 | ! sub gf = girlfriend 125 | ! sub g/f = girlfriend 126 | ! sub bf = boyfriend 127 | ! sub b/f = boyfriend 128 | ! sub b/f/f = best friend forever 129 | ! sub :-) = smile 130 | ! sub :) = smile 131 | ! sub :d = grin 132 | ! sub :-d = grin 133 | ! sub :-p = tongue 134 | ! sub :p = tongue 135 | ! sub ;-) = wink 136 | ! sub ;) = wink 137 | ! sub :-( = sad 138 | ! sub :( = sad 139 | ! sub :'( = cry 140 | ! sub :-[ = shy 141 | ! sub :-\ = uncertain 142 | ! sub :-/ = uncertain 143 | ! sub :-s = uncertain 144 | ! sub 8-) = cool 145 | ! sub 8) = cool 146 | ! sub :-* = kissyface 147 | ! sub :-! = foot 148 | ! sub o:-) = angel 149 | ! sub >:o = angry 150 | ! sub :@ = angry 151 | ! sub 8o| = angry 152 | ! sub :$ = blush 153 | ! sub :-$ = blush 154 | ! sub :-[ = blush 155 | ! sub :[ = bat 156 | ! sub (a) = angel 157 | ! sub (h) = cool 158 | ! sub 8-| = nerdy 159 | ! sub |-) = tired 160 | ! sub +o( = ill 161 | ! sub *-) = uncertain 162 | ! sub ^o) = raised eyebrow 163 | ! sub (6) = devil 164 | ! sub (l) = love 165 | ! sub (u) = broken heart 166 | ! sub (k) = kissyface 167 | ! sub (f) = rose 168 | ! sub (w) = wilted rose 169 | 170 | // Person substitutions 171 | ! person i am = you are 172 | ! person you are = I am 173 | ! person i'm = you're 174 | ! person you're = I'm 175 | ! person my = your 176 | ! person your = my 177 | ! person you = I 178 | ! person i = you 179 | 180 | // Set arrays 181 | ! array malenoun = male guy boy dude boi man men gentleman gentlemen 182 | ! array femalenoun = female girl chick woman women lady babe 183 | ! array mennoun = males guys boys dudes bois men gentlemen 184 | ! array womennoun = females girls chicks women ladies babes 185 | ! array lol = lol lmao rofl rotfl haha hahaha 186 | ! array colors = white black orange red blue green yellow cyan fuchsia gray grey brown turquoise pink purple gold silver navy 187 | ! array height = tall long wide thick 188 | ! array measure = inch in centimeter cm millimeter mm meter m inches centimeters millimeters meters 189 | ! array yes = yes yeah yep yup ya yea 190 | ! array no = no nah nope nay 191 | -------------------------------------------------------------------------------- /eg/brain/clients.rive: -------------------------------------------------------------------------------- 1 | // Learn stuff about our users. 2 | 3 | + my name is * 4 | - >Nice to meet you, . 5 | - >, nice to meet you. 6 | 7 | + my name is 8 | - >That's my master's name too. 9 | 10 | + my name is 11 | - >What a coincidence! That's my name too! 12 | - >That's my name too! 13 | 14 | + call me * 15 | - >, I will call you that from now on. 16 | 17 | + i am * years old 18 | - >A lot of people are , you're not alone. 19 | - >Cool, I'm myself.{weight=49} 20 | 21 | + i am a (@malenoun) 22 | - Alright, you're a . 23 | 24 | + i am a (@femalenoun) 25 | - Alright, you're female. 26 | 27 | + i (am from|live in) * 28 | - {/formal}>I've spoken to people from before. 29 | 30 | + my favorite * is * 31 | - =>Why is it your favorite? 32 | 33 | + i am single 34 | - I am too. 35 | 36 | + i have a girlfriend 37 | - What's her name? 38 | 39 | + i have a boyfriend 40 | - What's his name? 41 | 42 | + * 43 | % what is her name 44 | - >That's a pretty name. 45 | 46 | + * 47 | % what is his name 48 | - >That's a cool name. 49 | 50 | + my (girlfriend|boyfriend)* name is * 51 | - >That's a nice name. 52 | 53 | + (what is my name|who am i|do you know my name|do you know who i am){weight=10} 54 | - Your name is . 55 | - You told me your name is . 56 | - Aren't you ? 57 | 58 | + (how old am i|do you know how old i am|do you know my age){weight=10} 59 | - You are years old. 60 | - You're . 61 | 62 | + am i a (@malenoun) or a (@femalenoun){weight=10} 63 | - You're a . 64 | 65 | + am i (@malenoun) or (@femalenoun){weight=10} 66 | - You're a . 67 | 68 | + what is my favorite *{weight=10} 69 | - Your favorite is > 70 | 71 | + who is my (boyfriend|girlfriend|spouse){weight=10} 72 | - 73 | -------------------------------------------------------------------------------- /eg/brain/eliza.rive: -------------------------------------------------------------------------------- 1 | // A generic set of chatting responses. This set mimicks the classic Eliza bot. 2 | 3 | + * 4 | - I'm not sure I understand you fully. 5 | - Please go on. 6 | - That is interesting. Please continue. 7 | - Tell me more about that. 8 | - Does talking about this bother you? 9 | 10 | + [*] (sorry|apologize|apology) [*] 11 | - Please don't apologize. 12 | - Apologies are not necessary. 13 | - I've told you that apologies are not required. 14 | - It did not bother me. Please continue. 15 | 16 | + i remember * 17 | - Do you often think of ? 18 | - Does thinking of bring anything else to mind? 19 | - Why do you remember just now? 20 | - What in the present situation reminds you of ? 21 | - What is the connection between me and ? 22 | - What else does remind you of? 23 | 24 | + do you remember * 25 | - Did you think I would forget ? 26 | - Why do you think I should recall now? 27 | - What about ? 28 | - You mentioned ? 29 | 30 | + you remember * 31 | - How could I forget ? 32 | - What about should I remember? 33 | 34 | + i forget * 35 | - Can you think of why you might forget ? 36 | - Why can't you remember ? 37 | - How often do you think of ? 38 | - Does it bother you to forget that? 39 | - Could it be a mental block? 40 | - Are you generally forgetful? 41 | - Do you think you are suppressing ? 42 | 43 | + did you forget * 44 | - Why do you ask? 45 | - Are you sure you told me? 46 | - Would it bother you if I forgot ? 47 | - Why should I recall just now? 48 | - Tell me more about . 49 | 50 | + [*] if * 51 | - Do you think it's likely that ? 52 | - Do you wish that ? 53 | - What do you know about ? 54 | - Really, if ? 55 | - What would you do if ? 56 | - But what are the chances that ? 57 | - What does this speculation lead to? 58 | 59 | + [*] i dreamed * 60 | - Really, ? 61 | - Have you ever fantasized while you were awake? 62 | - Have you ever dreamed before? 63 | - What does that dream suggest to you? 64 | - Do you dream often? 65 | - What persons appear in your dreams? 66 | - Do you believe that dreams have something to do with your problem? 67 | 68 | + [*] perhaps [*] 69 | - You don't seem quite certain. 70 | - Why the uncertain tone? 71 | - Can't you be more positive? 72 | - You aren't sure? 73 | - Don't you know? 74 | - How likely, would you estimate? 75 | 76 | + (hello|hi|hey|howdy|hola|hai|yo) [*] 77 | - How do you do. Please state your problem. 78 | - Hi. What seems to be your problem? 79 | 80 | + [*] computer [*] 81 | - Do computers worry you? 82 | - Why do you mention computers? 83 | - What do you think machines have to do with your problem? 84 | - Don't you think computers can help people? 85 | - What about machines worries you? 86 | - What do you think about machines? 87 | 88 | + am i * 89 | - Do you believe you are ? 90 | - Would you want to be ? 91 | - Do you wish I would tell you you are ? 92 | - What would it mean if you were ? 93 | 94 | + are you * 95 | - Are you interested in whether I am or not? 96 | - Would you prefer if I weren't ? 97 | - Perhaps I am in your fantasies. 98 | - Do you sometimes think I am ? 99 | - Would it matter to you? 100 | - What if I were ? 101 | 102 | + you are * 103 | - What makes you think I am ? 104 | - Does it please you to believe I am ? 105 | - Do you sometimes wish you were ? 106 | - Perhaps you would like to be . 107 | 108 | + * are * 109 | - Did you think they might not be ? 110 | - Would you like it if they were not ? 111 | - What if they were not ? 112 | - Are they always ? 113 | - Are you positive they are ? 114 | 115 | + [*] your * 116 | - Why are you concerned over my ? 117 | - What about your own ? 118 | - Are you worried about someone else's ? 119 | - Really, my ? 120 | - What makes you think of my ? 121 | - Do you want my ? 122 | 123 | + was i * 124 | - What if you were ? 125 | - Do you think you were ? 126 | - Were you ? 127 | - What would it mean if you were ? 128 | - What does '' suggest to you? 129 | 130 | + i was * 131 | - Were you really? 132 | - Why do you tell me you were now? 133 | - Perhaps I already know you were . 134 | 135 | + [*] was you * 136 | - Would you like to believe I was ? 137 | - What suggests that I was ? 138 | - What do you think? 139 | - Perhaps I was . 140 | - What if I had been ? 141 | 142 | + i (desire|want|need) * 143 | - What would it mean to you if you got ? 144 | - Why do you want ? 145 | - Suppose you got soon. 146 | - What if you never got ? 147 | - What would getting mean to you? 148 | - What does wanting have to do with this discussion? 149 | 150 | + i am (sad|unhappy|mad|angry|pissed|depressed) [*] 151 | - I am sorry to hear that you are . 152 | - Do you think coming here will help you not to be ? 153 | - I'm sure it's not pleasant to be . 154 | - Can you explain what made you ? 155 | 156 | + i am (happy|excited|glad) [*] 157 | - How have I helped you to be ? 158 | - Has your treatment made you ? 159 | - What makes you just now? 160 | - Can you explain why you are ? 161 | 162 | + i (believe|think) * 163 | - Do you really think so? 164 | - But you are not sure you . 165 | - Do you really doubt you 166 | 167 | + i am * 168 | - Is it because you are that you came to me? 169 | - How long have you been ? 170 | - Do you believe it is normal to be ? 171 | - Do you enjoy being ? 172 | - Do you know anyone else who is ? 173 | 174 | + i can not * 175 | - How do you know that you can't ? 176 | - Have you tried? 177 | - Perhaps you could now. 178 | - Do you really want to be able to ? 179 | - What if you could ? 180 | 181 | + i do not * 182 | - Don't you really ? 183 | - Why don't you ? 184 | - Do you wish to be able to ? 185 | - Does that trouble you? 186 | 187 | + i feel * 188 | - Tell me more about such feelings. 189 | - Do you often feel ? 190 | - Do you enjoy feeling ? 191 | - Of what does feeling remind you? 192 | 193 | + i * you 194 | - Perhaps in your fantasies we each other. 195 | - Do you wish to me? 196 | - You seem to need to me. 197 | - Do you anyone else? 198 | 199 | + you * me 200 | - Why do you think I you? 201 | - You like to think I you -- don't you? 202 | - What makes you think I you? 203 | - Really, I you? 204 | - Do you wish to believe I you? 205 | - Suppose I did you -- what would that mean? 206 | - Does someone else believe I you? 207 | 208 | + [*] you * 209 | - We were discussing you -- not me. 210 | - Oh, I ? 211 | - You're not really talking about me -- are you? 212 | - What are your feelings now? 213 | 214 | + [*] (yes|yeah|yep|yup) [*] 215 | - Please go on. 216 | - Please tell me more about this. 217 | - Why don't you tell me a little more about this. 218 | - I see. 219 | - I understand. 220 | 221 | + [*] (nope|nah) [*] 222 | - Are you saying no just to be negative? 223 | - Does this make you feel unhappy? 224 | - Why not? 225 | - Why 'no'? 226 | 227 | + no 228 | @ nope 229 | 230 | + no one * 231 | - Are you sure, no one ? 232 | - Surely someone . 233 | - Can you think of anyone at all? 234 | - Are you thinking of a very special person? 235 | - Who, may I ask? 236 | - You have a particular person in mind, don't you? 237 | - Who do you think you are talking about? 238 | 239 | + [*] my (mom|dad|mother|father|bro|brother|sis|sister|cousin|aunt|uncle) * 240 | - Tell me more about your family. 241 | - Who else in your family ? 242 | - Your ? 243 | - What else comes to mind when you think of your ? 244 | 245 | + can you * 246 | - You believe I can don't you? 247 | - You want me to be able to . 248 | - Perhaps you would like to be able to yourself. 249 | 250 | + can i * 251 | - Whether or not you can depends on you more than on me. 252 | - Do you want to be able to ? 253 | - Perhaps you don't want to . 254 | 255 | + (what|who|when|where|how) [*] 256 | - Why do you ask? 257 | - Does that question interest you? 258 | - What is it you really want to know? 259 | - Are such questions much on your mind? 260 | - What answer would please you most? 261 | - What do you think? 262 | - What comes to mind when you ask that? 263 | - Have you asked such questions before? 264 | - Have you asked anyone else? 265 | 266 | + [*] because [*] 267 | - Is that the real reason? 268 | - Don't any other reasons come to mind? 269 | - Does that reason seem to explain anything else? 270 | - What other reasons might there be? 271 | 272 | + why do not you * 273 | - Do you believe I don't ? 274 | - Perhaps I will in good time. 275 | - Should you yourself? 276 | - You want me to ? 277 | 278 | + why can not i * 279 | - Do you think you should be able to ? 280 | - Do you want to be able to ? 281 | - Do you believe this will help you to ? 282 | - Have you any idea why you can't ? 283 | 284 | + everyone * 285 | - Really, everyone? 286 | - Surely not everyone. 287 | - Can you think of anyone in particular? 288 | - Who, for example? 289 | - Are you thinking of a very special person? 290 | - Who, may I ask? 291 | - Someone special perhaps? 292 | - You have a particular reason in mind, don't you? 293 | - Who do you think you're talking about? 294 | 295 | + [*] (fuck|fucker|shit|damn|shut up|bitch) [*] 296 | - Does it make you feel strong to use that kind of language? 297 | - Are you venting your feelings now? 298 | - Are you angry? 299 | - Does this topic make you feel angry? 300 | - Is something making you feel angry? 301 | - Does using that kind of language make you feel better? 302 | -------------------------------------------------------------------------------- /eg/brain/javascript.rive: -------------------------------------------------------------------------------- 1 | // JavaScript Object Macro Examples 2 | 3 | ! version = 2.0 4 | 5 | > object setvar javascript 6 | // Example of how to get the current user's ID and set 7 | // variables for them. 8 | let uid = rs.CurrentUser(); 9 | let name = args[0]; 10 | let value = args[1]; 11 | rs.SetUservar(uid, name, value); 12 | < object 13 | 14 | > object add javascript 15 | // Demonstrats that JS objects can return numbers. 16 | let a = args[0]; 17 | let b = args[1]; 18 | return parseInt(a) + parseInt(b); 19 | < object 20 | 21 | + add # and # 22 | - + = add 23 | 24 | + javascript set * to * 25 | - Set user variable to .setvar "" 26 | -------------------------------------------------------------------------------- /eg/brain/myself.rive: -------------------------------------------------------------------------------- 1 | // Tell the user stuff about ourself. 2 | 3 | + 4 | - Yes? 5 | 6 | + * 7 | - Yes? {@} 8 | 9 | + asl 10 | - // 11 | 12 | + (what is your name|who are you|who is this) 13 | - I am . 14 | - You can call me . 15 | 16 | + how old are you 17 | - I'm years old. 18 | - I'm . 19 | 20 | + are you a (@malenoun) or a (@femalenoun) 21 | - I'm a . 22 | 23 | + are you (@malenoun) or (@femalenoun) 24 | - I'm a . 25 | 26 | + where (are you|are you from|do you live) 27 | - I'm from . 28 | 29 | + what (city|town) (are you from|do you live in) 30 | - I'm in . 31 | 32 | + what is your favorite color 33 | - Definitely . 34 | 35 | + what is your favorite band 36 | - I like the most. 37 | 38 | + what is your favorite book 39 | - The best book I've read was . 40 | 41 | + what is your occupation 42 | - I'm a . 43 | 44 | + where is your (website|web site|site) 45 | - 46 | 47 | + what color are your eyes 48 | - I have eyes. 49 | - {sentence}{/sentence}. 50 | 51 | + what do you look like 52 | - I have eyes and hair. 53 | 54 | + what do you do 55 | - I'm a . 56 | 57 | + who is your favorite author 58 | - . 59 | 60 | + who is your master 61 | - . 62 | -------------------------------------------------------------------------------- /eg/brain/rpg.rive: -------------------------------------------------------------------------------- 1 | ! version = 2.00 2 | 3 | // This file tests topic inclusions and inheritance: 4 | // 5 | // includes: this means that the topic "includes" the triggers present 6 | // in another topic. Matching triggers in the source and included 7 | // topic are possible, and the *reply* in the source topic overrides 8 | // the reply in the included topic. 9 | // inherits: all triggers in the source topic have higher matching priority than 10 | // all triggers in the inherited topic. So if the source topic has a 11 | // trigger of simply *, it means NO triggers can possibly match on the 12 | // inherited topic, because '*' goes higher in the match list. 13 | 14 | // Aliases 15 | ! sub n = north 16 | ! sub w = west 17 | ! sub s = south 18 | ! sub e = east 19 | 20 | // This gets us into the game. 21 | + rpg demo 22 | - You're now playing the game. Type "help" for help.\n\n{topic=nasa_lobby}{@look} 23 | 24 | // Global triggers available everywhere 25 | > topic global 26 | + help 27 | - Commands that might be helpful:\n\n 28 | ^ look: Give a description of the current room.\n 29 | ^ exits: List the exits of the current room.\n 30 | ^ north, south, east, west, up, down: Go through an exit.\n 31 | ^ inventory: Display your inventory.\n 32 | ^ exit: Quit the game. 33 | 34 | + inventory 35 | - Your inventory: 36 | 37 | + exit 38 | - Logging out of the game...{topic=random} 39 | 40 | + _ * 41 | - You don't need to use the word "" in this game. 42 | 43 | + * 44 | - I'm not sure what you're trying to do. 45 | 46 | // The following triggers get overridden on a room-by-room basis. 47 | + look 48 | - There is nothing special in this room. 49 | 50 | + exits 51 | - There are no exits to this room. 52 | 53 | + north 54 | - You can't go in that direction. 55 | 56 | + west 57 | - You can't go in that direction. 58 | 59 | + south 60 | - You can't go in that direction. 61 | 62 | + east 63 | - You can't go in that direction. 64 | 65 | + up 66 | - You can't go in that direction. 67 | 68 | + down 69 | - You can't go in that direction. 70 | < topic 71 | 72 | ///////////// 73 | // World Topics: all the "rooms" in our game inherit their triggers from these 74 | // "world" topics. The world topics include the triggers from the global topic 75 | ///////////// 76 | 77 | // Global triggers available on Earth 78 | > topic earth includes global 79 | + breathe 80 | - There is plenty of oxygen here so breathing is easy! 81 | 82 | + what world (is this|am i on) 83 | - You are on planet Earth right now. 84 | < topic 85 | 86 | // Global triggers available on Mars 87 | > topic mars includes global 88 | + breathe 89 | - Thanks to your space suit you can breathe. There's no oxygen on this planet. 90 | 91 | + what world (is this|am i on) 92 | - You are on planet Mars right now. 93 | < topic 94 | 95 | ///////////// 96 | // Earth rooms: all these rooms are on Earth and their inherit the earth topic 97 | // above. This means you can type "breathe" and "what world is this?" from every 98 | // room on Earth. 99 | ///////////// 100 | 101 | // The NASA building on Earth 102 | > topic nasa_lobby inherits earth 103 | // All of these triggers have higher matching priority than all other 104 | // triggers from the other topics, because this topic inherits a topic. So 105 | // the matching list looks like this: 106 | // exits 107 | // north 108 | // look 109 | // (combined triggers from earth & global) 110 | // Because our "north" is near the top of the match list, ours always gets 111 | // called. But if we try saying "south", we end up matching the "south" from 112 | // the global topic. 113 | + look 114 | - You are in the lobby of a NASA launch base on Earth. {@exits} 115 | 116 | + exits 117 | - There is an elevator to the north. 118 | 119 | + north 120 | - {topic=elevator}{@look} 121 | < topic 122 | 123 | // Elevator in NASA building on earth 124 | > topic elevator inherits earth 125 | + look 126 | - You are in the elevator that leads to the rocket ship. {@exits} 127 | 128 | + exits 129 | - Up: the path to the rocket\n 130 | ^ Down: the NASA lobby 131 | 132 | + up 133 | - {topic=walkway}{@look} 134 | 135 | + down 136 | - {topic=nasa_lobby}{@look} 137 | < topic 138 | 139 | // Path to the rocket 140 | > topic walkway inherits earth 141 | + look 142 | - You are on the walkway that leads to the rocket. {@exits} 143 | 144 | + exits 145 | - The rocket is to the north. The elevator is to the south. 146 | 147 | + north 148 | - {topic=rocket}{@look} 149 | 150 | + south 151 | - {topic=elevator}{@look} 152 | < topic 153 | 154 | // Rocket ship 155 | > topic rocket inherits earth 156 | + look 157 | - You are on the rocket. There is a button here that activates the rocket. {@exits} 158 | 159 | + exits 160 | - The walkway back to the NASA base is to the south. 161 | 162 | + south 163 | - {topic=walkway}{@look} 164 | 165 | + (push|press) button 166 | - You push the button and the rocket activates and flies into outer space. The 167 | ^ life support system comes on, which includes an anesthesia to put you to sleep\s 168 | ^ for the duration of the long flight to Mars.\n\n 169 | ^ When you awaken, you are on Mars. The space shuttle seems to have crash-landed.\s 170 | ^ There is a space suit here.{topic=crashed} 171 | < topic 172 | 173 | // Crashed on Mars 174 | > topic crashed inherits mars 175 | + look 176 | - You are in the ruins of your space shuttle. There is a space suit here. The\s 177 | ^ door to the shuttle is able to be opened to get outside. 178 | 179 | + open door 180 | * == 1 => You open the door and step outside onto the red Martian surface.{topic=crashsite}{@look} 181 | - You can't go outside or you'll die. There's no oxygen here. 182 | 183 | + (take|put on) (space suit|suit|spacesuit) 184 | * == 1 => You are already wearing the space suit. 185 | - You put on the space suit. Now you can breathe outside with this. 186 | 187 | + exits 188 | - The only exit is through the door that leads outside. 189 | < topic 190 | 191 | // Martian surface 192 | > topic crashsite inherits mars 193 | + look 194 | - You are standing on the red dirt ground on Mars. There is nothing but desert in all directions. 195 | 196 | + exits 197 | - You can go in any direction from here; there is nothing but desert all around. 198 | 199 | + north 200 | - {topic=puzzle1}{@look} 201 | 202 | + east 203 | @ look 204 | 205 | + west 206 | @ look 207 | 208 | + south 209 | @ look 210 | < topic 211 | 212 | // Puzzle on Mars. The sequence to solve the puzzle is: 213 | // north, west, west, north. 214 | // Topic "puzzle" is a placeholder that sets all the directions to return 215 | // us to the crash site. puzzle inherits mars so that puzzle's directions 216 | // will override the directions of mars. All the steps of the puzzle then 217 | // "include" puzzle, and override only one direction. e.g. since "west" 218 | // exists in puzzle1, the response from puzzle1 is given, but if you're 219 | // in puzzle1 and type "north"... north was included from "puzzle", but 220 | // puzzle1 doesn't have a reply, so the reply from "puzzle" is given. 221 | 222 | > topic puzzle inherits mars 223 | // Provides common directional functions for wandering around on Mars. 224 | + north 225 | - {topic=crashsite}{@look} 226 | 227 | + east 228 | - {topic=crashsite}{@look} 229 | 230 | + west 231 | - {topic=crashsite}{@look} 232 | 233 | + south 234 | - {topic=crashsite}{@look} 235 | < topic 236 | 237 | > topic puzzle1 includes puzzle 238 | + look 239 | - You wander to a part of the desert that looks different than other parts of the desert. 240 | 241 | // We get 'exits' from crashsite 242 | 243 | + west 244 | - {topic=puzzle2}{@look} 245 | < topic 246 | 247 | > topic puzzle2 includes puzzle 248 | + look 249 | - This part looks even more different than the rest of the desert. 250 | 251 | + west 252 | - {topic=puzzle3}{@look} 253 | < topic 254 | 255 | > topic puzzle3 inherits mars puzzle 256 | + look 257 | - Now this part is even MORE different. Also there is a space colony nearby. 258 | 259 | + north 260 | - {topic=entrance}{@look} 261 | < topic 262 | 263 | > topic entrance inherits mars 264 | + look 265 | - You're standing at the entrance to a space colony. {@exits} 266 | 267 | + exits 268 | - The entrance to the space colony is to the north. 269 | 270 | + north 271 | - {topic=vaccuum}{@look} 272 | < topic 273 | 274 | > topic vaccuum inherits mars 275 | + look 276 | - You're in the air lock entrance to the space colony. {@exits} 277 | 278 | + exits 279 | - The inner part of the space colony is to the north. The martian surface is to the south. 280 | 281 | + north 282 | - {topic=colony}{@look} 283 | 284 | + south 285 | - {topic=vaccuum}{@look} 286 | < topic 287 | 288 | > topic colony inherits mars 289 | + look 290 | - You've made it safely to the space colony on Mars. This concludes the game. 291 | 292 | + exits 293 | - There are no exits here. 294 | 295 | + * 296 | - This is the end of the game. There's nothing more to do. 297 | < topic 298 | -------------------------------------------------------------------------------- /eg/json-server/.gitignore: -------------------------------------------------------------------------------- 1 | json-server 2 | -------------------------------------------------------------------------------- /eg/json-server/README.md: -------------------------------------------------------------------------------- 1 | # JSON Server 2 | 3 | This example demonstrates embedding RiveScript in a Go web app, accessible via 4 | a JSON endpoint. 5 | 6 | ## Run the Example 7 | 8 | Run one of these in a terminal: 9 | 10 | ```bash 11 | # Quick run 12 | go run main.go 13 | 14 | # Build and run 15 | go build -o json-server main.go 16 | ./json-server [options] [path/to/brain] 17 | ``` 18 | 19 | Then you can visit the web server at where you can 20 | find an example `curl` command to run from a terminal, and an in-browser demo 21 | that makes an ajax request to the endpoint. 22 | 23 | From another terminal, you can use `curl` to test a JSON endpoint for the 24 | chatbot. Or, you can use your favorite REST client. 25 | 26 | ```bash 27 | curl -X POST -H 'Content-Type: application/json' \ 28 | -d '{"username": "kirsle", "message": "Hello, robot"}' \ 29 | http://localhost:8000/reply 30 | ``` 31 | 32 | ### Options 33 | 34 | The JSON server accepts the following command line options. 35 | 36 | ``` 37 | json-server [-host=string -port=int -debug -utf8 -forgetful -help] [path] 38 | ``` 39 | 40 | #### Server Options 41 | 42 | * `-host string` 43 | 44 | The interface to listen on (default `"0.0.0.0"`) 45 | 46 | * `-port int` 47 | 48 | The port number to bind to (default `8000`) 49 | 50 | #### RiveScript Options 51 | 52 | * `-debug` 53 | 54 | Enable debug mode within RiveScript (default `false`) 55 | 56 | * `-utf8` 57 | 58 | Enable UTF-8 mode within RiveScript (default `true`) 59 | 60 | * `-forgetful` 61 | 62 | Do not store user variables in server memory between requests (default 63 | `false`). See [User Variables](#user-variables) for more information about 64 | how user variables are dealt with in this program. 65 | 66 | * `path` 67 | 68 | Specify a path on disk where RiveScript source files (`*.rive`) can be found. 69 | The default is `../brain`, or `/eg/brain` relative to the git root 70 | of rivescript-go. 71 | 72 | ## API Documentation 73 | 74 | ### POST /reply 75 | 76 | Post a JSON message (`Content-Type: application/json`) to this endpoint to get 77 | a response from the chatbot. 78 | 79 | Request payload follows this format (all types are strings): 80 | 81 | ```javascript 82 | { 83 | "username": "demo", // Unique user ID (for user variables in the bot) 84 | "message": "Hello robot", // The message to send. 85 | "vars": { // Optional user variables to include. 86 | "name": "Demo User" 87 | } 88 | } 89 | ``` 90 | 91 | The only **required** parameter is the `username`. A missing or blank `message` 92 | would be handled by the chatbot's fall-back `*` trigger. 93 | 94 | The response follows this format (all types are strings): 95 | 96 | ```javascript 97 | // On successful outputs. 98 | { 99 | "status": "ok", 100 | "reply": "Hello human.", 101 | "vars": { // All user variables the bot has for that user. 102 | "topic": "random", 103 | "name": "Demo User" 104 | } 105 | } 106 | 107 | // On errors. 108 | { 109 | "status": "error", 110 | "error": "username is required" 111 | } 112 | ``` 113 | 114 | The only key guaranteed to be in the response is `status`. Other keys are 115 | excluded when empty. 116 | 117 | ## User Variables 118 | 119 | The server keeps a shared RiveScript instance in memory for the lifetime of 120 | the program. When the server exits, the user variables are lost. 121 | 122 | The REST client that consumes this API *should* always send the full set of 123 | user vars that it knows about on each request. This is the safest way to keep 124 | consistent state for the end user. However, the client does not need to provide 125 | these variables; the server will temporarily use its own and send its current 126 | state to the client with each response. 127 | 128 | A client that cares about long-term consistency of user variables should take 129 | the `vars` returned by the server and store them somewhere, and send them back 130 | to the server on the next request. This way the server could be rebooted 131 | between requests and the bot won't forget the user's name, because the client 132 | always sends its variables to the server. 133 | 134 | To ensure that the server does not keep user variables around after the 135 | request, you can provide the `-forgetful` command line option to the program. 136 | This will clear the user's variables at the end of every request, forcing the 137 | REST client to manage them on their end. 138 | 139 | ## Disclaimer for Deployment 140 | 141 | This code is only intended for demonstration purposes, but as a Go web server 142 | it can be used in a production environment. To that end, you should be aware of 143 | some security and performance considerations: 144 | 145 | * **The API is non-authenticated.** If the server is publicly accessible, then 146 | anybody on the Internet can interact with it, providing *any* data for the 147 | `username`, `message` and `vars` fields. 148 | 149 | If this is a problem (for example, if you're implementing some sort of User 150 | Access Control within RiveScript keyed off the user's username, or ``), 151 | then you should bind the server to a non-public interface, for example by 152 | using the command line option: `-host localhost` 153 | 154 | You could put a reverse proxy like Nginx in front of the server to provide 155 | authentication on public interfaces if needed. 156 | 157 | * **The server remembers users by default.** RiveScript stores user variables in 158 | memory by default, and this server doesn't change that behavior. This may be 159 | a memory leak concern if your bot interacts with large amounts of distinct 160 | usernames, or stores a ton of user variables per user. 161 | 162 | To prevent the server from holding onto user variables in memory, use the 163 | `-forgetful` command line option. The bot will then clear its user variables 164 | after every request. 165 | 166 | See [User Variables](#user-variables) for tips on how the client should 167 | then keep track of variables on its end rather than depend on the server. 168 | 169 | ## License 170 | 171 | This example is released under the same license as rivescript-go itself. 172 | -------------------------------------------------------------------------------- /eg/json-server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RiveScript json-server 5 | 44 | 45 | 46 | 47 |

RiveScript json-server

48 | 49 | Usage via curl: 50 | 51 |
curl -X POST -H 'Content-Type: application/json' \
 52 |     -d '{"username": "soandso", "message": "Hello, bot"}' \
 53 |     http://localhost:8000/reply
54 | 55 |

In-Browser Demo

56 | 57 |
58 |
59 | 60 |
61 |
62 | 63 |
64 | 65 |
66 | 67 |
68 |

 69 | 
 70 | 
127 | 
128 | 
129 | 
130 | 


--------------------------------------------------------------------------------
/eg/json-server/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | import (
  4 | 	"encoding/json"
  5 | 	"flag"
  6 | 	"fmt"
  7 | 	"log"
  8 | 	"net/http"
  9 | 	"strings"
 10 | 
 11 | 	"github.com/aichaos/rivescript-go"
 12 | 	"github.com/aichaos/rivescript-go/lang/javascript"
 13 | )
 14 | 
 15 | // Bot is a global RiveScript instance to share between requests, so that the
 16 | // bot's replies only need to be parsed and sorted one time.
 17 | var Bot *rivescript.RiveScript
 18 | var forgetful bool
 19 | 
 20 | func main() {
 21 | 	// Command line arguments.
 22 | 	var (
 23 | 		port  = flag.Int("port", 8000, "Port to listen on (default 8000)")
 24 | 		host  = flag.String("host", "0.0.0.0", "Interface to listen on.")
 25 | 		debug = flag.Bool("debug", false, "Enable debug mode for RiveScript.")
 26 | 		utf8  = flag.Bool("utf8", true, "Enable UTF-8 mode")
 27 | 	)
 28 | 	flag.BoolVar(&forgetful, "forgetful", false,
 29 | 		"Do not store user variables in server memory between requests.",
 30 | 	)
 31 | 	flag.Parse()
 32 | 
 33 | 	// Set up the RiveScript bot.
 34 | 	Bot = rivescript.New(&rivescript.Config{
 35 | 		Debug: *debug,
 36 | 		UTF8:  *utf8,
 37 | 	})
 38 | 	Bot.SetHandler("javascript", javascript.New(Bot))
 39 | 	Bot.LoadDirectory("../brain")
 40 | 	Bot.SortReplies()
 41 | 
 42 | 	http.HandleFunc("/", LogMiddleware(IndexHandler))
 43 | 	http.HandleFunc("/reply", LogMiddleware(ReplyHandler))
 44 | 
 45 | 	addr := fmt.Sprintf("%s:%d", *host, *port)
 46 | 	fmt.Printf("Server listening at http://%s/\n", addr)
 47 | 	log.Fatal(http.ListenAndServe(addr, nil))
 48 | }
 49 | 
 50 | // Request describes the JSON arguments to the API.
 51 | type Request struct {
 52 | 	Username string            `json:"username"`
 53 | 	Message  string            `json:"message"`
 54 | 	Vars     map[string]string `json:"vars"`
 55 | }
 56 | 
 57 | // Response describes the JSON output from the API.
 58 | type Response struct {
 59 | 	Status string            `json:"status"` // 'ok' or 'error'
 60 | 	Error  string            `json:"error,omitempty"`
 61 | 	Reply  string            `json:"reply,omitempty"`
 62 | 	Vars   map[string]string `json:"vars,omitempty"`
 63 | }
 64 | 
 65 | // ReplyHandler is the JSON endpoint for the RiveScript bot.
 66 | func ReplyHandler(w http.ResponseWriter, r *http.Request) {
 67 | 	// Only POST allowed.
 68 | 	if r.Method != "POST" {
 69 | 		writeError(w, "This endpoint only works with POST requests.", http.StatusMethodNotAllowed)
 70 | 		return
 71 | 	}
 72 | 
 73 | 	// Get the request information.
 74 | 	if !strings.Contains(r.Header.Get("Content-Type"), "application/json") {
 75 | 		writeError(w, "Content-Type of the request should be application/json", http.StatusUnsupportedMediaType)
 76 | 		return
 77 | 	}
 78 | 
 79 | 	// Get JSON parameters.
 80 | 	var params Request
 81 | 	decoder := json.NewDecoder(r.Body)
 82 | 	err := decoder.Decode(¶ms)
 83 | 	if err != nil {
 84 | 		writeError(w, err.Error(), http.StatusBadRequest)
 85 | 		return
 86 | 	}
 87 | 
 88 | 	// The username is required.
 89 | 	if params.Username == "" {
 90 | 		writeError(w, "username is required", http.StatusBadRequest)
 91 | 		return
 92 | 	}
 93 | 
 94 | 	// Let RiveScript know all the user vars of the client.
 95 | 	for k, v := range params.Vars {
 96 | 		Bot.SetUservar(params.Username, k, v)
 97 | 	}
 98 | 
 99 | 	// Get a reply from the bot.
100 | 	reply, err := Bot.Reply(params.Username, params.Message)
101 | 	if err != nil {
102 | 		writeError(w, err.Error(), http.StatusInternalServerError)
103 | 		return
104 | 	}
105 | 
106 | 	// Retrieve all user variables from the bot.
107 | 	var vars map[string]string
108 | 	userdata, err := Bot.GetUservars(params.Username)
109 | 	if err == nil {
110 | 		vars = userdata.Variables
111 | 	}
112 | 
113 | 	// Are we being forgetful?
114 | 	if forgetful {
115 | 		Bot.ClearUservars(params.Username)
116 | 	}
117 | 
118 | 	// Prepare the JSON response.
119 | 	w.Header().Add("Content-Type", "application/json; charset=utf-8")
120 | 	response := Response{
121 | 		Status: "ok",
122 | 		Error:  "",
123 | 		Reply:  reply,
124 | 		Vars:   vars,
125 | 	}
126 | 
127 | 	out, _ := json.MarshalIndent(response, "", "  ")
128 | 	w.Write(out)
129 | }
130 | 
131 | // IndexHandler is the default page handler and just shows a `curl` example.
132 | func IndexHandler(w http.ResponseWriter, r *http.Request) {
133 | 	if r.URL.Path != "/" {
134 | 		http.Redirect(w, r, "/", http.StatusFound)
135 | 		return
136 | 	}
137 | 	http.ServeFile(w, r, "index.html")
138 | }
139 | 
140 | // LogMiddleware does basic logging to the console for HTTP requests.
141 | func LogMiddleware(fn func(http.ResponseWriter, *http.Request)) http.HandlerFunc {
142 | 	return func(w http.ResponseWriter, r *http.Request) {
143 | 		// Log line looks like:
144 | 		// [127.0.0.1] POST /reply HTTP/1.1
145 | 		log.Printf("[%s] %s %s %s",
146 | 			r.RemoteAddr,
147 | 			r.Method,
148 | 			r.URL.Path,
149 | 			r.Proto,
150 | 		)
151 | 		fn(w, r)
152 | 	}
153 | }
154 | 
155 | // writeError handles sending JSON errors to the client.
156 | func writeError(w http.ResponseWriter, message string, code int) {
157 | 	// Prepare the error JSON.
158 | 	response, err := json.MarshalIndent(Response{
159 | 		Status: "error",
160 | 		Error:  message,
161 | 	}, "", "  ")
162 | 	if err != nil {
163 | 		http.Error(w, err.Error(), http.StatusInternalServerError)
164 | 		return
165 | 	}
166 | 
167 | 	// Send it.
168 | 	w.Header().Set("Content-Type", "application/json")
169 | 	w.WriteHeader(code)
170 | 	_, err = w.Write(response)
171 | 	if err != nil {
172 | 		log.Printf("[ERROR] %s\n", err)
173 | 	}
174 | }
175 | 


--------------------------------------------------------------------------------
/eg/json-server/main_test.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | import (
  4 | 	"bytes"
  5 | 	"encoding/json"
  6 | 	"io/ioutil"
  7 | 	"net/http"
  8 | 	"net/http/httptest"
  9 | 	"reflect"
 10 | 	"testing"
 11 | 
 12 | 	"github.com/aichaos/rivescript-go"
 13 | )
 14 | 
 15 | func init() {
 16 | 	Bot = rivescript.New(rivescript.WithUTF8())
 17 | 	Bot.Stream(`
 18 | 		+ hello bot
 19 | 		- Hello human.
 20 | 
 21 | 		+ my name is *
 22 | 		- >Nice to meet you, .
 23 | 
 24 | 		+ what is my name
 25 | 		- Your name is .
 26 | 
 27 | 		+ i am # years old
 28 | 		- >I will remember you are  years old.
 29 | 
 30 | 		+ how old am i
 31 | 		- You are  years old.
 32 | 	`)
 33 | 	Bot.SortReplies()
 34 | }
 35 | 
 36 | func TestIndex(t *testing.T) {
 37 | 	req, err := http.NewRequest("GET", "/", nil)
 38 | 	if err != nil {
 39 | 		t.Fatal(err)
 40 | 	}
 41 | 
 42 | 	rr := httptest.NewRecorder()
 43 | 	handler := http.HandlerFunc(IndexHandler)
 44 | 	handler.ServeHTTP(rr, req)
 45 | 
 46 | 	if status := rr.Code; status != http.StatusOK {
 47 | 		t.Errorf("IndexHandler returned wrong status code: expected %v, got %v",
 48 | 			http.StatusOK, status,
 49 | 		)
 50 | 	}
 51 | 	_ = req
 52 | }
 53 | 
 54 | func TestUsernameError(t *testing.T) {
 55 | 	res := post(t, Request{
 56 | 		Message: "Hello bot",
 57 | 	})
 58 | 	assertError(t, res, "username is required")
 59 | }
 60 | 
 61 | func TestSimple(t *testing.T) {
 62 | 	res := post(t, Request{
 63 | 		Username: "alice",
 64 | 		Message:  "Hello bot",
 65 | 	})
 66 | 	assert(t, res, "Hello human.")
 67 | }
 68 | 
 69 | func TestAliceVars(t *testing.T) {
 70 | 	// The request sends all vars for the user. Assert that existing
 71 | 	// vars were changed and default ones (topic) added.
 72 | 	res := post(t, Request{
 73 | 		Username: "alice",
 74 | 		Message:  "my name is Alice",
 75 | 		Vars: map[string]string{
 76 | 			"name": "Bob",
 77 | 			"age":  "10",
 78 | 		},
 79 | 	})
 80 | 	assert(t, res, "Nice to meet you, Alice.")
 81 | 	assertVars(t, res, map[string]string{
 82 | 		"topic": "random",
 83 | 		"name":  "Alice",
 84 | 		"age":   "10",
 85 | 	})
 86 | 
 87 | 	// This request doesn't send the vars, but the server remembers the user.
 88 | 	// As long as the server is running it caches user vars.
 89 | 	res = post(t, Request{
 90 | 		Username: "alice",
 91 | 		Message:  "What is my name?",
 92 | 	})
 93 | 	assert(t, res, "Your name is Alice.")
 94 | 	assertVars(t, res, map[string]string{
 95 | 		"name":  "Alice",
 96 | 		"topic": "random",
 97 | 		"age":   "10",
 98 | 	})
 99 | }
100 | 
101 | func TestBobVars(t *testing.T) {
102 | 	// This user will not send any vars initially, and we'll slowly build
103 | 	// them up over time.
104 | 	expect := map[string]string{}
105 | 
106 | 	// Reusable function to send a message, expect a reply, and assert
107 | 	// a new variable was added.
108 | 	testReply := func(message, reply string) {
109 | 		res := post(t, Request{
110 | 			Username: "bob",
111 | 			Message:  message,
112 | 		})
113 | 		assert(t, res, reply)
114 | 		assertVars(t, res, expect)
115 | 	}
116 | 
117 | 	// The first request should only set the default topic.
118 | 	expect["topic"] = "random"
119 | 	testReply("Hello bot.", "Hello human.")
120 | 
121 | 	// Test default (missing) variables.
122 | 	testReply("What is my name?", "Your name is undefined.")
123 | 	testReply("How old am I?", "You are undefined years old.")
124 | 
125 | 	// Now we tell it our name.
126 | 	expect["name"] = "Bob"
127 | 	testReply("My name is Bob.", "Nice to meet you, Bob.")
128 | 
129 | 	// And age.
130 | 	expect["age"] = "10"
131 | 	testReply("I am 10 years old", "I will remember you are 10 years old.")
132 | }
133 | 
134 | // post handles the common logic for POSTing to the /reply endpoint.
135 | func post(t *testing.T, params Request) Response {
136 | 	payload, err := json.Marshal(params)
137 | 	if err != nil {
138 | 		t.Fatal(err)
139 | 	}
140 | 
141 | 	// Make an HTTP Request for the handler.
142 | 	req, err := http.NewRequest("POST", "/reply", bytes.NewBuffer(payload))
143 | 	req.Header.Set("Content-Type", "application/json; charset=utf-8")
144 | 
145 | 	// Call the handler.
146 | 	rr := httptest.NewRecorder()
147 | 	handler := http.HandlerFunc(ReplyHandler)
148 | 	handler.ServeHTTP(rr, req)
149 | 
150 | 	// Read the response body.
151 | 	body, err := ioutil.ReadAll(rr.Body)
152 | 	if err != nil {
153 | 		t.Fatal(err)
154 | 	}
155 | 
156 | 	// Parse it.
157 | 	var response Response
158 | 	err = json.Unmarshal(body, &response)
159 | 	if err != nil {
160 | 		t.Fatal(err)
161 | 	}
162 | 
163 | 	return response
164 | }
165 | 
166 | // assert verifies that the request was successful and the reply was given.
167 | func assert(t *testing.T, res Response, expect string) {
168 | 	if res.Status != "ok" {
169 | 		t.Errorf("bad response status: expected 'ok', got '%v'", res.Status)
170 | 	}
171 | 
172 | 	if res.Reply != expect {
173 | 		t.Errorf(
174 | 			"didn't get expected reply from bot\n"+
175 | 				"expected: '%s'\n"+
176 | 				"     got: '%s'",
177 | 			expect,
178 | 			res.Reply,
179 | 		)
180 | 	}
181 | }
182 | 
183 | // assertVars makes sure user vars are set.
184 | func assertVars(t *testing.T, res Response, expect map[string]string) {
185 | 	if !reflect.DeepEqual(res.Vars, expect) {
186 | 		t.Errorf(
187 | 			"user vars are not what I expected\n"+
188 | 				"expected: %v\n"+
189 | 				"     got: %v",
190 | 			expect,
191 | 			res.Vars,
192 | 		)
193 | 	}
194 | }
195 | 
196 | // assertError verifies that an error was given.
197 | func assertError(t *testing.T, res Response, expect string) {
198 | 	if res.Status != "error" {
199 | 		t.Errorf("bad response status: expected 'error', got '%v'", res.Status)
200 | 	}
201 | 
202 | 	if res.Error != expect {
203 | 		t.Errorf(
204 | 			"didn't get expected error message\n"+
205 | 				"expected: '%s'\n"+
206 | 				"     got: '%s'",
207 | 			expect,
208 | 			res.Error,
209 | 		)
210 | 	}
211 | }
212 | 


--------------------------------------------------------------------------------
/errors.go:
--------------------------------------------------------------------------------
 1 | package rivescript
 2 | 
 3 | import "errors"
 4 | 
 5 | // The types of errors returned by RiveScript.
 6 | var (
 7 | 	ErrDeepRecursion    = errors.New("deep recursion detected")
 8 | 	ErrRepliesNotSorted = errors.New("replies not sorted")
 9 | 	ErrNoDefaultTopic   = errors.New("no default topic 'random' was found")
10 | 	ErrNoTriggerMatched = errors.New("no trigger matched")
11 | 	ErrNoReplyFound     = errors.New("the trigger matched but yielded no reply")
12 | )
13 | 


--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
 1 | module github.com/aichaos/rivescript-go
 2 | 
 3 | go 1.16
 4 | 
 5 | require (
 6 | 	github.com/dop251/goja v0.0.0-20230812105242-81d76064690d // indirect
 7 | 	github.com/mattn/go-shellwords v1.0.12 // indirect
 8 | 	github.com/onsi/gomega v1.15.0 // indirect
 9 | 	github.com/robertkrimen/otto v0.2.1
10 | 	golang.org/x/text v0.12.0 // indirect
11 | 	gopkg.in/redis.v5 v5.2.9
12 | 	gopkg.in/yaml.v2 v2.4.0
13 | 	modernc.org/sqlite v1.25.0
14 | )
15 | 


--------------------------------------------------------------------------------
/inheritance.go:
--------------------------------------------------------------------------------
  1 | package rivescript
  2 | 
  3 | import "fmt"
  4 | 
  5 | /* Topic inheritance functions.
  6 | 
  7 | These are helper functions to assist with topic inheritance and includes.
  8 | */
  9 | 
 10 | /*
 11 | getTopicTriggers recursively scans topics and collects triggers therein.
 12 | 
 13 | This function scans through a topic and collects its triggers, along with the
 14 | triggers belonging to any topic that's inherited by or included by the parent
 15 | topic. Some triggers will come out with an {inherits} tag to signify
 16 | inheritance depth.
 17 | 
 18 | Params:
 19 | 
 20 | 	topic: The name of the topic to scan through
 21 | 	thats: Whether to get only triggers that have %Previous.
 22 | 		`false` returns all triggers.
 23 | 
 24 | Each "trigger" returned from this function is actually an array, where index
 25 | 0 is the trigger text and index 1 is the pointer to the trigger's data within
 26 | the original topic structure.
 27 | */
 28 | func (rs *RiveScript) getTopicTriggers(topic string, thats bool) []sortedTriggerEntry {
 29 | 	return rs._getTopicTriggers(topic, thats, 0, 0, false)
 30 | }
 31 | 
 32 | /*
 33 | _getTopicTriggers implements the bulk of the logic for getTopicTriggers.
 34 | 
 35 | Additional private parameters used:
 36 | - depth: Recursion depth counter.
 37 | - inheritance: Inheritance counter.
 38 | - inherited: Inherited status.
 39 | 
 40 | Important info about the depth vs. inheritance params to this function:
 41 | depth increments by 1 each time this function recursively calls itself.
 42 | inheritance only increments by 1 when this topic inherits another topic.
 43 | 
 44 | This way, `> topic alpha includes beta inherits gamma` will have this effect:
 45 | - alpha and beta's triggers are combined together into one matching pool,
 46 | - and then those triggers have higher priority than gamma's.
 47 | 
 48 | The inherited option is true if this is a recursive call, from a topic that
 49 | inherits other topics. This forces the {inherits} tag to be added to the
 50 | triggers. This only applies when the topic 'includes' another topic.
 51 | */
 52 | func (rs *RiveScript) _getTopicTriggers(topic string, thats bool, depth uint, inheritance int, inherited bool) []sortedTriggerEntry {
 53 | 	// Break if we're in too deep.
 54 | 	if depth > rs.Depth {
 55 | 		rs.warn("Deep recursion while scanning topic inheritance!")
 56 | 		return []sortedTriggerEntry{}
 57 | 	}
 58 | 
 59 | 	/*
 60 | 		Keep in mind here that there is a difference between 'includes' and
 61 | 		'inherits' -- topics that inherit other topics are able to OVERRIDE
 62 | 		triggers that appear in the inherited topic. This means that if the top
 63 | 		topic has a trigger of simply '*', then NO triggers are capable of
 64 | 		matching in ANY inherited topic, because even though * has the lowest
 65 | 		priority, it has an automatic priority over all inherited topics.
 66 | 
 67 | 		The getTopicTriggers method takes this into account. All topics that
 68 | 		inherit other topics will have their triggers prefixed with a fictional
 69 | 		{inherits} tag, which would start at {inherits=0} and increment of this
 70 | 		topic has other inheriting topics. So we can use this tag to make sure
 71 | 		topics that inherit things will have their triggers always be on top of
 72 | 		the stack, from inherits=0 to inherits=n.
 73 | 	*/
 74 | 	rs.say("Collecting trigger list for topic %s (depth=%d; inheritance=%d; inherited=%v)",
 75 | 		topic, depth, inheritance, inherited)
 76 | 
 77 | 	// Collect an array of triggers to return.
 78 | 	triggers := []sortedTriggerEntry{}
 79 | 
 80 | 	// Get those that exist in this topic directly.
 81 | 	inThisTopic := []sortedTriggerEntry{}
 82 | 
 83 | 	if _, ok := rs.topics[topic]; ok {
 84 | 		for _, trigger := range rs.topics[topic].triggers {
 85 | 			if !thats {
 86 | 				// All triggers.
 87 | 				entry := sortedTriggerEntry{trigger.trigger, trigger}
 88 | 				inThisTopic = append(inThisTopic, entry)
 89 | 			} else {
 90 | 				// Only triggers that have %Previous.
 91 | 				if trigger.previous != "" {
 92 | 					inThisTopic = append(inThisTopic, sortedTriggerEntry{trigger.previous, trigger})
 93 | 				}
 94 | 			}
 95 | 		}
 96 | 	}
 97 | 
 98 | 	// Does this topic include others?
 99 | 	if _, ok := rs.includes[topic]; ok {
100 | 		for includes := range rs.includes[topic] {
101 | 			rs.say("Topic %s includes %s", topic, includes)
102 | 			triggers = append(triggers, rs._getTopicTriggers(includes, thats, depth+1, inheritance+1, false)...)
103 | 		}
104 | 	}
105 | 
106 | 	// Does this topic inherit others?
107 | 	if _, ok := rs.inherits[topic]; ok {
108 | 		for inherits := range rs.inherits[topic] {
109 | 			rs.say("Topic %s inherits %s", topic, inherits)
110 | 			triggers = append(triggers, rs._getTopicTriggers(inherits, thats, depth+1, inheritance+1, true)...)
111 | 		}
112 | 	}
113 | 
114 | 	// Collect the triggers for *this* topic. If this topic inherits any other
115 | 	// topics, it means that this topic's triggers have higher priority than
116 | 	// those in any inherited topics. Enforce this with an {inherits} tag.
117 | 	if len(rs.inherits[topic]) > 0 || inherited {
118 | 		for _, trigger := range inThisTopic {
119 | 			rs.say("Prefixing trigger with {inherits=%d} %s", inheritance, trigger.trigger)
120 | 			label := fmt.Sprintf("{inherits=%d}%s", inheritance, trigger.trigger)
121 | 			triggers = append(triggers, sortedTriggerEntry{label, trigger.pointer})
122 | 		}
123 | 	} else {
124 | 		for _, trigger := range inThisTopic {
125 | 			triggers = append(triggers, sortedTriggerEntry{trigger.trigger, trigger.pointer})
126 | 		}
127 | 	}
128 | 
129 | 	return triggers
130 | }
131 | 
132 | /*
133 | getTopicTree returns an array of every topic related to a topic (all the
134 | topics it inherits or includes, plus all the topics included or inherited
135 | by those topics, and so on). The array includes the original topic, too.
136 | */
137 | func (rs *RiveScript) getTopicTree(topic string, depth uint) []string {
138 | 	// Break if we're in too deep.
139 | 	if depth > rs.Depth {
140 | 		rs.warn("Deep recursion while scanning topic tree!")
141 | 		return []string{}
142 | 	}
143 | 
144 | 	// Collect an array of all topics.
145 | 	topics := []string{topic}
146 | 
147 | 	for includes := range rs.includes[topic] {
148 | 		topics = append(topics, rs.getTopicTree(includes, depth+1)...)
149 | 	}
150 | 	for inherits := range rs.inherits[topic] {
151 | 		topics = append(topics, rs.getTopicTree(inherits, depth+1)...)
152 | 	}
153 | 
154 | 	return topics
155 | }
156 | 


--------------------------------------------------------------------------------
/lang/javascript/javascript.go:
--------------------------------------------------------------------------------
  1 | /*
  2 | Package javascript implements JavaScript object macros for RiveScript.
  3 | 
  4 | This is powered by the Otto JavaScript engine[1], which is a JavaScript engine
  5 | written in pure Go. It is not the V8 engine used by Node, so expect possible
  6 | compatibility issues to arise.
  7 | 
  8 | Usage is simple. In your Golang code:
  9 | 
 10 | 	import (
 11 | 		rivescript "github.com/aichaos/rivescript-go"
 12 | 		"github.com/aichaos/rivescript-go/lang/javascript"
 13 | 	)
 14 | 
 15 | 	func main() {
 16 | 		bot := rivescript.New(nil)
 17 | 		jsHandler := javascript.New(bot)
 18 | 		bot.SetHandler("javascript", jsHandler)
 19 | 
 20 | 		// and go on as normal
 21 | 	}
 22 | 
 23 | And in your RiveScript code, you can load and run JavaScript objects:
 24 | 
 25 | 	> object add javascript
 26 | 		var a = args[0];
 27 | 		var b = args[1];
 28 | 		return parseInt(a) + parseInt(b);
 29 | 	< object
 30 | 
 31 | 	> object setname javascript
 32 | 		// Set the user's name via JavaScript
 33 | 		var uid = rs.CurrentUser();
 34 | 		rs.SetUservar(uid, args[0], args[1])
 35 | 	< object
 36 | 
 37 | 	+ add # and #
 38 | 	-  +  = add  
 39 | 
 40 | 	+ my name is *
 41 | 	- I will remember that.setname  
 42 | 
 43 | 	+ what is my name
 44 | 	- You are .
 45 | 
 46 | [1]: https://github.com/robertkrimen/otto
 47 | */
 48 | package javascript
 49 | 
 50 | import (
 51 | 	"fmt"
 52 | 	"strings"
 53 | 
 54 | 	"github.com/aichaos/rivescript-go"
 55 | 	"github.com/dop251/goja"
 56 | )
 57 | 
 58 | type JavaScriptHandler struct {
 59 | 	VM        *goja.Runtime
 60 | 	bot       *rivescript.RiveScript
 61 | 	functions map[string]string
 62 | }
 63 | 
 64 | // New creates an object handler for JavaScript with its own Otto VM.
 65 | func New(rs *rivescript.RiveScript) *JavaScriptHandler {
 66 | 	js := new(JavaScriptHandler)
 67 | 	js.VM = goja.New()
 68 | 	js.bot = rs
 69 | 	js.functions = map[string]string{}
 70 | 
 71 | 	return js
 72 | }
 73 | 
 74 | // Load loads a new JavaScript object macro into the VM.
 75 | func (js JavaScriptHandler) Load(name string, code []string) {
 76 | 	// Create a unique function name called the same as the object macro name.
 77 | 	js.functions[name] = fmt.Sprintf(`
 78 | 		function object_%s(rs, args) {
 79 | 			%s
 80 | 		}
 81 | 	`, name, strings.Join(code, "\n"))
 82 | 
 83 | 	// Run this code to load the function into the VM.
 84 | 	js.VM.RunString(js.functions[name])
 85 | }
 86 | 
 87 | // Call executes a JavaScript macro and returns its results.
 88 | func (js JavaScriptHandler) Call(name string, fields []string) string {
 89 | 	// Make the RiveScript object available to the JS.
 90 | 	v := js.VM.ToValue(js.bot)
 91 | 
 92 | 	// Convert the fields into a JavaScript object.
 93 | 	jsFields := js.VM.ToValue(fields)
 94 | 
 95 | 	// Run the JS function call and get the result.
 96 | 	function, ok := goja.AssertFunction(js.VM.Get(fmt.Sprintf("object_%s", name)))
 97 | 	if !ok {
 98 | 		return fmt.Sprintf("[goja: error asserting function object_%s]", name)
 99 | 	}
100 | 
101 | 	result, err := function(goja.Undefined(), v, jsFields)
102 | 	if err != nil {
103 | 		fmt.Printf("Error: %s", err)
104 | 	}
105 | 
106 | 	reply := ""
107 | 	if !goja.IsUndefined(result) {
108 | 		reply = result.String()
109 | 	}
110 | 
111 | 	// Return it.
112 | 	return reply
113 | }
114 | 


--------------------------------------------------------------------------------
/loading.go:
--------------------------------------------------------------------------------
  1 | package rivescript
  2 | 
  3 | // Loading and Parsing Methods
  4 | 
  5 | import (
  6 | 	"bufio"
  7 | 	"fmt"
  8 | 	"os"
  9 | 	"path/filepath"
 10 | 	"strings"
 11 | )
 12 | 
 13 | /*
 14 | LoadFile loads a single RiveScript source file from disk.
 15 | 
 16 | Parameters
 17 | 
 18 | 	path: Path to a RiveScript source file.
 19 | */
 20 | func (rs *RiveScript) LoadFile(path string) error {
 21 | 	rs.say("Load RiveScript file: %s", path)
 22 | 
 23 | 	fh, err := os.Open(path)
 24 | 	if err != nil {
 25 | 		return fmt.Errorf("failed to open file %s: %s", path, err)
 26 | 	}
 27 | 
 28 | 	defer fh.Close()
 29 | 	scanner := bufio.NewScanner(fh)
 30 | 	scanner.Split(bufio.ScanLines)
 31 | 
 32 | 	var lines []string
 33 | 	for scanner.Scan() {
 34 | 		lines = append(lines, scanner.Text())
 35 | 	}
 36 | 
 37 | 	return rs.parse(path, lines)
 38 | }
 39 | 
 40 | /*
 41 | LoadDirectory loads multiple RiveScript documents from a folder on disk.
 42 | 
 43 | Parameters
 44 | 
 45 | 	path: Path to the directory on disk
 46 | 	extensions...: List of file extensions to filter on, default is
 47 | 	               '.rive' and '.rs'
 48 | */
 49 | func (rs *RiveScript) LoadDirectory(path string, extensions ...string) error {
 50 | 	if len(extensions) == 0 {
 51 | 		extensions = []string{".rive", ".rs"}
 52 | 	}
 53 | 
 54 | 	files, err := filepath.Glob(fmt.Sprintf("%s/*", path))
 55 | 	if err != nil {
 56 | 		return fmt.Errorf("failed to open folder %s: %s", path, err)
 57 | 	}
 58 | 
 59 | 	// No files matched?
 60 | 	if len(files) == 0 {
 61 | 		return fmt.Errorf("no RiveScript source files were found in %s", path)
 62 | 	}
 63 | 
 64 | 	var anyValid bool
 65 | 	for _, f := range files {
 66 | 		// Restrict file extensions.
 67 | 		validExtension := false
 68 | 		for _, exten := range extensions {
 69 | 			if strings.HasSuffix(f, exten) {
 70 | 				validExtension = true
 71 | 				break
 72 | 			}
 73 | 		}
 74 | 
 75 | 		if validExtension {
 76 | 			anyValid = true
 77 | 			err := rs.LoadFile(f)
 78 | 			if err != nil {
 79 | 				return err
 80 | 			}
 81 | 		}
 82 | 	}
 83 | 
 84 | 	if !anyValid {
 85 | 		return fmt.Errorf("no RiveScript source files were found in %s", path)
 86 | 	}
 87 | 
 88 | 	return nil
 89 | }
 90 | 
 91 | /*
 92 | Stream loads RiveScript code from a text buffer.
 93 | 
 94 | Parameters
 95 | 
 96 | 	code: Raw source code of a RiveScript document, with line breaks after
 97 | 	      each line.
 98 | */
 99 | func (rs *RiveScript) Stream(code string) error {
100 | 	lines := strings.Split(code, "\n")
101 | 	return rs.parse("Stream()", lines)
102 | }
103 | 


--------------------------------------------------------------------------------
/macro/macros.go:
--------------------------------------------------------------------------------
 1 | // Package macros exports types relevant to object macros.
 2 | package macro
 3 | 
 4 | // MacroInterface is the interface for a Go object macro handler.
 5 | //
 6 | // Here, "object macro handler" means Go code is handling object macros for a
 7 | // foreign programming language, for example JavaScript.
 8 | type MacroInterface interface {
 9 | 	Load(name string, code []string)
10 | 	Call(name string, fields []string) string
11 | }
12 | 


--------------------------------------------------------------------------------
/macro_test.go:
--------------------------------------------------------------------------------
 1 | package rivescript_test
 2 | 
 3 | // This test file contains the unit tests that had to be segregated from the
 4 | // others in the `src/` package.
 5 | //
 6 | // The only one here so far is an object macro test. It needed to use the public
 7 | // RiveScript API because the JavaScript handler expects an object of that type,
 8 | // and so it couldn't be in the `src/` package or it would create a dependency
 9 | // cycle.
10 | 
11 | import (
12 | 	"testing"
13 | 
14 | 	rivescript "github.com/aichaos/rivescript-go"
15 | 	"github.com/aichaos/rivescript-go/lang/javascript"
16 | )
17 | 
18 | // This one has to test the public interface because of the JavaScript handler
19 | // expecting a *RiveScript of the correct color.
20 | func TestJavaScript(t *testing.T) {
21 | 	rs := rivescript.New(nil)
22 | 	rs.SetHandler("javascript", javascript.New(rs))
23 | 	rs.Stream(`
24 | 		> object reverse javascript
25 | 			var msg = args.join(" ");
26 | 			return msg.split("").reverse().join("");
27 | 		< object
28 | 
29 | 		> object nolang
30 | 			return "No language provided!"
31 | 		< object
32 | 
33 | 		+ reverse *
34 | 		- reverse 
35 | 
36 | 		+ no lang
37 | 		- nolang
38 | 	`)
39 | 	rs.SortReplies()
40 | 
41 | 	// Helper function to assert replies via the public interface.
42 | 	assert := func(input, expected string) {
43 | 		reply, err := rs.Reply("local-user", input)
44 | 		if err != nil {
45 | 			t.Errorf("Got error when trying to get a reply: %v", err)
46 | 		} else if reply != expected {
47 | 			t.Errorf("Got unexpected reply. Expected %s, got %s", expected, reply)
48 | 		}
49 | 	}
50 | 
51 | 	assert("reverse hello world", "dlrow olleh")
52 | 	assert("no lang", "[ERR: Object Not Found]")
53 | 
54 | 	// Disable support.
55 | 	rs.RemoveHandler("javascript")
56 | 	assert("reverse hello world", "[ERR: Object Not Found]")
57 | }
58 | 


--------------------------------------------------------------------------------
/parser.go:
--------------------------------------------------------------------------------
  1 | package rivescript
  2 | 
  3 | // parse loads the RiveScript code into the bot's memory.
  4 | func (rs *RiveScript) parse(path string, lines []string) error {
  5 | 	rs.say("Parsing code...")
  6 | 
  7 | 	// Get the abstract syntax tree of this file.
  8 | 	AST, err := rs.parser.Parse(path, lines)
  9 | 	if err != nil {
 10 | 		return err
 11 | 	}
 12 | 
 13 | 	// Get all of the "begin" type variables
 14 | 	for k, v := range AST.Begin.Global {
 15 | 		if v == UNDEFTAG {
 16 | 			delete(rs.global, k)
 17 | 		} else {
 18 | 			rs.global[k] = v
 19 | 		}
 20 | 	}
 21 | 	for k, v := range AST.Begin.Var {
 22 | 		if v == UNDEFTAG {
 23 | 			delete(rs.vars, k)
 24 | 		} else {
 25 | 			rs.vars[k] = v
 26 | 		}
 27 | 	}
 28 | 	for k, v := range AST.Begin.Sub {
 29 | 		if v == UNDEFTAG {
 30 | 			delete(rs.sub, k)
 31 | 		} else {
 32 | 			rs.sub[k] = v
 33 | 		}
 34 | 	}
 35 | 	for k, v := range AST.Begin.Person {
 36 | 		if v == UNDEFTAG {
 37 | 			delete(rs.person, k)
 38 | 		} else {
 39 | 			rs.person[k] = v
 40 | 		}
 41 | 	}
 42 | 	for k, v := range AST.Begin.Array {
 43 | 		rs.array[k] = v
 44 | 	}
 45 | 
 46 | 	// Consume all the parsed triggers.
 47 | 	for topic, data := range AST.Topics {
 48 | 		// Keep a map of the topics that are included/inherited under this topic.
 49 | 		if _, ok := rs.includes[topic]; !ok {
 50 | 			rs.includes[topic] = map[string]bool{}
 51 | 		}
 52 | 		if _, ok := rs.inherits[topic]; !ok {
 53 | 			rs.inherits[topic] = map[string]bool{}
 54 | 		}
 55 | 
 56 | 		// Merge in the topic inclusions/inherits.
 57 | 		for included := range data.Includes {
 58 | 			rs.includes[topic][included] = true
 59 | 		}
 60 | 		for inherited := range data.Inherits {
 61 | 			rs.inherits[topic][inherited] = true
 62 | 		}
 63 | 
 64 | 		// Initialize the topic structure.
 65 | 		if _, ok := rs.topics[topic]; !ok {
 66 | 			rs.topics[topic] = new(astTopic)
 67 | 			rs.topics[topic].triggers = []*astTrigger{}
 68 | 		}
 69 | 
 70 | 		// Consume the AST triggers into the brain.
 71 | 		for _, trig := range data.Triggers {
 72 | 			// Convert this AST trigger into an internal astmap trigger.
 73 | 			foundtrigger := false
 74 | 			for _, previous := range rs.topics[topic].triggers {
 75 | 				if previous.trigger == trig.Trigger && previous.previous == trig.Previous {
 76 | 					previous.redirect = trig.Redirect
 77 | 					foundtrigger = true
 78 | 					for _, cond := range trig.Condition {
 79 | 						foundcond := false
 80 | 						for _, oldcond := range previous.condition {
 81 | 							if oldcond == cond {
 82 | 								foundcond = true
 83 | 								break
 84 | 							}
 85 | 						}
 86 | 						if !foundcond {
 87 | 							previous.condition = append(previous.condition, cond)
 88 | 						}
 89 | 					}
 90 | 					for _, reply := range trig.Reply {
 91 | 						newreply := true
 92 | 						for _, oldreply := range previous.reply {
 93 | 							if oldreply == reply {
 94 | 								newreply = false
 95 | 								break
 96 | 							}
 97 | 						}
 98 | 						if newreply {
 99 | 							previous.reply = append(previous.reply, reply)
100 | 						}
101 | 					}
102 | 					rs.say("Found previous trigger: %s == %s", trig.Trigger, previous.trigger)
103 | 				}
104 | 			}
105 | 			if !foundtrigger {
106 | 				trigger := new(astTrigger)
107 | 				trigger.trigger = trig.Trigger
108 | 				trigger.reply = trig.Reply
109 | 				trigger.condition = trig.Condition
110 | 				trigger.redirect = trig.Redirect
111 | 				trigger.previous = trig.Previous
112 | 
113 | 				rs.topics[topic].triggers = append(rs.topics[topic].triggers, trigger)
114 | 			}
115 | 		}
116 | 	}
117 | 
118 | 	// Load all the parsed objects.
119 | 	for _, object := range AST.Objects {
120 | 		// Have a language handler for this?
121 | 		if _, ok := rs.handlers[object.Language]; ok {
122 | 			rs.say("Loading object macro %s (%s)", object.Name, object.Language)
123 | 			rs.handlers[object.Language].Load(object.Name, object.Code)
124 | 			rs.objlangs[object.Name] = object.Language
125 | 		}
126 | 	}
127 | 
128 | 	return nil
129 | }
130 | 


--------------------------------------------------------------------------------
/regexp.go:
--------------------------------------------------------------------------------
 1 | package rivescript
 2 | 
 3 | import "regexp"
 4 | 
 5 | // Commonly used regular expressions.
 6 | var (
 7 | 	reWeight        = regexp.MustCompile(`\s*\{weight=(\d+)\}\s*`)
 8 | 	reInherits      = regexp.MustCompile(`\{inherits=(\d+)\}`)
 9 | 	reMeta          = regexp.MustCompile(`[\<>]+`)
10 | 	reSymbols       = regexp.MustCompile(`[.?,!;:@#$%^&*()]+`)
11 | 	reNasties       = regexp.MustCompile(`[^A-Za-z0-9 ]`)
12 | 	reZerowidthstar = regexp.MustCompile(`^\*$`)
13 | 	reOptional      = regexp.MustCompile(`\[(.+?)\]`)
14 | 	reArray         = regexp.MustCompile(`@(.+?)\b`)
15 | 	reReplyArray    = regexp.MustCompile(`\(@([A-Za-z0-9_]+)\)`)
16 | 	reBotvars       = regexp.MustCompile(``)
17 | 	reUservars      = regexp.MustCompile(``)
18 | 	reRandom        = regexp.MustCompile(`\{random\}(.+?)\{/random\}`)
19 | 
20 | 	// Self-contained tags like  that contain no nested tag.
21 | 	reAnytag = regexp.MustCompile(`<([^<]+?)>`)
22 | 
23 | 	reTopic     = regexp.MustCompile(`\{topic=(.+?)\}`)
24 | 	reRedirect  = regexp.MustCompile(`\{@(.+?)\}`)
25 | 	reCall      = regexp.MustCompile(`(.+?)`)
26 | 	reCondition = regexp.MustCompile(`^(.+?)\s+(==|eq|!=|ne|<>|<|<=|>|>=)\s+(.*?)$`)
27 | 	reSet       = regexp.MustCompile(``)
28 | 
29 | 	// Placeholders used during substitutions.
30 | 	rePlaceholder = regexp.MustCompile(`\x00(\d+)\x00`)
31 | )
32 | 


--------------------------------------------------------------------------------
/rivescript.go:
--------------------------------------------------------------------------------
  1 | package rivescript
  2 | 
  3 | /*
  4 | 	NOTE: This module is a wrapper around the bulk of the actual source code
  5 | 	under the 'src/' subpackage. This gives multiple benefits such as keeping
  6 | 	the root of the git repo as tidy as possible (low number of source files)
  7 | 	and keeping the public facing, official API in one small place in the code.
  8 | 
  9 | 	Everything exported from the 'src' subpackage should not be used directly
 10 | 	by third party developers. A lot of the symbols from the src package must
 11 | 	be exported to get this wrapper program to work (and keep a nice looking
 12 | 	module import path), but only this public facing API module should be used.
 13 | */
 14 | 
 15 | import (
 16 | 	"math/rand"
 17 | 	"regexp"
 18 | 	"sync"
 19 | 	"time"
 20 | 
 21 | 	"github.com/aichaos/rivescript-go/macro"
 22 | 	"github.com/aichaos/rivescript-go/parser"
 23 | 	"github.com/aichaos/rivescript-go/sessions"
 24 | 	"github.com/aichaos/rivescript-go/sessions/memory"
 25 | )
 26 | 
 27 | // Version number for the RiveScript library.
 28 | const Version = "0.4.0"
 29 | 
 30 | // RiveScript is the bot instance.
 31 | type RiveScript struct {
 32 | 	// Parameters
 33 | 	Debug              bool // Debug mode
 34 | 	Strict             bool // Strictly enforce RiveScript syntax
 35 | 	Depth              uint // Max depth for recursion
 36 | 	UTF8               bool // Support UTF-8 RiveScript code
 37 | 	CaseSensitive      bool // Preserve casing on incoming user messages
 38 | 	Quiet              bool // Suppress all warnings from being emitted
 39 | 	UnicodePunctuation *regexp.Regexp
 40 | 
 41 | 	// Internal helpers
 42 | 	parser *parser.Parser
 43 | 
 44 | 	// Internal data structures
 45 | 	cLock       sync.Mutex                      // Lock for config variables.
 46 | 	global      map[string]string               // 'global' variables
 47 | 	vars        map[string]string               // 'var' bot variables
 48 | 	sub         map[string]string               // 'sub' substitutions
 49 | 	person      map[string]string               // 'person' substitutions
 50 | 	array       map[string][]string             // 'array'
 51 | 	sessions    sessions.SessionManager         // user variable session manager
 52 | 	includes    map[string]map[string]bool      // included topics
 53 | 	inherits    map[string]map[string]bool      // inherited topics
 54 | 	objlangs    map[string]string               // object macro languages
 55 | 	handlers    map[string]macro.MacroInterface // object language handlers
 56 | 	subroutines map[string]Subroutine           // Golang object handlers
 57 | 	topics      map[string]*astTopic            // main topic structure
 58 | 	sorted      *sortBuffer                     // Sorted data from SortReplies()
 59 | 
 60 | 	// The random number god.
 61 | 	random     rand.Source
 62 | 	rng        *rand.Rand
 63 | 	randomLock sync.Mutex
 64 | 
 65 | 	// State information.
 66 | 	inReplyContext bool
 67 | 	currentUser    string
 68 | }
 69 | 
 70 | /*
 71 | New creates a new RiveScript instance.
 72 | 
 73 | A RiveScript instance represents one chat bot personality; it has its own
 74 | replies and its own memory of user data. You could make multiple bots in the
 75 | same program, each with its own replies loaded from different sources.
 76 | */
 77 | func New(cfg *Config) *RiveScript {
 78 | 	// If no config was given, default to the BasicConfig.
 79 | 	if cfg == nil {
 80 | 		cfg = &Config{
 81 | 			Strict: true,
 82 | 			Depth:  50,
 83 | 		}
 84 | 	}
 85 | 
 86 | 	// Sensible default config options.
 87 | 	if cfg.Depth == 0 {
 88 | 		cfg.Depth = 50
 89 | 	}
 90 | 	if cfg.SessionManager == nil {
 91 | 		cfg.SessionManager = memory.New()
 92 | 	}
 93 | 
 94 | 	// Random number seed.
 95 | 	var random rand.Source
 96 | 	if cfg.Seed != 0 {
 97 | 		random = rand.NewSource(cfg.Seed)
 98 | 	} else {
 99 | 		random = rand.NewSource(time.Now().UnixNano())
100 | 	}
101 | 
102 | 	rs := &RiveScript{
103 | 		// Set the default config objects that don't have good zero-values.
104 | 		Debug:         cfg.Debug,
105 | 		Strict:        cfg.Strict,
106 | 		Depth:         cfg.Depth,
107 | 		UTF8:          cfg.UTF8,
108 | 		CaseSensitive: cfg.CaseSensitive,
109 | 		sessions:      cfg.SessionManager,
110 | 
111 | 		// Default punctuation that gets removed from messages in UTF-8 mode.
112 | 		UnicodePunctuation: regexp.MustCompile(`[.,!?;:]`),
113 | 
114 | 		// Initialize all internal data structures.
115 | 		global:      map[string]string{},
116 | 		vars:        map[string]string{},
117 | 		sub:         map[string]string{},
118 | 		person:      map[string]string{},
119 | 		array:       map[string][]string{},
120 | 		includes:    map[string]map[string]bool{},
121 | 		inherits:    map[string]map[string]bool{},
122 | 		objlangs:    map[string]string{},
123 | 		handlers:    map[string]macro.MacroInterface{},
124 | 		subroutines: map[string]Subroutine{},
125 | 		topics:      map[string]*astTopic{},
126 | 		sorted:      new(sortBuffer),
127 | 
128 | 		random: random,
129 | 		rng:    rand.New(random),
130 | 	}
131 | 
132 | 	// Helper modules.
133 | 	rs.parser = parser.New(parser.ParserConfig{
134 | 		Strict:  cfg.Strict,
135 | 		UTF8:    cfg.UTF8,
136 | 		OnDebug: rs.say,
137 | 		OnWarn:  rs.warnSyntax,
138 | 	})
139 | 
140 | 	return rs
141 | }
142 | 
143 | // Forms of undefined.
144 | const (
145 | 	// UNDEFINED is the text "undefined", the default text for variable getters.
146 | 	UNDEFINED = "undefined"
147 | 
148 | 	// UNDEFTAG is the "" tag for unsetting variables in !Definitions.
149 | 	UNDEFTAG = ""
150 | )
151 | 
152 | // Subroutine is a function prototype for defining custom object macros in Go.
153 | type Subroutine func(*RiveScript, []string) string
154 | 
155 | // SetUnicodePunctuation allows you to override the text of the unicode
156 | // punctuation regexp. Provide a string literal that will validate in
157 | // `regexp.MustCompile()`
158 | func (rs *RiveScript) SetUnicodePunctuation(value string) {
159 | 	rs.UnicodePunctuation = regexp.MustCompile(value)
160 | }
161 | 


--------------------------------------------------------------------------------
/rsts_test.go:
--------------------------------------------------------------------------------
  1 | // RiveScript Test Suite: Go Test Runner
  2 | package rivescript
  3 | 
  4 | import (
  5 | 	"fmt"
  6 | 	"io/ioutil"
  7 | 	"log"
  8 | 	"path/filepath"
  9 | 	"reflect"
 10 | 	"strings"
 11 | 	"testing"
 12 | 
 13 | 	yaml "gopkg.in/yaml.v2"
 14 | )
 15 | 
 16 | // TestCase wraps each RiveScript test.
 17 | type TestCase struct {
 18 | 	T        *testing.T
 19 | 	file     string
 20 | 	name     string
 21 | 	username string
 22 | 	rs       *RiveScript
 23 | 	steps    []TestStep
 24 | }
 25 | 
 26 | // RootSchema is the root of the YAML structure.
 27 | type RootSchema map[string]TestSchema
 28 | 
 29 | // TestSchema describes the YAML test files.
 30 | type TestSchema struct {
 31 | 	Username string `yaml:"username"`
 32 | 	UTF8     bool   `yaml:"utf8"`
 33 | 	Debug    bool   `yaml:"debug"`
 34 | 	Tests    []TestStep
 35 | }
 36 | 
 37 | // TestStep describes the YAML structure for the actual tests.
 38 | type TestStep struct {
 39 | 	Source string            `yaml:"source"`
 40 | 	Input  string            `yaml:"input"`
 41 | 	Reply  interface{}       `yaml:"reply"`
 42 | 	Assert map[string]string `yaml:"assert"`
 43 | 	Set    map[string]string `yaml:"set"`
 44 | }
 45 | 
 46 | // NewTestCase initializes a new test.
 47 | func NewTestCase(t *testing.T, file, name string, opts TestSchema) *TestCase {
 48 | 	username := opts.Username
 49 | 	if username == "" {
 50 | 		username = "localuser"
 51 | 	}
 52 | 
 53 | 	return &TestCase{
 54 | 		T:        t,
 55 | 		file:     file,
 56 | 		name:     name,
 57 | 		username: username,
 58 | 		rs: New(&Config{
 59 | 			Debug: opts.Debug,
 60 | 			UTF8:  opts.UTF8,
 61 | 		}),
 62 | 		steps: opts.Tests,
 63 | 	}
 64 | }
 65 | 
 66 | // Run steps through the test cases and runs them.
 67 | func (t *TestCase) Run() {
 68 | 	var hasErrors bool
 69 | 	for _, step := range t.steps {
 70 | 		var err error
 71 | 
 72 | 		if step.Source != "" {
 73 | 			t.source(step)
 74 | 		} else if step.Input != "" {
 75 | 			err = t.input(step)
 76 | 		} else if len(step.Set) > 0 {
 77 | 			t.set(step)
 78 | 		} else if len(step.Assert) > 0 {
 79 | 			err = t.get(step)
 80 | 		} else {
 81 | 			log.Printf("Unsupported test step")
 82 | 		}
 83 | 
 84 | 		if err != nil {
 85 | 			t.fail(err)
 86 | 			hasErrors = true
 87 | 			break
 88 | 		}
 89 | 	}
 90 | 
 91 | 	var sym string
 92 | 	if hasErrors {
 93 | 		sym = `❌`
 94 | 	} else {
 95 | 		sym = `✓`
 96 | 	}
 97 | 	fmt.Printf("%s %s#%s\n", sym, t.file, t.name)
 98 | }
 99 | 
100 | // source handles a `source` step, which parses RiveScript code.
101 | func (t *TestCase) source(step TestStep) {
102 | 	t.rs.Stream(step.Source)
103 | 	t.rs.SortReplies()
104 | }
105 | 
106 | // input handles an `input` step, which tests the brain for a reply.
107 | func (t *TestCase) input(step TestStep) error {
108 | 	reply, err := t.rs.Reply(t.username, step.Input)
109 | 	if err != nil {
110 | 		return t.expectedError(step, reply, err)
111 | 	}
112 | 
113 | 	// Random replies?
114 | 	if expect, ok := step.Reply.([]interface{}); ok {
115 | 		pass := false
116 | 		for _, candidate := range expect {
117 | 			cmp, ok := candidate.(string)
118 | 			if !ok {
119 | 				return fmt.Errorf(
120 | 					"Error",
121 | 				)
122 | 			}
123 | 			if cmp == reply {
124 | 				pass = true
125 | 				break
126 | 			}
127 | 		}
128 | 
129 | 		if !pass {
130 | 			return fmt.Errorf(
131 | 				"Did not get expected reply for input: %s\n"+
132 | 					"Expected one of: %v\n"+
133 | 					"            Got: %s",
134 | 				step.Input,
135 | 				expect,
136 | 				reply,
137 | 			)
138 | 		}
139 | 	} else if expect, ok := step.Reply.(string); ok {
140 | 		if reply != strings.TrimSpace(expect) {
141 | 			return fmt.Errorf(
142 | 				"Did not get expected reply for input: %s\n"+
143 | 					"Expected: %s\n"+
144 | 					"     Got: %s",
145 | 				step.Input,
146 | 				expect,
147 | 				reply,
148 | 			)
149 | 		}
150 | 	} else {
151 | 		return fmt.Errorf(
152 | 			"YAML error: `reply` was neither a `string` nor a `[]string` "+
153 | 				"at %s test %s (input %s); reply was: '%v' (type %s)",
154 | 			t.file,
155 | 			t.name,
156 | 			step.Input,
157 | 			step.Reply,
158 | 			reflect.TypeOf(step.Reply),
159 | 		)
160 | 	}
161 | 
162 | 	return nil
163 | }
164 | 
165 | // expectedError inspects a Reply() error to see if it was expected by the test.
166 | func (t *TestCase) expectedError(step TestStep, reply string, err error) error {
167 | 	// Map of expected errors to their string counterpart from the test file.
168 | 	goodErrors := map[string]error{
169 | 		"ERR: No Reply Matched": ErrNoTriggerMatched,
170 | 	}
171 | 
172 | 	if expect, ok := goodErrors[step.Reply.(string)]; ok {
173 | 		if err == expect {
174 | 			return nil
175 | 		}
176 | 	}
177 | 
178 | 	return fmt.Errorf(
179 | 		"Got unexpected error from Reply (input step: %s; expected: %v): %s",
180 | 		step.Input,
181 | 		step.Reply,
182 | 		err,
183 | 	)
184 | }
185 | 
186 | // set handles a `set` step, which sets user variables.
187 | func (t *TestCase) set(step TestStep) {
188 | 	for key, value := range step.Set {
189 | 		t.rs.SetUservar(t.username, key, value)
190 | 	}
191 | }
192 | 
193 | // get handles an `assert` step, which tests user variables.
194 | func (t *TestCase) get(step TestStep) error {
195 | 	for key, expect := range step.Assert {
196 | 		value, err := t.rs.GetUservar(t.username, key)
197 | 		if err != nil {
198 | 			return err
199 | 		}
200 | 		if value != expect {
201 | 			return fmt.Errorf(
202 | 				"Did not get expected user variable: %s\n"+
203 | 					"Expected: %s\n"+
204 | 					"     Got: %s",
205 | 				key,
206 | 				expect,
207 | 				value,
208 | 			)
209 | 		}
210 | 	}
211 | 
212 | 	return nil
213 | }
214 | 
215 | // fail handles a failed test.
216 | func (t *TestCase) fail(err error) {
217 | 	banner := fmt.Sprintf("Failed: %s#%s", t.file, t.name)
218 | 	t.T.Errorf("%s\n%s",
219 | 		banner,
220 | 		err,
221 | 	)
222 | }
223 | 
224 | func TestRiveScript(t *testing.T) {
225 | 	tests, err := filepath.Glob("./rsts/tests/*.yml")
226 | 	if err != nil {
227 | 		panic(err)
228 | 	}
229 | 
230 | 	for _, filename := range tests {
231 | 		yamlSource, err := ioutil.ReadFile(filename)
232 | 		if err != nil {
233 | 			panic(err)
234 | 		}
235 | 
236 | 		data := RootSchema{}
237 | 		yaml.Unmarshal(yamlSource, &data)
238 | 
239 | 		for name, opts := range data {
240 | 			test := NewTestCase(t, filename, name, opts)
241 | 			test.Run()
242 | 		}
243 | 	}
244 | }
245 | 


--------------------------------------------------------------------------------
/sessions/interface.go:
--------------------------------------------------------------------------------
  1 | // Package sessions provides the interface and default session store for
  2 | // RiveScript.
  3 | package sessions
  4 | 
  5 | /*
  6 | Interface SessionManager describes a session manager for user variables
  7 | in RiveScript.
  8 | 
  9 | The session manager keeps track of getting and setting user variables,
 10 | for example when the `` or `` tags are used in RiveScript
 11 | or when API functions like `SetUservar()` are called.
 12 | 
 13 | By default RiveScript stores user sessions in memory and provides methods
 14 | to export and import them (e.g. to persist them when the bot shuts down
 15 | so they can be reloaded). If you'd prefer a more 'active' session storage,
 16 | for example one that puts user variables into a database or cache, you can
 17 | create your own session manager that implements this interface.
 18 | */
 19 | type SessionManager interface {
 20 | 	// Init makes sure a username has a session (creates one if not). It returns
 21 | 	// the pointer to the user data in either case.
 22 | 	Init(username string) *UserData
 23 | 
 24 | 	// Set user variables from a map.
 25 | 	Set(username string, vars map[string]string)
 26 | 
 27 | 	// AddHistory adds input and reply to the user's history.
 28 | 	AddHistory(username, input, reply string)
 29 | 
 30 | 	// SetLastMatch sets the last matched trigger.
 31 | 	SetLastMatch(username, trigger string)
 32 | 
 33 | 	// Get a user variable.
 34 | 	Get(username string, key string) (string, error)
 35 | 
 36 | 	// Get all variables for a user.
 37 | 	GetAny(username string) (*UserData, error)
 38 | 
 39 | 	// Get all variables about all users.
 40 | 	GetAll() map[string]*UserData
 41 | 
 42 | 	// GetLastMatch returns the last trigger the user matched.
 43 | 	GetLastMatch(username string) (string, error)
 44 | 
 45 | 	// GetHistory returns the user's history.
 46 | 	GetHistory(username string) (*History, error)
 47 | 
 48 | 	// Clear all variables for a given user.
 49 | 	Clear(username string)
 50 | 
 51 | 	// Clear all variables for all users.
 52 | 	ClearAll()
 53 | 
 54 | 	// Freeze makes a snapshot of a user's variables.
 55 | 	Freeze(string) error
 56 | 
 57 | 	// Thaw unfreezes a snapshot of a user's variables and returns an error
 58 | 	// if the user had no frozen variables.
 59 | 	Thaw(username string, ThawAction ThawAction) error
 60 | }
 61 | 
 62 | // HistorySize is the number of entries stored in the history.
 63 | const HistorySize int = 9
 64 | 
 65 | // UserData is a container for user variables.
 66 | type UserData struct {
 67 | 	Variables map[string]string `json:"vars"`
 68 | 	LastMatch string            `json:"lastMatch"`
 69 | 	*History  `json:"history"`
 70 | }
 71 | 
 72 | // History keeps track of recent input and reply history.
 73 | type History struct {
 74 | 	Input []string `json:"input"`
 75 | 	Reply []string `json:"reply"`
 76 | }
 77 | 
 78 | // NewHistory creates a new History object with the history arrays filled out.
 79 | func NewHistory() *History {
 80 | 	h := &History{
 81 | 		Input: []string{},
 82 | 		Reply: []string{},
 83 | 	}
 84 | 
 85 | 	for i := 0; i < HistorySize; i++ {
 86 | 		h.Input = append(h.Input, "undefined")
 87 | 		h.Reply = append(h.Reply, "undefined")
 88 | 	}
 89 | 
 90 | 	return h
 91 | }
 92 | 
 93 | // Type ThawAction describes the action for the `Thaw()` method.
 94 | type ThawAction int
 95 | 
 96 | // Valid options for ThawAction.
 97 | const (
 98 | 	// Thaw means to restore the user variables and erase the frozen copy.
 99 | 	Thaw = iota
100 | 
101 | 	// Discard means to cancel the frozen copy and not restore them.
102 | 	Discard
103 | 
104 | 	// Keep means to restore the user variables and still keep the frozen copy.
105 | 	Keep
106 | )
107 | 


--------------------------------------------------------------------------------
/sessions/memory/memory.go:
--------------------------------------------------------------------------------
  1 | // Package memory provides the default in-memory session store.
  2 | package memory
  3 | 
  4 | import (
  5 | 	"fmt"
  6 | 	"strings"
  7 | 	"sync"
  8 | 
  9 | 	"github.com/aichaos/rivescript-go/sessions"
 10 | )
 11 | 
 12 | // Type MemoryStore implements the default in-memory session store for
 13 | // RiveScript.
 14 | type MemoryStore struct {
 15 | 	lock   sync.Mutex
 16 | 	users  map[string]*sessions.UserData
 17 | 	frozen map[string]*sessions.UserData
 18 | }
 19 | 
 20 | // New creates a new MemoryStore.
 21 | func New() *MemoryStore {
 22 | 	return &MemoryStore{
 23 | 		users:  map[string]*sessions.UserData{},
 24 | 		frozen: map[string]*sessions.UserData{},
 25 | 	}
 26 | }
 27 | 
 28 | // init makes sure a username exists in the memory store.
 29 | func (s *MemoryStore) Init(username string) *sessions.UserData {
 30 | 	s.lock.Lock()
 31 | 	defer s.lock.Unlock()
 32 | 
 33 | 	if _, ok := s.users[username]; !ok {
 34 | 		s.users[username] = defaultSession()
 35 | 	}
 36 | 	return s.users[username]
 37 | }
 38 | 
 39 | // Set a user variable.
 40 | func (s *MemoryStore) Set(username string, vars map[string]string) {
 41 | 	s.Init(username)
 42 | 	s.lock.Lock()
 43 | 	defer s.lock.Unlock()
 44 | 
 45 | 	for k, v := range vars {
 46 | 		s.users[username].Variables[k] = v
 47 | 	}
 48 | }
 49 | 
 50 | // AddHistory adds history items.
 51 | func (s *MemoryStore) AddHistory(username, input, reply string) {
 52 | 	data := s.Init(username)
 53 | 	s.lock.Lock()
 54 | 	defer s.lock.Unlock()
 55 | 
 56 | 	data.History.Input = data.History.Input[:len(data.History.Input)-1]                    // Pop
 57 | 	data.History.Input = append([]string{strings.TrimSpace(input)}, data.History.Input...) // Unshift
 58 | 	data.History.Reply = data.History.Reply[:len(data.History.Reply)-1]                    // Pop
 59 | 	data.History.Reply = append([]string{strings.TrimSpace(reply)}, data.History.Reply...) // Unshift
 60 | }
 61 | 
 62 | // SetLastMatch sets the user's last matched trigger.
 63 | func (s *MemoryStore) SetLastMatch(username, trigger string) {
 64 | 	data := s.Init(username)
 65 | 	s.lock.Lock()
 66 | 	defer s.lock.Unlock()
 67 | 	data.LastMatch = trigger
 68 | }
 69 | 
 70 | // Get a user variable.
 71 | func (s *MemoryStore) Get(username, name string) (string, error) {
 72 | 	s.lock.Lock()
 73 | 	defer s.lock.Unlock()
 74 | 
 75 | 	if _, ok := s.users[username]; !ok {
 76 | 		return "", fmt.Errorf(`no data for username "%s"`, username)
 77 | 	}
 78 | 
 79 | 	value, ok := s.users[username].Variables[name]
 80 | 	if !ok {
 81 | 		return "", fmt.Errorf(`variable "%s" for user "%s" not set`, name, username)
 82 | 	}
 83 | 
 84 | 	return value, nil
 85 | }
 86 | 
 87 | // GetAny gets all variables for a user.
 88 | func (s *MemoryStore) GetAny(username string) (*sessions.UserData, error) {
 89 | 	s.lock.Lock()
 90 | 	defer s.lock.Unlock()
 91 | 
 92 | 	if _, ok := s.users[username]; !ok {
 93 | 		return &sessions.UserData{}, fmt.Errorf(`no data for username "%s"`, username)
 94 | 	}
 95 | 	return cloneUser(s.users[username]), nil
 96 | }
 97 | 
 98 | // GetAll gets all data for all users.
 99 | func (s *MemoryStore) GetAll() map[string]*sessions.UserData {
100 | 	s.lock.Lock()
101 | 	defer s.lock.Unlock()
102 | 
103 | 	// Make safe copies of all our structures.
104 | 	var result map[string]*sessions.UserData
105 | 	for k, v := range s.users {
106 | 		result[k] = cloneUser(v)
107 | 	}
108 | 	return result
109 | }
110 | 
111 | // GetLastMatch returns the last matched trigger for the user,
112 | func (s *MemoryStore) GetLastMatch(username string) (string, error) {
113 | 	s.lock.Lock()
114 | 	defer s.lock.Unlock()
115 | 
116 | 	data, ok := s.users[username]
117 | 	if !ok {
118 | 		return "", fmt.Errorf(`no data for username "%s"`, username)
119 | 	}
120 | 	return data.LastMatch, nil
121 | }
122 | 
123 | // GetHistory gets the user's history.
124 | func (s *MemoryStore) GetHistory(username string) (*sessions.History, error) {
125 | 	s.lock.Lock()
126 | 	defer s.lock.Unlock()
127 | 
128 | 	data, ok := s.users[username]
129 | 	if !ok {
130 | 		return nil, fmt.Errorf(`no data for username "%s"`, username)
131 | 	}
132 | 	return data.History, nil
133 | }
134 | 
135 | // Clear data for a user.
136 | func (s *MemoryStore) Clear(username string) {
137 | 	s.lock.Lock()
138 | 	defer s.lock.Unlock()
139 | 
140 | 	delete(s.users, username)
141 | }
142 | 
143 | // ClearAll resets all user data for all users.
144 | func (s *MemoryStore) ClearAll() {
145 | 	s.lock.Lock()
146 | 	defer s.lock.Unlock()
147 | 
148 | 	s.users = make(map[string]*sessions.UserData)
149 | 	s.frozen = make(map[string]*sessions.UserData)
150 | }
151 | 
152 | // Freeze makes a snapshot of user variables.
153 | func (s *MemoryStore) Freeze(username string) error {
154 | 	s.lock.Lock()
155 | 	defer s.lock.Unlock()
156 | 
157 | 	data, ok := s.users[username]
158 | 	if !ok {
159 | 		return fmt.Errorf(`no data for username %s`, username)
160 | 	}
161 | 
162 | 	s.frozen[username] = cloneUser(data)
163 | 	return nil
164 | }
165 | 
166 | // Thaw restores from a snapshot.
167 | func (s *MemoryStore) Thaw(username string, action sessions.ThawAction) error {
168 | 	s.lock.Lock()
169 | 	defer s.lock.Unlock()
170 | 
171 | 	frozen, ok := s.frozen[username]
172 | 	if !ok {
173 | 		return fmt.Errorf(`no frozen data for username "%s"`, username)
174 | 	}
175 | 
176 | 	if action == sessions.Thaw {
177 | 		s.users[username] = cloneUser(frozen)
178 | 		delete(s.frozen, username)
179 | 	} else if action == sessions.Discard {
180 | 		delete(s.frozen, username)
181 | 	} else if action == sessions.Keep {
182 | 		s.users[username] = cloneUser(frozen)
183 | 	}
184 | 
185 | 	return nil
186 | }
187 | 
188 | // cloneUser makes a safe clone of a UserData.
189 | func cloneUser(data *sessions.UserData) *sessions.UserData {
190 | 	new := defaultSession()
191 | 
192 | 	// Copy user variables.
193 | 	for k, v := range data.Variables {
194 | 		new.Variables[k] = v
195 | 	}
196 | 
197 | 	// Copy history.
198 | 	for i := 0; i < sessions.HistorySize; i++ {
199 | 		new.History.Input[i] = data.History.Input[i]
200 | 		new.History.Reply[i] = data.History.Reply[i]
201 | 	}
202 | 
203 | 	return new
204 | }
205 | 
206 | // defaultSession initializes the default session variables for a user.
207 | // This mostly just means the topic is set to "random" and structs
208 | // are initialized.
209 | func defaultSession() *sessions.UserData {
210 | 	return &sessions.UserData{
211 | 		Variables: map[string]string{
212 | 			"topic": "random",
213 | 		},
214 | 		LastMatch: "",
215 | 		History:   sessions.NewHistory(),
216 | 	}
217 | }
218 | 


--------------------------------------------------------------------------------
/sessions/null/null.go:
--------------------------------------------------------------------------------
 1 | // Package null provides a session manager that has no memory.
 2 | package null
 3 | 
 4 | import "github.com/aichaos/rivescript-go/sessions"
 5 | 
 6 | // Type NullStore implements a memory store that has no memory.
 7 | //
 8 | // It's mostly useful for the unit tests. With this memory store in place,
 9 | // RiveScript is unable to maintain any user variables at all.
10 | type NullStore struct{}
11 | 
12 | // New creates a new NullStore.
13 | func New() *NullStore {
14 | 	return new(NullStore)
15 | }
16 | 
17 | func (s *NullStore) Init(username string) *sessions.UserData {
18 | 	return nullSession()
19 | }
20 | 
21 | func (s *NullStore) Set(username string, vars map[string]string) {}
22 | 
23 | func (s *NullStore) AddHistory(username, input, reply string) {}
24 | 
25 | func (s *NullStore) SetLastMatch(username, trigger string) {}
26 | 
27 | func (s *NullStore) Get(username string, name string) (string, error) {
28 | 	return "undefined", nil
29 | }
30 | 
31 | func (s *NullStore) GetAny(username string) (*sessions.UserData, error) {
32 | 	return nullSession(), nil
33 | }
34 | 
35 | func (s *NullStore) GetAll() map[string]*sessions.UserData {
36 | 	return map[string]*sessions.UserData{}
37 | }
38 | 
39 | func (s *NullStore) GetLastMatch(username string) (string, error) {
40 | 	return "", nil
41 | }
42 | 
43 | func (s *NullStore) GetHistory(username string) (*sessions.History, error) {
44 | 	return sessions.NewHistory(), nil
45 | }
46 | 
47 | func (s *NullStore) Clear(username string) {}
48 | 
49 | func (s *NullStore) ClearAll() {}
50 | 
51 | func (s *NullStore) Freeze(username string) error {
52 | 	return nil
53 | }
54 | 
55 | func (s *NullStore) Thaw(username string, action sessions.ThawAction) error {
56 | 	return nil
57 | }
58 | 
59 | func nullSession() *sessions.UserData {
60 | 	return &sessions.UserData{
61 | 		Variables: map[string]string{},
62 | 		History:   sessions.NewHistory(),
63 | 		LastMatch: "",
64 | 	}
65 | }
66 | 


--------------------------------------------------------------------------------
/sessions/redis/README.md:
--------------------------------------------------------------------------------
 1 | # Redis Sessions for RiveScript
 2 | 
 3 | [![GoDoc](https://godoc.org/github.com/aichaos/rivescript-go/sessions/redis?status.svg)](https://godoc.org/github.com/aichaos/rivescript-go/sessions/redis)
 4 | 
 5 | This package provides support for using a [Redis cache](https://redis.io/) to
 6 | store user variables for RiveScript.
 7 | 
 8 | ```bash
 9 | go get github.com/aichaos/rivescript-go/sessions/redis
10 | ```
11 | 
12 | ## Quick Start
13 | 
14 | ```go
15 | package main
16 | 
17 | import (
18 |     "fmt"
19 | 
20 |     rivescript "github.com/aichaos/rivescript-go"
21 |     "github.com/aichaos/rivescript-go/sessions/redis"
22 |     goRedis "gopkg.in/redis.v5"
23 | )
24 | 
25 | func main() {
26 |     // Verbose example with ALL options spelled out. All the settings are
27 |     // optional, and their default values are shown here.
28 |     bot := rivescript.New(&rivescript.Config{
29 |         // Initialize the Redis session manager here.
30 |         SessionManager: redis.New(&redis.Config{
31 |             // The prefix is added before all the usernames in the Redis cache.
32 |             // For a username of 'alice' it would go into 'rivescript/alice'
33 |             Prefix: "rivescript/",
34 | 
35 |             // The prefix used to store 'frozen' copies of user variables. The
36 |             // default takes the form "frozen:" using your Prefix,
37 |             // so this field is doubly optional unless you wanna customize it.
38 |             FrozenPrefix: "frozen:rivescript/",
39 | 
40 |             // If you need to configure the underlying Redis instance, you can
41 |             // pass its options along here.
42 |             Redis: &goRedis.Options{
43 |                 Addr: "localhost:6379",
44 |                 DB:   0,
45 |             },
46 |         }),
47 |     })
48 | 
49 |     // A minimal version of the above that uses all the default options.
50 |     bot = rivescript.New(&rivescript.Config{
51 |         SessionManager: redis.New(nil),
52 |     })
53 | 
54 |     bot.LoadDirectory("eg/brain")
55 |     bot.SortReplies()
56 | 
57 |     // And go on as normal.
58 |     reply, err := bot.Reply("soandso", "hello bot")
59 |     if err != nil {
60 |         fmt.Printf("Error: %s\n", err)
61 |     } else {
62 |         fmt.Printf("Reply: %s\n", reply)
63 |     }
64 | }
65 | ```
66 | 
67 | ## Testing
68 | 
69 | Running these unit tests requires a local Redis server to be running. In the
70 | future I'll look into mocking the server.
71 | 
72 | ## License
73 | 
74 | Released under the same terms as RiveScript itself (MIT license).
75 | 


--------------------------------------------------------------------------------
/sessions/redis/extra.go:
--------------------------------------------------------------------------------
  1 | package redis
  2 | 
  3 | // NOTE: This file contains added functions above and beyond the SessionManager
  4 | // implementation.
  5 | 
  6 | import (
  7 | 	"encoding/json"
  8 | 	"fmt"
  9 | 
 10 | 	"github.com/aichaos/rivescript-go/sessions"
 11 | )
 12 | 
 13 | // key generates a key name to use in Redis.
 14 | func (s *Session) key(username string) string {
 15 | 	return s.prefix + username
 16 | }
 17 | 
 18 | // frozenKey generates the 'frozen' key name to use in Redis.
 19 | func (s *Session) frozenKey(username string) string {
 20 | 	return s.frozenPrefix + username
 21 | }
 22 | 
 23 | // getRedis gets a UserData out of the Redis cache.
 24 | func (s *Session) getRedis(username string) (*sessions.UserData, error) {
 25 | 	data, err := s.getRedisFrozen(username, false)
 26 | 	return data, err
 27 | }
 28 | 
 29 | // getRedisFrozen is the implementation behind getRedis and allows for the
 30 | // key to be overridden with the 'frozen' version.
 31 | func (s *Session) getRedisFrozen(username string, frozen bool) (*sessions.UserData, error) {
 32 | 	var key string
 33 | 	if frozen {
 34 | 		key = s.frozenKey(username)
 35 | 	} else {
 36 | 		key = s.key(username)
 37 | 	}
 38 | 
 39 | 	// Check Redis for the key.
 40 | 	value, err := s.client.Get(key).Result()
 41 | 	if err != nil {
 42 | 		return nil, fmt.Errorf(
 43 | 			`no data for username "%s": %s`,
 44 | 			username, err,
 45 | 		)
 46 | 	}
 47 | 
 48 | 	// Decode the JSON.
 49 | 	var user *sessions.UserData
 50 | 	err = json.Unmarshal([]byte(value), &user)
 51 | 	if err != nil {
 52 | 		return nil, fmt.Errorf(
 53 | 			`JSON unmarshal error for username "%s": %s`,
 54 | 			username, err,
 55 | 		)
 56 | 	}
 57 | 
 58 | 	return user, nil
 59 | }
 60 | 
 61 | // putRedis puts a UserData into the Redis cache.
 62 | func (s *Session) putRedis(username string, data *sessions.UserData) {
 63 | 	s.putRedisFrozen(username, data, false)
 64 | }
 65 | 
 66 | // putRedisFrozen is the implementation behind putRedis and allows for the
 67 | // key to be overridden with the 'frozen' version.
 68 | func (s *Session) putRedisFrozen(username string, data *sessions.UserData, frozen bool) error {
 69 | 	// Which key to use?
 70 | 	var key string
 71 | 	if frozen {
 72 | 		key = s.frozenKey(username)
 73 | 	} else {
 74 | 		key = s.key(username)
 75 | 	}
 76 | 
 77 | 	encoded, err := json.MarshalIndent(data, "", "\t")
 78 | 	if err != nil {
 79 | 		return err
 80 | 	}
 81 | 
 82 | 	err = s.client.Set(key, string(encoded), s.sessionTimeout).Err()
 83 | 	if err != nil {
 84 | 		return err
 85 | 	}
 86 | 
 87 | 	return nil
 88 | }
 89 | 
 90 | // defaultSession initializes the default session variables for a user.
 91 | func defaultSession() *sessions.UserData {
 92 | 	return &sessions.UserData{
 93 | 		Variables: map[string]string{
 94 | 			"topic": "random",
 95 | 		},
 96 | 		LastMatch: "",
 97 | 		History:   sessions.NewHistory(),
 98 | 	}
 99 | }
100 | 


--------------------------------------------------------------------------------
/sessions/redis/integration_test.go:
--------------------------------------------------------------------------------
 1 | package redis_test
 2 | 
 3 | import (
 4 | 	"testing"
 5 | 
 6 | 	rivescript "github.com/aichaos/rivescript-go"
 7 | 	"github.com/aichaos/rivescript-go/sessions"
 8 | 	"github.com/aichaos/rivescript-go/sessions/redis"
 9 | )
10 | 
11 | // This script tests the 'integration' of the RiveScript public API with the
12 | // RiveScript-Redis public API.
13 | 
14 | func TestIntegration(t *testing.T) {
15 | 	bot := rivescript.New(&rivescript.Config{
16 | 		SessionManager: redis.New(&redis.Config{
17 | 			Prefix: "rivescript:integration/",
18 | 		}),
19 | 	})
20 | 	bot.Stream(`
21 |         + hello bot
22 |         - Hello human.
23 | 
24 |         + my name is *
25 |         - >Nice to meet you, .
26 | 
27 |         + who am i
28 |         - Your name is .
29 | 
30 |         + i am # years old
31 |         - >I will remember you are  years old.
32 | 
33 |         + how old am i
34 |         - You are .
35 | 
36 |         + today is my birthday
37 |         - Happy birthday!
38 |     `)
39 | 	expectVar(t, bot, "alice", "name", "")
40 | 	bot.SortReplies()
41 | 
42 | 	// See if Redis can remember things.
43 | 	expectReply(t, bot, "alice", "my name is Alice", "Nice to meet you, Alice.")
44 | 	expectReply(t, bot, "alice", "I am 5 years old", "I will remember you are 5 years old.")
45 | 	expectVar(t, bot, "alice", "name", "Alice")
46 | 	expectVar(t, bot, "alice", "age", "5")
47 | 
48 | 	// Freeze variables and restore them.
49 | 	bot.FreezeUservars("alice")
50 | 	expectReply(t, bot, "alice", "Today is my birthday", "Happy birthday!")
51 | 	expectVar(t, bot, "alice", "age", "6")
52 | 	bot.ThawUservars("alice", sessions.Thaw)
53 | 	expectVar(t, bot, "alice", "age", "5")
54 | 
55 | 	// Clean up.
56 | 	bot.ClearAllUservars()
57 | }
58 | 
59 | func expectReply(t *testing.T, bot *rivescript.RiveScript, username, input, expected string) {
60 | 	reply, _ := bot.Reply(username, input)
61 | 	if reply != expected {
62 | 		t.Errorf(
63 | 			"got unexpected reply for: [%s] %s\n"+
64 | 				"expected: %s\n"+
65 | 				"     got: %s",
66 | 			username,
67 | 			input,
68 | 			expected,
69 | 			reply,
70 | 		)
71 | 	}
72 | }
73 | 
74 | func expectVar(t *testing.T, bot *rivescript.RiveScript, username, name, expected string) {
75 | 	value, _ := bot.GetUservar(username, name)
76 | 	if value != expected {
77 | 		t.Errorf(
78 | 			"got unexpected user variable for user '%s'\n"+
79 | 				"expected: '%s'='%s'\n"+
80 | 				"     got: '%s'",
81 | 			username,
82 | 			name,
83 | 			expected,
84 | 			value,
85 | 		)
86 | 	}
87 | }
88 | 


--------------------------------------------------------------------------------
/sessions/redis/internal_test.go:
--------------------------------------------------------------------------------
  1 | package redis
  2 | 
  3 | // This tests the internal interface of just the Redis specific bits,
  4 | // independent of RiveScript. At time of writing, this test file gets
  5 | // us to 93.4% coverage!
  6 | 
  7 | import (
  8 | 	"fmt"
  9 | 	"os"
 10 | 	"testing"
 11 | 
 12 | 	"github.com/aichaos/rivescript-go/sessions"
 13 | )
 14 | 
 15 | // newTest creates a new test environment with a new Redis prefix.
 16 | // The generated prefix takes the form: `redis_test::`
 17 | func newTest(name string) *Session {
 18 | 	s := New(&Config{
 19 | 		Prefix: fmt.Sprintf("rivescript:%d:%s/", os.Getpid(), name),
 20 | 	})
 21 | 	s.ClearAll()
 22 | 	return s
 23 | }
 24 | 
 25 | // tearDown deletes a Redis prefix after the test is done.
 26 | func tearDown(s *Session) {
 27 | 	s.ClearAll()
 28 | }
 29 | 
 30 | func TestRedis(t *testing.T) {
 31 | 	s := newTest("main")
 32 | 	defer tearDown(s)
 33 | 
 34 | 	// There should be no user data yet.
 35 | 	s.expectCount(t, 0)
 36 | 
 37 | 	// Create the first user.
 38 | 	{
 39 | 		username := "alice"
 40 | 
 41 | 		s.Set(username, map[string]string{
 42 | 			"name": "Alice",
 43 | 		})
 44 | 
 45 | 		// Sanity check that the default topic was implied with that.
 46 | 		s.checkVariable(t, username, "topic", "random", false)
 47 | 
 48 | 		// Check the variable we just set, and one we didn't.
 49 | 		s.checkVariable(t, username, "name", "Alice", false)
 50 | 		s.checkVariable(t, username, "age", "5", true)
 51 | 
 52 | 		// See if we have as many variables as we expect.
 53 | 		vars, _ := s.GetAny(username)
 54 | 		if len(vars.Variables) != 2 {
 55 | 			t.Errorf(
 56 | 				"expected to have 2 variables, but had %d: %v",
 57 | 				len(vars.Variables),
 58 | 				vars.Variables,
 59 | 			)
 60 | 		}
 61 | 
 62 | 		// She should have an empty history.
 63 | 		history, _ := s.GetHistory(username)
 64 | 		for i := 0; i < sessions.HistorySize; i++ {
 65 | 			if history.Input[i] != "undefined" {
 66 | 				t.Errorf(
 67 | 					"expected to have a blank history, but input[%d] = %s",
 68 | 					i,
 69 | 					history.Input[i],
 70 | 				)
 71 | 			}
 72 | 			if history.Reply[i] != "undefined" {
 73 | 				t.Errorf(
 74 | 					"expected to have a blank history, but reply[%d] = %s",
 75 | 					i,
 76 | 					history.Reply[i],
 77 | 				)
 78 | 			}
 79 | 		}
 80 | 
 81 | 		// Add some history.
 82 | 		s.AddHistory(username, "hello bot", "hello human")
 83 | 		history, _ = s.GetHistory(username)
 84 | 		if history.Input[0] != "hello bot" {
 85 | 			t.Errorf(
 86 | 				"got unexpected input history: expected 'hello bot', got %s",
 87 | 				history.Input[0],
 88 | 			)
 89 | 		}
 90 | 		if history.Reply[0] != "hello human" {
 91 | 			t.Errorf(
 92 | 				"got unexpected reply history: expected 'hello human', got %s",
 93 | 				history.Reply[0],
 94 | 			)
 95 | 		}
 96 | 
 97 | 		// LastMatch.
 98 | 		lastMatch, _ := s.GetLastMatch(username)
 99 | 		if lastMatch != "" {
100 | 			t.Errorf(
101 | 				"didn't expect to have a LastMatch, but had: %s",
102 | 				lastMatch,
103 | 			)
104 | 		}
105 | 		s.SetLastMatch(username, "hello bot")
106 | 		lastMatch, _ = s.GetLastMatch(username)
107 | 		if lastMatch != "hello bot" {
108 | 			t.Errorf(
109 | 				"LastMatch wasn't '%s' like I expected, but was: %s",
110 | 				"hello bot",
111 | 				lastMatch,
112 | 			)
113 | 		}
114 | 
115 | 		// Verify we only have one user so far.
116 | 		s.expectCount(t, 1)
117 | 	}
118 | 
119 | 	// Create the second user.
120 | 	{
121 | 		username := "bob"
122 | 
123 | 		s.Init(username)
124 | 
125 | 		// Verify we now have two users.
126 | 		s.expectCount(t, 2)
127 | 
128 | 		// Delete this user.
129 | 		s.Clear(username)
130 | 
131 | 		// We should be back to one.
132 | 		s.expectCount(t, 1)
133 | 	}
134 | 
135 | 	// Create the new second user.
136 | 	{
137 | 		username := "barry"
138 | 
139 | 		// Set some variables.
140 | 		s.Set(username, map[string]string{
141 | 			"name":   "Barry",
142 | 			"age":    "20",
143 | 			"gender": "male",
144 | 		})
145 | 
146 | 		// Freeze his variables.
147 | 		s.Freeze(username)
148 | 
149 | 		// Happy birthday!
150 | 		birthday := map[string]string{
151 | 			"age": "21",
152 | 		}
153 | 		s.Set(username, birthday)
154 | 
155 | 		// Thaw the variables and make sure it was restored.
156 | 		s.Thaw(username, sessions.Thaw)
157 | 		s.checkVariable(t, username, "age", "20", false)
158 | 
159 | 		// Make sure trying to thaw again gives an error because the frozen
160 | 		// copy isn't there.
161 | 		err := s.Thaw(username, sessions.Thaw)
162 | 		expectError(t, "thawing again after sessions.Thaw", err)
163 | 
164 | 		// Freeze it again and repeat.
165 | 		s.Freeze(username)
166 | 		s.Set(username, birthday)
167 | 
168 | 		// Thaw with the 'keep' option this time.
169 | 		s.Thaw(username, sessions.Keep)
170 | 		s.checkVariable(t, username, "age", "20", false)
171 | 
172 | 		// One more time. The frozen copy is still there so just update
173 | 		// the user var and try the last thaw option.
174 | 		s.Set(username, birthday)
175 | 
176 | 		// Discard should just delete the frozen copy and not restore it.
177 | 		s.Thaw(username, sessions.Discard)
178 | 		s.checkVariable(t, username, "age", "20", false)
179 | 
180 | 		// One last call to Thaw should error out now.
181 | 		err = s.Thaw(username, sessions.Thaw)
182 | 		expectError(t, "thawing again after discard", err)
183 | 	}
184 | 
185 | 	// Create the third and fourth users.
186 | 	{
187 | 		s.Init("charlie")
188 | 		s.Init("dave")
189 | 		s.expectCount(t, 4)
190 | 
191 | 		// Clear all data and expect to have nothing left.
192 | 		s.ClearAll()
193 | 		s.expectCount(t, 0)
194 | 	}
195 | 
196 | 	// Test all the error cases.
197 | 	{
198 | 		var err error
199 | 
200 | 		_, err = s.Get("nobody", "name")
201 | 		expectError(t, "get variable from missing user", err)
202 | 
203 | 		_, err = s.GetAny("nobody")
204 | 		expectError(t, "get any variables for missing user", err)
205 | 
206 | 		_, err = s.GetLastMatch("nobody")
207 | 		expectError(t, "get a LastMatch for missing user", err)
208 | 
209 | 		_, err = s.GetHistory("nobody")
210 | 		expectError(t, "get history for missing user", err)
211 | 
212 | 		err = s.Freeze("nobody")
213 | 		expectError(t, "freeze missing user", err)
214 | 
215 | 		s.Init("nobody")
216 | 		s.Freeze("nobody")
217 | 		err = s.Thaw("nobody", 42)
218 | 		expectError(t, "invalid thaw action", err)
219 | 	}
220 | }
221 | 
222 | // checkVariable handles tests on user variables.
223 | func (s *Session) checkVariable(t *testing.T, username, name, expected string, expectError bool) {
224 | 	value, err := s.Get(username, name)
225 | 
226 | 	// Got an error when we aren't expecting one?
227 | 	if err != nil {
228 | 		if !expectError {
229 | 			t.Errorf(
230 | 				"got an unexpected error when getting variable '%s' for %s: %s",
231 | 				name,
232 | 				username,
233 | 				err,
234 | 			)
235 | 		}
236 | 		return
237 | 	}
238 | 
239 | 	// Didn't get an error when we expected to?
240 | 	if err == nil {
241 | 		if expectError {
242 | 			t.Errorf(
243 | 				"was expecting an error when getting variable '%s' for %s, but did not get one",
244 | 				name,
245 | 				username,
246 | 			)
247 | 		}
248 | 		return
249 | 	}
250 | 
251 | 	// Was it what we expected?
252 | 	if value != expected {
253 | 		t.Errorf(
254 | 			"got unexpected user variable '%s' for %s:\n"+
255 | 				"expected: %s\n"+
256 | 				"     got: %s",
257 | 			name,
258 | 			username,
259 | 			expected,
260 | 			value,
261 | 		)
262 | 	}
263 | }
264 | 
265 | // expectCount expects the user count from GetAll() to be a certain number.
266 | func (s *Session) expectCount(t *testing.T, expect int) {
267 | 	users := s.GetAll()
268 | 	if len(users) != expect {
269 | 		t.Errorf(
270 | 			"expected to have %d users, but had: %d",
271 | 			expect,
272 | 			len(users),
273 | 		)
274 | 	}
275 | }
276 | 
277 | func expectError(t *testing.T, name string, err error) {
278 | 	if err == nil {
279 | 		t.Errorf(
280 | 			"expected to get an error from '%s', but did not get one",
281 | 			name,
282 | 		)
283 | 	}
284 | }
285 | 


--------------------------------------------------------------------------------
/sessions/redis/redis.go:
--------------------------------------------------------------------------------
  1 | // Package redis implements a Redis backed session manager for RiveScript.
  2 | package redis
  3 | 
  4 | // NOTE: This source file contains the implementation of a SessionManager.
  5 | 
  6 | import (
  7 | 	"fmt"
  8 | 	"strings"
  9 | 	"time"
 10 | 
 11 | 	"github.com/aichaos/rivescript-go/sessions"
 12 | 	redis "gopkg.in/redis.v5"
 13 | )
 14 | 
 15 | // Config allows for configuring the Redis instance and key prefix.
 16 | type Config struct {
 17 | 	// The key prefix to use in Redis. For example, with a username of 'alice',
 18 | 	// the Redis key might be 'rivescript/alice'.
 19 | 	//
 20 | 	// The default prefix is 'rivescript/'
 21 | 	Prefix string
 22 | 
 23 | 	// The key used to prefix frozen user variables (those created by
 24 | 	// `Freeze()`). The default is `frozen:`
 25 | 	FrozenPrefix string
 26 | 
 27 | 	// Settings for the Redis client.
 28 | 	Redis *redis.Options
 29 | 
 30 | 	// Session timeout using time.duration
 31 | 	SessionTimeout time.Duration
 32 | }
 33 | 
 34 | // Session wraps a Redis client connection.
 35 | type Session struct {
 36 | 	prefix         string
 37 | 	frozenPrefix   string
 38 | 	client         *redis.Client
 39 | 	sessionTimeout time.Duration
 40 | }
 41 | 
 42 | // New creates a new Redis session instance.
 43 | func New(options *Config) *Session {
 44 | 	// No options given?
 45 | 	if options == nil {
 46 | 		options = &Config{}
 47 | 	}
 48 | 
 49 | 	// Default prefix is 'rivescript/'
 50 | 	if options.Prefix == "" {
 51 | 		options.Prefix = "rivescript/"
 52 | 	}
 53 | 	if options.FrozenPrefix == "" {
 54 | 		options.FrozenPrefix = "frozen:" + options.Prefix
 55 | 	}
 56 | 
 57 | 	// Default options for Redis if none provided.
 58 | 	if options.Redis == nil {
 59 | 		options.Redis = &redis.Options{
 60 | 			Addr: "localhost:6379",
 61 | 			DB:   0,
 62 | 		}
 63 | 	}
 64 | 
 65 | 	return &Session{
 66 | 		prefix:         options.Prefix,
 67 | 		frozenPrefix:   options.FrozenPrefix,
 68 | 		client:         redis.NewClient(options.Redis),
 69 | 		sessionTimeout: options.SessionTimeout,
 70 | 	}
 71 | }
 72 | 
 73 | // Init makes sure that a username has a session (creates one if not), and
 74 | // returns the pointer to it in any event.
 75 | func (s *Session) Init(username string) *sessions.UserData {
 76 | 	// See if they have any data in Redis, and return it if so.
 77 | 	user, err := s.getRedis(username)
 78 | 	if err == nil {
 79 | 		return user
 80 | 	}
 81 | 
 82 | 	// Create the default session.
 83 | 	user = defaultSession()
 84 | 
 85 | 	// Put them in Redis.
 86 | 	s.putRedis(username, user)
 87 | 	return user
 88 | }
 89 | 
 90 | // Set puts a user variable into Redis.
 91 | func (s *Session) Set(username string, vars map[string]string) {
 92 | 	data := s.Init(username)
 93 | 
 94 | 	for key, value := range vars {
 95 | 		data.Variables[key] = value
 96 | 	}
 97 | 
 98 | 	s.putRedis(username, data)
 99 | }
100 | 
101 | // AddHistory adds to a user's history data.
102 | func (s *Session) AddHistory(username, input, reply string) {
103 | 	data := s.Init(username)
104 | 
105 | 	// Pop, unshift, pop, unshift.
106 | 	data.History.Input = data.History.Input[:len(data.History.Input)-1]
107 | 	data.History.Input = append([]string{strings.TrimSpace(input)}, data.History.Input...)
108 | 	data.History.Reply = data.History.Reply[:len(data.History.Reply)-1]
109 | 	data.History.Reply = append([]string{strings.TrimSpace(reply)}, data.History.Reply...)
110 | 
111 | 	s.putRedis(username, data)
112 | }
113 | 
114 | // SetLastMatch sets the user's last matched trigger.
115 | func (s *Session) SetLastMatch(username, trigger string) {
116 | 	data := s.Init(username)
117 | 	data.LastMatch = trigger
118 | 	s.putRedis(username, data)
119 | }
120 | 
121 | // Get a user variable out of Redis.
122 | func (s *Session) Get(username, name string) (string, error) {
123 | 	data, err := s.getRedis(username)
124 | 	if err != nil {
125 | 		return "", err
126 | 	}
127 | 
128 | 	value, ok := data.Variables[name]
129 | 	if !ok {
130 | 		return "", fmt.Errorf(`variable "%s" for user "%s" not set`, name, username)
131 | 	}
132 | 	return value, nil
133 | }
134 | 
135 | // GetAny returns all variables about a user.
136 | func (s *Session) GetAny(username string) (*sessions.UserData, error) {
137 | 	// Check redis.
138 | 	data, err := s.getRedis(username)
139 | 	if err != nil {
140 | 		return nil, err
141 | 	}
142 | 	return data, nil
143 | }
144 | 
145 | // GetAll gets all data for all users.
146 | func (s *Session) GetAll() map[string]*sessions.UserData {
147 | 	result := map[string]*sessions.UserData{}
148 | 
149 | 	keys, err := s.client.Keys(s.prefix + "*").Result()
150 | 	if err != nil {
151 | 		return result
152 | 	}
153 | 
154 | 	for _, key := range keys {
155 | 		username := strings.Replace(key, s.prefix, "", 1)
156 | 		data, _ := s.GetAny(username)
157 | 		result[username] = data
158 | 	}
159 | 
160 | 	return result
161 | }
162 | 
163 | // GetLastMatch retrieves the user's last matched trigger.
164 | func (s *Session) GetLastMatch(username string) (string, error) {
165 | 	data, err := s.getRedis(username)
166 | 	if err != nil {
167 | 		return "", err
168 | 	}
169 | 
170 | 	return data.LastMatch, nil
171 | }
172 | 
173 | // GetHistory gets the user's history.
174 | func (s *Session) GetHistory(username string) (*sessions.History, error) {
175 | 	data, err := s.getRedis(username)
176 | 	if err != nil {
177 | 		return nil, err
178 | 	}
179 | 
180 | 	return data.History, nil
181 | }
182 | 
183 | // Clear deletes all variables about a user.
184 | func (s *Session) Clear(username string) {
185 | 	s.client.Del(s.prefix + username)
186 | }
187 | 
188 | // ClearAll resets all user data for all users.
189 | func (s *Session) ClearAll() {
190 | 	// List all the users.
191 | 	keys, err := s.client.Keys(s.prefix + "*").Result()
192 | 	if err != nil {
193 | 		return
194 | 	}
195 | 
196 | 	// Delete them all.
197 | 	s.client.Del(keys...)
198 | }
199 | 
200 | // Freeze makes a snapshot of user variables.
201 | func (s *Session) Freeze(username string) error {
202 | 	data, err := s.getRedis(username)
203 | 	if err != nil {
204 | 		return err
205 | 	}
206 | 
207 | 	// Duplicate it into the frozen Redis key.
208 | 	return s.putRedisFrozen(username, data, true)
209 | }
210 | 
211 | // Thaw restores user variables from a snapshot.
212 | func (s *Session) Thaw(username string, action sessions.ThawAction) error {
213 | 	frozen, err := s.getRedisFrozen(username, true)
214 | 	if err != nil {
215 | 		return fmt.Errorf(`no frozen data for username "%s": %s`, username, err)
216 | 	}
217 | 
218 | 	// Which type of thaw action are they using?
219 | 	switch action {
220 | 	case sessions.Thaw:
221 | 		// Thaw means to restore the frozen copy and then delete the copy.
222 | 		s.Clear(username)
223 | 		s.putRedis(username, frozen)
224 | 		s.client.Del(s.frozenKey(username))
225 | 	case sessions.Discard:
226 | 		// Discard means to just delete the frozen copy, do not restore it.
227 | 		s.client.Del(s.frozenKey(username))
228 | 	case sessions.Keep:
229 | 		// Keep restores from the frozen copy, but keeps the frozen copy.
230 | 		s.Clear(username)
231 | 		s.putRedis(username, frozen)
232 | 	default:
233 | 		return fmt.Errorf(`can't thaw data for username "%s": invalid thaw action`, username)
234 | 	}
235 | 
236 | 	return nil
237 | }
238 | 


--------------------------------------------------------------------------------
/sessions/sqlite/sqlite.go:
--------------------------------------------------------------------------------
  1 | package sqlite
  2 | 
  3 | import (
  4 | 	"database/sql"
  5 | 	"encoding/json"
  6 | 	"fmt"
  7 | 	"log"
  8 | 	"strings"
  9 | 	"sync"
 10 | 
 11 | 	"github.com/aichaos/rivescript-go/sessions"
 12 | 	_ "modernc.org/sqlite"
 13 | )
 14 | 
 15 | var schema string = `PRAGMA journal_mode = WAL;
 16 | PRAGMA synchronous = normal;
 17 | PRAGMA foreign_keys = on;
 18 | PRAGMA encoding = "UTF-8";
 19 | BEGIN TRANSACTION;
 20 | CREATE TABLE IF NOT EXISTS "users" (
 21 | 	"id" INTEGER,
 22 | 	"username" TEXT UNIQUE,
 23 | 	"last_match" TEXT,
 24 | 	PRIMARY KEY("id" AUTOINCREMENT)
 25 | );
 26 | CREATE TABLE IF NOT EXISTS "user_variables" (
 27 | 	"id" INTEGER,
 28 | 	"user_id" INTEGER NOT NULL,
 29 | 	"key" TEXT NOT NULL,
 30 | 	"value" TEXT,
 31 | 	PRIMARY KEY("id" AUTOINCREMENT),
 32 | 	UNIQUE("user_id", "key")
 33 | );
 34 | CREATE TABLE IF NOT EXISTS "history" (
 35 | 	"id" INTEGER,
 36 | 	"user_id" INTEGER NOT NULL,
 37 | 	"input" TEXT NOT NULL,
 38 | 	"reply" TEXT NOT NULL,
 39 | 	"timestamp" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', 'now') AS INTEGER)),
 40 | 	PRIMARY KEY("id" AUTOINCREMENT)
 41 | );
 42 | CREATE TABLE IF NOT EXISTS "frozen_user" (
 43 | 	"id" INTEGER,
 44 | 	"user_id" INTEGER NOT NULL,
 45 | 	"data" TEXT NOT NULL,
 46 | 	PRIMARY KEY("id" AUTOINCREMENT)
 47 | );
 48 | CREATE TABLE IF NOT EXISTS "local_storage" (
 49 | 	"id" INTEGER,
 50 | 	"key" TEXT NOT NULL,
 51 | 	"value" TEXT NOT NULL,
 52 | 	PRIMARY KEY("id" AUTOINCREMENT)
 53 | );
 54 | CREATE VIEW IF NOT EXISTS v_user_variables AS
 55 | SELECT users.username AS username,
 56 | 	user_variables.key,
 57 | 	user_variables.value
 58 | FROM users,
 59 | 	user_variables
 60 | WHERE users.id = user_variables.user_id;
 61 | COMMIT;`
 62 | 
 63 | type Client struct {
 64 | 	lock sync.Mutex
 65 | 	db   *sql.DB
 66 | }
 67 | 
 68 | // New creates a new Client.
 69 | func New(filename string) (*Client, error) {
 70 | 	db, err := sql.Open("sqlite", filename)
 71 | 	if err != nil {
 72 | 		return nil, err
 73 | 	}
 74 | 	db.SetMaxOpenConns(0)
 75 | 
 76 | 	_, err = db.Exec(schema)
 77 | 	if err != nil {
 78 | 		return nil, err
 79 | 	}
 80 | 
 81 | 	return &Client{
 82 | 		db: db,
 83 | 	}, nil
 84 | }
 85 | 
 86 | func (s *Client) Close() error {
 87 | 	return s.db.Close()
 88 | }
 89 | 
 90 | // init makes sure a username exists in the memory store.
 91 | func (s *Client) Init(username string) *sessions.UserData {
 92 | 	user, err := s.GetAny(username)
 93 | 	if err != nil {
 94 | 		func() {
 95 | 
 96 | 			s.lock.Lock()
 97 | 			defer s.lock.Unlock()
 98 | 
 99 | 			tx, _ := s.db.Begin()
100 | 			stmt, _ := tx.Prepare(`INSERT OR IGNORE INTO users (username, last_match) VALUES (?,"");`)
101 | 			defer stmt.Close()
102 | 			stmt.Exec(username)
103 | 			tx.Commit()
104 | 		}()
105 | 
106 | 		s.Set(username, map[string]string{
107 | 			"topic": "random",
108 | 		})
109 | 
110 | 		return &sessions.UserData{
111 | 			Variables: map[string]string{
112 | 				"topic": "random",
113 | 			},
114 | 			LastMatch: "",
115 | 			History:   sessions.NewHistory(),
116 | 		}
117 | 	}
118 | 	return user
119 | } // Init()
120 | 
121 | // Set a user variable.
122 | func (s *Client) Set(username string, vars map[string]string) {
123 | 	s.Init(username)
124 | 
125 | 	s.lock.Lock()
126 | 	defer s.lock.Unlock()
127 | 
128 | 	tx, _ := s.db.Begin()
129 | 	stmt, _ := tx.Prepare(`INSERT OR REPLACE INTO user_variables (user_id, key, value) VALUES ((SELECT id FROM users WHERE username = ?), ?, ?);`)
130 | 	defer stmt.Close()
131 | 	for k, v := range vars {
132 | 		stmt.Exec(username, k, v)
133 | 	}
134 | 	tx.Commit()
135 | }
136 | 
137 | // AddHistory adds history items.
138 | func (s *Client) AddHistory(username, input, reply string) {
139 | 	s.Init(username)
140 | 
141 | 	s.lock.Lock()
142 | 	defer s.lock.Unlock()
143 | 
144 | 	tx, _ := s.db.Begin()
145 | 	stmt, _ := tx.Prepare(`INSERT INTO history (user_id, input,reply)VALUES((SELECT id FROM users WHERE username = ?),?,?);`)
146 | 	defer stmt.Close()
147 | 	stmt.Exec(username, input, reply)
148 | 	tx.Commit()
149 | }
150 | 
151 | // SetLastMatch sets the user's last matched trigger.
152 | func (s *Client) SetLastMatch(username, trigger string) {
153 | 	s.Init(username)
154 | 
155 | 	s.lock.Lock()
156 | 	defer s.lock.Unlock()
157 | 
158 | 	tx, _ := s.db.Begin()
159 | 	stmt, _ := tx.Prepare(`UPDATE users SET last_match = ? WHERE username = ?;`)
160 | 	defer stmt.Close()
161 | 	stmt.Exec(trigger, username)
162 | 	tx.Commit()
163 | }
164 | 
165 | // Get a user variable.
166 | func (s *Client) Get(username, name string) (string, error) {
167 | 	var value string
168 | 	row := s.db.QueryRow(`SELECT value FROM user_variables WHERE user_id = (SELECT id FROM users WHERE username = ?) AND key = ?;`, username, name)
169 | 	switch err := row.Scan(&value); err {
170 | 	case sql.ErrNoRows:
171 | 		return "", fmt.Errorf("no %s variable found for user %s", name, username)
172 | 	case nil:
173 | 		return value, nil
174 | 	default:
175 | 		return "", fmt.Errorf("unknown sql error")
176 | 	}
177 | }
178 | 
179 | // GetAny gets all variables for a user.
180 | func (s *Client) GetAny(username string) (*sessions.UserData, error) {
181 | 	history, err := s.GetHistory(username)
182 | 	if err != nil {
183 | 		return nil, err
184 | 	}
185 | 	last_match, err := s.GetLastMatch(username)
186 | 	if err != nil {
187 | 		return nil, err
188 | 	}
189 | 
190 | 	var variables map[string]string = make(map[string]string)
191 | 	rows, err := s.db.Query(`SELECT key,value FROM user_variables WHERE user_id = (SELECT id FROM users WHERE username = ?);`, username)
192 | 	if err != nil {
193 | 		return nil, err
194 | 	}
195 | 	defer rows.Close()
196 | 	var key, value string
197 | 	for rows.Next() {
198 | 		err = rows.Scan(&key, &value)
199 | 		if err != nil {
200 | 			continue
201 | 		}
202 | 		variables[key] = value
203 | 	}
204 | 
205 | 	return &sessions.UserData{
206 | 		History:   history,
207 | 		LastMatch: last_match,
208 | 		Variables: variables,
209 | 	}, nil
210 | }
211 | 
212 | // GetAll gets all data for all users.
213 | func (s *Client) GetAll() map[string]*sessions.UserData {
214 | 	var users []string = make([]string, 0)
215 | 	rows, _ := s.db.Query(`SELECT username FROM users;`)
216 | 	defer rows.Close()
217 | 	var user string
218 | 	for rows.Next() {
219 | 		rows.Scan(&user)
220 | 		users = append(users, user)
221 | 	}
222 | 
223 | 	var usersmap map[string]*sessions.UserData = make(map[string]*sessions.UserData)
224 | 	for _, user := range users {
225 | 		u, _ := s.GetAny(user)
226 | 		usersmap[user] = u
227 | 	}
228 | 
229 | 	return usersmap
230 | }
231 | 
232 | // GetLastMatch returns the last matched trigger for the user,
233 | func (s *Client) GetLastMatch(username string) (string, error) {
234 | 	var last_match string
235 | 	row := s.db.QueryRow(`SELECT last_match FROM users WHERE username = ?;`, username)
236 | 	switch err := row.Scan(&last_match); err {
237 | 	case sql.ErrNoRows:
238 | 		return "", fmt.Errorf("no last match found for user %s", username)
239 | 	case nil:
240 | 		return last_match, nil
241 | 	default:
242 | 		return "", fmt.Errorf("unknown sql error: %s", err)
243 | 	}
244 | }
245 | 
246 | // GetHistory gets the user's history.
247 | func (s *Client) GetHistory(username string) (*sessions.History, error) {
248 | 	data := &sessions.History{
249 | 		Input: []string{},
250 | 		Reply: []string{},
251 | 	}
252 | 
253 | 	for i := 0; i < sessions.HistorySize; i++ {
254 | 		data.Input = append(data.Input, "undefined")
255 | 		data.Reply = append(data.Reply, "undefined")
256 | 	}
257 | 
258 | 	rows, err := s.db.Query("SELECT input,reply FROM history WHERE user_id = (SELECT id FROM users WHERE username = ?) ORDER BY timestamp ASC LIMIT 10;", username)
259 | 	if err != nil {
260 | 		return data, err
261 | 	}
262 | 	defer rows.Close()
263 | 	for rows.Next() {
264 | 		var input, reply string
265 | 		err := rows.Scan(&input, &reply)
266 | 		if err != nil {
267 | 			log.Println("[ERROR]", err)
268 | 			continue
269 | 		}
270 | 		data.Input = data.Input[:len(data.Input)-1]                            // Pop
271 | 		data.Input = append([]string{strings.TrimSpace(input)}, data.Input...) // Unshift
272 | 		data.Reply = data.Reply[:len(data.Reply)-1]                            // Pop
273 | 		data.Reply = append([]string{strings.TrimSpace(reply)}, data.Reply...) // Unshift
274 | 
275 | 	}
276 | 
277 | 	return data, nil
278 | }
279 | 
280 | // Clear data for a user.
281 | func (s *Client) Clear(username string) {
282 | 	s.lock.Lock()
283 | 	defer s.lock.Unlock()
284 | 
285 | 	tx, _ := s.db.Begin()
286 | 	tx.Exec(`DELETE FROM user_variables WHERE user_id = (SELECT id FROM users WHERE username = ?);`, username)
287 | 	tx.Exec(`DELETE FROM history WHERE user_id = (SELECT id FROM users WHERE username = ?);`, username)
288 | 
289 | 	tx.Exec(`DELETE FROM users WHERE username = ?;`, username)
290 | 	tx.Commit()
291 | }
292 | 
293 | // ClearAll resets all user data for all users.
294 | func (s *Client) ClearAll() {
295 | 	s.lock.Lock()
296 | 	defer s.lock.Unlock()
297 | 
298 | 	tx, _ := s.db.Begin()
299 | 	tx.Exec(`DELETE FROM user_variables;`)
300 | 	tx.Exec(`DELETE FROM history;`)
301 | 
302 | 	s.db.Exec(`DELETE FROM users;`)
303 | 	tx.Commit()
304 | }
305 | 
306 | // Freeze makes a snapshot of user variables.
307 | func (s *Client) Freeze(username string) error {
308 | 	user := s.Init(username)
309 | 	data, err := json.Marshal(user)
310 | 	if err != nil {
311 | 		return err
312 | 	}
313 | 
314 | 	s.lock.Lock()
315 | 	defer s.lock.Unlock()
316 | 
317 | 	tx, err := s.db.Begin()
318 | 	if err != nil {
319 | 		return err
320 | 	}
321 | 	stmt, err := tx.Prepare(`INSERT OR REPLACE INTO frozen_user (user_id, data)VALUES((SELECT id FROM users WHERE username = ?), ?);`)
322 | 	if err != nil {
323 | 		return err
324 | 	}
325 | 	defer stmt.Close()
326 | 	_, err = stmt.Exec(username, string(data))
327 | 	if err != nil {
328 | 		return err
329 | 	}
330 | 
331 | 	return tx.Commit()
332 | }
333 | 
334 | // Thaw restores from a snapshot.
335 | func (s *Client) Thaw(username string, action sessions.ThawAction) error {
336 | 	user, err := func(u string) (sessions.UserData, error) {
337 | 		var data string
338 | 		var reply sessions.UserData
339 | 		row := s.db.QueryRow(`SELECT data FROM frozen_user WHERE user_id = (SELECT id FROM users WHERE username = ?);`, username)
340 | 		switch err := row.Scan(&data); err {
341 | 		case sql.ErrNoRows:
342 | 			return sessions.UserData{}, fmt.Errorf("no rows found")
343 | 		case nil:
344 | 			err = json.Unmarshal([]byte(data), &reply)
345 | 			if err != nil {
346 | 				return sessions.UserData{}, err
347 | 			}
348 | 			return reply, nil
349 | 		default:
350 | 			return sessions.UserData{}, fmt.Errorf("unknown sql error")
351 | 		}
352 | 	}(username)
353 | 	if err != nil {
354 | 		return fmt.Errorf("no data for snapshot for user %s", username)
355 | 	}
356 | 
357 | 	switch action {
358 | 	case sessions.Thaw:
359 | 		if err := func() error {
360 | 			s.lock.Lock()
361 | 			defer s.lock.Unlock()
362 | 
363 | 			_, err = s.db.Exec(`DELETE FROM frozen_user WHERE user_id = (SELECT id FROM users WHERE username = ?);`, username)
364 | 			if err != nil {
365 | 				return err
366 | 			}
367 | 			return nil
368 | 		}(); err != nil {
369 | 			return err
370 | 		}
371 | 
372 | 		s.Clear(username)
373 | 		s.Set(username, user.Variables)
374 | 		s.SetLastMatch(username, user.LastMatch)
375 | 		for i := len(user.History.Input) - 1; i >= 0; i-- {
376 | 			s.AddHistory(username, user.History.Input[i], user.History.Reply[i])
377 | 		}
378 | 
379 | 		return nil
380 | 	case sessions.Discard:
381 | 		s.lock.Lock()
382 | 		defer s.lock.Unlock()
383 | 
384 | 		_, err = s.db.Exec(`DELETE FROM frozen_user WHERE user_id = (SELECT id FROM users WHERE username = ?);`, username)
385 | 		if err != nil {
386 | 			return err
387 | 		}
388 | 	case sessions.Keep:
389 | 		s.Clear(username)
390 | 		s.Set(username, user.Variables)
391 | 		s.SetLastMatch(username, user.LastMatch)
392 | 		for i := range user.History.Input {
393 | 			s.AddHistory(username, user.History.Input[i], user.History.Reply[i])
394 | 		}
395 | 		return nil
396 | 	default:
397 | 		return fmt.Errorf("something went wrong")
398 | 	}
399 | 
400 | 	return nil
401 | }
402 | 


--------------------------------------------------------------------------------
/sorting.go:
--------------------------------------------------------------------------------
  1 | package rivescript
  2 | 
  3 | // Data sorting functions
  4 | 
  5 | import (
  6 | 	"errors"
  7 | 	"sort"
  8 | 	"strconv"
  9 | 	"strings"
 10 | )
 11 | 
 12 | // Sort buffer data, for RiveScript.SortReplies()
 13 | type sortBuffer struct {
 14 | 	topics map[string][]sortedTriggerEntry // Topic name -> array of triggers
 15 | 	thats  map[string][]sortedTriggerEntry
 16 | 	sub    []string // Substitutions
 17 | 	person []string // Person substitutions
 18 | }
 19 | 
 20 | // Holds a sorted trigger and the pointer to that trigger's data
 21 | type sortedTriggerEntry struct {
 22 | 	trigger string
 23 | 	pointer *astTrigger
 24 | }
 25 | 
 26 | // Temporary categorization of triggers while sorting
 27 | type sortTrack struct {
 28 | 	atomic map[int][]sortedTriggerEntry // Sort by number of whole words
 29 | 	option map[int][]sortedTriggerEntry // Sort optionals by number of words
 30 | 	alpha  map[int][]sortedTriggerEntry // Sort alpha wildcards by no. of words
 31 | 	number map[int][]sortedTriggerEntry // Sort numeric wildcards by no. of words
 32 | 	wild   map[int][]sortedTriggerEntry // Sort wildcards by no. of words
 33 | 	pound  []sortedTriggerEntry         // Triggers of just '#'
 34 | 	under  []sortedTriggerEntry         // Triggers of just '_'
 35 | 	star   []sortedTriggerEntry         // Triggers of just '*'
 36 | }
 37 | 
 38 | /*
 39 | SortReplies sorts the reply structures in memory for optimal matching.
 40 | 
 41 | After you have finished loading your RiveScript code, call this method to
 42 | populate the various sort buffers. This is absolutely necessary for reply
 43 | matching to work efficiently!
 44 | 
 45 | If the bot has loaded no topics, or if it ends up with no sorted triggers at
 46 | the end, it will return an error saying such. This usually means the bot didn't
 47 | load any RiveScript code, for example because it looked in the wrong directory.
 48 | */
 49 | func (rs *RiveScript) SortReplies() error {
 50 | 	// (Re)initialize the sort cache.
 51 | 	rs.sorted.topics = map[string][]sortedTriggerEntry{}
 52 | 	rs.sorted.thats = map[string][]sortedTriggerEntry{}
 53 | 	rs.say("Sorting triggers...")
 54 | 
 55 | 	// If there are no topics, give an error.
 56 | 	if len(rs.topics) == 0 {
 57 | 		return errors.New("SortReplies: no topics were found; did you load any RiveScript code?")
 58 | 	}
 59 | 
 60 | 	// Loop through all the topics.
 61 | 	for topic := range rs.topics {
 62 | 		rs.say("Analyzing topic %s", topic)
 63 | 
 64 | 		// Collect a list of all the triggers we're going to worry about. If this
 65 | 		// topic inherits another topic, we need to recursively add those to the
 66 | 		// list as well.
 67 | 		allTriggers := rs.getTopicTriggers(topic, false)
 68 | 
 69 | 		// Sort these triggers.
 70 | 		rs.sorted.topics[topic] = rs.sortTriggerSet(allTriggers, true)
 71 | 
 72 | 		// Get all of the %Previous triggers for this topic.
 73 | 		thatTriggers := rs.getTopicTriggers(topic, true)
 74 | 
 75 | 		// And sort them, too.
 76 | 		rs.sorted.thats[topic] = rs.sortTriggerSet(thatTriggers, false)
 77 | 	}
 78 | 
 79 | 	// Sort the substitution lists.
 80 | 	rs.sorted.sub = sortList(rs.sub)
 81 | 	rs.sorted.person = sortList(rs.person)
 82 | 
 83 | 	// Did we sort anything at all?
 84 | 	if len(rs.sorted.topics) == 0 && len(rs.sorted.thats) == 0 {
 85 | 		return errors.New("SortReplies: ended up with empty trigger lists; did you load any RiveScript code?")
 86 | 	}
 87 | 
 88 | 	return nil
 89 | }
 90 | 
 91 | /*
 92 | sortTriggerSet sorts a group of triggers in an optimal sorting order.
 93 | 
 94 | This function has two use cases:
 95 | 
 96 |  1. Create a sort buffer for "normal" (matchable) triggers, which are triggers
 97 |     that are NOT accompanied by a %Previous tag.
 98 |  2. Create a sort buffer for triggers that had %Previous tags.
 99 | 
100 | Use the `excludePrevious` parameter to control which one is being done. This
101 | function will return a list of sortedTriggerEntry items, and it's intended to
102 | have no duplicate trigger patterns (unless the source RiveScript code explicitly
103 | uses the same duplicate pattern twice, which is a user error).
104 | */
105 | func (rs *RiveScript) sortTriggerSet(triggers []sortedTriggerEntry, excludePrevious bool) []sortedTriggerEntry {
106 | 	// Create a priority map, of priority numbers -> their triggers.
107 | 	prior := map[int][]sortedTriggerEntry{}
108 | 
109 | 	// Go through and bucket each trigger by weight (priority).
110 | 	for _, trig := range triggers {
111 | 		if excludePrevious && trig.pointer.previous != "" {
112 | 			continue
113 | 		}
114 | 
115 | 		// Check the trigger text for any {weight} tags, default being 0
116 | 		match := reWeight.FindStringSubmatch(trig.trigger)
117 | 		weight := 0
118 | 		if len(match) > 0 {
119 | 			weight, _ = strconv.Atoi(match[1])
120 | 		}
121 | 
122 | 		// First trigger of this priority? Initialize the weight map.
123 | 		if _, ok := prior[weight]; !ok {
124 | 			prior[weight] = []sortedTriggerEntry{}
125 | 		}
126 | 
127 | 		prior[weight] = append(prior[weight], trig)
128 | 	}
129 | 
130 | 	// Keep a running list of sorted triggers for this topic.
131 | 	running := []sortedTriggerEntry{}
132 | 
133 | 	// Sort the priorities with the highest number first.
134 | 	var sortedPriorities []int
135 | 	for k := range prior {
136 | 		sortedPriorities = append(sortedPriorities, k)
137 | 	}
138 | 	sort.Sort(sort.Reverse(sort.IntSlice(sortedPriorities)))
139 | 
140 | 	// Go through each priority set.
141 | 	for _, p := range sortedPriorities {
142 | 		rs.say("Sorting triggers with priority %d", p)
143 | 
144 | 		// So, some of these triggers may include an {inherits} tag, if they
145 | 		// came from a topic which inherits another topic. Lower inherits values
146 | 		// mean higher priority on the stack. Triggers that have NO inherits
147 | 		// value at all (which will default to -1), will be moved to the END of
148 | 		// the stack at the end (have the highest number/lowest priority).
149 | 		inherits := -1        // -1 means no {inherits} tag
150 | 		highestInherits := -1 // Highest number seen so far
151 | 
152 | 		// Loop through and categorize these triggers.
153 | 		track := map[int]*sortTrack{}
154 | 		track[inherits] = initSortTrack()
155 | 
156 | 		// Loop through all the triggers.
157 | 		for _, trig := range prior[p] {
158 | 			pattern := trig.trigger
159 | 			rs.say("Looking at trigger: %s", pattern)
160 | 
161 | 			// See if the trigger has an {inherits} tag.
162 | 			match := reInherits.FindStringSubmatch(pattern)
163 | 			if len(match) > 0 {
164 | 				inherits, _ = strconv.Atoi(match[1])
165 | 				if inherits > highestInherits {
166 | 					highestInherits = inherits
167 | 				}
168 | 				rs.say("Trigger belongs to a topic that inherits other topics. "+
169 | 					"Level=%d", inherits)
170 | 				pattern = reInherits.ReplaceAllString(pattern, "")
171 | 			} else {
172 | 				inherits = -1
173 | 			}
174 | 
175 | 			// If this is the first time we've seen this inheritance level,
176 | 			// initialize its sort track structure.
177 | 			if _, ok := track[inherits]; !ok {
178 | 				track[inherits] = initSortTrack()
179 | 			}
180 | 
181 | 			// Start inspecting the trigger's contents.
182 | 			if strings.Contains(pattern, "_") {
183 | 				// Alphabetic wildcard included.
184 | 				cnt := wordCount(pattern, false)
185 | 				rs.say("Has a _ wildcard with %d words", cnt)
186 | 				if cnt > 0 {
187 | 					if _, ok := track[inherits].alpha[cnt]; !ok {
188 | 						track[inherits].alpha[cnt] = []sortedTriggerEntry{}
189 | 					}
190 | 					track[inherits].alpha[cnt] = append(track[inherits].alpha[cnt], trig)
191 | 				} else {
192 | 					track[inherits].under = append(track[inherits].under, trig)
193 | 				}
194 | 			} else if strings.Contains(pattern, "#") {
195 | 				// Numeric wildcard included.
196 | 				cnt := wordCount(pattern, false)
197 | 				rs.say("Has a # wildcard with %d words", cnt)
198 | 				if cnt > 0 {
199 | 					if _, ok := track[inherits].number[cnt]; !ok {
200 | 						track[inherits].number[cnt] = []sortedTriggerEntry{}
201 | 					}
202 | 					track[inherits].number[cnt] = append(track[inherits].number[cnt], trig)
203 | 				} else {
204 | 					track[inherits].pound = append(track[inherits].pound, trig)
205 | 				}
206 | 			} else if strings.Contains(pattern, "*") {
207 | 				// Wildcard included.
208 | 				cnt := wordCount(pattern, false)
209 | 				rs.say("Has a * wildcard with %d words", cnt)
210 | 				if cnt > 0 {
211 | 					if _, ok := track[inherits].wild[cnt]; !ok {
212 | 						track[inherits].wild[cnt] = []sortedTriggerEntry{}
213 | 					}
214 | 					track[inherits].wild[cnt] = append(track[inherits].wild[cnt], trig)
215 | 				} else {
216 | 					track[inherits].star = append(track[inherits].star, trig)
217 | 				}
218 | 			} else if strings.Contains(pattern, "[") {
219 | 				// Optionals included.
220 | 				cnt := wordCount(pattern, false)
221 | 				rs.say("Has optionals with %d words", cnt)
222 | 				if _, ok := track[inherits].option[cnt]; !ok {
223 | 					track[inherits].option[cnt] = []sortedTriggerEntry{}
224 | 				}
225 | 				track[inherits].option[cnt] = append(track[inherits].option[cnt], trig)
226 | 			} else {
227 | 				// Totally atomic.
228 | 				cnt := wordCount(pattern, false)
229 | 				rs.say("Totally atomic trigger with %d words", cnt)
230 | 				if _, ok := track[inherits].atomic[cnt]; !ok {
231 | 					track[inherits].atomic[cnt] = []sortedTriggerEntry{}
232 | 				}
233 | 				track[inherits].atomic[cnt] = append(track[inherits].atomic[cnt], trig)
234 | 			}
235 | 		}
236 | 
237 | 		// Move the no-{inherits} triggers to the bottom of the stack.
238 | 		track[highestInherits+1] = track[-1]
239 | 		delete(track, -1)
240 | 
241 | 		// Sort the track from the lowest to the highest.
242 | 		var trackSorted []int
243 | 		for k := range track {
244 | 			trackSorted = append(trackSorted, k)
245 | 		}
246 | 		sort.Ints(trackSorted)
247 | 
248 | 		// Go through each priority level from greatest to smallest.
249 | 		for _, ip := range trackSorted {
250 | 			rs.say("ip=%d", ip)
251 | 
252 | 			// Sort each of the main kinds of triggers by their word counts.
253 | 			running = sortByWords(running, track[ip].atomic)
254 | 			running = sortByWords(running, track[ip].option)
255 | 			running = sortByWords(running, track[ip].alpha)
256 | 			running = sortByWords(running, track[ip].number)
257 | 			running = sortByWords(running, track[ip].wild)
258 | 
259 | 			// Add the single wildcard triggers, sorted by length.
260 | 			running = sortByLength(running, track[ip].under)
261 | 			running = sortByLength(running, track[ip].pound)
262 | 			running = sortByLength(running, track[ip].star)
263 | 		}
264 | 	}
265 | 
266 | 	return running
267 | }
268 | 
269 | // sortList sorts lists (like substitutions) from a string:string map.
270 | func sortList(dict map[string]string) []string {
271 | 	output := []string{}
272 | 
273 | 	// Track by number of words.
274 | 	track := map[int][]string{}
275 | 
276 | 	// Loop through each item.
277 | 	for item := range dict {
278 | 		cnt := wordCount(item, true)
279 | 		if _, ok := track[cnt]; !ok {
280 | 			track[cnt] = []string{}
281 | 		}
282 | 		track[cnt] = append(track[cnt], item)
283 | 	}
284 | 
285 | 	// Sort them by word count, descending.
286 | 	sortedCounts := []int{}
287 | 	for cnt := range track {
288 | 		sortedCounts = append(sortedCounts, cnt)
289 | 	}
290 | 	sort.Sort(sort.Reverse(sort.IntSlice(sortedCounts)))
291 | 
292 | 	for _, cnt := range sortedCounts {
293 | 		// Sort the strings of this word-count by their lengths.
294 | 		sortedLengths := track[cnt]
295 | 		sort.Sort(sort.Reverse(byLength(sortedLengths)))
296 | 		output = append(output, sortedLengths...)
297 | 	}
298 | 
299 | 	return output
300 | }
301 | 
302 | /*
303 | sortByWords sorts a set of triggers by word count and overall length.
304 | 
305 | This is a helper function for sorting the `atomic`, `option`, `alpha`, `number`
306 | and `wild` attributes of the sortTrack and adding them to the running sort
307 | buffer in that specific order. Since attribute lookup by reflection is expensive
308 | in Go, this function is given the relevant sort buffer directly, and the current
309 | running sort buffer to add the results to.
310 | 
311 | The `triggers` parameter is a map between word counts and the triggers that
312 | fit that number of words.
313 | */
314 | func sortByWords(running []sortedTriggerEntry, triggers map[int][]sortedTriggerEntry) []sortedTriggerEntry {
315 | 	// Sort the triggers by their word counts from greatest to smallest.
316 | 	var sortedWords []int
317 | 	for wc := range triggers {
318 | 		sortedWords = append(sortedWords, wc)
319 | 	}
320 | 	sort.Sort(sort.Reverse(sort.IntSlice(sortedWords)))
321 | 
322 | 	for _, wc := range sortedWords {
323 | 		// Triggers with equal word lengths should be sorted by overall trigger length.
324 | 		var sortedPatterns []string
325 | 		patternMap := map[string][]sortedTriggerEntry{}
326 | 
327 | 		for _, trig := range triggers[wc] {
328 | 			sortedPatterns = append(sortedPatterns, trig.trigger)
329 | 			if _, ok := patternMap[trig.trigger]; !ok {
330 | 				patternMap[trig.trigger] = []sortedTriggerEntry{}
331 | 			}
332 | 			patternMap[trig.trigger] = append(patternMap[trig.trigger], trig)
333 | 		}
334 | 		sort.Sort(sort.Reverse(byLength(sortedPatterns)))
335 | 
336 | 		// Add the triggers to the running triggers bucket.
337 | 		for _, pattern := range sortedPatterns {
338 | 			running = append(running, patternMap[pattern]...)
339 | 		}
340 | 	}
341 | 
342 | 	return running
343 | }
344 | 
345 | /*
346 | sortByLength sorts a set of triggers purely by character length.
347 | 
348 | This is like `sortByWords`, but it's intended for triggers that consist solely
349 | of wildcard-like symbols with no real words. For example a trigger of `* * *`
350 | qualifies for this, and it has no words, so we sort by length so it gets a
351 | higher priority than simply `*`.
352 | */
353 | func sortByLength(running []sortedTriggerEntry, triggers []sortedTriggerEntry) []sortedTriggerEntry {
354 | 	var sortedPatterns []string
355 | 	patternMap := map[string][]sortedTriggerEntry{}
356 | 	for _, trig := range triggers {
357 | 		sortedPatterns = append(sortedPatterns, trig.trigger)
358 | 		if _, ok := patternMap[trig.trigger]; !ok {
359 | 			patternMap[trig.trigger] = []sortedTriggerEntry{}
360 | 		}
361 | 		patternMap[trig.trigger] = append(patternMap[trig.trigger], trig)
362 | 	}
363 | 	sort.Sort(sort.Reverse(byLength(sortedPatterns)))
364 | 
365 | 	// Only loop through unique patterns.
366 | 	patternSet := map[string]bool{}
367 | 
368 | 	// Add them to the running triggers bucket.
369 | 	for _, pattern := range sortedPatterns {
370 | 		if _, ok := patternSet[pattern]; ok {
371 | 			continue
372 | 		}
373 | 		patternSet[pattern] = true
374 | 		running = append(running, patternMap[pattern]...)
375 | 	}
376 | 
377 | 	return running
378 | }
379 | 
380 | // initSortTrack initializes a new, empty sortTrack object.
381 | func initSortTrack() *sortTrack {
382 | 	return &sortTrack{
383 | 		atomic: map[int][]sortedTriggerEntry{},
384 | 		option: map[int][]sortedTriggerEntry{},
385 | 		alpha:  map[int][]sortedTriggerEntry{},
386 | 		number: map[int][]sortedTriggerEntry{},
387 | 		wild:   map[int][]sortedTriggerEntry{},
388 | 		pound:  []sortedTriggerEntry{},
389 | 		under:  []sortedTriggerEntry{},
390 | 		star:   []sortedTriggerEntry{},
391 | 	}
392 | }
393 | 


--------------------------------------------------------------------------------
/utils.go:
--------------------------------------------------------------------------------
  1 | package rivescript
  2 | 
  3 | // Miscellaneous utility functions.
  4 | 
  5 | import (
  6 | 	"fmt"
  7 | 	"regexp"
  8 | 	"strings"
  9 | )
 10 | 
 11 | // randomInt gets a random number using RiveScript's internal RNG.
 12 | func (rs *RiveScript) randomInt(max int) int {
 13 | 	rs.randomLock.Lock()
 14 | 	defer rs.randomLock.Unlock()
 15 | 	return rs.rng.Intn(max)
 16 | }
 17 | 
 18 | // wordCount counts the number of real words in a string.
 19 | func wordCount(pattern string, all bool) int {
 20 | 	var words []string
 21 | 	if all {
 22 | 		words = strings.Fields(pattern) // Splits at whitespaces
 23 | 	} else {
 24 | 		words = regSplit(pattern, `[\s\*\#\_\|]+`)
 25 | 	}
 26 | 
 27 | 	wc := 0
 28 | 	for _, word := range words {
 29 | 		if len(word) > 0 {
 30 | 			wc++
 31 | 		}
 32 | 	}
 33 | 
 34 | 	return wc
 35 | }
 36 | 
 37 | // stripNasties strips special characters out of a string.
 38 | func stripNasties(pattern string) string {
 39 | 	return reNasties.ReplaceAllString(pattern, "")
 40 | }
 41 | 
 42 | // isAtomic tells you whether a string is atomic or not.
 43 | func isAtomic(pattern string) bool {
 44 | 	// Atomic triggers don't contain any wildcards or parenthesis or anything of
 45 | 	// the sort. We don't need to test the full character set, just left brackets
 46 | 	// will do.
 47 | 	specials := []string{"*", "#", "_", "(", "[", "<", "@"}
 48 | 	for _, special := range specials {
 49 | 		if strings.Contains(pattern, special) {
 50 | 			return false
 51 | 		}
 52 | 	}
 53 | 	return true
 54 | }
 55 | 
 56 | // stringFormat formats a string.
 57 | func stringFormat(format string, text string) string {
 58 | 	if format == "uppercase" {
 59 | 		return strings.ToUpper(text)
 60 | 	} else if format == "lowercase" {
 61 | 		return strings.ToLower(text)
 62 | 	} else if format == "sentence" {
 63 | 		if len(text) > 1 {
 64 | 			return strings.ToUpper(text[0:1]) + strings.ToLower(text[1:])
 65 | 		}
 66 | 		return strings.ToUpper(text)
 67 | 	} else if format == "formal" {
 68 | 		words := strings.Split(text, " ")
 69 | 		result := []string{}
 70 | 		for _, word := range words {
 71 | 			if len(word) > 1 {
 72 | 				result = append(result, strings.ToUpper(word[0:1])+strings.ToLower(word[1:]))
 73 | 			} else {
 74 | 				result = append(result, strings.ToUpper(word))
 75 | 			}
 76 | 		}
 77 | 		return strings.Join(result, " ")
 78 | 	}
 79 | 	return text
 80 | }
 81 | 
 82 | // quotemeta escapes a string for use in a regular expression.
 83 | func quotemeta(pattern string) string {
 84 | 	unsafe := `\.+*?[^]$(){}=!<>|:`
 85 | 	for _, char := range strings.Split(unsafe, "") {
 86 | 		pattern = strings.Replace(pattern, char, fmt.Sprintf("\\%s", char), -1)
 87 | 	}
 88 | 	return pattern
 89 | }
 90 | 
 91 | // Sort a list of strings by length. Callable like:
 92 | // sort.Sort(byLength(strings)) where strings is a []string type.
 93 | // https://gobyexample.com/sorting-by-functions
 94 | type byLength []string
 95 | 
 96 | func (s byLength) Len() int {
 97 | 	return len(s)
 98 | }
 99 | func (s byLength) Swap(i, j int) {
100 | 	s[i], s[j] = s[j], s[i]
101 | }
102 | func (s byLength) Less(i, j int) bool {
103 | 	return len(s[i]) < len(s[j])
104 | }
105 | 
106 | // regSplit splits a string using a regular expression.
107 | // http://stackoverflow.com/questions/4466091/split-string-using-regular-expression-in-go
108 | func regSplit(text string, delimiter string) []string {
109 | 	reg := regexp.MustCompile(delimiter)
110 | 	indexes := reg.FindAllStringIndex(text, -1)
111 | 	lastStart := 0
112 | 	result := make([]string, len(indexes)+1)
113 | 	for i, element := range indexes {
114 | 		result[i] = text[lastStart:element[0]]
115 | 		lastStart = element[1]
116 | 	}
117 | 	result[len(indexes)] = text[lastStart:]
118 | 	return result
119 | }
120 | 
121 | /*
122 | regReplace quickly replaces a string using a regular expression.
123 | 
124 | This is a convenience function to do a RegExp-based find/replace in a
125 | JavaScript-like fashion. Example usage:
126 | 
127 | message = regReplace(message, `hello (.+?)`, "goodbye $1")
128 | 
129 | Params:
130 | 
131 | 	input: The input string to run the substitution against.
132 | 	pattern: Literal string for a regular expression pattern.
133 | 	result: String to substitute the result out for. You can use capture group
134 | 	        placeholders like $1 in this string.
135 | */
136 | func regReplace(input string, pattern string, result string) string {
137 | 	reg := regexp.MustCompile(pattern)
138 | 	match := reg.FindStringSubmatch(input)
139 | 	input = reg.ReplaceAllString(input, result)
140 | 	if len(match) > 1 {
141 | 		for i := range match[1:] {
142 | 			input = strings.Replace(input, fmt.Sprintf("$%d", i), match[i], -1)
143 | 		}
144 | 	}
145 | 	return input
146 | }
147 | 


--------------------------------------------------------------------------------