├── .circleci └── config.yml ├── .codecov.yml ├── .gitmodules ├── .golangci.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── _config.yml ├── _docs ├── .gitignore ├── archetypes │ └── default.md ├── config.yml ├── content │ ├── _index.md │ ├── basics │ │ ├── 01_minimal.md │ │ ├── 02_useful.md │ │ ├── 03_events.md │ │ ├── 04_permissions.md │ │ ├── 05_integration.md │ │ ├── 06_reactions.md │ │ └── _index.md │ ├── modules │ │ └── _index.md │ ├── quick │ │ └── _index.md │ └── recipes │ │ ├── 01_configuration.md │ │ ├── 02_slack.md │ │ ├── 03_events.md │ │ ├── 04_cron.md │ │ ├── 05_adapter.md │ │ ├── 06_memory.md │ │ ├── 07_testing.md │ │ ├── 08_docker.md │ │ └── _index.md ├── deploy.sh ├── layouts │ ├── partials │ │ ├── custom-header.html │ │ ├── footer.html │ │ ├── header.html │ │ ├── logo.html │ │ ├── menu-footer.html │ │ └── menu.html │ └── shortcodes │ │ └── include.html ├── static │ ├── css │ │ ├── syntax.css │ │ └── theme-custom.css │ └── images │ │ ├── android-icon-144x144.png │ │ ├── android-icon-192x192.png │ │ ├── android-icon-36x36.png │ │ ├── android-icon-48x48.png │ │ ├── android-icon-72x72.png │ │ ├── android-icon-96x96.png │ │ ├── apple-icon-114x114.png │ │ ├── apple-icon-120x120.png │ │ ├── apple-icon-144x144.png │ │ ├── apple-icon-152x152.png │ │ ├── apple-icon-180x180.png │ │ ├── apple-icon-57x57.png │ │ ├── apple-icon-60x60.png │ │ ├── apple-icon-72x72.png │ │ ├── apple-icon-76x76.png │ │ ├── apple-icon-precomposed.png │ │ ├── apple-icon.png │ │ ├── bot.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── favicon.ico │ │ ├── favicon.png │ │ ├── manifest.json │ │ ├── ms-icon-144x144.png │ │ ├── ms-icon-150x150.png │ │ ├── ms-icon-310x310.png │ │ └── ms-icon-70x70.png └── update_examples.sh ├── _examples ├── 01_minimal │ ├── go.mod │ ├── go.sum │ └── main.go ├── 02_useful │ ├── go.mod │ ├── go.sum │ └── main.go ├── 03_custom_events │ ├── go.mod │ ├── go.sum │ └── main.go ├── 04_auth │ ├── go.mod │ ├── go.sum │ └── main.go ├── 05_http │ ├── go.mod │ ├── go.sum │ └── main.go ├── 06_react │ ├── go.mod │ ├── go.sum │ └── main.go ├── 07_config │ ├── bot.go │ ├── config.go │ ├── go.mod │ ├── go.sum │ └── main.go ├── Makefile └── README.md ├── adapter.go ├── adapter_test.go ├── auth.go ├── auth_test.go ├── bot.go ├── bot_test.go ├── brain.go ├── brain_test.go ├── config.go ├── config_test.go ├── error.go ├── error_test.go ├── events.go ├── go.mod ├── go.sum ├── joetest ├── bot.go ├── bot_test.go ├── brain.go ├── brain_test.go ├── storage.go ├── storage_test.go ├── testing.go └── testing_test.go ├── message.go ├── message_test.go ├── reactions ├── events.go └── reactions.go ├── storage.go ├── storage_test.go └── user.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # CircleCI 2.0 configuration file for Go. 2 | # 3 | # Check https://circleci.com/docs/2.0/language-go/ for more details 4 | version: 2.1 5 | 6 | orbs: 7 | codecov: codecov/codecov@1.0.4 8 | 9 | jobs: 10 | build: 11 | docker: 12 | - image: cimg/go:1.14 13 | 14 | steps: 15 | - checkout 16 | - run: go test ./... -race -coverprofile=coverage.txt -covermode=atomic 17 | - codecov/upload: 18 | file: coverage.txt 19 | - run: cd _examples && make check 20 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | # The patch diff coverage report in pull requests is confusing at best so its 2 | # disabled. Note that normal coverage is still collected. 3 | 4 | coverage: 5 | status: 6 | patch: 7 | default: 8 | enabled: no 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "_docs/themes/learn"] 2 | path = _docs/themes/learn 3 | url = https://github.com/matcornic/hugo-theme-learn.git 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | govet: 3 | check-shadowing: false 4 | golint: 5 | min-confidence: 0 6 | gocyclo: 7 | min-complexity: 10 8 | maligned: 9 | suggest-new: true 10 | dupl: 11 | threshold: 100 12 | goconst: 13 | min-len: 2 14 | min-occurrences: 2 15 | misspell: 16 | locale: US 17 | lll: 18 | line-length: 140 19 | goimports: 20 | local-prefixes: github.com/golangci/golangci-lint 21 | gocritic: 22 | enabled-tags: 23 | - performance 24 | - style 25 | - experimental 26 | - diagnostic 27 | - opinionated 28 | disabled-checks: 29 | - unnamedResult 30 | 31 | linters: 32 | enable-all: true 33 | disable: 34 | - maligned 35 | - prealloc 36 | - interfacer 37 | - wsl 38 | - gomnd 39 | 40 | service: 41 | golangci-lint-version: 1.23.x # use the fixed version to not introduce new linters unexpectedly 42 | prepare: 43 | - GO111MODULE=on go mod vendor # required currently or golangci breaks 44 | 45 | issues: 46 | exclude-use-default: false 47 | exclude-rules: 48 | - text: "G104: Errors unhandled." 49 | path: ".+_test\\.go" 50 | linters: 51 | - gosec 52 | 53 | - text: "should have a package comment, unless it's in another file for this package" 54 | linters: 55 | - golint 56 | 57 | - text: "Using the variable on range scope `c` in function literal" 58 | path: ".+_test\\.go" 59 | linters: 60 | - scopelint 61 | 62 | - text: "`ctx` is a global variable" 63 | path: ".+_test\\.go" 64 | linters: 65 | - gochecknoglobals 66 | 67 | - text: "Function 'TestBrain_RegisterHandler' is too long" 68 | path: ".+_test\\.go" 69 | linters: 70 | - funlen 71 | 72 | - text: "Line contains TODO/BUG/FIXME" 73 | linters: 74 | - godox 75 | 76 | - text: "hugeParam" 77 | linters: 78 | - gocritic 79 | 80 | - text: "should have comment or be unexported" 81 | path: "main.go" 82 | linters: 83 | - golint 84 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 5 | 6 | **THIS SOFTWARE IS STILL IN ALPHA AND THERE ARE NO GUARANTEES REGARDING API STABILITY YET.** 7 | 8 | Once we reach the v1.0 release, this project will adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 9 | 10 | ## [Unreleased] 11 | _Nothing so far_ 12 | 13 | ## [v0.12.0] - 2024-10-09 14 | - Fix issue on Windows machines go-joe/joe#51 15 | 16 | ## [v0.11.0] - 2020-07-26 17 | - Use error wrapping from standard library instead of `github.com/pgk/errors` 18 | - Update Module to Go 1.14 19 | - Change default log level from Debug to Info 20 | - Add `WithLogLevel(…)` option for changing the default log level 21 | 22 | ## [v0.10.0] - 2019-10-26 23 | - Allow event handlers to also use scalar event types (fixes #14) 24 | - Add new `FinishEventContent(…)` function to finish event processing with multiple handlers early 25 | - **Breaking change:** Message handlers registered via `Bot.Respond(…)` and `Bot.RespondRegex(…)` now abort early if the pattern matches 26 | - This allows users to specify a default response when nothing else matches (see #25) 27 | 28 | ## [v0.9.0] - 2019-10-22 29 | - Add `Auth.Users()` and `Auth.UserPermissions(…)` functions to allow retrieving all users as well as users permissions. 30 | - Allow adapters to implement the optional `ReactionAwareAdapter` interface if they support emoji reactions 31 | - Add new `reactions` package which contains a compiled list of all officially supported reactions 32 | - Components may now return the new `ErrNotImplemented` if they do not support a feature 33 | - Add new `reactions.Event` that may be emitted by an Adapter so users can listen for it 34 | 35 | ## [v0.8.0] - 2019-04-21 36 | - Make `Auth.Grant(…)` idempotent and do not unnecessarily add smaller scopes 37 | - Support extending permissions via `Auth.Grant(…)` 38 | - Add boolean return value to `Auth.Grant(…)` to indicate if a new permission was granted 39 | - Add `Auth.Revoke(…)` to remove permissions 40 | - Fix flaky unit test TestBrain_Memory 41 | - Fix flaky TestCLIAdapter_Register test 42 | - Add new `Storage` type which manages encoding/decoding, concurrent access and logging for a `Memory` 43 | - Factor out `Memory` related logic from Brain into new `Storage` type 44 | - Removed `Brain.SetMemory(…)`, `Brain.Set(…)`, `Brain.Get(…)`, `Brain.Delete(…)`, `Brain.Memories(…)`, `Brain.Close(…)` 45 | - All functions above except `Brain.Memories(…)` are now available as functions on the `Bot.Store` field 46 | - The `Auth` type no longer uses the `Memory` interface but instead requires an instance of the new `Storage` type 47 | - Removed the `BrainMemoryEvent` without replacement 48 | - Add `joetest.Storage` type to streamline making assertions on a bots storage/memory 49 | - Change the `Memory` interface to treat values as `[]byte` and not `string` 50 | - Remove `Memories()` function from `Memory` interface and instead add a `Keys()` function 51 | - `NewConfig(…)` now requires an instance of a `Storage` 52 | 53 | ## [v0.7.0] - 2019-04-18 54 | - Add ReceiveMessageEvent.Data field to allow using the underlying message type of the adapters 55 | - Add ReceiveMessageEvent.AuthorID field to identify the author of the message 56 | - Add Message.Data field which contains a copy of the ReceiveMessageEvent.Data value 57 | - Add Message.AuthorID field which contains a copy of the ReceiveMessageEvent.AuthorID value 58 | - Add Auth.Grant(…) and Auth.CheckPermission(…) functions to allow implementing user permissions 59 | - Add Brain.Close() function to let the brain implement the Memory interface 60 | - Add Brain.SetMemory(…) function to give more control over a joe.Brain 61 | - Fix joetest.Bot.Start(…) function to return only when actually _all_ initialization is done 62 | 63 | ## [v0.6.0] - 2019-03-30 64 | - implement `NewConfig` function to allow create configuration for unit tests of modules 65 | 66 | ## [v0.5.0] - 2019-03-18 67 | - Fixed nil pointer panic in slack adapter when context is nil 68 | 69 | ## [v0.4.0] - 2019-03-18 70 | - Change type of `Module` from function to interface to allow more flexibility 71 | - Introduce new `ModuleFunc` type to migrate old modules to new interface type 72 | 73 | ## [v0.3.0] - 2019-03-17 74 | - Event handler functions can now accept interfaces instead of structs 75 | - Add new `github.com/go-joe/joe/joetest` package for unit tests 76 | - Add new `joetest.Brain` type 77 | - Add new `WithLogger(…)` option 78 | - Switch license from MIT to BSD-3-Clause 79 | - Move `TestingT` type into new `joetest` package 80 | - Move `TestBot` type into new `joetest` package and rename to `joetest.Bot` 81 | - Fixed flaky unit test of `CLIAdapter` 82 | 83 | ## [v0.2.0] - 2019-03-10 84 | - Add a lot more unit tests 85 | - Add `TestBot.Start()` and `TestBot.Stop()`to ease synchronously starting and stopping bot in unit tests 86 | - Add `TestBot.EmitSync(…)` to emit events synchronously in unit tests 87 | - Remove obsolete context argument from `NewTest(…)` function 88 | - Errors from passing invalid expressions to `Bot.Respond(…)` are now returned in `Bot.Run()` 89 | - Events are now processed in the exact same order in which they are emitted 90 | - All pending events are now processed before the brain event loop returns 91 | - Replace context argument from `Brain.HandleEvents()` with new `Brain.Shutdown()` function 92 | - `Adapter` interface was simplified again to directly use the `Brain` 93 | - Remove unnecessary `t` argument from `TestBot.EmitSync(…)` function 94 | - Deleted `Brain.Close()` because it was not actually meant to be used to close the brain and is thus confusing 95 | 96 | ## [v0.1.0] - 2019-03-03 97 | 98 | Initial release, note that Joe is still in alpha and the API is not yet considered 99 | stable before the v1.0.0 release. 100 | 101 | [Unreleased]: https://github.com/go-joe/joe/compare/v0.12.0...HEAD 102 | [v0.12.0]: https://github.com/go-joe/joe/compare/v0.11.0...v0.12.0 103 | [v0.11.0]: https://github.com/go-joe/joe/compare/v0.10.0...v0.11.0 104 | [v0.10.0]: https://github.com/go-joe/joe/compare/v0.9.0...v0.10.0 105 | [v0.9.0]: https://github.com/go-joe/joe/compare/v0.8.0...v0.9.0 106 | [v0.8.0]: https://github.com/go-joe/joe/compare/v0.7.0...v0.8.0 107 | [v0.7.0]: https://github.com/go-joe/joe/compare/v0.6.0...v0.7.0 108 | [v0.6.0]: https://github.com/go-joe/joe/compare/v0.5.0...v0.6.0 109 | [v0.5.0]: https://github.com/go-joe/joe/compare/v0.4.0...v0.5.0 110 | [v0.4.0]: https://github.com/go-joe/joe/compare/v0.3.0...v0.4.0 111 | [v0.3.0]: https://github.com/go-joe/joe/compare/v0.2.0...v0.3.0 112 | [v0.2.0]: https://github.com/go-joe/joe/compare/v0.1.0...v0.2.0 113 | [v0.1.0]: https://github.com/go-joe/joe/releases/tag/v0.1.0 114 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish 4 | to make via an [issue on Github][issues] *before* making a change. 5 | 6 | Please note we have a code of conduct. Please follow it in all your interactions 7 | with the project. 8 | 9 | ## Pull Request Process 10 | 11 | 0. Everything should start with an issue: ["Talk, then code"][talk-code] 12 | 1. Cover all your changes with unit tests, when unsure how, ask for help 13 | 2. Run all unit tests with the race detector on 14 | 3. Run the linters locally via `golangci-lint run` 15 | 4. Update the [CHANGELOG.md](CHANGELOG.md) with the changes you made (in the "Unreleased" section) 16 | 5. Consider updating the [README.md](README.md) with details of your changes. 17 | When in doubt, lets discuss the need together in the corresponding Github issue. 18 | 6. Check that all examples are up to date and do still compile with the `check` Makefile target in the `_examples` directory. 19 | 20 | ## Code of Conduct 21 | 22 | We follow the **Gopher Code of Conduct** as described at https://golang.org/conduct `\ʕ◔ϖ◔ʔ/` 23 | 24 | ### Our Pledge 25 | 26 | In the interest of fostering an open and welcoming environment, we as 27 | contributors and maintainers pledge to making participation in our project and 28 | our community a harassment-free experience for everyone, regardless of age, body 29 | size, disability, ethnicity, gender identity and expression, level of experience, 30 | nationality, personal appearance, race, religion, or sexual identity and 31 | orientation. 32 | 33 | ### Our Standards 34 | 35 | Examples of behavior that contributes to creating a positive environment 36 | include: 37 | 38 | * Using welcoming and inclusive language 39 | * Being respectful of differing viewpoints and experiences 40 | * Gracefully accepting constructive criticism 41 | * Focusing on what is best for the community 42 | * Showing empathy towards other community members 43 | 44 | Examples of unacceptable behavior by participants include: 45 | 46 | * The use of sexualized language or imagery and unwelcome sexual attention or 47 | advances 48 | * Trolling, insulting/derogatory comments, and personal or political attacks 49 | * Public or private harassment 50 | * Publishing others' private information, such as a physical or electronic 51 | address, without explicit permission 52 | * Other conduct which could reasonably be considered inappropriate in a 53 | professional setting 54 | 55 | ### Our Responsibilities 56 | 57 | Project maintainers are responsible for clarifying the standards of acceptable 58 | behavior and are expected to take appropriate and fair corrective action in 59 | response to any instances of unacceptable behavior. 60 | 61 | Project maintainers have the right and responsibility to remove, edit, or 62 | reject comments, commits, code, wiki edits, issues, and other contributions 63 | that are not aligned to this Code of Conduct, or to ban temporarily or 64 | permanently any contributor for other behaviors that they deem inappropriate, 65 | threatening, offensive, or harmful. 66 | 67 | ### Scope 68 | 69 | This Code of Conduct applies both within project spaces and in public spaces 70 | when an individual is representing the project or its community. Examples of 71 | representing a project or community include using an official project e-mail 72 | address, posting via an official social media account, or acting as an appointed 73 | representative at an online or offline event. Representation of a project may be 74 | further defined and clarified by project maintainers. 75 | 76 | ### Conflict Resolution 77 | 78 | We do not believe that all conflict is bad; healthy debate and disagreement 79 | often yield positive results. However, it is never okay to be disrespectful or 80 | to engage in behavior that violates the project’s code of conduct. 81 | 82 | If you see someone violating the code of conduct, you are encouraged to address 83 | the behavior directly with those involved. Many issues can be resolved quickly 84 | and easily, and this gives people more control over the outcome of their dispute. 85 | If you are unable to resolve the matter for any reason, or if the behavior is 86 | threatening or harassing, report it. We are dedicated to providing an environment 87 | where participants feel welcome and safe. 88 | 89 | ### Attribution 90 | 91 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 92 | available at [http://contributor-covenant.org/version/1/4][version] 93 | 94 | [issues]: https://github.com/go-joe/joe/issues 95 | [talk-code]: https://dave.cheney.net/2019/02/18/talk-then-code 96 | [homepage]: http://contributor-covenant.org 97 | [version]: http://contributor-covenant.org/version/1/4/ 98 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Friedrich Große 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
A general-purpose bot library inspired by Hubot but written in Go.
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 | Adapters let you interact with the outside world by receiving and sending messages. 16 |
17 | 18 | - Slack Adapter: https://github.com/go-joe/slack-adapter 19 | - Rocket.Chat Adapter: https://github.com/dwmunster/rocket-adapter 20 | - Telegram Adapter: https://github.com/robertgzr/joe-telegram-adapter 21 | - IRC Adapter: https://github.com/akrennmair/joe-irc-adapter 22 | - Mattermost Adapter: https://github.com/dwmunster/joe-mattermost-adapter 23 | - VK Adapter: https://github.com/tdakkota/joe-vk-adapter 24 | 25 | ### Memory Modules 26 |27 | Memory modules let you persist key value data so it can be accessed again later. 28 |
29 | 30 | - Redis Memory: https://github.com/go-joe/redis-memory 31 | - File Memory: https://github.com/go-joe/file-memory 32 | - Bolt Memory: https://github.com/robertgzr/joe-bolt-memory 33 | - SQLite Memory: https://github.com/warmans/sqlite-memory 34 | 35 | ### Other Modules 36 |37 | General purpose Modules may register handlers or emit events. 38 |
39 | 40 | - HTTP Server: https://github.com/go-joe/http-server 41 | - Cron Jobs: https://github.com/go-joe/cron 42 | -------------------------------------------------------------------------------- /_docs/content/quick/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Quick Start" 3 | slug = "quick" 4 | weight = 1 5 | pre = "1. " 6 | +++ 7 | 8 | ## Installation 9 | 10 | Joe is a software library that is packaged as [Go module][go-modules]. You can get it via: 11 | 12 | ```sh 13 | go get github.com/go-joe/joe 14 | ``` 15 | 16 | ### Your First Bot 17 | 18 | {{< include "/basics/01_minimal.md" >}} 19 | 20 | ### Run it 21 | 22 | To run the code above, save it as `main.go` and then execute it via `go run main.go`. By default Joe uses 23 | a CLI adapter which makes the bot read messages from stdin and respond on stdout. 24 | 25 | ### Next Steps 26 | 27 | Please refer to the [**Basic Usage**](/basics) section to learn how to write a 28 | full Joe Bot, using the [adapter](/modules/#chat-adapters) of your choice (e.g. Slack). If you want to dive 29 | right in and want to know what modules are currently provided by the community, 30 | then have a look at the [**Available Modules**](/modules) section. Last but not 31 | least, you can find more instructions and best practices in the [**Recipes**](/recipes) section. 32 | 33 | Happy hacking :robot: 34 | 35 | [go-modules]: https://github.com/golang/go/wiki/Modules 36 | -------------------------------------------------------------------------------- /_docs/content/recipes/01_configuration.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Bot Configuration" 3 | slug = "config" 4 | weight = 1 5 | +++ 6 | 7 | After you created your first bot you will likely have some configuration you 8 | want to pass to it when you set it up. Sometimes the available configuration 9 | might even determine what modules the bot should use. For instance if the user 10 | passed a slack token, you can use the Slack chat adapter. Otherwise you might 11 | want to fallback to the CLI adapter so you can easily develop your bot locally. 12 | 13 | This tutorial shows a pattern that allows you to implement such a use case. 14 | 15 | ### The Configuration type 16 | 17 | First we need a structure that can hold all our configurable parameters. For 18 | this tutorial we have the slack token, an HTTP listen address as well as an 19 | address to Redis. Note that all fields are optional since the bot can fallback 20 | to defaults (CLI instead of slack, im-memory instead of Redis) or disable a 21 | feature all together (e.g. no HTTP server). 22 | 23 | [embedmd]:# (../../../_examples/07_config/config.go /\/\/ Config holds all/ /return modules\n\}/) 24 | ```go 25 | // Config holds all parameters to setup a new chat bot. 26 | type Config struct { 27 | SlackToken string // slack token, if empty we fallback to the CLI 28 | HTTPListen string // optional HTTP listen address to receive callbacks 29 | RedisAddr string // optional address to store keys in Redis 30 | } 31 | 32 | // Modules creates a list of joe.Modules that can be used with this configuration. 33 | func (conf Config) Modules() []joe.Module { 34 | var modules []joe.Module 35 | 36 | if conf.SlackToken != "" { 37 | modules = append(modules, slack.Adapter(conf.SlackToken)) 38 | } 39 | 40 | if conf.HTTPListen != "" { 41 | modules = append(modules, joehttp.Server(conf.HTTPListen)) 42 | } 43 | 44 | if conf.RedisAddr != "" { 45 | modules = append(modules, redis.Memory(conf.RedisAddr)) 46 | } 47 | 48 | return modules 49 | } 50 | ``` 51 | 52 | We also want to define our own Bot type on which we can define our handlers. To 53 | create a new instance we will also provide a `New(…)` function which accepts the 54 | previously defined configuration type. 55 | 56 | [embedmd]:# (../../../_examples/07_config/bot.go /type Bot/ /return b\n\}/) 57 | ```go 58 | type Bot struct { 59 | *joe.Bot // Anonymously embed the joe.Bot type so we can use its functions easily. 60 | conf Config // You can keep other fields here as well. 61 | } 62 | 63 | func New(conf Config) *Bot { 64 | b := &Bot{ 65 | Bot: joe.New("joe", conf.Modules()...), 66 | } 67 | 68 | // Define any custom event and message handlers here 69 | b.Brain.RegisterHandler(b.GitHubCallback) 70 | b.Respond("do stuff", b.DoStuffCommand) 71 | 72 | return b 73 | } 74 | ``` 75 | 76 | From here on you can extend the `New` function to do other setup as well such as 77 | connecting to third-party APIs or setting up cron jobs. If you want to enforce 78 | the existence of some configuration parameters or you generally want to validate 79 | the passed parameters you can do this via a new `Config.Validate()` function that 80 | is called before creating a new Bot: 81 | 82 | [embedmd]:# (../../../_examples/07_config/config.go /func \(conf Config\) Validate/ $) 83 | ```go 84 | func (conf Config) Validate() error { 85 | if conf.HTTPListen == "" { 86 | return errors.New("missing HTTP listen address") 87 | } 88 | return nil 89 | } 90 | ``` 91 | 92 | [embedmd]:# (../../../_examples/07_config/bot.go /func New2/ /return b, nil\n\}/) 93 | ```go 94 | func New2(conf Config) (*Bot, error) { 95 | if err := conf.Validate(); err != nil { 96 | return nil, fmt.Errorf("invalid configuration: %w", err) 97 | } 98 | 99 | b := &Bot{ 100 | Bot: joe.New("joe", conf.Modules()...), 101 | } 102 | 103 | // Define any custom event and message handlers here 104 | b.Brain.RegisterHandler(b.GitHubCallback) 105 | b.Respond("do stuff", b.DoStuffCommand) 106 | 107 | return b, nil 108 | } 109 | ``` 110 | -------------------------------------------------------------------------------- /_docs/content/recipes/02_slack.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Advanced Slack Features" 3 | slug = "slack" 4 | weight = 2 5 | +++ 6 | 7 | One of the first questions of new users of Joe is, whether feature _X_, which 8 | they know from Slack, is supported in Joe as well. Usually the answer is, that 9 | we want to keep the generic `Adapter` interface minimal and thus we cannot add 10 | direct support for this feature since it would make it very hard to port it over 11 | to other chat adapters (e.g. to IRC). 12 | 13 | Luckily however there is a pattern that you can use which allows you to use all 14 | features of the Chat adapter of your choice, without requiring the Joe library 15 | to know about them. 16 | 17 | First you need to define a custom type for your bot. This is a useful pattern in 18 | any case since it simplifies writing your handlers when you need access to other 19 | Joe features such as memory persistence or authentication. 20 | 21 | ```go 22 | package main 23 | 24 | import ( 25 | "github.com/go-joe/joe" 26 | "github.com/nlopes/slack" 27 | ) 28 | 29 | // Bot is your own custom bot type. It has access to all features of the slack 30 | // API in its handler functions. 31 | type Bot struct { 32 | *joe.Bot // Anonymously embed the joe.Bot type so we can use its functions easily. 33 | Slack *slack.Client 34 | } 35 | ``` 36 | 37 | To create a new instance of your bot you probably want to implement a `New(…)` 38 | function. Details on how to configure your bot when you have multiple parameters 39 | can be found in the [Bot Configuration](/recipes/config) recipe. 40 | 41 | ```go 42 | import ( 43 | "github.com/go-joe/joe" 44 | joeSlack "github.com/go-joe/slack-adapter/v2" 45 | "github.com/nlopes/slack" 46 | ) 47 | 48 | func New(slackToken string) *Bot { 49 | b := &Bot{ 50 | Bot: joe.New("joe", joeSlack.Adapter(slackToken)), 51 | Slack: slack.New(slackToken), 52 | } 53 | 54 | b.Respond("do stuff", b.DoStuffCommand) 55 | 56 | // other setup may happen here as well 57 | 58 | return b 59 | } 60 | ``` 61 | 62 | Now when you have a handler that should use a slack specific feature you can 63 | define it as function on your own `Bot` type and use the `Slack` field 64 | to access the client directly. 65 | 66 | ```go 67 | func (b *Bot) DoStuffCommand(msg joe.Message) error { 68 | if b.Slack == nil { 69 | // In case this command does not even make sense without your custom 70 | // functionality you may want to return early in the command. 71 | // Having such a check is only useful if you actually create the Bot such 72 | // that users can create a new instance even if they do not provide a slack token. 73 | return msg.RespondE("Cannot do stuff because Slack integration is not enabled") 74 | } 75 | 76 | // Access to specific functionality is accessible via an extra field on the Bot. 77 | // This example uses a specific feature of Slack to style the message using 78 | // multiple message blocks. Of course in practice you have access to all features 79 | // exposed to you via the Slack Go client. 80 | var blocks []slack.Block 81 | blocks = append(blocks, slack.NewSectionBlock("Foo!", nil, nil)) 82 | blocks = append(blocks, slack.NewDividerBlock()) 83 | blocks = append(blocks, b.createMessageBlocks()...) 84 | blocks = append(blocks, slack.NewDividerBlock()) 85 | 86 | _, _, err := b.Slack.PostMessageContext(ctx, channel, 87 | slack.MsgOptionBlocks(blocks...), 88 | slack.MsgOptionPostMessageParameters( 89 | slack.PostMessageParameters{ 90 | LinkNames: 1, 91 | Parse: "full", 92 | AsUser: true, 93 | }, 94 | ), 95 | ) 96 | 97 | // You can still use all regular adapter features easily. 98 | msg.Respond("OK") 99 | return nil 100 | } 101 | ``` 102 | 103 | Of course you can use the same pattern to access specific features of other chat 104 | adapters as well. 105 | 106 | In case you need to close your chat adapter client explicitly when the bot is 107 | shutting down, you can register a shutdown event handler function like this: 108 | 109 | ```go 110 | 111 | func New(slackToken string) *Bot { 112 | b := &Bot{ 113 | Bot: joe.New("joe", joeSlack.Adapter(slackToken)), 114 | Slack: slack.New(slackToken), 115 | } 116 | 117 | // other setup tasks are usually here 118 | 119 | b.Brain.RegisterHandler(b.Shutdown) 120 | 121 | return b 122 | } 123 | 124 | 125 | 126 | func (b *Bot) Shutdown(joe.ShutdownEvent) { 127 | // TODO: implement your cleanup logic 128 | } 129 | ``` 130 | -------------------------------------------------------------------------------- /_docs/content/recipes/03_events.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "The Event System" 3 | slug = "events" 4 | weight = 3 5 | +++ 6 | 7 | Joe is powered by an asynchronous event system which is also used internally when 8 | you register your message handlers using the [`Bot.Respond(…)`][1] function. 9 | What happens is that the chat Adapter emits a [`joe.ReceiveMessageEvent`][2] for each 10 | incoming message. The handler function that you registered is executed if the bot 11 | sees such an event with a message text that matches your regular expression. 12 | 13 | ```go 14 | func (b *Bot) RespondRegex(expr string, fun func(Message) error) { 15 | // other code omitted for brevity … 16 | 17 | b.Brain.RegisterHandler(func(ctx context.Context, evt ReceiveMessageEvent) error { 18 | matches := regex.FindStringSubmatch(evt.Text) 19 | if len(matches) == 0 { 20 | return nil 21 | } 22 | 23 | // If the event text matches our regular expression we can already mark 24 | // the event context as done so the Brain does not run any other handlers 25 | // that might match the received message. 26 | FinishEventContent(ctx) 27 | 28 | return fun(Message{ 29 | ID: evt.ID, 30 | Text: evt.Text, 31 | // and other many fields 32 | }) 33 | }) 34 | } 35 | ``` 36 | 37 | This means that you could also register message handlers directly on the 38 | `joe.ReceiveMessageEvent` yourself if you wanted to (e.g. if you want to get 39 | notified for each incoming message). 40 | 41 | ### The Brain 42 | 43 | This event system is implemented in the [`joe.Brain`][4]. When it sees a new event it 44 | finds all registered event handlers for the event type and then executes them 45 | all in the same sequence in which they have been registered. 46 | 47 | By default _all_ matching handlers are executed but you can prevent other 48 | handlers from being executed after your handler, by calling the 49 | [`joe.FinishEventContent()`][3] function. As you can see in the code snippet above, 50 | this is also what happens automatically to message handlers you register via `Bot.Respond(…)`. 51 | 52 | The Brain itself also emits two events to signal when it is starting up and when it 53 | is shutting down: 54 | 55 | - `joe.InitEvent` 56 | - `joe.ShutdownEvent` 57 | 58 | You can use those events both in unit tests as well as your own logic to hook into 59 | the lifecycle of the bot. 60 | 61 | ### Chaining events 62 | 63 | The event system is also useful for other kinds of events. For instance, as you 64 | can see in the next recipes, this is how [cron jobs](/recipes/cron) are 65 | implemented in Joe. Generally speaking, there are _sources_ that can trigger 66 | events and there are _handlers_ that get executed when the matching event is 67 | emitted. Since handlers can also be event sources, this means you can chain 68 | events asynchronously. 69 | 70 | For example we can setup a handler that should be executed for each incoming HTTP 71 | request. It should check if the request came from GitLab and if so, decode the 72 | request body it into another event type: 73 | 74 | ```go 75 | import ( 76 | "fmt" 77 | 78 | "encoding/json" 79 | joehttp "github.com/go-joe/http-server" 80 | ) 81 | 82 | func (b *Bot) HTTPCallback(req joehttp.RequestEvent) error { 83 | if req.Header.Get("X-Gitlab-Event") == "" { 84 | return nil 85 | } 86 | 87 | var event GitLabEvent 88 | err := json.Unmarshal(req.Body, &event) 89 | if err != nil { 90 | return fmt.Errorf("failed to unmarshal gitlab event as JSON: %w", err) 91 | } 92 | 93 | b.Brain.Emit(event) 94 | return nil 95 | } 96 | ``` 97 | 98 | Now we can define another handler that will be executed on the `GitLabEvent` type: 99 | 100 | ```go 101 | func (b *Bot) GitLabCallback(event GitLabEvent) error { 102 | b.Logger.Info("Received gitlab event", 103 | zap.String("event_type", event.EventType), 104 | zap.String("object_kind", event.ObjectKind), 105 | zap.String("action", event.ObjectAttributes.Action), 106 | zap.String("project", event.Project.PathWithNamespace), 107 | zap.String("title", event.ObjectAttributes.Title), 108 | zap.String("url", event.ObjectAttributes.URL), 109 | ) 110 | 111 | switch event.EventType { 112 | case "merge_request": 113 | return b.HandleMergeRequestEvent(event) 114 | 115 | case "note": 116 | return b.HandleGitlabNoteEvent(event) 117 | 118 | default: 119 | b.Logger.Info("Unknown event from gitlab", zap.String("object_kind", event.ObjectKind)) 120 | return nil 121 | } 122 | } 123 | ``` 124 | 125 | Finally to make this all work together, we need to register the two handlers 126 | when we setup the bot: 127 | 128 | ```go 129 | func New(conf Config) *Bot { 130 | b := &Bot{ 131 | Bot: joe.New("joe", conf.Modules()...), 132 | } 133 | 134 | // Define any custom event and message handlers here 135 | b.Brain.RegisterHandler(b.HTTPCallback) 136 | b.Brain.RegisterHandler(b.GitLabCallback) 137 | 138 | return b 139 | } 140 | ``` 141 | 142 | If you want to learn more about how the Brain works internally, start by looking 143 | at [the GoDoc][4] and then [the code itself][5]. 144 | 145 | Happy event hacking :robot:. 146 | 147 | [1]: https://godoc.org/github.com/go-joe/joe#Bot.Respond 148 | [2]: https://godoc.org/github.com/go-joe/joe#ReceiveMessageEvent 149 | [3]: https://godoc.org/github.com/go-joe/joe#FinishEventContent 150 | [4]: https://godoc.org/github.com/go-joe/joe#Brain 151 | [5]: https://github.com/go-joe/joe/blob/master/brain.go 152 | -------------------------------------------------------------------------------- /_docs/content/recipes/04_cron.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Cron Jobs" 3 | slug = "cron" 4 | weight = 4 5 | +++ 6 | 7 | The [Cron module][module] allows you to run arbitrary functions or [emit events](/recipes/events) 8 | on a schedule using cron expressions or a specific time interval. 9 | 10 | For instance you might want to trigger a function that should be executed every 11 | day at midnight. 12 | 13 | ```go 14 | import "github.com/go-joe/cron" 15 | 16 | func main() { 17 | b := joe.New("example-bot", 18 | cron.ScheduleEvent("0 0 * * *"), 19 | ) 20 | 21 | err := b.Run() 22 | if err != nil { 23 | b.Logger.Fatal(err.Error()) 24 | } 25 | } 26 | 27 | func AtMidnight(cron.Event) { 28 | // do something spooky 👻 29 | } 30 | ``` 31 | 32 | The `cron.ScheduleEvent` will emit a `cron.Event` at `"0 0 * * *"` which is the 33 | [cron expression][cron] for "every day at midnight". 34 | 35 | If you find cron expressions hard to read, you can also use the `cron.ScheduleEventEvery(…)` 36 | function which accepts a `time.Duration` as the first argument. 37 | 38 | ```go 39 | import ( 40 | "time" 41 | "github.com/go-joe/cron" 42 | ) 43 | 44 | func main() { 45 | b := joe.New("example-bot", 46 | cron.ScheduleEventEvery(time.Hour), 47 | ) 48 | 49 | err := b.Run() 50 | if err != nil { 51 | b.Logger.Fatal(err.Error()) 52 | } 53 | } 54 | 55 | func EveryHour(cron.Event) { 56 | // do something funky 🤘 57 | } 58 | ``` 59 | 60 | If you have multiple cron jobs running (e.g. the one at midnight and the hourly one) 61 | you now have the problem that your two registered functions get executed for _every_ 62 | `cron.Event`. Instead what you actually want is that `EveryHour` is executed 63 | independently of `AtMidnight`. This can be fixed by emitting your own custom event types: 64 | 65 | ```go 66 | type MidnightEvent struct{} 67 | 68 | type HourEvent struct {} 69 | 70 | func main() { 71 | b := joe.New("example-bot", 72 | cron.ScheduleEvent("0 0 * * *", MidnightEvent{}), 73 | cron.ScheduleEventEvery(time.Hour, HourEvent{}), 74 | ) 75 | 76 | err := b.Run() 77 | if err != nil { 78 | b.Logger.Fatal(err.Error()) 79 | } 80 | } 81 | 82 | func AtMidnight(MidnightEvent) { 83 | // 👻👻👻 84 | } 85 | 86 | func EveryHour(HourEvent) { 87 | // ⏰⏰⏰ 88 | } 89 | ``` 90 | 91 | Emitting your own events can also be useful if you want to trigger existing handlers 92 | in more than one way (e.g. directly and via cron): 93 | 94 | ```go 95 | type DoStuffEvent struct {} 96 | 97 | func main() { 98 | b := joe.New("example-bot", 99 | cron.ScheduleEventEvery(time.Hour, DoStuffEvent{}), 100 | ) 101 | 102 | b.Respond("do it", func(joe.Message) error { 103 | b.Brain.Emit(DoStuffEvent{}) 104 | }) 105 | 106 | err := b.Run() 107 | if err != nil { 108 | b.Logger.Fatal(err.Error()) 109 | } 110 | } 111 | 112 | func DoStuff(DoStuffEvent) { 113 | // Do this every hour and when the user asks us to 114 | } 115 | ``` 116 | 117 | In practice you will likely want to execute functions directly without having to 118 | create those extra types. This can be done with the `cron.ScheduleFunc(…)` and 119 | `cron.ScheduleFuncEvery(…)` functions which work like the functions we saw early 120 | just with closures instead of event types: 121 | 122 | ```go 123 | package main 124 | 125 | import ( 126 | "time" 127 | "github.com/go-joe/joe" 128 | "github.com/go-joe/cron" 129 | ) 130 | 131 | type MyEvent struct {} 132 | 133 | func main() { 134 | b := joe.New("example-bot", 135 | // emit a cron.Event once every day at midnight 136 | cron.ScheduleEvent("0 0 * * *"), 137 | 138 | // emit your own custom event every day at 09:00 139 | cron.ScheduleEvent("0 9 * * *", MyEvent{}), 140 | 141 | // cron expressions can be hard to read and might be overkill 142 | cron.ScheduleEventEvery(time.Hour, MyEvent{}), 143 | 144 | // sometimes its easier to use a function 145 | cron.ScheduleFunc("0 9 * * *", func() { /* TODO */ }), 146 | 147 | // functions can also be scheduled on simple intervals 148 | cron.ScheduleFuncEvery(5*time.Minute, func() { /* TODO */ }), 149 | ) 150 | 151 | err := b.Run() 152 | if err != nil { 153 | b.Logger.Fatal(err.Error()) 154 | } 155 | } 156 | ``` 157 | 158 | [module]: https://github.com/go-joe/cron 159 | [cron]: https://en.wikipedia.org/wiki/Cron#Overview 160 | -------------------------------------------------------------------------------- /_docs/content/recipes/05_adapter.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Implement a new Adapter" 3 | slug = "adapter" 4 | weight = 5 5 | +++ 6 | 7 | Adapters let you interact with the outside world by receiving and sending messages. 8 | Joe currently has the following seven Adapter implementations: 9 | 10 | - CLI Adapter: https://github.com/go-joe/joe 11 | - Slack Adapter: https://github.com/go-joe/slack-adapter 12 | - Rocket.Chat Adapter: https://github.com/dwmunster/rocket-adapter 13 | - Telegram Adapter: https://github.com/robertgzr/joe-telegram-adapter 14 | - IRC Adapter: https://github.com/akrennmair/joe-irc-adapter 15 | - Mattermost Adapter: https://github.com/dwmunster/joe-mattermost-adapter 16 | - VK Adapter: https://github.com/tdakkota/joe-vk-adapter 17 | 18 | If you want to integrate with a chat service that is not listed above, you can 19 | write your own Adapter implementation. 20 | 21 | ### Adapters are Modules 22 | 23 | Firstly, your adapter should be available as [`joe.Module`][module] so it can 24 | easily be integrated into the bot via the [`joe.New(…)`][new] function. 25 | 26 | The `Module` interface looks like this: 27 | 28 | ```go 29 | // A Module is an optional Bot extension that can add new capabilities such as 30 | // a different Memory implementation or Adapter. 31 | type Module interface { 32 | Apply(*Config) error 33 | } 34 | ``` 35 | 36 | To easily implement a Module without having to declare an `Apply` function on 37 | your chat adapter type, you can use the `joe.ModuleFunc` type. For instance the 38 | Slack adapter uses the following, to implement it's `Adapter(…)` function: 39 | 40 | ```go 41 | // Adapter returns a new Slack adapter as joe.Module. 42 | // 43 | // Apart from the typical joe.ReceiveMessageEvent event, this adapter also emits 44 | // the joe.UserTypingEvent. The ReceiveMessageEvent.Data field is always a 45 | // pointer to the corresponding github.com/nlopes/slack.MessageEvent instance. 46 | func Adapter(token string, opts ...Option) joe.Module { 47 | return joe.ModuleFunc(func(joeConf *joe.Config) error { 48 | conf, err := newConf(token, joeConf, opts) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | a, err := NewAdapter(joeConf.Context, conf) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | joeConf.SetAdapter(a) 59 | return nil 60 | }) 61 | } 62 | ``` 63 | 64 | The passed `*joe.Config` parameter can be used to lookup general options such as 65 | the `context.Context` used by the bot. Additionally you can create a named 66 | logger via the `Config.Logger(…)` function and you can register extra handlers 67 | or [emit events](/recipes/events) via the `Config.EventEmitter()` function. 68 | 69 | Most importantly for an Adapter implementation however is, that it finally needs 70 | to register itself via the `Config.SetAdapter(…)` function. 71 | 72 | By defining an `Adapter(…)` function in your package, it is now possible to use 73 | your adapter as Module passed to `joe.New(…)`. Additionally your `NewAdapter(…)` 74 | function is useful to directly create a new adapter instance which can be used 75 | during unit tests. Last but not least, the options pattern has proven useful in 76 | this kind of setup and is considered good practice when writing modules in general. 77 | 78 | ### The Adapter Interface 79 | 80 | ```go 81 | // An Adapter connects the bot with the chat by enabling it to receive and send 82 | // messages. Additionally advanced adapters can emit more events than just the 83 | // ReceiveMessageEvent (e.g. the slack adapter also emits the UserTypingEvent). 84 | // All adapter events must be setup in the RegisterAt function of the Adapter. 85 | // 86 | // Joe provides a default CLIAdapter implementation which connects the bot with 87 | // the local shell to receive messages from stdin and print messages to stdout. 88 | type Adapter interface { 89 | RegisterAt(*Brain) 90 | Send(text, channel string) error 91 | Close() error 92 | } 93 | ``` 94 | 95 | The most straight forwards function to implement should be the `Send(…)` and 96 | `Close(…)` functions. The `Send` function should output the given text to the 97 | specified channel as the Bot. The initial connection and authentication to send 98 | these messages should have been setup earlier by your `Adapter` function as 99 | shown above. When the bot shuts down, it will call the `Close()` function of 100 | your adapter so you can terminate your connection and release all resources you 101 | have opened. 102 | 103 | In order to also _receive_ messages and pass them to Joe's event handler you 104 | need to implement a `RegisterAt(*joe.Brain)` function. This function gets called 105 | during the setup of the bot and allows the adapter to directly access to the Brain. 106 | This function must not block and thus will typically spawn a new goroutine which 107 | should be stopped when the `Close()` function of your adapter implementation is 108 | called. 109 | 110 | In this goroutine you should listen for new messages from your chat application 111 | (e.g. via a callback or polling it). When a new message is received, you need to 112 | emit it as `joe.ReceiveMessageEvent` to the brain. 113 | 114 | E.g. for the Slack adapter, this looks like this: 115 | 116 | ```go 117 | func (a *BotAdapter) handleMessageEvent(ev *slack.MessageEvent, brain *joe.Brain) { 118 | // Check if the message comes from ourselves. 119 | if ev.User == a.userID { 120 | // Message is from us, ignore it! 121 | return 122 | } 123 | 124 | // Check if we have a direct message, or standard channel post. 125 | selfLink := a.userLink(a.userID) 126 | direct := strings.HasPrefix(ev.Msg.Channel, "D") 127 | if !direct && !strings.Contains(ev.Msg.Text, selfLink) { 128 | // Message is not meant for us! 129 | return 130 | } 131 | 132 | text := strings.TrimSpace(strings.TrimPrefix(ev.Text, selfLink)) 133 | brain.Emit(joe.ReceiveMessageEvent{ 134 | Text: text, 135 | Channel: ev.Channel, 136 | ID: ev.Timestamp, // slack uses the message timestamps as identifiers within the channel 137 | AuthorID: ev.User, 138 | Data: ev, 139 | }) 140 | } 141 | ``` 142 | 143 | In the snippet above you can see some of the common pitfalls: 144 | 145 | - the adapter should ignore it's own messages or it risks ending up in an infinitive loop 146 | - the adapter must make sure the message is actually intended for the bot 147 | - maybe the message needs to be trimmed 148 | - you should try and fill all fields of the `joe.ReceiveMessageEvent` 149 | 150 | ### Optional Interfaces 151 | 152 | Currently there is only a single optional interface that can be implemented by an 153 | Adapter, which is the `joe.ReactionAwareAdapter`: 154 | 155 | ```go 156 | // ReactionAwareAdapter is an optional interface that Adapters can implement if 157 | // they support reacting to messages with emojis. 158 | type ReactionAwareAdapter interface { 159 | React(reactions.Reaction, Message) error 160 | } 161 | ``` 162 | 163 | This interface is meant for chat adapters that have emoji support to attach 164 | reactions to previously received messages (e.g. :thumbsup: or :robot:). 165 | 166 | ### Getting Help 167 | 168 | Generally writing an adapter should not be very hard but it's a good idea to 169 | look at the other adapter implementations to get a better understanding of how 170 | to implement your own. If you have questions or need help, simply open an 171 | issue at the [Joe repository at GitHub](https://github.com/go-joe/joe/issues/new). 172 | 173 | Happy adaptering :robot::tada: 174 | 175 | [module]: https://godoc.org/github.com/go-joe/joe#Module 176 | [new]: https://godoc.org/github.com/go-joe/joe#New 177 | -------------------------------------------------------------------------------- /_docs/content/recipes/06_memory.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Implement a new Memory" 3 | slug = "memory" 4 | weight = 6 5 | +++ 6 | 7 | Memory modules let you persist key value data so it can be accessed again later. 8 | Joe currently has the following five Memory implementations: 9 | 10 | - In-Memory: https://github.com/go-joe/joe 11 | - Redis Memory: https://github.com/go-joe/redis-memory 12 | - File Memory: https://github.com/go-joe/file-memory 13 | - Bolt Memory: https://github.com/robertgzr/joe-bolt-memory 14 | - SQLite Memory: https://github.com/warmans/sqlite-memory 15 | 16 | If you want to use some other system or technology to let your bot to persist 17 | records, you can write your own Memory implementation. 18 | 19 | ### Memories are Modules 20 | 21 | Firstly, your memory should be available as [`joe.Module`][module] so it can 22 | easily be integrated into the bot via the [`joe.New(…)`][new] function. 23 | 24 | The `Module` interface looks like this: 25 | 26 | ```go 27 | // A Module is an optional Bot extension that can add new capabilities such as 28 | // a different Memory implementation or Adapter. 29 | type Module interface { 30 | Apply(*Config) error 31 | } 32 | ``` 33 | 34 | To easily implement a Module without having to declare an `Apply` function on 35 | your Memory type, you can use the `joe.ModuleFunc` type. For instance the 36 | Redis memory uses the following, to implement it's `Memory(…)` function: 37 | 38 | ```go 39 | // Memory returns a joe Module that configures the bot to use Redis as key-value 40 | // store. 41 | func Memory(addr string, opts ...Option) joe.Module { 42 | return joe.ModuleFunc(func(joeConf *joe.Config) error { 43 | conf := Config{Addr: addr} 44 | for _, opt := range opts { 45 | err := opt(&conf) 46 | if err != nil { 47 | return err 48 | } 49 | } 50 | 51 | if conf.Logger == nil { 52 | conf.Logger = joeConf.Logger("redis") 53 | } 54 | 55 | memory, err := NewMemory(conf) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | joeConf.SetMemory(memory) 61 | return nil 62 | }) 63 | } 64 | ``` 65 | 66 | The passed `*joe.Config` parameter can be used to lookup general options such as 67 | the `context.Context` used by the bot. Additionally you can create a named 68 | logger via the `Config.Logger(…)` function. 69 | 70 | Most importantly for a Memory implementation however is, that it finally needs 71 | to register itself via the `Config.SetMemory(…)` function. 72 | 73 | By defining a `Memory(…)` function in your package, it is now possible to use 74 | your memory as Module passed to `joe.New(…)`. Additionally your `NewMemory(…)` 75 | function is useful to directly create a new memory instance which can be used 76 | during unit tests. Last but not least, the options pattern has proven useful in 77 | this kind of setup and is considered good practice when writing modules in general. 78 | 79 | ### The Memory Interface 80 | 81 | ```go 82 | // The Memory interface allows the bot to persist data as key-value pairs. 83 | // The default implementation of the Memory is to store all keys and values in 84 | // a map (i.e. in-memory). Other implementations typically offer actual long term 85 | // persistence into a file or to Redis. 86 | type Memory interface { 87 | Set(key string, value []byte) error 88 | Get(key string) ([]byte, bool, error) 89 | Delete(key string) (bool, error) 90 | Keys() ([]string, error) 91 | Close() error 92 | } 93 | ``` 94 | 95 | Looking at the interface you can see that the Memory must implement all CRUD 96 | operations (Create, Read, Update & Delete) as well as a function to retrieve all 97 | previously stored keys and a function to close the connection and release any 98 | held resources. 99 | 100 | ### Storage encoding 101 | 102 | Each Memory implementation manages key value data, where the keys are strings 103 | and the values are only bytes. In the event handlers, the memory can be 104 | accessed via the bots concrete `Storage` type which accepts values as interfaces 105 | and provides read access by unmarshalling values back into types via a pointer, 106 | very much like you may know already from Go's standard library (e.g. `encoding/json`). 107 | 108 | To encode the given `interface{}` values into the `[]byte` that is passed to your 109 | Memory implementation, the storage also has a `MemoryEncoder` which is defined as: 110 | 111 | ```go 112 | // A MemoryEncoder is used to encode and decode any values that are stored in 113 | // the Memory. The default implementation that is used by the Storage uses a 114 | // JSON encoding. 115 | type MemoryEncoder interface { 116 | Encode(value interface{}) ([]byte, error) 117 | Decode(data []byte, target interface{}) error 118 | } 119 | ``` 120 | 121 | If you want, you can change the encoding from JSON to something else (e.g. to 122 | implement encryption) by providing a type that implements this interface and 123 | then using the `joeConf.SetMemoryEncoder(…)` function in your Module during the setup. 124 | 125 | ### Getting Help 126 | 127 | Generally writing a new Memory implementation should not be very hard but it's a 128 | good idea to look at the other Memory implementations to get a better 129 | understanding of how to implement your own. If you have questions or need help, 130 | simply open an issue at the [Joe repository at GitHub](https://github.com/go-joe/joe/issues/new). 131 | 132 | Happy coding :robot: 133 | 134 | [module]: https://godoc.org/github.com/go-joe/joe#Module 135 | [new]: https://godoc.org/github.com/go-joe/joe#New 136 | -------------------------------------------------------------------------------- /_docs/content/recipes/07_testing.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Unit Tests" 3 | slug = "testing" 4 | hidden = true 5 | weight = 7 6 | +++ 7 | 8 |UNDER CONSTRUCTION
9 | -------------------------------------------------------------------------------- /_docs/content/recipes/08_docker.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Deploying via Docker" 3 | slug = "docker" 4 | weight = 8 5 | +++ 6 | 7 | {{% notice note %}} 8 | Like all recipes, this tutorial only shows one way to do things. 9 | If you think you can improve the example please open an issue or pull request at 10 | [our GitHub repository](https://github.com/go-joe/joe/issues). 11 | {{% /notice %}} 12 | 13 | At some point you will want to deploy your Bot to a server so it can run 24/7. 14 | Chances are you already deploy other services using Docker so it makes sense to 15 | also bundle and deploy Joe in that way. This recipe gives you some advice on how 16 | that can be done but there is typically more than one way to achieve this. 17 | 18 | Let's start with a simple `Dockerfile`: 19 | 20 | ```docker 21 | # We use busybox as base image to create very small Docker images. 22 | # Before you use this, you should check if there is a newer version you want to use. 23 | FROM busybox:1.31.1 24 | 25 | # We add the minmal dependencies the Go runtime requires to be able to run your 26 | # binary. Depending on your code you might not even have to use those but we 27 | # include them for completeness. 28 | ADD build/zoneinfo.zip /usr/local/go/lib/time/zoneinfo.zip 29 | ADD build/*.crt /etc/ssl/certs/ 30 | 31 | # Finally we add the binary that contains your bot. Here we called it "my-bot" 32 | # but you can choose the name freely as long as you update it in all three places 33 | # below as well as in the Makefile below. 34 | ADD build/my-bot /bin/my-bot 35 | 36 | ENTRYPOINT ["my-bot"] 37 | ``` 38 | 39 | To build this image we create the following `Makefile`: 40 | 41 | ```makefile 42 | SHELL=/bin/bash 43 | 44 | IMAGE=example.com/my-bot # replace with your own image name 45 | VERSION=0.42 # Your image version. Make sure to update this to deploy a new version 46 | 47 | .PHONY: build 48 | build: 49 | mkdir -p build 50 | cp "$$GOROOT/lib/time/zoneinfo.zip" build/zoneinfo.zip 51 | cp /etc/ssl/certs/*.crt ./build/ 52 | CGO_ENABLED=0 go build -v -tags netgo -ldflags "-extldflags '-static' -w" -o build/my-bot 53 | docker build -t $(IMAGE):$(VERSION) . 54 | rm -Rf build 55 | 56 | .PHONY: push 57 | push: 58 | docker push $(IMAGE):$(VERSION) 59 | ``` 60 | 61 | This Makefile should be put next to your code. It will create a temporary `build` 62 | directory, copy over timezone and certificate files and then statically build your 63 | bot. 64 | 65 | The image can be built and pushed in a single command via `make build push`. 66 | 67 | Finally if you want to deploy the Bot to Kubernetes you can use the following Manifest: 68 | 69 | ```yaml 70 | kind: Deployment 71 | apiVersion: apps/v1beta1 72 | metadata: 73 | name: my-bot 74 | spec: 75 | replicas: 1 76 | strategy: 77 | type: Recreate 78 | template: 79 | metadata: { labels: { app: my-bot } } 80 | spec: 81 | containers: 82 | - name: bot 83 | image: example.com/my-bot:0.42 # Make sure to update the image name and version according to your Makefile 84 | imagePullPolicy: Always 85 | ports: 86 | - containerPort: 80 87 | env: 88 | - name: "SLACK_TOKEN" 89 | value: "…" 90 | - name: "HTTP" 91 | value: ":80" 92 | # Add other environment settings you may need. Consider using Kubernetes secrets for the credentials. 93 | --- 94 | 95 | kind: Service 96 | apiVersion: v1 97 | metadata: { name: my-bot } 98 | spec: 99 | selector: { app: my-bot } 100 | ports: 101 | - port: 80 102 | 103 | --- 104 | 105 | kind: Ingress 106 | apiVersion: extensions/v1beta1 107 | metadata: 108 | name: my-bot 109 | spec: 110 | rules: 111 | - host: my-bot.example.com 112 | http: 113 | paths: 114 | - backend: { serviceName: my-bot, servicePort: 80} 115 | ``` 116 | -------------------------------------------------------------------------------- /_docs/content/recipes/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Recipes" 3 | weight = 4 4 | pre = "4. " 5 | +++ 6 | 7 | This section contains best practices and more detailed instructions on how to 8 | build and deploy your own chat bot. 9 | 10 | You can either browse these, using the items in the navigation menu or review them 11 | one by one using your keyboards arrow keys 12 | & . 13 | -------------------------------------------------------------------------------- /_docs/deploy.sh: -------------------------------------------------------------------------------- 1 | hugo && rsync -avz --delete public/ joe-bot.net:/var/lib/caddy/joe-bot.net 2 | -------------------------------------------------------------------------------- /_docs/layouts/partials/custom-header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | -------------------------------------------------------------------------------- /_docs/layouts/partials/footer.html: -------------------------------------------------------------------------------- 1 | {{ if .Params.chapter }} 2 | 3 | {{ end }} 4 | 5 | {{ partial "custom-comments.html" . }} 6 | 7 | 8 | 53 | 54 | 55 | 56 |