├── .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 |

Joe Bot :robot:

2 |

A general-purpose bot library inspired by Hubot but written in Go.

3 |

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |

12 | 13 | --- 14 | 15 | Joe is a library used to write chat bots in [the Go programming language][go]. 16 | It is very much inspired by the awesome [Hubot][hubot] framework developed by the 17 | folks at Github and brings its power to people who want to implement chat bots using Go. 18 | 19 | ## Getting Started 20 | 21 | Joe is a software library that is packaged as [Go module][go-modules]. You can get it via: 22 | 23 | ``` 24 | go get github.com/go-joe/joe 25 | ``` 26 | 27 | ### Example usage 28 | 29 | **You can find all code examples, more explanation and complete recipes at https://joe-bot.net** 30 | 31 | Each bot consists of a chat _Adapter_ (e.g. to integrate with Slack), a _Memory_ 32 | implementation to remember key-value data (e.g. using Redis) and a _Brain_ which 33 | routes new messages or custom events (e.g. receiving an HTTP call) to the 34 | corresponding registered _handler_ functions. 35 | 36 | By default `joe.New(…)` uses the CLI adapter which makes the bot read messages 37 | from stdin and respond on stdout. Additionally the bot will store key value 38 | data in-memory which means it will forget anything you told it when it is restarted. 39 | This default setup is useful for local development without any dependencies but 40 | you will quickly want to add other _Modules_ to extend the bots capabilities. 41 | 42 | The following example connects the Bot with a Slack workspace and stores 43 | key-value data in Redis. To allow the message handlers to access the memory we 44 | define them as functions on a custom `ExampleBot`type which embeds the `joe.Bot`. 45 | 46 | [embedmd]:# (_examples/02_useful/main.go) 47 | ```go 48 | package main 49 | 50 | import ( 51 | "fmt" 52 | 53 | "github.com/go-joe/joe" 54 | "github.com/go-joe/redis-memory" 55 | "github.com/go-joe/slack-adapter/v2" 56 | ) 57 | 58 | type ExampleBot struct { 59 | *joe.Bot 60 | } 61 | 62 | func main() { 63 | b := &ExampleBot{ 64 | Bot: joe.New("example", 65 | redis.Memory("localhost:6379"), 66 | slack.Adapter("xoxb-1452345…"), 67 | ), 68 | } 69 | 70 | b.Respond("remember (.+) is (.+)", b.Remember) 71 | b.Respond("what is (.+)", b.WhatIs) 72 | 73 | err := b.Run() 74 | if err != nil { 75 | b.Logger.Fatal(err.Error()) 76 | } 77 | } 78 | 79 | func (b *ExampleBot) Remember(msg joe.Message) error { 80 | key, value := msg.Matches[0], msg.Matches[1] 81 | msg.Respond("OK, I'll remember %s is %s", key, value) 82 | return b.Store.Set(key, value) 83 | } 84 | 85 | func (b *ExampleBot) WhatIs(msg joe.Message) error { 86 | key := msg.Matches[0] 87 | var value string 88 | ok, err := b.Store.Get(key, &value) 89 | if err != nil { 90 | return fmt.Errorf("failed to retrieve key %q from brain: %w", key, err) 91 | } 92 | 93 | if ok { 94 | msg.Respond("%s is %s", key, value) 95 | } else { 96 | msg.Respond("I do not remember %q", key) 97 | } 98 | 99 | return nil 100 | } 101 | ``` 102 | 103 | ## Available modules 104 | 105 | Joe ships with no third-party modules such as Redis integration to avoid pulling 106 | in more dependencies than you actually require. There are however already some 107 | modules that you can use directly to extend the functionality of your bot without 108 | writing too much code yourself. 109 | 110 | If you have written a module and want to share it, please add it to this list and 111 | open a pull request. 112 | 113 | ### Chat Adapters 114 | 115 | - Slack Adapter: https://github.com/go-joe/slack-adapter 116 | - Rocket.Chat Adapter: https://github.com/dwmunster/rocket-adapter 117 | - Telegram Adapter: https://github.com/robertgzr/joe-telegram-adapter 118 | - IRC Adapter: https://github.com/akrennmair/joe-irc-adapter 119 | - Mattermost Adapter: https://github.com/dwmunster/joe-mattermost-adapter 120 | - VK Adapter: https://github.com/tdakkota/joe-vk-adapter 121 | 122 | ### Memory Modules 123 | 124 | - Redis Memory: https://github.com/go-joe/redis-memory 125 | - File Memory: https://github.com/go-joe/file-memory 126 | - Bolt Memory: https://github.com/robertgzr/joe-bolt-memory 127 | - Sqlite Memory: https://github.com/warmans/sqlite-memory 128 | 129 | ### Other Modules 130 | 131 | - HTTP Server: https://github.com/go-joe/http-server 132 | - Cron Jobs: https://github.com/go-joe/cron 133 | 134 | ## Built With 135 | 136 | * [zap](https://github.com/uber-go/zap) - Blazing fast, structured, leveled logging in Go 137 | * [multierr](https://github.com/uber-go/multierr) - Package multierr allows combining one or more errors together 138 | * [testify](https://github.com/stretchr/testify) - A simple unit test library 139 | 140 | ## Contributing 141 | 142 | Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of 143 | conduct and on the process for submitting pull requests to this repository. 144 | 145 | ## Versioning 146 | 147 | **THIS SOFTWARE IS STILL IN ALPHA AND THERE ARE NO GUARANTEES REGARDING API STABILITY YET.** 148 | 149 | All significant (e.g. breaking) changes are documented in the [CHANGELOG.md](CHANGELOG.md). 150 | 151 | After the v1.0 release we plan to use [SemVer](http://semver.org/) for versioning. 152 | For the versions available, see the [tags on this repository][tags]. 153 | 154 | ## Authors 155 | 156 | - **Friedrich Große** - *Initial work* - [fgrosse](https://github.com/fgrosse) 157 | 158 | See also the list of [contributors][contributors] who participated in this project. 159 | 160 | ## License 161 | 162 | This project is licensed under the BSD-3-Clause License - see the [LICENSE](LICENSE) file for details. 163 | 164 | ## Acknowledgments 165 | 166 | - [Hubot][hubot] and its great community for the inspiration 167 | - [embedmd][campoy-embedmd] for a cool tool to embed source code in markdown files 168 | 169 | [go]: https://golang.org 170 | [hubot]: https://hubot.github.com/ 171 | [go-modules]: https://github.com/golang/go/wiki/Modules 172 | [joe-http]: https://github.com/go-joe/http-server 173 | [tags]: https://github.com/go-joe/joe/tags 174 | [contributors]: https://github.com/go-joe/joe/contributors 175 | [campoy-embedmd]: https://github.com/campoy/embedmd 176 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-hacker -------------------------------------------------------------------------------- /_docs/.gitignore: -------------------------------------------------------------------------------- 1 | public 2 | -------------------------------------------------------------------------------- /_docs/archetypes/default.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "{{ replace .Name "-" " " | title }}" 3 | date: {{ .Date }} 4 | draft: true 5 | --- 6 | 7 | -------------------------------------------------------------------------------- /_docs/config.yml: -------------------------------------------------------------------------------- 1 | baseURL: "https://joe-bot.net/" 2 | languageCode: "en-us" 3 | title: "Joe Bot" 4 | theme: "learn" 5 | 6 | params: 7 | themeVariant: "custom" # see static/css/theme-custom.css 8 | author: Friedrich Große 9 | description: A general-purpose bot library 10 | editURL: "https://github.com/go-joe/joe/edit/master/_docs/content/" 11 | 12 | enableEmoji: true 13 | 14 | # Code highlighting configuration 15 | # See https://gohugo.io/content-management/syntax-highlighting/ 16 | pygmentsCodeFences: true 17 | pygmentsStyle: monokai 18 | pygmentsCodefencesGuessSyntax: true 19 | PygmentsUseClasses: true 20 | 21 | # Activate search index 22 | outputs: 23 | home: [HTML, RSS, JSON] 24 | 25 | # Add additional links in the navigation menu 26 | menu: 27 | shortcuts: 28 | - name: GitHub Repo 29 | url: 'https://github.com/go-joe/joe' 30 | weight: 10 31 | - name: GoDoc 32 | url: 'https://godoc.org/github.com/go-joe/joe' 33 | weight: 20 34 | 35 | privacy: 36 | disqus: 37 | disable: true 38 | googleAnalytics: 39 | disable: true 40 | instagram: 41 | disable: true 42 | twitter: 43 | disable: true 44 | vimeo: 45 | disable: true 46 | youtube: 47 | disable: true 48 | -------------------------------------------------------------------------------- /_docs/content/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Start" 3 | +++ 4 | 5 |

- Joe Bot -

6 | 7 | Joe is a library used to write chat bots in [the Go programming language][go]. 8 | 9 | Fork me on GitHub 10 | 11 | ## Features 12 | 13 | - **Chat adapters** for Slack, Rocket.Chat, Telegram, Mattermost, IRC and () VK. Adding your own is easy as well. 14 | - **Event processing system** to consume HTTP callbacks (e.g. from GitHub) or to trigger events on a schedule using Cron expressions 15 | - **Persistence** of key-value data (e.g. using Redis or SQL) 16 | - **User permissions** to restrict some actions to privileged users 17 | - **Unit tests** are first class citizens, Joe has high code coverage and ships with a dedicated package to facilitate your own tests 18 | 19 | ## Design 20 | 21 | - **Minimal**: Joe ships with no third-party dependencies except for logging and error handling. 22 | - **Modular**: choose your own chat adapter (e.g. Slack), memory implementation (e.g. Redis) and more. 23 | - **Batteries included**: you can start developing your bot on the CLI without extra configuration. 24 | - **Simple**: your own message & event handlers are simple and easy to understand functions without too much cruft or boilerplate setup. 25 | 26 | ## Getting Started 27 | 28 | To get started writing your own bot with Joe, head over to the 29 | [**Quickstart**](/quick) section or directly have a look at the 30 | [**Basic Tutorials**](/basics) to learn the core concepts. 31 | If you want to dive right in and want to know what modules are currently provided 32 | by the community, then have a look at the [**Available Modules**](/modules) section. 33 | Last but not least, you can find more instructions and best practices in the [**Recipes**](/recipes) section. 34 | 35 | ## Contact & Contributing 36 | 37 | To contribute to Joe, you can either write your own Module (e.g. to integrate 38 | another chat adapter) or work on Joe's code directly. You can of course also 39 | extend or improve this documentation or help with reviewing issues and pull 40 | requests at https://github.com/go-joe/joe. Further details about how to 41 | contribute can be found in the [CONTRIBUTING.md][contributing] file. 42 | 43 | ## License 44 | 45 | The Joe library is licensed under the [BSD-3-Clause License][license]. 46 | 47 | [go]: https://golang.org 48 | [hubot]: https://hubot.github.com/ 49 | [license]: https://github.com/go-joe/joe/blob/master/LICENSE 50 | [contributing]: https://github.com/go-joe/joe/blob/master/CONTRIBUTING.md 51 | -------------------------------------------------------------------------------- /_docs/content/basics/01_minimal.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Minimal Example" 3 | slug = "minimal" 4 | weight = 1 5 | pre = "a) " 6 | +++ 7 | 8 | The simplest chat bot listens for messages on a chat _Adapter_ and then executes 9 | a _Handler_ function if it sees a message directed to the bot that matches a given pattern. 10 | 11 | For example a bot that responds to a message "ping" with the answer "PONG" looks like this: 12 | 13 | [embedmd]:# (../../../_examples/01_minimal/main.go) 14 | ```go 15 | package main 16 | 17 | import "github.com/go-joe/joe" 18 | 19 | func main() { 20 | b := joe.New("example-bot") 21 | b.Respond("ping", Pong) 22 | 23 | err := b.Run() 24 | if err != nil { 25 | b.Logger.Fatal(err.Error()) 26 | } 27 | } 28 | 29 | func Pong(msg joe.Message) error { 30 | msg.Respond("PONG") 31 | return nil 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /_docs/content/basics/02_useful.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Useful Example" 3 | slug = "useful" 4 | weight = 2 5 | pre = "b) " 6 | +++ 7 | 8 | Each bot consists of a chat _Adapter_ (e.g. to integrate with Slack), a _Memory_ 9 | implementation to remember key-value data (e.g. using Redis) and a _Brain_ which 10 | routes new messages or custom events (e.g. receiving an HTTP call) to the 11 | corresponding registered _handler_ functions. 12 | 13 | By default `joe.New(…)` uses the CLI adapter which makes the bot read messages 14 | from stdin and respond on stdout. Additionally the bot will store key value 15 | data in-memory which means it will forget anything you told it when it is restarted. 16 | This default setup is useful for local development without any dependencies but 17 | you will quickly want to add other [_Modules_](/modules) to extend the bots capabilities. 18 | 19 | For instance we can extend the previous example to connect the Bot with a Slack 20 | workspace and store key-value data in Redis. To allow the message handlers to 21 | access the memory we define them as functions on a custom `ExampleBot`type which 22 | embeds the `joe.Bot`. 23 | 24 | [embedmd]:# (../../../_examples/02_useful/main.go) 25 | ```go 26 | package main 27 | 28 | import ( 29 | "fmt" 30 | 31 | "github.com/go-joe/joe" 32 | "github.com/go-joe/redis-memory" 33 | "github.com/go-joe/slack-adapter/v2" 34 | ) 35 | 36 | type ExampleBot struct { 37 | *joe.Bot 38 | } 39 | 40 | func main() { 41 | b := &ExampleBot{ 42 | Bot: joe.New("example", 43 | redis.Memory("localhost:6379"), 44 | slack.Adapter("xoxb-1452345…"), 45 | ), 46 | } 47 | 48 | b.Respond("remember (.+) is (.+)", b.Remember) 49 | b.Respond("what is (.+)", b.WhatIs) 50 | 51 | err := b.Run() 52 | if err != nil { 53 | b.Logger.Fatal(err.Error()) 54 | } 55 | } 56 | 57 | func (b *ExampleBot) Remember(msg joe.Message) error { 58 | key, value := msg.Matches[0], msg.Matches[1] 59 | msg.Respond("OK, I'll remember %s is %s", key, value) 60 | return b.Store.Set(key, value) 61 | } 62 | 63 | func (b *ExampleBot) WhatIs(msg joe.Message) error { 64 | key := msg.Matches[0] 65 | var value string 66 | ok, err := b.Store.Get(key, &value) 67 | if err != nil { 68 | return fmt.Errorf("failed to retrieve key %q from brain: %w", key, err) 69 | } 70 | 71 | if ok { 72 | msg.Respond("%s is %s", key, value) 73 | } else { 74 | msg.Respond("I do not remember %q", key) 75 | } 76 | 77 | return nil 78 | } 79 | ``` 80 | -------------------------------------------------------------------------------- /_docs/content/basics/03_events.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Handling Custom Events" 3 | slug = "events" 4 | weight = 3 5 | pre = "c) " 6 | +++ 7 | 8 | {{% notice note %}} 9 | More information on the events system can now be found in the [**Events recipe**](/recipes/events) 10 | {{% /notice %}} 11 | 12 | The previous example should give you an idea already on how to write simple chat 13 | bots. It is missing one important part however: how can a bot trigger any 14 | interaction proactively, i.e. without a message from a user. 15 | 16 | To solve this problem, joe's Brain implements an event handler that you can hook 17 | into. In fact the `Bot.Respond(…)` function that we used in the earlier examples 18 | is doing exactly that to listen for any `joe.ReceiveMessageEvent` that match the 19 | specified regular expression and then execute the handler function. 20 | 21 | Implementing custom events is easy because you can emit any type as event and 22 | register handlers that match only this type. What this exactly means is best 23 | demonstrated with another example: 24 | 25 | [embedmd]:# (../../../_examples/03_custom_events/main.go) 26 | ```go 27 | package main 28 | 29 | import ( 30 | "time" 31 | 32 | "github.com/go-joe/joe" 33 | ) 34 | 35 | type ExampleBot struct { 36 | *joe.Bot 37 | Channel string // example for your custom bot configuration 38 | } 39 | 40 | type CustomEvent struct { 41 | Data string // just an example of attaching any data with a custom event 42 | } 43 | 44 | func main() { 45 | b := &ExampleBot{ 46 | Bot: joe.New("example"), 47 | Channel: "CDEADBEAF", // example reference to a slack channel 48 | } 49 | 50 | // Register our custom event handler. Joe inspects the function signature to 51 | // understand that this function should be invoked whenever a CustomEvent 52 | // is emitted. 53 | b.Brain.RegisterHandler(b.HandleCustomEvent) 54 | 55 | // For example purposes emit a CustomEvent in a second. 56 | time.AfterFunc(time.Second, func() { 57 | b.Brain.Emit(CustomEvent{Data: "Hello World!"}) 58 | }) 59 | 60 | err := b.Run() 61 | if err != nil { 62 | b.Logger.Fatal(err.Error()) 63 | } 64 | } 65 | 66 | // HandleCustomEvent handles any CustomEvent that is emitted. Joe also supports 67 | // event handlers that return an error or accept a context.Context as first argument. 68 | func (b *ExampleBot) HandleCustomEvent(evt CustomEvent) { 69 | b.Say(b.Channel, "Received custom event: %v", evt.Data) 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /_docs/content/basics/04_permissions.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Checking User Permissions" 3 | slug = "permissions" 4 | weight = 4 5 | pre = "d) " 6 | +++ 7 | 8 | Joe supports a simple way to manage user permissions. For instance you may want 9 | to define a message handler that will run an operation which only admins should 10 | be allowed to trigger. 11 | 12 | To implement this, joe has a concept of permission scopes. A scope is a string 13 | which is _granted_ to a specific user ID so you can later check if the author of 14 | the event you are handling (e.g. a message from Slack) has this scope or any 15 | scope that _contains_ it. 16 | 17 | Scopes are interpreted in a hierarchical way where scope _A_ can contain scope 18 | _B_ if _A_ is a prefix to _B_. For example, you can check if a user is allowed 19 | to read or write from the "Example" API by checking the `api.example.read` or 20 | `api.example.write` scope. When you grant the scope to a user you can now either 21 | decide only to grant the very specific `api.example.read` scope which means the 22 | user will not have write permissions or you can allow people write-only access 23 | via the `api.example.write` scope. 24 | 25 | Alternatively you can also grant any access to the Example API via `api.example` 26 | which includes both the read and write scope beneath it. If you want you 27 | could also allow even more general access to everything in the api via the 28 | `api` scope. 29 | 30 | Scopes can be granted statically in code or dynamically in a handler like this: 31 | 32 | [embedmd]:# (../../../_examples/04_auth/main.go) 33 | ```go 34 | package main 35 | 36 | import "github.com/go-joe/joe" 37 | 38 | type ExampleBot struct { 39 | *joe.Bot 40 | } 41 | 42 | func main() { 43 | b := &ExampleBot{ 44 | Bot: joe.New("HAL"), 45 | } 46 | 47 | // If you know the user ID in advance you may hard-code it at startup. 48 | b.Auth.Grant("api.example", "DAVE") 49 | 50 | // An example of a message handler that checks permissions. 51 | b.Respond("open the pod bay doors", b.OpenPodBayDoors) 52 | 53 | err := b.Run() 54 | if err != nil { 55 | b.Logger.Fatal(err.Error()) 56 | } 57 | } 58 | 59 | func (b *ExampleBot) OpenPodBayDoors(msg joe.Message) error { 60 | err := b.Auth.CheckPermission("api.example.admin", msg.AuthorID) 61 | if err != nil { 62 | return msg.RespondE("I'm sorry Dave, I'm afraid I can't do that") 63 | } 64 | 65 | return msg.RespondE("OK") 66 | } 67 | ``` 68 | -------------------------------------------------------------------------------- /_docs/content/basics/05_integration.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Working with Web Applications" 3 | slug = "integration" 4 | weight = 5 5 | pre = "e) " 6 | +++ 7 | 8 | {{% notice note %}} 9 | More information on how to consume HTTP callbacks can now be found in the [**Events recipe**](/recipes/events/#chaining-events) 10 | {{% /notice %}} 11 | 12 | You may want to integrate your bot with applications such as GitHub or GitLab to 13 | trigger a handler or just send a message to Slack. Usually this is done by 14 | providing an HTTP callback to those applications so they can POST data when 15 | there is an event. 16 | 17 | We already saw in the previous section that is is very easy to implement custom 18 | events so we will use this feature to implement HTTP integrations as well. Since 19 | this is such a dominant use-case we already provide the 20 | [`github.com/go-joe/http-server`][joe-http] module to make it easy for everybody 21 | to write their own custom integrations. 22 | 23 | [embedmd]:# (../../../_examples/05_http/main.go) 24 | ```go 25 | package main 26 | 27 | import ( 28 | "context" 29 | "errors" 30 | 31 | joehttp "github.com/go-joe/http-server" 32 | "github.com/go-joe/joe" 33 | ) 34 | 35 | type ExampleBot struct { 36 | *joe.Bot 37 | } 38 | 39 | func main() { 40 | b := &ExampleBot{Bot: joe.New("example", 41 | joehttp.Server(":8080"), 42 | )} 43 | 44 | b.Brain.RegisterHandler(b.HandleHTTP) 45 | 46 | err := b.Run() 47 | if err != nil { 48 | b.Logger.Fatal(err.Error()) 49 | } 50 | } 51 | 52 | func (b *ExampleBot) HandleHTTP(context.Context, joehttp.RequestEvent) error { 53 | return errors.New("TODO: Add your custom logic here") 54 | } 55 | ``` 56 | 57 | [joe-http]: https://github.com/go-joe/http-server 58 | -------------------------------------------------------------------------------- /_docs/content/basics/06_reactions.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Reactions & Emojis" 3 | slug = "reactions" 4 | weight = 6 5 | pre = "f) " 6 | +++ 7 | 8 | Joe supports reacting to messages with Emojis if the chat adapter has this feature. 9 | For instance in Slack your bot could add a :robot: emoji to a message to indicate to 10 | the user, that the bot has seen it. Another use case of reactions is that you 11 | may want to trigger an action if the _user_ attaches an emoji to a message. 12 | 13 | Currently the following chat adapters support reactions: 14 | 15 | - CLI Adapter: https://github.com/go-joe/joe 16 | - Slack Adapter: https://github.com/go-joe/slack-adapter 17 | - Mattermost Adapter: https://github.com/dwmunster/joe-mattermost-adapter 18 | 19 | The following example shows how you can use reactions in your message handlers: 20 | 21 | [embedmd]:# (../../../_examples/06_react/main.go) 22 | ```go 23 | package main 24 | 25 | import ( 26 | "fmt" 27 | 28 | "github.com/go-joe/joe" 29 | "github.com/go-joe/joe/reactions" 30 | ) 31 | 32 | func main() { 33 | b := joe.New("example-bot") 34 | b.Respond("hello", MyHandler) 35 | b.Brain.RegisterHandler(ReceiveReaction) 36 | 37 | err := b.Run() 38 | if err != nil { 39 | b.Logger.Fatal(err.Error()) 40 | } 41 | } 42 | 43 | func MyHandler(msg joe.Message) error { 44 | err := msg.React(reactions.Thumbsup) 45 | if err != nil { 46 | msg.Respond("Sorry but there was an issue attaching a reaction: %v", err) 47 | } 48 | 49 | // custom reactions are also possible 50 | _ = msg.React(reactions.Reaction{Shortcode: "foo"}) 51 | 52 | return err 53 | } 54 | 55 | func ReceiveReaction(evt reactions.Event) error { 56 | fmt.Printf("Received event: %+v", evt) 57 | return nil 58 | } 59 | ``` 60 | 61 | If you try to react to a message, when your chat adapter does not support this 62 | feature, the `Message.React(…)` function will return the `joe.ErrNotImplemented` 63 | sentinel error. 64 | -------------------------------------------------------------------------------- /_docs/content/basics/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Basic Usage" 3 | path = "basics" 4 | weight = 2 5 | pre = "2. " 6 | +++ 7 | 8 | Learn the basic concepts of how to built a bot with Joe. You can find a 9 | selection of topics in the navigation Menu. 10 | 11 | Alternatively you can use your keyboards arrow keys 12 | & 13 | or navigate using the 14 | and 15 | icons on the left and right of the screen. 16 | 17 | If you are interested in more tutorials and best practices you should take a 18 | look at the [**Recipes**](/recipes) section. 19 | 20 | \ʕ◔ϖ◔ʔ/ 21 | -------------------------------------------------------------------------------- /_docs/content/modules/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Available Modules" 3 | slug = "modules" 4 | weight = 3 5 | pre = "3. " 6 | +++ 7 | 8 | Joe ships with no third-party modules such as Redis integration to avoid pulling 9 | in more dependencies than you actually require. There are however already some 10 | modules that you can use directly to extend the functionality of your bot without 11 | writing too much code yourself. 12 | 13 | ### Chat Adapters 14 |

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 |
57 |
58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | {{/* DISABLE MERMAID JS (used for diagrams) since its not yet used but slows down page load 71 | 72 | 73 | 76 | */}} 77 | 78 | {{ partial "custom-footer.html" . }} 79 | 80 | 81 | -------------------------------------------------------------------------------- /_docs/layouts/partials/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ hugo.Generator }} 7 | {{ partial "meta.html" . }} 8 | {{ partial "favicon.html" . }} 9 | {{ .Title }} :: {{ .Site.Title }} 10 | 11 | {{ $assetBusting := not .Site.Params.disableAssetsBusting }} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {{with .Site.Params.themeVariant}} 22 | 23 | {{end}} 24 | 25 | 26 | 27 | 37 | {{ partial "custom-header.html" . }} 38 | 39 | 40 | {{ partial "menu.html" . }} 41 |
42 |
43 |
44 | {{if not .IsHome}} 45 |
46 |
47 | {{ if and (or .IsPage .IsSection) .Site.Params.editURL }} 48 | {{ $File := .File }} 49 | {{ $Site := .Site }} 50 | {{with $File.Path }} 51 | 57 | {{ end }} 58 | {{ end }} 59 | {{$toc := (and (not .Params.disableToc) (not .Params.chapter))}} 60 | 78 | {{ if $toc }} 79 | {{ partial "toc.html" . }} 80 | {{ end }} 81 |
82 |
83 | {{ end }} 84 |
85 | {{ partial "tags.html" . }} 86 |
87 | {{ if .Params.chapter }} 88 |
89 | {{ end }} 90 |
91 | {{if and (not .IsHome) (not .Params.chapter) }} 92 |

93 | {{ if eq .Kind "taxonomy" }} 94 | {{.Kind}} :: 95 | {{ end }} 96 | {{.Title}} 97 |

98 | {{end}} 99 | 100 | {{define "breadcrumb"}} 101 | {{$parent := .page.Parent }} 102 | {{ if $parent }} 103 | {{ $value := (printf "%s %s" $parent.RelPermalink $parent.Title .value) }} 104 | {{ template "breadcrumb" dict "page" $parent "value" $value }} 105 | {{else}} 106 | {{.value|safeHTML}} 107 | {{end}} 108 | {{end}} 109 | 110 | -------------------------------------------------------------------------------- /_docs/layouts/partials/logo.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /_docs/layouts/partials/menu-footer.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Built with ❤️ using Hugo

4 | 8 |
9 | -------------------------------------------------------------------------------- /_docs/layouts/partials/menu.html: -------------------------------------------------------------------------------- 1 | 91 | 92 | 93 | {{ define "section-tree-nav" }} 94 | {{ $showvisitedlinks := .showvisitedlinks }} 95 | {{ $currentNode := .currentnode }} 96 | {{with .sect}} 97 | {{if .IsSection}} 98 | {{safeHTML .Params.head}} 99 |
  • 104 | 105 | {{safeHTML .Params.Pre}}{{or .Params.menuTitle .LinkTitle .Title}}{{safeHTML .Params.Post}} 106 | {{ if $showvisitedlinks}} 107 | 108 | {{ end }} 109 | 110 | {{ $numberOfPages := (add (len .Pages) (len .Sections)) }} 111 | {{ if ne $numberOfPages 0 }} 112 |
      113 | {{ $currentNode.Scratch.Set "pages" .Pages }} 114 | {{ if .Sections}} 115 | {{ $currentNode.Scratch.Set "pages" (.Pages | union .Sections) }} 116 | {{end}} 117 | {{ $pages := ($currentNode.Scratch.Get "pages") }} 118 | 119 | {{if eq .Site.Params.ordersectionsby "title"}} 120 | {{ range $pages.ByTitle }} 121 | {{ if and .Params.hidden (not $.showhidden) }} 122 | {{else}} 123 | {{ template "section-tree-nav" dict "sect" . "currentnode" $currentNode "showvisitedlinks" $showvisitedlinks }} 124 | {{end}} 125 | {{ end }} 126 | {{else}} 127 | {{ range $pages.ByWeight }} 128 | {{ if and .Params.hidden (not $.showhidden) }} 129 | {{else}} 130 | {{ template "section-tree-nav" dict "sect" . "currentnode" $currentNode "showvisitedlinks" $showvisitedlinks }} 131 | {{end}} 132 | {{ end }} 133 | {{end}} 134 |
    135 | {{ end }} 136 |
  • 137 | {{else}} 138 | {{ if not .Params.Hidden }} 139 |
  • 140 | 141 | {{safeHTML .Params.Pre}}{{or .Params.menuTitle .LinkTitle .Title}}{{safeHTML .Params.Post}} 142 | {{ if $showvisitedlinks}}{{end}} 143 | 144 |
  • 145 | {{ end }} 146 | {{end}} 147 | {{ end }} 148 | {{ end }} 149 | 150 | -------------------------------------------------------------------------------- /_docs/layouts/shortcodes/include.html: -------------------------------------------------------------------------------- 1 | {{ with .Site.GetPage (.Get 0) }}{{ .Content }}{{end}} 2 | -------------------------------------------------------------------------------- /_docs/static/css/syntax.css: -------------------------------------------------------------------------------- 1 | /* Background */ .chroma { color: #f8f8f2; background-color: #272822 } 2 | /* Error */ .chroma .err { color: #960050; background-color: #1e0010 } 3 | /* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } 4 | /* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; width: auto; overflow: auto; display: block; } 5 | /* LineHighlight */ .chroma .hl { display: block; width: 100%;background-color: #ffffcc } 6 | /* LineNumbersTable */ .chroma .lnt { margin-right: 0.4em; padding: 0 0.4em 0 0.4em; } 7 | /* LineNumbers */ .chroma .ln { margin-right: 0.4em; padding: 0 0.4em 0 0.4em; } 8 | /* Keyword */ .chroma .k { color: #f92672 } 9 | /* KeywordConstant */ .chroma .kc { color: #66d9ef } 10 | /* KeywordDeclaration */ .chroma .kd { color: #f92672 } 11 | /* KeywordNamespace */ .chroma .kn { color: #f92672 } 12 | /* KeywordPseudo */ .chroma .kp { color: #66d9ef } 13 | /* KeywordReserved */ .chroma .kr { color: #66d9ef } 14 | /* KeywordType */ .chroma .kt { color: #66d9ef } 15 | /* NameAttribute */ .chroma .na { color: #a6e22e } 16 | /* ??? */ .chroma .nb { color: #e6db74 } 17 | /* NameClass */ .chroma .nc { color: #a6e22e } 18 | /* NameConstant */ .chroma .no { color: #66d9ef } 19 | /* NameDecorator */ .chroma .nd { color: #a6e22e } 20 | /* NameException */ .chroma .ne { color: #a6e22e } 21 | /* NameFunction */ .chroma .nf { color: #a6e22e } 22 | /* NameOther */ .chroma .nx { color: #ffffff } 23 | /* NameTag */ .chroma .nt { color: #f92672 } 24 | /* Literal */ .chroma .l { color: #ae81ff } 25 | /* LiteralDate */ .chroma .ld { color: #e6db74 } 26 | /* LiteralString */ .chroma .s { color: #e6db74 } 27 | /* LiteralStringAffix */ .chroma .sa { color: #e6db74 } 28 | /* LiteralStringBacktick */ .chroma .sb { color: #e6db74 } 29 | /* LiteralStringChar */ .chroma .sc { color: #e6db74 } 30 | /* LiteralStringDelimiter */ .chroma .dl { color: #e6db74 } 31 | /* LiteralStringDoc */ .chroma .sd { color: #e6db74 } 32 | /* LiteralStringDouble */ .chroma .s2 { color: #e6db74 } 33 | /* LiteralStringEscape */ .chroma .se { color: #ae81ff } 34 | /* LiteralStringHeredoc */ .chroma .sh { color: #e6db74 } 35 | /* LiteralStringInterpol */ .chroma .si { color: #e6db74 } 36 | /* LiteralStringOther */ .chroma .sx { color: #e6db74 } 37 | /* LiteralStringRegex */ .chroma .sr { color: #e6db74 } 38 | /* LiteralStringSingle */ .chroma .s1 { color: #e6db74 } 39 | /* LiteralStringSymbol */ .chroma .ss { color: #e6db74 } 40 | /* LiteralNumber */ .chroma .m { color: #ae81ff } 41 | /* LiteralNumberBin */ .chroma .mb { color: #ae81ff } 42 | /* LiteralNumberFloat */ .chroma .mf { color: #ae81ff } 43 | /* LiteralNumberHex */ .chroma .mh { color: #ae81ff } 44 | /* LiteralNumberInteger */ .chroma .mi { color: #ae81ff } 45 | /* LiteralNumberIntegerLong */ .chroma .il { color: #ae81ff } 46 | /* LiteralNumberOct */ .chroma .mo { color: #ae81ff } 47 | /* Operator */ .chroma .o { color: #f92672 } 48 | /* OperatorWord */ .chroma .ow { color: #f92672 } 49 | /* Comment */ .chroma .c { color: #75715e } 50 | /* CommentHashbang */ .chroma .ch { color: #75715e } 51 | /* CommentMultiline */ .chroma .cm { color: #75715e } 52 | /* CommentSingle */ .chroma .c1 { color: #75715e } 53 | /* CommentSpecial */ .chroma .cs { color: #75715e } 54 | /* CommentPreproc */ .chroma .cp { color: #75715e } 55 | /* CommentPreprocFile */ .chroma .cpf { color: #75715e } 56 | /* GenericDeleted */ .chroma .gd { color: #f92672 } 57 | /* GenericEmph */ .chroma .ge { font-style: italic } 58 | /* GenericInserted */ .chroma .gi { color: #a6e22e } 59 | /* GenericStrong */ .chroma .gs { font-weight: bold } 60 | /* GenericSubheading */ .chroma .gu { color: #75715e } 61 | -------------------------------------------------------------------------------- /_docs/static/css/theme-custom.css: -------------------------------------------------------------------------------- 1 | @import "syntax.css"; 2 | 3 | body { 4 | font-family: "Raleway", sans-serif; 5 | font-weight: 300; 6 | 7 | } 8 | 9 | @media (min-width: 20rem) { 10 | h1 { 11 | font-size: 2em; 12 | } 13 | 14 | h1#joe-bot img { 15 | height: 34px; 16 | } 17 | 18 | #fork-me-on-github { 19 | font-size: 12px; 20 | right: -7em; 21 | top: 2em; 22 | } 23 | pre code { 24 | font-size: 12px; 25 | } 26 | } 27 | 28 | @media (min-width: 48rem) { 29 | body { 30 | font-size: 16px !important; 31 | } 32 | 33 | h1#joe-bot { 34 | font-size: 3em; 35 | } 36 | 37 | h1#joe-bot img { 38 | height: 52px; 39 | } 40 | 41 | #fork-me-on-github { 42 | font-size: 12px; 43 | right: -6em; 44 | top: 3em; 45 | } 46 | pre code { 47 | font-size: 12px; 48 | } 49 | } 50 | 51 | @media (min-width: 58rem) { 52 | body { 53 | font-size: 20px !important; 54 | } 55 | 56 | h1#joe-bot { 57 | font-size: 3em; 58 | } 59 | 60 | h1#joe-bot img { 61 | height: 65px; 62 | } 63 | 64 | #fork-me-on-github { 65 | right: -6em; 66 | top: 3em; 67 | } 68 | pre code { 69 | font-size: 15px; 70 | } 71 | } 72 | 73 | :root{ 74 | 75 | --MAIN-TEXT-color:#323232; /* Color of text by default */ 76 | --MAIN-TITLES-TEXT-color: #5e5e5e; /* Color of titles h2-h3-h4-h5 */ 77 | --MAIN-LINK-color:#1C90F3; /* Color of links */ 78 | --MAIN-LINK-HOVER-color:#167ad0; /* Color of hovered links */ 79 | --MAIN-ANCHOR-color: #1C90F3; /* color of anchors on titles */ 80 | 81 | --MENU-HEADER-BG-color:#1C90F3; /* Background color of menu header */ 82 | --MENU-HEADER-BORDER-color:#33a1ff; /*Color of menu header border */ 83 | 84 | --MENU-SEARCH-BG-color: white; /* Search field background color (by default borders + icons) */ 85 | --MENU-SEARCH-BOX-color: black; /* Override search field border color */ 86 | --MENU-SEARCH-BOX-ICONS-color: #a1d2fd; /* Override search field icons color */ 87 | 88 | --MENU-SECTIONS-ACTIVE-BG-color:#20272b; /* Background color of the active section and its childs */ 89 | --MENU-SECTIONS-BG-color:#252c31; /* Background color of other sections */ 90 | --MENU-SECTIONS-LINK-color: #ccc; /* Color of links in menu */ 91 | --MENU-SECTIONS-LINK-HOVER-color: #e6e6e6; /* Color of links in menu, when hovered */ 92 | --MENU-SECTION-ACTIVE-CATEGORY-color: #777; /* Color of active category text */ 93 | --MENU-SECTION-ACTIVE-CATEGORY-BG-color: #fff; /* Color of background for the active category (only) */ 94 | 95 | --MENU-VISITED-color: #33a1ff; /* Color of 'page visited' icons in menu */ 96 | --MENU-SECTION-HR-color: #20272b; /* Color of
    separator in menu */ 97 | 98 | } 99 | 100 | h1#joe-bot img { 101 | vertical-align: text-top !important; 102 | } 103 | 104 | body { 105 | color: var(--MAIN-TEXT-color) !important; 106 | } 107 | 108 | textarea:focus, input[type="email"]:focus, input[type="number"]:focus, input[type="password"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="text"]:focus, input[type="url"]:focus, input[type="color"]:focus, input[type="date"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="week"]:focus, select[multiple=multiple]:focus { 109 | border-color: none; 110 | box-shadow: none; 111 | } 112 | 113 | h2, h3, h4, h5 { 114 | color: var(--MAIN-TITLES-TEXT-color) !important; 115 | } 116 | 117 | a { 118 | color: var(--MAIN-LINK-color); 119 | } 120 | 121 | .anchor { 122 | color: var(--MAIN-ANCHOR-color); 123 | } 124 | 125 | a:hover { 126 | color: var(--MAIN-LINK-HOVER-color); 127 | } 128 | 129 | #sidebar ul li.visited > a .read-icon { 130 | color: var(--MENU-VISITED-color); 131 | } 132 | 133 | #body a.highlight:after { 134 | display: block; 135 | content: ""; 136 | height: 1px; 137 | width: 0%; 138 | -webkit-transition: width 0.5s ease; 139 | -moz-transition: width 0.5s ease; 140 | -ms-transition: width 0.5s ease; 141 | transition: width 0.5s ease; 142 | background-color: var(--MAIN-LINK-HOVER-color); 143 | } 144 | #sidebar { 145 | background-color: var(--MENU-SECTIONS-BG-color); 146 | } 147 | #sidebar #header-wrapper { 148 | background: var(--MENU-HEADER-BG-color); 149 | color: var(--MENU-SEARCH-BOX-color); 150 | border-color: var(--MENU-HEADER-BORDER-color); 151 | } 152 | #sidebar .searchbox { 153 | border-color: var(--MENU-SEARCH-BOX-color); 154 | background: var(--MENU-SEARCH-BG-color); 155 | margin: 16px 16px 0; 156 | } 157 | #sidebar ul.topics > li.parent, #sidebar ul.topics > li.active { 158 | background: var(--MENU-SECTIONS-ACTIVE-BG-color); 159 | } 160 | #sidebar .searchbox * { 161 | color: var(--MENU-SEARCH-BOX-ICONS-color); 162 | } 163 | 164 | #sidebar a { 165 | color: var(--MENU-SECTIONS-LINK-color); 166 | } 167 | 168 | #sidebar a:hover { 169 | color: var(--MENU-SECTIONS-LINK-HOVER-color); 170 | } 171 | 172 | #sidebar ul li.active > a { 173 | background: var(--MENU-SECTION-ACTIVE-CATEGORY-BG-color); 174 | color: var(--MENU-SECTION-ACTIVE-CATEGORY-color) !important; 175 | } 176 | 177 | #sidebar hr { 178 | border-color: var(--MENU-SECTION-HR-color); 179 | } 180 | 181 | /* Headings */ 182 | h1, h2, h3, h4, h5, h6 { 183 | margin-bottom: .5rem; 184 | font-weight: bold; 185 | line-height: 1.25; 186 | color: #313131; 187 | text-rendering: optimizeLegibility; 188 | } 189 | h1 { 190 | margin-bottom: 28px; 191 | } 192 | h2 { 193 | margin-top: 1rem; 194 | font-size: 1.5rem; 195 | } 196 | h3 { 197 | margin-top: 1.5rem; 198 | font-size: 1.25rem; 199 | } 200 | h4, h5, h6 { 201 | margin-top: 1rem; 202 | font-size: 1rem; 203 | } 204 | 205 | #logo h1 { 206 | font-size: 2rem; 207 | margin-bottom: 28px; 208 | } 209 | 210 | .content { 211 | padding-top: 0; 212 | } 213 | 214 | #body-inner p { 215 | text-align: justify; 216 | } 217 | 218 | div.post { 219 | margin-bottom: 2em; 220 | } 221 | 222 | .posts { 223 | margin-top: 26.5px; 224 | } 225 | 226 | .gopher { 227 | font-family: Menlo, Monaco, "Courier New", monospace; 228 | font-size: 85%; 229 | color: #bf616a; 230 | white-space: nowrap; 231 | float: right; 232 | padding: .25em 0 .25em .5em; 233 | } 234 | 235 | pre { 236 | border-radius: 8px; 237 | font-size: 70%; 238 | overflow: auto; 239 | white-space: pre; 240 | word-wrap: normal; 241 | line-height: 1.45; 242 | } 243 | 244 | .copyright, 245 | .poweredby 246 | { 247 | font-size: 12px; 248 | font-weight: 300; 249 | } 250 | 251 | .center { 252 | display: block; 253 | 254 | margin-right: auto; 255 | margin-left: auto; 256 | } 257 | /* Changing from font-awesome 4 to 5, the class pull-right was removed */ 258 | .pull-right { 259 | float: right; 260 | } 261 | 262 | ul.posts { 263 | list-style: none; 264 | padding-left: 0; 265 | } 266 | 267 | ul.posts li { 268 | padding-bottom: 1em; 269 | } 270 | 271 | ul.posts li .title { 272 | display: block; 273 | line-height: 25px; 274 | padding-bottom: 10px 275 | } 276 | 277 | ul.posts li .title a { 278 | font-size: 1.5rem; 279 | } 280 | 281 | code { 282 | padding: .25em .5em; 283 | font-size: 85%; 284 | color: #bf616a; 285 | background-color: #f9f9f9; 286 | border: 0; 287 | border-radius: 20px; 288 | white-space: pre !important; 289 | } 290 | 291 | /* Keep copy to clipboard buttons on big code examples */ 292 | pre code + .copy-to-clipboard { 293 | display: inline-block; 294 | } 295 | 296 | /* Disable copy to clipboard buttons on small code examples */ 297 | code + .copy-to-clipboard { 298 | display: none; 299 | } 300 | 301 | section#footer { 302 | padding: 0px; 303 | margin-left: 16px; 304 | margin-top: 16px; 305 | } 306 | 307 | /***** Fork me on Github *****/ 308 | 309 | #fork-me-on-github { 310 | opacity : 0.8; 311 | background-color : black; 312 | color : #FFF; 313 | display : block; 314 | position : fixed; 315 | z-index : 10; 316 | padding : 0.5em 5em 0.4em 5em; 317 | -webkit-transform : rotate(45deg) scale(0.75, 1); 318 | transform : rotate(45deg) scale(0.75, 1); 319 | box-shadow : 0 0 0.5em rgba(0, 0, 0, 0.5); 320 | text-shadow : 0 0 0.75em #444; 321 | font : bold 16px/1.2em Arial, Sans-Serif; 322 | text-align : center; 323 | text-decoration : none; 324 | letter-spacing : .05em; 325 | } 326 | 327 | #fork-me-on-github:before { 328 | content : ''; 329 | position : absolute; 330 | top : 0; 331 | bottom : 0; 332 | left : 0; 333 | right : 0; 334 | border : 2px rgba(255, 255, 255, 0.7) dashed; 335 | margin : -0.3em -5em; 336 | -webkit-transform : scale(0.7); 337 | transform : scale(0.7); 338 | } 339 | 340 | #fork-me-on-github:hover { opacity : 1; } 341 | 342 | h2#features+ul, h2#design+ul { 343 | font-size: 90%; 344 | } 345 | 346 | h1#joe-bot { 347 | margin-bottom: 0; 348 | } 349 | 350 | h1#joe-bot+p { 351 | text-align: center; 352 | margin-top: 0; 353 | } 354 | -------------------------------------------------------------------------------- /_docs/static/images/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/android-icon-144x144.png -------------------------------------------------------------------------------- /_docs/static/images/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/android-icon-192x192.png -------------------------------------------------------------------------------- /_docs/static/images/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/android-icon-36x36.png -------------------------------------------------------------------------------- /_docs/static/images/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/android-icon-48x48.png -------------------------------------------------------------------------------- /_docs/static/images/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/android-icon-72x72.png -------------------------------------------------------------------------------- /_docs/static/images/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/android-icon-96x96.png -------------------------------------------------------------------------------- /_docs/static/images/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/apple-icon-114x114.png -------------------------------------------------------------------------------- /_docs/static/images/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/apple-icon-120x120.png -------------------------------------------------------------------------------- /_docs/static/images/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/apple-icon-144x144.png -------------------------------------------------------------------------------- /_docs/static/images/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/apple-icon-152x152.png -------------------------------------------------------------------------------- /_docs/static/images/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/apple-icon-180x180.png -------------------------------------------------------------------------------- /_docs/static/images/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/apple-icon-57x57.png -------------------------------------------------------------------------------- /_docs/static/images/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/apple-icon-60x60.png -------------------------------------------------------------------------------- /_docs/static/images/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/apple-icon-72x72.png -------------------------------------------------------------------------------- /_docs/static/images/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/apple-icon-76x76.png -------------------------------------------------------------------------------- /_docs/static/images/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/apple-icon-precomposed.png -------------------------------------------------------------------------------- /_docs/static/images/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/apple-icon.png -------------------------------------------------------------------------------- /_docs/static/images/bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/bot.png -------------------------------------------------------------------------------- /_docs/static/images/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /_docs/static/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/favicon-16x16.png -------------------------------------------------------------------------------- /_docs/static/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/favicon-32x32.png -------------------------------------------------------------------------------- /_docs/static/images/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/favicon-96x96.png -------------------------------------------------------------------------------- /_docs/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/favicon.ico -------------------------------------------------------------------------------- /_docs/static/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/favicon.png -------------------------------------------------------------------------------- /_docs/static/images/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "icons": [ 4 | { 5 | "src": "\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /_docs/static/images/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/ms-icon-144x144.png -------------------------------------------------------------------------------- /_docs/static/images/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/ms-icon-150x150.png -------------------------------------------------------------------------------- /_docs/static/images/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/ms-icon-310x310.png -------------------------------------------------------------------------------- /_docs/static/images/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-joe/joe/4644f17b55eb0614b3f43bf63c14a75c063bb556/_docs/static/images/ms-icon-70x70.png -------------------------------------------------------------------------------- /_docs/update_examples.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | type grep >/dev/null 2>&1 || { echo >&2 'ERROR: script requires "grep"'; exit 1; } 4 | 5 | echo "# Searching for files that use embedmd.." 6 | files=$(grep -r -l -E '\[embedmd\]:# \(.+\)' content) 7 | if [[ -z "$files" ]]; then 8 | echo >&2 'ERROR: did not find any file that uses embedmd' 9 | exit 1 10 | fi 11 | 12 | set -e -o pipefail 13 | 14 | for f in $files; do 15 | echo "> Updating $f" 16 | embedmd -w "$f" 17 | done 18 | -------------------------------------------------------------------------------- /_examples/01_minimal/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-joe/joe/examples/minimal 2 | 3 | go 1.14 4 | 5 | require github.com/go-joe/joe v0.0.0 6 | 7 | replace github.com/go-joe/joe => ../.. 8 | -------------------------------------------------------------------------------- /_examples/01_minimal/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 8 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 9 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 10 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 11 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 12 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 13 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 14 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 15 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 19 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 22 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 23 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 24 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 25 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 26 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 27 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 28 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 29 | go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= 30 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 31 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 32 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 33 | go.uber.org/zap v1.14.0 h1:/pduUoebOeeJzTDFuoMgC6nRkiasr1sBCIEorly7m4o= 34 | go.uber.org/zap v1.14.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 35 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 36 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 37 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 38 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 39 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 40 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 41 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 42 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 43 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 44 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 45 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 46 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 47 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 48 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 49 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 50 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= 51 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 52 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 53 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 54 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 55 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 56 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 57 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 58 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 59 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 60 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 61 | -------------------------------------------------------------------------------- /_examples/01_minimal/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/go-joe/joe" 4 | 5 | func main() { 6 | b := joe.New("example-bot") 7 | b.Respond("ping", Pong) 8 | 9 | err := b.Run() 10 | if err != nil { 11 | b.Logger.Fatal(err.Error()) 12 | } 13 | } 14 | 15 | func Pong(msg joe.Message) error { 16 | msg.Respond("PONG") 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /_examples/02_useful/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-joe/joe/examples/useful 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/go-joe/joe v0.9.0 7 | github.com/go-joe/redis-memory v0.3.1 8 | github.com/go-joe/slack-adapter/v2 v2.0.0 9 | ) 10 | 11 | replace github.com/go-joe/joe => ../.. 12 | -------------------------------------------------------------------------------- /_examples/02_useful/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-joe/joe" 7 | "github.com/go-joe/redis-memory" 8 | "github.com/go-joe/slack-adapter/v2" 9 | ) 10 | 11 | type ExampleBot struct { 12 | *joe.Bot 13 | } 14 | 15 | func main() { 16 | b := &ExampleBot{ 17 | Bot: joe.New("example", 18 | redis.Memory("localhost:6379"), 19 | slack.Adapter("xoxb-1452345…"), 20 | ), 21 | } 22 | 23 | b.Respond("remember (.+) is (.+)", b.Remember) 24 | b.Respond("what is (.+)", b.WhatIs) 25 | 26 | err := b.Run() 27 | if err != nil { 28 | b.Logger.Fatal(err.Error()) 29 | } 30 | } 31 | 32 | func (b *ExampleBot) Remember(msg joe.Message) error { 33 | key, value := msg.Matches[0], msg.Matches[1] 34 | msg.Respond("OK, I'll remember %s is %s", key, value) 35 | return b.Store.Set(key, value) 36 | } 37 | 38 | func (b *ExampleBot) WhatIs(msg joe.Message) error { 39 | key := msg.Matches[0] 40 | var value string 41 | ok, err := b.Store.Get(key, &value) 42 | if err != nil { 43 | return fmt.Errorf("failed to retrieve key %q from brain: %w", key, err) 44 | } 45 | 46 | if ok { 47 | msg.Respond("%s is %s", key, value) 48 | } else { 49 | msg.Respond("I do not remember %q", key) 50 | } 51 | 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /_examples/03_custom_events/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-joe/joe/examples/custom-events 2 | 3 | go 1.14 4 | 5 | require github.com/go-joe/joe v0.0.0 6 | 7 | replace github.com/go-joe/joe => ../.. 8 | -------------------------------------------------------------------------------- /_examples/03_custom_events/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 8 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 9 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 10 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 11 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 12 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 13 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 14 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 15 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 19 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 22 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 23 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 24 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 25 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 26 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 27 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 28 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 29 | go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= 30 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 31 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 32 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 33 | go.uber.org/zap v1.14.0 h1:/pduUoebOeeJzTDFuoMgC6nRkiasr1sBCIEorly7m4o= 34 | go.uber.org/zap v1.14.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 35 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 36 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 37 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 38 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 39 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 40 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 41 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 42 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 43 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 44 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 45 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 46 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 47 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 48 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 49 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 50 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= 51 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 52 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 53 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 54 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 55 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 56 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 57 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 58 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 59 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 60 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 61 | -------------------------------------------------------------------------------- /_examples/03_custom_events/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-joe/joe" 7 | ) 8 | 9 | type ExampleBot struct { 10 | *joe.Bot 11 | Channel string // example for your custom bot configuration 12 | } 13 | 14 | type CustomEvent struct { 15 | Data string // just an example of attaching any data with a custom event 16 | } 17 | 18 | func main() { 19 | b := &ExampleBot{ 20 | Bot: joe.New("example"), 21 | Channel: "CDEADBEAF", // example reference to a slack channel 22 | } 23 | 24 | // Register our custom event handler. Joe inspects the function signature to 25 | // understand that this function should be invoked whenever a CustomEvent 26 | // is emitted. 27 | b.Brain.RegisterHandler(b.HandleCustomEvent) 28 | 29 | // For example purposes emit a CustomEvent in a second. 30 | time.AfterFunc(time.Second, func() { 31 | b.Brain.Emit(CustomEvent{Data: "Hello World!"}) 32 | }) 33 | 34 | err := b.Run() 35 | if err != nil { 36 | b.Logger.Fatal(err.Error()) 37 | } 38 | } 39 | 40 | // HandleCustomEvent handles any CustomEvent that is emitted. Joe also supports 41 | // event handlers that return an error or accept a context.Context as first argument. 42 | func (b *ExampleBot) HandleCustomEvent(evt CustomEvent) { 43 | b.Say(b.Channel, "Received custom event: %v", evt.Data) 44 | } 45 | -------------------------------------------------------------------------------- /_examples/04_auth/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-joe/joe/examples/auth 2 | 3 | go 1.14 4 | 5 | require github.com/go-joe/joe v0.0.0 6 | 7 | replace github.com/go-joe/joe => ../.. 8 | -------------------------------------------------------------------------------- /_examples/04_auth/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 8 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 9 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 10 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 11 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 12 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 13 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 14 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 15 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 19 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 22 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 23 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 24 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 25 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 26 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 27 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 28 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 29 | go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= 30 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 31 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 32 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 33 | go.uber.org/zap v1.14.0 h1:/pduUoebOeeJzTDFuoMgC6nRkiasr1sBCIEorly7m4o= 34 | go.uber.org/zap v1.14.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 35 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 36 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 37 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 38 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 39 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 40 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 41 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 42 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 43 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 44 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 45 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 46 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 47 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 48 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 49 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 50 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= 51 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 52 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 53 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 54 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 55 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 56 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 57 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 58 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 59 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 60 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 61 | -------------------------------------------------------------------------------- /_examples/04_auth/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/go-joe/joe" 4 | 5 | type ExampleBot struct { 6 | *joe.Bot 7 | } 8 | 9 | func main() { 10 | b := &ExampleBot{ 11 | Bot: joe.New("HAL"), 12 | } 13 | 14 | // If you know the user ID in advance you may hard-code it at startup. 15 | b.Auth.Grant("api.example", "DAVE") 16 | 17 | // An example of a message handler that checks permissions. 18 | b.Respond("open the pod bay doors", b.OpenPodBayDoors) 19 | 20 | err := b.Run() 21 | if err != nil { 22 | b.Logger.Fatal(err.Error()) 23 | } 24 | } 25 | 26 | func (b *ExampleBot) OpenPodBayDoors(msg joe.Message) error { 27 | err := b.Auth.CheckPermission("api.example.admin", msg.AuthorID) 28 | if err != nil { 29 | return msg.RespondE("I'm sorry Dave, I'm afraid I can't do that") 30 | } 31 | 32 | return msg.RespondE("OK") 33 | } 34 | -------------------------------------------------------------------------------- /_examples/05_http/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-joe/joe/examples/http 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/go-joe/http-server v0.4.1 7 | github.com/go-joe/joe v0.4.0 8 | ) 9 | 10 | replace github.com/go-joe/joe => ../.. 11 | -------------------------------------------------------------------------------- /_examples/05_http/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/go-joe/http-server v0.4.1 h1:6e02QT5jcEPIiO0lKDwhcZvj/azlxUychAkchgRLLpk= 8 | github.com/go-joe/http-server v0.4.1/go.mod h1:CHlMovf6edlUFH1wDH3CdrqR/CeEF1rYzVYIyCZfayA= 9 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 10 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 11 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 12 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 13 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 14 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 15 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 16 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 17 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 21 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 22 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 23 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 24 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 25 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 26 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 27 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 28 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 29 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 30 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 31 | go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= 32 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 33 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 34 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 35 | go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o= 36 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 37 | go.uber.org/zap v1.14.0 h1:/pduUoebOeeJzTDFuoMgC6nRkiasr1sBCIEorly7m4o= 38 | go.uber.org/zap v1.14.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 39 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 40 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 41 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 42 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 43 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 44 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 45 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 46 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 47 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 48 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 49 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 50 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 51 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 52 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 53 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 54 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= 55 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 56 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 57 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 58 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 59 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 60 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 61 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 62 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 63 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 64 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 65 | -------------------------------------------------------------------------------- /_examples/05_http/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | joehttp "github.com/go-joe/http-server" 8 | "github.com/go-joe/joe" 9 | ) 10 | 11 | type ExampleBot struct { 12 | *joe.Bot 13 | } 14 | 15 | func main() { 16 | b := &ExampleBot{Bot: joe.New("example", 17 | joehttp.Server(":8080"), 18 | )} 19 | 20 | b.Brain.RegisterHandler(b.HandleHTTP) 21 | 22 | err := b.Run() 23 | if err != nil { 24 | b.Logger.Fatal(err.Error()) 25 | } 26 | } 27 | 28 | func (b *ExampleBot) HandleHTTP(context.Context, joehttp.RequestEvent) error { 29 | return errors.New("TODO: Add your custom logic here") 30 | } 31 | -------------------------------------------------------------------------------- /_examples/06_react/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-joe/joe/examples/react 2 | 3 | go 1.14 4 | 5 | require github.com/go-joe/joe v0.0.0 6 | 7 | replace github.com/go-joe/joe => ../.. 8 | -------------------------------------------------------------------------------- /_examples/06_react/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 8 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 9 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 10 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 11 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 12 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 13 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 14 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 15 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 19 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 22 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 23 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 24 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 25 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 26 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 27 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 28 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 29 | go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= 30 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 31 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 32 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 33 | go.uber.org/zap v1.14.0 h1:/pduUoebOeeJzTDFuoMgC6nRkiasr1sBCIEorly7m4o= 34 | go.uber.org/zap v1.14.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 35 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 36 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 37 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 38 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 39 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 40 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 41 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 42 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 43 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 44 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 45 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 46 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 47 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 48 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 49 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 50 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= 51 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 52 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 53 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 54 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 55 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 56 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 57 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 58 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 59 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 60 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 61 | -------------------------------------------------------------------------------- /_examples/06_react/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-joe/joe" 7 | "github.com/go-joe/joe/reactions" 8 | ) 9 | 10 | func main() { 11 | b := joe.New("example-bot") 12 | b.Respond("hello", MyHandler) 13 | b.Brain.RegisterHandler(ReceiveReaction) 14 | 15 | err := b.Run() 16 | if err != nil { 17 | b.Logger.Fatal(err.Error()) 18 | } 19 | } 20 | 21 | func MyHandler(msg joe.Message) error { 22 | err := msg.React(reactions.Thumbsup) 23 | if err != nil { 24 | msg.Respond("Sorry but there was an issue attaching a reaction: %v", err) 25 | } 26 | 27 | // custom reactions are also possible 28 | _ = msg.React(reactions.Reaction{Shortcode: "foo"}) 29 | 30 | return err 31 | } 32 | 33 | func ReceiveReaction(evt reactions.Event) error { 34 | fmt.Printf("Received event: %+v", evt) 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /_examples/07_config/bot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | joehttp "github.com/go-joe/http-server" 7 | "github.com/go-joe/joe" 8 | ) 9 | 10 | type Bot struct { 11 | *joe.Bot // Anonymously embed the joe.Bot type so we can use its functions easily. 12 | conf Config // You can keep other fields here as well. 13 | } 14 | 15 | func New(conf Config) *Bot { 16 | b := &Bot{ 17 | Bot: joe.New("joe", conf.Modules()...), 18 | } 19 | 20 | // Define any custom event and message handlers here 21 | b.Brain.RegisterHandler(b.GitHubCallback) 22 | b.Respond("do stuff", b.DoStuffCommand) 23 | 24 | return b 25 | } 26 | 27 | func New2(conf Config) (*Bot, error) { 28 | if err := conf.Validate(); err != nil { 29 | return nil, fmt.Errorf("invalid configuration: %w", err) 30 | } 31 | 32 | b := &Bot{ 33 | Bot: joe.New("joe", conf.Modules()...), 34 | } 35 | 36 | // Define any custom event and message handlers here 37 | b.Brain.RegisterHandler(b.GitHubCallback) 38 | b.Respond("do stuff", b.DoStuffCommand) 39 | 40 | return b, nil 41 | } 42 | 43 | func (b *Bot) GitHubCallback(joehttp.RequestEvent) { 44 | // Handler only provided for completeness. 45 | } 46 | 47 | func (b *Bot) DoStuffCommand(joe.Message) error { 48 | // Handler only provided for completeness. 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /_examples/07_config/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | 6 | joehttp "github.com/go-joe/http-server" 7 | "github.com/go-joe/joe" 8 | "github.com/go-joe/redis-memory" 9 | "github.com/go-joe/slack-adapter/v2" 10 | ) 11 | 12 | // Config holds all parameters to setup a new chat bot. 13 | type Config struct { 14 | SlackToken string // slack token, if empty we fallback to the CLI 15 | HTTPListen string // optional HTTP listen address to receive callbacks 16 | RedisAddr string // optional address to store keys in Redis 17 | } 18 | 19 | // Modules creates a list of joe.Modules that can be used with this configuration. 20 | func (conf Config) Modules() []joe.Module { 21 | var modules []joe.Module 22 | 23 | if conf.SlackToken != "" { 24 | modules = append(modules, slack.Adapter(conf.SlackToken)) 25 | } 26 | 27 | if conf.HTTPListen != "" { 28 | modules = append(modules, joehttp.Server(conf.HTTPListen)) 29 | } 30 | 31 | if conf.RedisAddr != "" { 32 | modules = append(modules, redis.Memory(conf.RedisAddr)) 33 | } 34 | 35 | return modules 36 | } 37 | 38 | func (conf Config) Validate() error { 39 | if conf.HTTPListen == "" { 40 | return errors.New("missing HTTP listen address") 41 | } 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /_examples/07_config/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-joe/joe/examples/config 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/go-joe/http-server v0.5.0 7 | github.com/go-joe/joe v0.9.0 8 | github.com/go-joe/redis-memory v0.3.2 9 | github.com/go-joe/slack-adapter/v2 v2.0.0 10 | github.com/gomodule/redigo v2.0.0+incompatible // indirect 11 | ) 12 | 13 | replace github.com/go-joe/joe => ../.. 14 | -------------------------------------------------------------------------------- /_examples/07_config/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | func main() { 8 | b, err := New2(Config{ 9 | SlackToken: "xoxb-1452345…", 10 | HTTPListen: ":80", 11 | }) 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | 16 | err = b.Run() 17 | if err != nil { 18 | b.Logger.Fatal(err.Error()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /_examples/Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | EXAMPLES = $(shell find . -name 'main.go' | sort) 4 | .PHONY: all $(EXAMPLES) 5 | 6 | # all makes sure all examples actually compile and then updates the README.md 7 | all: $(EXAMPLES) 8 | embedmd -w ../README.md 9 | 10 | # check makes sure all examples actually compile and the README.md is in sync 11 | check: $(EXAMPLES) 12 | @echo "embedmd -d ../README.md" && test -z "$$(embedmd -d ../README.md)" || (echo -e "\nERROR: Examples in README.md are out of sync!\nPlease run \"embedmd -w $(abspath ../README.md)\"" && false) 13 | 14 | # We are building the files simply to detect any compile time issues. 15 | */main.go: 16 | @gofmt -d "$(@D)" 17 | @test -z "$$(gofmt -l "$(@D)")" || (echo "ERROR: bad source code formatting in $(@D)" && false) 18 | cd "$(dir $@)" && go build -o /dev/null -v 19 | cd "$(dir $@)" && go mod tidy 20 | -------------------------------------------------------------------------------- /_examples/README.md: -------------------------------------------------------------------------------- 1 | # Joe Examples 2 | 3 | This directory contains the example code that you can find on the main 4 | [README.md](../README.md) of this repository. The purpose is to be able to 5 | be able to actually compile the provided examples to make sure they do net get 6 | outdated as Joe's API develops. The necessary automation to keep the examples 7 | and the README.md in sync is done via [embedmd](https://github.com/campoy/embedmd). 8 | -------------------------------------------------------------------------------- /adapter.go: -------------------------------------------------------------------------------- 1 | package joe 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "runtime" 10 | "sync" 11 | 12 | "github.com/go-joe/joe/reactions" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | // An Adapter connects the bot with the chat by enabling it to receive and send 17 | // messages. Additionally advanced adapters can emit more events than just the 18 | // ReceiveMessageEvent (e.g. the slack adapter also emits the UserTypingEvent). 19 | // All adapter events must be setup in the RegisterAt function of the Adapter. 20 | // 21 | // Joe provides a default CLIAdapter implementation which connects the bot with 22 | // the local shell to receive messages from stdin and print messages to stdout. 23 | type Adapter interface { 24 | RegisterAt(*Brain) 25 | Send(text, channel string) error 26 | Close() error 27 | } 28 | 29 | // ReactionAwareAdapter is an optional interface that Adapters can implement if 30 | // they support reacting to messages with emojis. 31 | type ReactionAwareAdapter interface { 32 | React(reactions.Reaction, Message) error 33 | } 34 | 35 | // The CLIAdapter is the default Adapter implementation that the bot uses if no 36 | // other adapter was configured. It emits a ReceiveMessageEvent for each line it 37 | // receives from stdin and prints all sent messages to stdout. 38 | // 39 | // The CLIAdapter does not set the Message.Data field. 40 | type CLIAdapter struct { 41 | Prefix string 42 | Input io.ReadCloser 43 | Output io.Writer 44 | Logger *zap.Logger 45 | Author string // used to set the author of the messages, defaults to os.Getenv("USER) 46 | mu sync.Mutex // protects the Output and closing channel 47 | closing chan chan error 48 | } 49 | 50 | // NewCLIAdapter creates a new CLIAdapter. The caller must call Close 51 | // to make the CLIAdapter stop reading messages and emitting events. 52 | func NewCLIAdapter(name string, logger *zap.Logger) *CLIAdapter { 53 | return &CLIAdapter{ 54 | Prefix: fmt.Sprintf("%s > ", name), 55 | Input: os.Stdin, 56 | Output: os.Stdout, 57 | Logger: logger, 58 | Author: os.Getenv("USER"), 59 | closing: make(chan chan error), 60 | } 61 | } 62 | 63 | // RegisterAt starts the CLIAdapter by reading messages from stdin and emitting 64 | // a ReceiveMessageEvent for each of them. Additionally the adapter hooks into 65 | // the InitEvent to print a nice prefix to stdout to show to the user it is 66 | // ready to accept input. 67 | func (a *CLIAdapter) RegisterAt(brain *Brain) { 68 | brain.RegisterHandler(func(evt InitEvent) { 69 | _ = a.print(a.Prefix) 70 | }) 71 | 72 | go a.loop(brain) 73 | } 74 | 75 | func (a *CLIAdapter) loop(brain *Brain) { 76 | input := a.readLines() 77 | 78 | // The adapter loop is built to stay responsive even if the Brain stops 79 | // processing events so we can safely close the CLIAdapter. 80 | // 81 | // We want to print the prefix each time when the Brain has completely 82 | // processed a ReceiveMessageEvent and before we are emitting the next one. 83 | // This gives us a shell-like behavior which signals to the user that she 84 | // can input more data on the CLI. This channel is buffered so we do not 85 | // block the Brain when it executes the callback. 86 | callback := make(chan Event, 1) 87 | callbackFun := func(evt Event) { 88 | callback <- evt 89 | } 90 | 91 | var lines = input // channel represents the case that we receive a new message 92 | 93 | for { 94 | select { 95 | case msg, ok := <-lines: 96 | if !ok { 97 | // no more input from stdin 98 | lines = nil // disable this case and wait for closing signal 99 | continue 100 | } 101 | 102 | lines = nil // disable this case and wait for the callback 103 | brain.Emit(ReceiveMessageEvent{Text: msg, AuthorID: a.Author}, callbackFun) 104 | 105 | case <-callback: 106 | // This case is executed after all ReceiveMessageEvent handlers have 107 | // completed and we can continue with the next line. 108 | _ = a.print(a.Prefix) 109 | lines = input // activate first case again 110 | 111 | case result := <-a.closing: 112 | if lines == nil { 113 | // We were just waiting for our callback 114 | _ = a.print(a.Prefix) 115 | } 116 | 117 | _ = a.print("\n") 118 | result <- a.Input.Close() 119 | return 120 | } 121 | } 122 | } 123 | 124 | // ReadLines reads lines from stdin and returns them in a channel. 125 | // All strings in the returned channel will not include the trailing newline. 126 | // The channel is closed automatically when a.Input is closed. 127 | func (a *CLIAdapter) readLines() <-chan string { 128 | r := bufio.NewReader(a.Input) 129 | lines := make(chan string) 130 | go func() { 131 | var platformSpecificNum int 132 | switch runtime.GOOS { 133 | case "windows": 134 | platformSpecificNum = 2 135 | default: 136 | platformSpecificNum = 1 137 | } 138 | 139 | // This goroutine will exit when we call a.Input.Close() which will make 140 | // r.ReadString(…) return an io.EOF. 141 | for { 142 | line, err := r.ReadString('\n') 143 | switch { 144 | case err == io.EOF: 145 | close(lines) 146 | return 147 | case err != nil: 148 | a.Logger.Error("Failed to read messages from input", zap.Error(err)) 149 | return 150 | } 151 | 152 | lines <- line[:len(line)-platformSpecificNum] 153 | } 154 | }() 155 | 156 | return lines 157 | } 158 | 159 | // Send implements the Adapter interface by sending the given text to stdout. 160 | // The channel argument is required by the Adapter interface but is otherwise ignored. 161 | func (a *CLIAdapter) Send(text, channel string) error { 162 | return a.print(text + "\n") 163 | } 164 | 165 | // React implements the optional ReactionAwareAdapter interface by simply 166 | // printing the given reaction as UTF8 emoji to the CLI. 167 | func (a *CLIAdapter) React(r reactions.Reaction, _ Message) error { 168 | return a.print(r.String() + "\n") 169 | } 170 | 171 | // Close makes the CLIAdapter stop emitting any new events or printing any output. 172 | // Calling this function more than once will result in an error. 173 | func (a *CLIAdapter) Close() error { 174 | if a.closing == nil { 175 | return errors.New("already closed") 176 | } 177 | 178 | a.Logger.Debug("Closing CLIAdapter") 179 | callback := make(chan error) 180 | a.closing <- callback 181 | err := <-callback 182 | 183 | // Mark CLIAdapter as closed by setting its closing channel to nil. 184 | // This will prevent any more output to be printed after this function returns. 185 | a.mu.Lock() 186 | a.closing = nil 187 | a.mu.Unlock() 188 | 189 | return err 190 | } 191 | 192 | func (a *CLIAdapter) print(msg string) error { 193 | a.mu.Lock() 194 | if a.closing == nil { 195 | return errors.New("adapter is closed") 196 | } 197 | _, err := fmt.Fprint(a.Output, msg) 198 | a.mu.Unlock() 199 | 200 | return err 201 | } 202 | -------------------------------------------------------------------------------- /adapter_test.go: -------------------------------------------------------------------------------- 1 | package joe_test 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "testing" 7 | 8 | "github.com/go-joe/joe" 9 | "github.com/go-joe/joe/joetest" 10 | "github.com/go-joe/joe/reactions" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | "go.uber.org/zap/zaptest" 14 | ) 15 | 16 | func cliTestAdapter(t *testing.T) (a *joe.CLIAdapter, output *bytes.Buffer) { 17 | logger := zaptest.NewLogger(t) 18 | a = joe.NewCLIAdapter("test", logger) 19 | output = new(bytes.Buffer) 20 | a.Output = output 21 | a.Author = "TestUser" // ensure tests never depend on external factors such as os.Getenv(…) 22 | return a, output 23 | } 24 | 25 | func TestCLIAdapter_Register(t *testing.T) { 26 | input := new(bytes.Buffer) 27 | a, output := cliTestAdapter(t) 28 | a.Input = ioutil.NopCloser(input) 29 | brain := joetest.NewBrain(t) 30 | messages := brain.Events() 31 | 32 | input.WriteString("Hello\n") 33 | input.WriteString("World\n") 34 | 35 | // Start the Goroutine of the adapter which consumes the input 36 | a.RegisterAt(brain.Brain) 37 | 38 | msg1 := <-messages 39 | msg2 := <-messages 40 | 41 | assert.Equal(t, "Hello", msg1.Data.(joe.ReceiveMessageEvent).Text) 42 | assert.Equal(t, "World", msg2.Data.(joe.ReceiveMessageEvent).Text) 43 | 44 | // Stop the brain to make sure we are done with all callbacks 45 | brain.Finish() 46 | 47 | // Close the adapter to finish up the test 48 | assert.NoError(t, a.Close()) 49 | assert.Contains(t, output.String(), "test > ") 50 | } 51 | 52 | func TestCLIAdapter_Send(t *testing.T) { 53 | a, output := cliTestAdapter(t) 54 | err := a.Send("Hello World", "") 55 | require.NoError(t, err) 56 | assert.Equal(t, "Hello World\n", output.String()) 57 | } 58 | 59 | func TestCLIAdapter_React(t *testing.T) { 60 | a, output := cliTestAdapter(t) 61 | err := a.React(reactions.Thumbsup, joe.Message{}) 62 | require.NoError(t, err) 63 | assert.Equal(t, "👍\n", output.String()) 64 | } 65 | 66 | func TestCLIAdapter_Send_Author(t *testing.T) { 67 | input := new(bytes.Buffer) 68 | a, _ := cliTestAdapter(t) 69 | a.Input = ioutil.NopCloser(input) 70 | a.Author = "Friedrich" 71 | brain := joetest.NewBrain(t) 72 | messages := brain.Events() 73 | 74 | input.WriteString("Test\n") 75 | 76 | // Start the Goroutine of the adapter which consumes the input 77 | a.RegisterAt(brain.Brain) 78 | 79 | msg := <-messages 80 | assert.Equal(t, "Friedrich", msg.Data.(joe.ReceiveMessageEvent).AuthorID) 81 | 82 | brain.Finish() 83 | assert.NoError(t, a.Close()) 84 | } 85 | 86 | func TestCLIAdapter_Close(t *testing.T) { 87 | input := new(bytes.Buffer) 88 | a, output := cliTestAdapter(t) 89 | a.Input = ioutil.NopCloser(input) 90 | brain := joe.NewBrain(a.Logger) 91 | a.RegisterAt(brain) 92 | 93 | err := a.Close() 94 | require.NoError(t, err) 95 | assert.Equal(t, "\n", output.String()) 96 | 97 | err = a.Close() 98 | assert.EqualError(t, err, "already closed") 99 | } 100 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package joe 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "go.uber.org/zap" 9 | ) 10 | 11 | // ErrNotAllowed is returned if the user is not allowed access to a specific scope. 12 | const ErrNotAllowed = Error("not allowed") 13 | 14 | // permissionKeyPrefix is the key prefix in the Storage that all permission keys have. 15 | const permissionKeyPrefix = "joe.permissions." 16 | 17 | // Auth implements logic to add user authorization checks to your bot. 18 | type Auth struct { 19 | logger *zap.Logger 20 | store *Storage 21 | } 22 | 23 | // NewAuth creates a new Auth instance. 24 | func NewAuth(logger *zap.Logger, store *Storage) *Auth { 25 | return &Auth{ 26 | logger: logger, 27 | store: store, 28 | } 29 | } 30 | 31 | // CheckPermission checks if a user has permissions to access a resource under a 32 | // given scope. If the user is not permitted access this function returns 33 | // ErrNotAllowed. 34 | // 35 | // Scopes are interpreted in a hierarchical way where scope A can contain scope B 36 | // if A is a prefix to B. For example, you can check if a user is allowed to 37 | // read or write from the "Example" API by checking the "api.example.read" or 38 | // "api.example.write" scope. When you grant the scope to a user you can now 39 | // either decide only to grant the very specific "api.example.read" scope which 40 | // means the user will not have write permissions or you can allow people 41 | // write-only access via "api.example.write". 42 | // 43 | // Alternatively you can also grant any access to the Example API via "api.example" 44 | // which includes both the read and write scope beneath it. If you choose to, you 45 | // could also allow even more general access to everything in the api via the 46 | // "api" scope. The empty scope "" cannot be granted and will thus always return 47 | // an error in the permission check. 48 | func (a *Auth) CheckPermission(scope, userID string) error { 49 | key := a.permissionsKey(userID) 50 | permissions, err := a.loadPermissions(key) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | a.logger.Debug("Checking user permissions", 56 | zap.String("requested_scope", scope), 57 | zap.String("user_id", userID), 58 | ) 59 | 60 | for _, p := range permissions { 61 | if strings.HasPrefix(scope, p) { 62 | return nil 63 | } 64 | } 65 | 66 | return ErrNotAllowed 67 | } 68 | 69 | // Users returns a list of user IDs having one or more permission scopes. 70 | func (a *Auth) Users() ([]string, error) { 71 | a.logger.Debug("Retrieving all user IDs from storage") 72 | 73 | keys, err := a.store.Keys() 74 | if err != nil { 75 | return nil, fmt.Errorf("failed to load permissions: %w", err) 76 | } 77 | 78 | var userIDs []string 79 | for _, key := range keys { 80 | if strings.HasPrefix(key, permissionKeyPrefix) { 81 | userID := strings.TrimPrefix(key, permissionKeyPrefix) 82 | userIDs = append(userIDs, userID) 83 | } 84 | } 85 | 86 | return userIDs, nil 87 | } 88 | 89 | // UserPermissions returns all permission scopes for a specific user. 90 | func (a *Auth) UserPermissions(userID string) ([]string, error) { 91 | a.logger.Debug("Retrieving user permissions from storage", 92 | zap.String("user_id", userID), 93 | ) 94 | 95 | key := a.permissionsKey(userID) 96 | permissions, err := a.loadPermissions(key) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | return permissions, nil 102 | } 103 | 104 | func (a *Auth) loadPermissions(key string) ([]string, error) { 105 | var permissions []string 106 | ok, err := a.store.Get(key, &permissions) 107 | if err != nil { 108 | return nil, fmt.Errorf("failed to load user permissions: %w", err) 109 | } 110 | 111 | if !ok { 112 | return nil, nil 113 | } 114 | 115 | return permissions, nil 116 | } 117 | 118 | // Grant adds a permission scope to the given user. When a scope was granted 119 | // to a specific user it can be checked later via CheckPermission(…). 120 | // The returned boolean indicates whether the scope was actually added (i.e. true) 121 | // or the user already had the granted scope (false). 122 | // 123 | // Note that granting a scope is an idempotent operations so granting the same 124 | // scope multiple times is a safe operation and will not change the internal 125 | // permissions that are written to the Memory. 126 | // 127 | // The empty scope cannot be granted and trying to do so will result in an error. 128 | // If you want to grant access to all scopes you should prefix them with a 129 | // common scope such as "root." or "api.". 130 | func (a *Auth) Grant(scope, userID string) (bool, error) { 131 | if scope == "" { 132 | return false, errors.New("scope cannot be empty") 133 | } 134 | 135 | key := a.permissionsKey(userID) 136 | oldPermissions, err := a.loadPermissions(key) 137 | if err != nil { 138 | return false, err 139 | } 140 | 141 | newPermissions := make([]string, 0, len(oldPermissions)+1) 142 | for _, p := range oldPermissions { 143 | if strings.HasPrefix(scope, p) { 144 | // The user already has this or a scope that "contains" it 145 | return false, nil 146 | } 147 | 148 | if !strings.HasPrefix(p, scope) { 149 | newPermissions = append(newPermissions, p) 150 | } 151 | } 152 | 153 | a.logger.Info("Granting user permission", 154 | zap.String("userID", userID), 155 | zap.String("scope", scope), 156 | ) 157 | 158 | newPermissions = append(newPermissions, scope) 159 | err = a.updatePermissions(key, newPermissions) 160 | return true, err 161 | } 162 | 163 | // Revoke removes a previously granted permission from a user. If the user does 164 | // not currently have the revoked scope this function returns false and no error. 165 | // 166 | // If you are trying to revoke a permission but the user was previously granted 167 | // a scope that contains the revoked scope this function returns an error. 168 | func (a *Auth) Revoke(scope, userID string) (bool, error) { 169 | if scope == "" { 170 | return false, errors.New("scope cannot be empty") 171 | } 172 | 173 | key := a.permissionsKey(userID) 174 | oldPermissions, err := a.loadPermissions(key) 175 | if err != nil { 176 | return false, err 177 | } 178 | 179 | if len(oldPermissions) == 0 { 180 | return false, nil 181 | } 182 | 183 | var revoked bool 184 | newPermissions := make([]string, 0, len(oldPermissions)) 185 | for _, p := range oldPermissions { 186 | if p == scope { 187 | revoked = true 188 | continue 189 | } 190 | 191 | if strings.HasPrefix(scope, p) { 192 | return false, fmt.Errorf("cannot revoke scope %q because the user still has the more general scope %q", scope, p) 193 | } 194 | 195 | newPermissions = append(newPermissions, p) 196 | } 197 | 198 | if !revoked { 199 | return false, nil 200 | } 201 | 202 | a.logger.Info("Revoking user permission", 203 | zap.String("userID", userID), 204 | zap.String("scope", scope), 205 | ) 206 | 207 | if len(newPermissions) == 0 { 208 | _, err := a.store.Delete(key) 209 | if err != nil { 210 | return false, fmt.Errorf("failed to delete last user permission: %w", err) 211 | } 212 | 213 | return true, nil 214 | } 215 | 216 | err = a.updatePermissions(key, newPermissions) 217 | return true, err 218 | } 219 | 220 | func (a *Auth) updatePermissions(key string, permissions []string) error { 221 | err := a.store.Set(key, permissions) 222 | if err != nil { 223 | return fmt.Errorf("failed to update user permissions: %w", err) 224 | } 225 | 226 | return nil 227 | } 228 | 229 | func (a *Auth) permissionsKey(userID string) string { 230 | return permissionKeyPrefix + userID 231 | } 232 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package joe 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "go.uber.org/zap" 8 | "go.uber.org/zap/zapcore" 9 | ) 10 | 11 | // Config is the configuration of a Bot that can be used or changed during setup 12 | // in a Module. Some configuration settings such as the Logger are read only can 13 | // only be accessed via the corresponding getter function of the Config. 14 | type Config struct { 15 | Context context.Context 16 | Name string 17 | HandlerTimeout time.Duration 18 | 19 | logger *zap.Logger 20 | logLevel zapcore.Level 21 | brain *Brain 22 | store *Storage 23 | adapter Adapter 24 | errs []error 25 | } 26 | 27 | // NewConfig creates a new Config that is used to setup the underlying 28 | // components of a Bot. For the typical use case you do not have to create a 29 | // Config yourself but rather configure a Bot by passing the corresponding 30 | // Modules to joe.New(…). 31 | func NewConfig(logger *zap.Logger, brain *Brain, store *Storage, adapter Adapter) Config { 32 | return Config{ 33 | adapter: adapter, 34 | logger: logger, 35 | brain: brain, 36 | store: store, 37 | } 38 | } 39 | 40 | // The EventEmitter can be used by a Module by calling Config.EventEmitter(). 41 | // Events are emitted asynchronously so every call to Emit is non-blocking. 42 | type EventEmitter interface { 43 | Emit(event interface{}, callbacks ...func(Event)) 44 | } 45 | 46 | // EventEmitter returns the EventEmitter that can be used to send events to the 47 | // Bot and other modules. 48 | func (c *Config) EventEmitter() EventEmitter { 49 | return c.brain 50 | } 51 | 52 | // Logger returns a new named logger. 53 | func (c *Config) Logger(name string) *zap.Logger { 54 | return c.logger.Named(name) 55 | } 56 | 57 | // SetMemory can be used to change the Memory implementation of the bot. 58 | func (c *Config) SetMemory(mem Memory) { 59 | c.store.SetMemory(mem) 60 | } 61 | 62 | // SetMemoryEncoder can be used to change the MemoryEncoder implementation of 63 | // the bot. 64 | func (c *Config) SetMemoryEncoder(enc MemoryEncoder) { 65 | c.store.SetMemoryEncoder(enc) 66 | } 67 | 68 | // SetAdapter can be used to change the Adapter implementation of the Bot. 69 | func (c *Config) SetAdapter(a Adapter) { 70 | c.adapter = a 71 | } 72 | 73 | // RegisterHandler can be used to register an event handler in a Module. 74 | func (c *Config) RegisterHandler(fun interface{}) { 75 | c.brain.RegisterHandler(fun) 76 | } 77 | 78 | // WithContext is an option to replace the default context of a bot. 79 | func WithContext(ctx context.Context) Module { 80 | return contextModule(func(conf *Config) error { 81 | conf.Context = ctx 82 | return nil 83 | }) 84 | } 85 | 86 | // WithHandlerTimeout is an option to set a timeout on event handlers functions. 87 | // By default no timeout is enforced. 88 | func WithHandlerTimeout(timeout time.Duration) Module { 89 | return ModuleFunc(func(conf *Config) error { 90 | conf.HandlerTimeout = timeout 91 | return nil 92 | }) 93 | } 94 | 95 | // WithLogger is an option to replace the default logger of a bot. 96 | func WithLogger(logger *zap.Logger) Module { 97 | return loggerModule(func(conf *Config) error { 98 | conf.logger = logger 99 | return nil 100 | }) 101 | } 102 | 103 | // WithLogLevel is an option to change the default log level of a bot. 104 | func WithLogLevel(level zapcore.Level) Module { 105 | return loggerModule(func(conf *Config) error { 106 | conf.logLevel = level 107 | return nil 108 | }) 109 | } 110 | 111 | type contextModule func(*Config) error 112 | 113 | func (fun contextModule) Apply(conf *Config) error { 114 | return fun(conf) 115 | } 116 | 117 | type loggerModule func(*Config) error 118 | 119 | func (fun loggerModule) Apply(conf *Config) error { 120 | return fun(conf) 121 | } 122 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package joe 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "go.uber.org/zap" 9 | "go.uber.org/zap/zaptest" 10 | ) 11 | 12 | func TestConfig(t *testing.T) { 13 | logger := zaptest.NewLogger(t) 14 | brain := NewBrain(logger) 15 | store := NewStorage(logger) 16 | conf := Config{ 17 | brain: brain, 18 | store: store, 19 | logger: logger, 20 | } 21 | 22 | assert.Equal(t, brain, conf.EventEmitter()) 23 | assert.NotNil(t, logger, conf.Logger("test")) 24 | 25 | adapter := new(MockAdapter) 26 | conf.SetAdapter(adapter) 27 | assert.Equal(t, adapter, conf.adapter) 28 | 29 | mem := newInMemory() 30 | conf.SetMemory(mem) 31 | assert.Equal(t, mem, store.memory) 32 | 33 | enc := jsonEncoder{} 34 | conf.SetMemoryEncoder(enc) 35 | assert.Equal(t, enc, store.encoder) 36 | 37 | conf.RegisterHandler(func(InitEvent) {}) 38 | } 39 | 40 | func TestWithContext(t *testing.T) { 41 | var conf Config 42 | mod := WithContext(ctx) 43 | err := mod.Apply(&conf) 44 | assert.NoError(t, err) 45 | assert.Equal(t, ctx, conf.Context) 46 | } 47 | 48 | func TestWithHandlerTimeout(t *testing.T) { 49 | var conf Config 50 | mod := WithHandlerTimeout(42 * time.Millisecond) 51 | err := mod.Apply(&conf) 52 | assert.NoError(t, err) 53 | assert.Equal(t, 42*time.Millisecond, conf.HandlerTimeout) 54 | } 55 | 56 | func TestWithLogLevel(t *testing.T) { 57 | mod := WithLogLevel(zap.ErrorLevel) 58 | 59 | logger := newLogger([]Module{mod}) 60 | 61 | assert.Nil(t, logger.Check(zap.DebugLevel, "test")) 62 | assert.Nil(t, logger.Check(zap.InfoLevel, "test")) 63 | assert.NotNil(t, logger.Check(zap.ErrorLevel, "test")) 64 | } 65 | 66 | // TestNewLogger simply tests that the zap logger configuration in newLogger() 67 | // doesn't panic. 68 | func TestNewLogger(t *testing.T) { 69 | newLogger(nil) 70 | } 71 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package joe 2 | 3 | // Error is the error type used by Joe. This allows joe errors to be defined as 4 | // constants following https://dave.cheney.net/2016/04/07/constant-errors. 5 | type Error string 6 | 7 | // Error implements the "error" interface of the standard library. 8 | func (err Error) Error() string { 9 | return string(err) 10 | } 11 | 12 | // ErrNotImplemented is returned if the user tries to use a feature that is not 13 | // implemented on the corresponding components (e.g. the Adapter). For instance, 14 | // not all Adapter implementations may support emoji reactions and trying to 15 | // attach a reaction to a message might return this error. 16 | const ErrNotImplemented = Error("not implemented") 17 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package joe 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestError(t *testing.T) { 10 | var err error = Error("test") // compiler check to make sure we are actually implementing the "error" interface 11 | assert.Equal(t, "test", err.Error()) 12 | } 13 | -------------------------------------------------------------------------------- /events.go: -------------------------------------------------------------------------------- 1 | package joe 2 | 3 | // The InitEvent is the first event that is handled by the Brain after the Bot 4 | // is started via Bot.Run(). 5 | type InitEvent struct{} 6 | 7 | // The ShutdownEvent is the last event that is handled by the Brain before it 8 | // stops handling any events after the bot context is done. 9 | type ShutdownEvent struct{} 10 | 11 | // The ReceiveMessageEvent is typically emitted by an Adapter when the Bot sees 12 | // a new message from the chat. 13 | type ReceiveMessageEvent struct { 14 | ID string // The ID of the message, identifying it at least uniquely within the Channel 15 | Text string // The message text. 16 | AuthorID string // A string identifying the author of the message on the adapter. 17 | Channel string // The channel over which the message was received. 18 | 19 | // A message may optionally also contain additional information that was 20 | // received by the Adapter (e.g. with the slack adapter this may be the 21 | // *slack.MessageEvent. Each Adapter implementation should document if and 22 | // what information is available here, if any at all. 23 | Data interface{} 24 | } 25 | 26 | // The UserTypingEvent is emitted by the Adapter and indicates that the Bot 27 | // sees that a user is typing. This event may not be emitted on all Adapter 28 | // implementations but only when it is actually supported (e.g. on slack). 29 | type UserTypingEvent struct { 30 | User User 31 | Channel string 32 | } 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-joe/joe 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/stretchr/testify v1.4.0 7 | go.uber.org/multierr v1.5.0 8 | go.uber.org/zap v1.14.0 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 8 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 9 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 10 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 11 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 12 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 13 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 14 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 15 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 19 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 22 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 23 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 24 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 25 | go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= 26 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 27 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 28 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 29 | go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= 30 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 31 | go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= 32 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 33 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 34 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 35 | go.uber.org/zap v1.14.0 h1:/pduUoebOeeJzTDFuoMgC6nRkiasr1sBCIEorly7m4o= 36 | go.uber.org/zap v1.14.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 37 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 38 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 39 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 40 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 41 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 42 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 43 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 44 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 45 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 46 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 47 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 48 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 49 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 50 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 51 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 52 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= 53 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 54 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 55 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 56 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 57 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 58 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 59 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 60 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 61 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 62 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 63 | -------------------------------------------------------------------------------- /joetest/bot.go: -------------------------------------------------------------------------------- 1 | package joetest 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "io/ioutil" 8 | "time" 9 | 10 | "github.com/go-joe/joe" 11 | "go.uber.org/zap/zaptest" 12 | ) 13 | 14 | // Bot wraps a *joe.Bot for unit testing. 15 | type Bot struct { 16 | *joe.Bot 17 | T TestingT 18 | Input io.Writer 19 | Output io.Reader 20 | Timeout time.Duration // defaults to 1s 21 | 22 | runErr chan error 23 | } 24 | 25 | // NewBot creates a new *Bot instance that can be used in unit tests. 26 | // The Bots will use a CLIAdapter which accepts messages from the Bot.Input 27 | // and write all output to Bot.Output. The logger is a zaptest.Logger which 28 | // sends all logs through the passed TestingT (usually a *testing.T instance). 29 | // 30 | // For ease of testing a Bot can be started and stopped without a cancel via 31 | // Bot.Start() and Bot.Stop(). 32 | func NewBot(t TestingT, modules ...joe.Module) *Bot { 33 | ctx := context.Background() 34 | logger := zaptest.NewLogger(t) 35 | input := new(bytes.Buffer) 36 | output := new(bytes.Buffer) 37 | 38 | b := &Bot{ 39 | T: t, 40 | Input: input, 41 | Output: output, 42 | Timeout: time.Second, 43 | runErr: make(chan error, 1), // buffered so we can return from Bot.Run without blocking 44 | } 45 | 46 | testAdapter := joe.ModuleFunc(func(conf *joe.Config) error { 47 | a := joe.NewCLIAdapter("test", conf.Logger("adapter")) 48 | a.Input = ioutil.NopCloser(input) 49 | a.Output = output 50 | conf.SetAdapter(a) 51 | return nil 52 | }) 53 | 54 | // The testAdapter and logger modules must be passed first so the caller can 55 | // actually inject a different Adapter or logger if required. 56 | testModules := []joe.Module{ 57 | joe.WithLogger(logger), 58 | joe.WithContext(ctx), 59 | testAdapter, 60 | } 61 | 62 | b.Bot = joe.New("test", append(testModules, modules...)...) 63 | return b 64 | } 65 | 66 | // EmitSync emits the given event on the Brain and blocks until all registered 67 | // handlers have completely processed it. 68 | func (b *Bot) EmitSync(event interface{}) { 69 | b.T.Helper() 70 | 71 | done := make(chan bool) 72 | callback := func(joe.Event) { done <- true } 73 | b.Brain.Emit(event, callback) 74 | 75 | select { 76 | case <-done: 77 | // ok, cool 78 | case <-time.After(b.Timeout): 79 | b.T.Errorf("EmitSync timed out") 80 | b.T.FailNow() 81 | } 82 | } 83 | 84 | // Start executes the Bot.Run() function and stores its error result in a channel 85 | // so the caller can eventually execute Bot.Stop() and receive the result. 86 | // This function blocks until the event handler is actually running and emits 87 | // the InitEvent. 88 | func (b *Bot) Start() { 89 | started := make(chan bool) 90 | 91 | type InitTestEvent struct{} 92 | b.Brain.RegisterHandler(func(evt InitTestEvent) { 93 | started <- true 94 | }) 95 | 96 | // When this event is handled we know the bot has completed its startup and 97 | // is ready to process events. The joe.InitEvent isn't really an option here 98 | // because it only marks that the bot is starting but we do not know when 99 | // all other init handlers are done (e.g. for the CLI adapter). 100 | b.Brain.Emit(InitTestEvent{}) 101 | 102 | go func() { 103 | // The error will be available by calling Bot.Stop() 104 | err := b.Run() 105 | if err != nil { 106 | close(started) 107 | } 108 | }() 109 | 110 | <-started 111 | } 112 | 113 | // Run wraps Bot.Run() in order to allow stopping a Bot without having to 114 | // inject another context. 115 | func (b *Bot) Run() error { 116 | b.T.Helper() 117 | err := b.Bot.Run() 118 | b.runErr <- err // b.runErr is buffered so we can return immediately 119 | return err 120 | } 121 | 122 | // Stop stops a running Bot and blocks until it has completed. If Bot.Run() 123 | // returned an error it is passed to the Errorf function of the TestingT that 124 | // was used to create the Bot. 125 | func (b *Bot) Stop() { 126 | ctx, cancel := context.WithCancel(context.Background()) 127 | defer cancel() 128 | 129 | go b.Brain.Shutdown(ctx) 130 | 131 | select { 132 | case err := <-b.runErr: 133 | if err != nil { 134 | b.T.Errorf("Bot.Run() returned an error: %v", err) 135 | } 136 | case <-time.After(b.Timeout): 137 | b.T.Errorf("Stop timed out") 138 | b.T.FailNow() 139 | } 140 | } 141 | 142 | // ReadOutput consumes all data from b.Output and returns it as a string so you 143 | // can easily make assertions on it. 144 | func (b *Bot) ReadOutput() string { 145 | out, err := ioutil.ReadAll(b.Output) 146 | if err != nil { 147 | b.T.Errorf("failed to read all output of bot: %v", err) 148 | return "" 149 | } 150 | 151 | return string(out) 152 | } 153 | -------------------------------------------------------------------------------- /joetest/bot_test.go: -------------------------------------------------------------------------------- 1 | package joetest 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/go-joe/joe" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestBot(t *testing.T) { 15 | b := NewBot(t) 16 | 17 | var seenEvents []TestEvent 18 | b.Brain.RegisterHandler(func(evt TestEvent) { 19 | seenEvents = append(seenEvents, evt) 20 | }) 21 | 22 | b.Start() 23 | assert.Equal(t, "test > ", b.ReadOutput()) 24 | 25 | b.EmitSync(TestEvent{N: 123}) 26 | b.Stop() 27 | 28 | assert.Equal(t, []TestEvent{{N: 123}}, seenEvents) 29 | } 30 | 31 | func TestBotEmitSyncTimeout(t *testing.T) { 32 | mock := new(mockT) 33 | b := NewBot(mock) 34 | b.Timeout = time.Millisecond 35 | 36 | ctx, cancel := context.WithCancel(context.Background()) 37 | defer cancel() 38 | b.Brain.RegisterHandler(func(evt TestEvent) { 39 | <-ctx.Done() 40 | }) 41 | 42 | b.Start() 43 | b.EmitSync(TestEvent{}) 44 | b.Stop() 45 | 46 | require.Len(t, mock.Errors, 2) 47 | assert.True(t, mock.failed) 48 | assert.True(t, mock.fatal) 49 | assert.Equal(t, "EmitSync timed out", mock.Errors[0]) 50 | assert.Equal(t, "Stop timed out", mock.Errors[1]) 51 | } 52 | 53 | func TestBot_RegistrationErrors(t *testing.T) { 54 | b := NewBot(t) 55 | 56 | b.Brain.RegisterHandler(func(evt *TestEvent) { 57 | // handlers cannot use pointers for the event type so registering this 58 | // handler function should create a registration error. 59 | }) 60 | 61 | b.Start() 62 | 63 | select { 64 | case err := <-b.runErr: 65 | require.Error(t, err) 66 | assert.True(t, strings.HasPrefix(err.Error(), "invalid event handlers: ")) 67 | t.Log(err.Error()) 68 | case <-time.After(b.Timeout): 69 | b.T.Errorf("Timeout") 70 | } 71 | } 72 | 73 | func TestBot_RegistrationErrors2(t *testing.T) { 74 | b := NewBot(t) 75 | 76 | b.RespondRegex("invalid regex: (", func(joe.Message) error { 77 | return joe.ErrNotImplemented 78 | }) 79 | 80 | b.Start() 81 | 82 | select { 83 | case err := <-b.runErr: 84 | require.Error(t, err) 85 | assert.True(t, strings.HasPrefix(err.Error(), "invalid event handlers: ")) 86 | t.Log(err.Error()) 87 | case <-time.After(b.Timeout): 88 | b.T.Errorf("Timeout") 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /joetest/brain.go: -------------------------------------------------------------------------------- 1 | package joetest 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | 8 | "github.com/go-joe/joe" 9 | "go.uber.org/zap/zaptest" 10 | ) 11 | 12 | // Brain wraps the joe.Brain for unit testing. 13 | type Brain struct { 14 | *joe.Brain 15 | 16 | mu sync.Mutex 17 | events []interface{} 18 | eventsChan chan joe.Event 19 | } 20 | 21 | // NewBrain creates a new Brain that can be used for unit testing. The Brain 22 | // registers to all events except the (init and shutdown event) and records them 23 | // for later access. The event handling loop of the Brain (i.e. Brain.HandleEvents()) 24 | // is automatically started by this function in a new goroutine and the caller 25 | // must call Brain.Finish() at the end of their tests. 26 | func NewBrain(t TestingT) *Brain { 27 | logger := zaptest.NewLogger(t) 28 | b := &Brain{ 29 | Brain: joe.NewBrain(logger), 30 | eventsChan: make(chan joe.Event, 100), 31 | } 32 | 33 | initialized := make(chan bool) 34 | b.RegisterHandler(b.observeEvent) 35 | b.RegisterHandler(func(joe.InitEvent) { 36 | initialized <- true 37 | }) 38 | 39 | go b.HandleEvents() 40 | <-initialized 41 | 42 | return b 43 | } 44 | 45 | func (b *Brain) observeEvent(evt interface{}) { 46 | switch evt.(type) { 47 | case joe.InitEvent, joe.ShutdownEvent: 48 | return 49 | } 50 | 51 | select { 52 | case b.eventsChan <- joe.Event{Data: evt}: 53 | // ok, lets move on 54 | default: 55 | // nobody is listening, also fine 56 | } 57 | 58 | b.mu.Lock() 59 | b.events = append(b.events, evt) 60 | b.mu.Unlock() 61 | } 62 | 63 | // Finish stops the event handler loop of the Brain and waits until all pending 64 | // events have been processed. 65 | func (b *Brain) Finish() { 66 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 67 | defer cancel() 68 | b.Brain.Shutdown(ctx) 69 | } 70 | 71 | // RecordedEvents returns all events the Brain has processed except the 72 | // joe.InitEvent and joe.ShutdownEvent. 73 | func (b *Brain) RecordedEvents() []interface{} { 74 | b.mu.Lock() 75 | events := make([]interface{}, len(b.events)) 76 | copy(events, b.events) 77 | b.mu.Unlock() 78 | 79 | return events 80 | } 81 | 82 | // Events returns a channel that receives all emitted events. 83 | func (b *Brain) Events() <-chan joe.Event { 84 | return b.eventsChan 85 | } 86 | -------------------------------------------------------------------------------- /joetest/brain_test.go: -------------------------------------------------------------------------------- 1 | package joetest 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | type TestEvent struct{ N int } 10 | 11 | func TestBrain(t *testing.T) { 12 | b := NewBrain(t) 13 | 14 | b.Emit(TestEvent{1}) 15 | b.Emit(TestEvent{2}) 16 | b.Emit(TestEvent{3}) 17 | b.Emit(TestEvent{4}) 18 | 19 | b.Finish() 20 | 21 | expectedEvents := []interface{}{ 22 | TestEvent{1}, 23 | TestEvent{2}, 24 | TestEvent{3}, 25 | TestEvent{4}, 26 | } 27 | 28 | actualEvents := b.RecordedEvents() 29 | assert.Equal(t, expectedEvents, actualEvents) 30 | } 31 | -------------------------------------------------------------------------------- /joetest/storage.go: -------------------------------------------------------------------------------- 1 | package joetest 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/go-joe/joe" 7 | "go.uber.org/zap/zaptest" 8 | ) 9 | 10 | // Storage wraps a joe.Storage for unit testing purposes. 11 | type Storage struct { 12 | *joe.Storage 13 | T TestingT 14 | } 15 | 16 | // NewStorage creates a new Storage. 17 | func NewStorage(t TestingT) *Storage { 18 | logger := zaptest.NewLogger(t) 19 | return &Storage{ 20 | Storage: joe.NewStorage(logger), 21 | T: t, 22 | } 23 | } 24 | 25 | // MustSet assigns the value to the given key and fails the test immediately if 26 | // there was an error. 27 | func (s *Storage) MustSet(key string, value interface{}) { 28 | err := s.Set(key, value) 29 | if err != nil { 30 | s.T.Fatal("Failed to set key in storage:", err) 31 | } 32 | } 33 | 34 | // AssertEquals checks that the actual value under the given key equals an 35 | // expected value. 36 | func (s *Storage) AssertEquals(key string, expectedVal interface{}) { 37 | typ := reflect.TypeOf(expectedVal) 38 | actual := reflect.New(typ) 39 | ok, err := s.Get(key, actual.Interface()) 40 | if err != nil { 41 | s.T.Errorf("Error while getting key %q from storage: %v", key, err) 42 | return 43 | } 44 | 45 | if !ok { 46 | s.T.Errorf("Expected storage to contain key %q but it does not", key) 47 | return 48 | } 49 | 50 | actualVal := actual.Elem().Interface() 51 | if !reflect.DeepEqual(expectedVal, actualVal) { 52 | s.T.Errorf("Value of key %q does not equal expected value\ngot: %#v\nwant: %#v", 53 | key, 54 | actualVal, 55 | expectedVal, 56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /joetest/storage_test.go: -------------------------------------------------------------------------------- 1 | package joetest 2 | 3 | import "testing" 4 | 5 | func TestStorage(t *testing.T) { 6 | type CustomType struct{ N int } 7 | 8 | cases := []struct { 9 | key string 10 | value interface{} 11 | }{ 12 | {"test.string", "foobar"}, 13 | {"test.bool", true}, 14 | {"test.int", 42}, 15 | {"test.float", 3.14159265359}, 16 | {"test.string_slice", []string{"foo", "bar"}}, 17 | {"test.struct", CustomType{1234}}, 18 | {"test.ptr", &CustomType{1234}}, 19 | } 20 | 21 | for _, c := range cases { 22 | store := NewStorage(t) 23 | store.MustSet(c.key, c.value) 24 | store.AssertEquals(c.key, c.value) 25 | } 26 | } 27 | 28 | func TestStorage_AssertEqualsError(t *testing.T) { 29 | mock := new(mockT) 30 | store := NewStorage(mock) 31 | store.AssertEquals("does-not-exist", "xxx") 32 | 33 | if len(mock.Errors) != 1 { 34 | t.Fatal("Expected one error but got none") 35 | } 36 | 37 | expected := `Expected storage to contain key "does-not-exist" but it does not` 38 | if mock.Errors[0] != expected { 39 | t.Errorf("Expected errors %q but got %q", expected, mock.Errors[0]) 40 | } 41 | 42 | store.MustSet("test", "foo") 43 | store.AssertEquals("test", "bar") 44 | 45 | if len(mock.Errors) != 2 { 46 | t.Fatalf("Expected one error but got %d", len(mock.Errors)) 47 | } 48 | 49 | expected = `Value of key "test" does not equal expected value 50 | got: "foo" 51 | want: "bar"` 52 | if mock.Errors[1] != expected { 53 | t.Errorf("Expected errors %q but got %q", expected, mock.Errors[1]) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /joetest/testing.go: -------------------------------------------------------------------------------- 1 | // Package joetest implements helpers to implement unit tests for bots. 2 | package joetest 3 | 4 | // TestingT is the minimum required subset of the testing API used in the 5 | // joetest package. TestingT is implemented both by *testing.T and *testing.B. 6 | type TestingT interface { 7 | Logf(string, ...interface{}) 8 | Errorf(string, ...interface{}) 9 | Fail() 10 | Failed() bool 11 | Fatal(args ...interface{}) 12 | Name() string 13 | FailNow() 14 | Helper() 15 | } 16 | -------------------------------------------------------------------------------- /joetest/testing_test.go: -------------------------------------------------------------------------------- 1 | package joetest 2 | 3 | import "fmt" 4 | 5 | type mockT struct { 6 | Errors []string 7 | failed bool 8 | fatal bool 9 | } 10 | 11 | func (m *mockT) Logf(string, ...interface{}) {} 12 | 13 | func (m *mockT) Errorf(msg string, args ...interface{}) { 14 | if len(args) > 0 { 15 | msg = fmt.Sprintf(msg, args...) 16 | } 17 | m.Errors = append(m.Errors, msg) 18 | } 19 | 20 | func (m *mockT) Fail() { 21 | m.failed = true 22 | } 23 | 24 | func (m *mockT) Failed() bool { 25 | return m.failed 26 | } 27 | 28 | func (m *mockT) Fatal(args ...interface{}) { 29 | m.failed = true 30 | m.fatal = true 31 | } 32 | 33 | func (*mockT) Name() string { 34 | return "mock" 35 | } 36 | 37 | func (m *mockT) FailNow() { 38 | m.Fatal() 39 | } 40 | 41 | func (*mockT) Helper() {} 42 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package joe 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/go-joe/joe/reactions" 8 | ) 9 | 10 | // A Message is automatically created from a ReceiveMessageEvent and then passed 11 | // to the RespondFunc that was registered via Bot.Respond(…) or Bot.RespondRegex(…) 12 | // when the message matches the regular expression of the handler. 13 | type Message struct { 14 | Context context.Context 15 | ID string // The ID of the message, identifying it at least uniquely within the Channel 16 | Text string 17 | AuthorID string 18 | Channel string 19 | Matches []string // contains all sub matches of the regular expression that matched the Text 20 | Data interface{} // corresponds to the ReceiveMessageEvent.Data field 21 | 22 | adapter Adapter 23 | } 24 | 25 | // Respond is a helper function to directly send a response back to the channel 26 | // the message originated from. This function ignores any error when sending the 27 | // response. If you want to handle the error use Message.RespondE instead. 28 | func (msg *Message) Respond(text string, args ...interface{}) { 29 | _ = msg.RespondE(text, args...) 30 | } 31 | 32 | // RespondE is a helper function to directly send a response back to the channel 33 | // the message originated from. If there was an error it will be returned from 34 | // this function. 35 | func (msg *Message) RespondE(text string, args ...interface{}) error { 36 | if len(args) > 0 { 37 | text = fmt.Sprintf(text, args...) 38 | } 39 | 40 | return msg.adapter.Send(text, msg.Channel) 41 | } 42 | 43 | // React attempts to let the Adapter attach the given reaction to this message. 44 | // If the adapter does not support this feature this function will return 45 | // ErrNotImplemented. 46 | func (msg *Message) React(reaction reactions.Reaction) error { 47 | adapter, ok := msg.adapter.(ReactionAwareAdapter) 48 | if !ok { 49 | return ErrNotImplemented 50 | } 51 | 52 | return adapter.React(reaction, *msg) 53 | } 54 | -------------------------------------------------------------------------------- /message_test.go: -------------------------------------------------------------------------------- 1 | package joe 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/go-joe/joe/reactions" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | func TestMessage_Respond(t *testing.T) { 13 | a := new(MockAdapter) 14 | msg := Message{adapter: a, Channel: "test"} 15 | 16 | a.On("Send", "Hello world, The Answer is 42", "test").Return(nil) 17 | msg.Respond("Hello %s, The Answer is %d", "world", 42) 18 | a.AssertExpectations(t) 19 | } 20 | 21 | func TestMessage_RespondE(t *testing.T) { 22 | a := new(MockAdapter) 23 | msg := Message{adapter: a, Channel: "test"} 24 | 25 | err := errors.New("a wild issue occurred") 26 | a.On("Send", "Hello world", "test").Return(err) 27 | actual := msg.RespondE("Hello world") 28 | 29 | assert.Equal(t, err, actual) 30 | a.AssertExpectations(t) 31 | } 32 | 33 | func TestMessage_React_NotImplemented(t *testing.T) { 34 | a := new(MockAdapter) 35 | msg := Message{adapter: a} 36 | 37 | err := msg.React(reactions.Thumbsup) 38 | assert.Equal(t, ErrNotImplemented, err) 39 | a.AssertExpectations(t) 40 | } 41 | 42 | func TestMessage_React(t *testing.T) { 43 | a := new(ExtendedMockAdapter) 44 | msg := Message{adapter: a} 45 | 46 | err := errors.New("this clearly failed") 47 | a.On("React", reactions.Thumbsup, msg).Return(err) 48 | actual := msg.React(reactions.Thumbsup) 49 | 50 | assert.Equal(t, err, actual) 51 | a.AssertExpectations(t) 52 | } 53 | 54 | type MockAdapter struct { 55 | mock.Mock 56 | } 57 | 58 | func (a *MockAdapter) RegisterAt(b *Brain) { 59 | a.Called(b) 60 | } 61 | 62 | func (a *MockAdapter) Send(text, channel string) error { 63 | args := a.Called(text, channel) 64 | return args.Error(0) 65 | } 66 | 67 | func (a *MockAdapter) Close() error { 68 | args := a.Called() 69 | return args.Error(0) 70 | } 71 | 72 | type ExtendedMockAdapter struct { 73 | MockAdapter 74 | } 75 | 76 | func (a *ExtendedMockAdapter) React(r reactions.Reaction, msg Message) error { 77 | args := a.Called(r, msg) 78 | return args.Error(0) 79 | } 80 | -------------------------------------------------------------------------------- /reactions/events.go: -------------------------------------------------------------------------------- 1 | package reactions 2 | 3 | // An Event may be emitted by a chat Adapter to indicate that a message 4 | // received a reaction. 5 | type Event struct { 6 | Reaction Reaction 7 | MessageID string 8 | Channel string 9 | AuthorID string 10 | } 11 | -------------------------------------------------------------------------------- /storage.go: -------------------------------------------------------------------------------- 1 | package joe 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sort" 7 | "sync" 8 | 9 | "go.uber.org/zap" 10 | ) 11 | 12 | // A Storage provides a convenient interface to a Memory implementation. It is 13 | // responsible for how the actual key value data is encoded and provides 14 | // concurrent access as well as logging. 15 | // 16 | // The default Storage that is returned by joe.NewStorage() encodes values as 17 | // JSON and stores them in-memory. 18 | type Storage struct { 19 | logger *zap.Logger 20 | mu sync.RWMutex 21 | memory Memory 22 | encoder MemoryEncoder 23 | } 24 | 25 | // The Memory interface allows the bot to persist data as key-value pairs. 26 | // The default implementation of the Memory is to store all keys and values in 27 | // a map (i.e. in-memory). Other implementations typically offer actual long term 28 | // persistence into a file or to redis. 29 | type Memory interface { 30 | Set(key string, value []byte) error 31 | Get(key string) ([]byte, bool, error) 32 | Delete(key string) (bool, error) 33 | Keys() ([]string, error) 34 | Close() error 35 | } 36 | 37 | // A MemoryEncoder is used to encode and decode any values that are stored in 38 | // the Memory. The default implementation that is used by the Storage uses a 39 | // JSON encoding. 40 | type MemoryEncoder interface { 41 | Encode(value interface{}) ([]byte, error) 42 | Decode(data []byte, target interface{}) error 43 | } 44 | 45 | type inMemory struct { 46 | data map[string][]byte 47 | } 48 | 49 | type jsonEncoder struct{} 50 | 51 | // NewStorage creates a new Storage instance that encodes values as JSON and 52 | // stores them in-memory. You can change the memory and encoding via the 53 | // provided setters. 54 | func NewStorage(logger *zap.Logger) *Storage { 55 | return &Storage{ 56 | logger: logger, 57 | memory: newInMemory(), 58 | encoder: new(jsonEncoder), 59 | } 60 | } 61 | 62 | // SetMemory assigns a different Memory implementation. 63 | func (s *Storage) SetMemory(m Memory) { 64 | s.mu.Lock() 65 | s.memory = m 66 | s.mu.Unlock() 67 | } 68 | 69 | // SetMemoryEncoder assigns a different MemoryEncoder. 70 | func (s *Storage) SetMemoryEncoder(enc MemoryEncoder) { 71 | s.mu.Lock() 72 | s.encoder = enc 73 | s.mu.Unlock() 74 | } 75 | 76 | // Keys returns all keys known to the Memory. 77 | func (s *Storage) Keys() ([]string, error) { 78 | s.mu.RLock() 79 | keys, err := s.memory.Keys() 80 | s.mu.RUnlock() 81 | 82 | sort.Strings(keys) 83 | return keys, err 84 | } 85 | 86 | // Set encodes the given data and stores it in the Memory that is managed by the 87 | // Storage. 88 | func (s *Storage) Set(key string, value interface{}) error { 89 | data, err := s.encoder.Encode(value) 90 | if err != nil { 91 | return fmt.Errorf("encode data: %w", err) 92 | } 93 | 94 | s.mu.Lock() 95 | s.logger.Debug("Writing data to memory", zap.String("key", key)) 96 | err = s.memory.Set(key, data) 97 | s.mu.Unlock() 98 | 99 | return err 100 | } 101 | 102 | // Get retrieves the value under the requested key and decodes it into the 103 | // passed "value" argument which must be a pointer. The boolean return value 104 | // indicates if the value actually existed in the Memory and is false if it did 105 | // not. It is legal to pass as the value if you only want to check if 106 | // the given key exists but you do not actually care about the concrete value. 107 | func (s *Storage) Get(key string, value interface{}) (bool, error) { 108 | s.mu.RLock() 109 | s.logger.Debug("Retrieving data from memory", zap.String("key", key)) 110 | data, ok, err := s.memory.Get(key) 111 | s.mu.RUnlock() 112 | if err != nil { 113 | return false, err 114 | } 115 | 116 | if !ok || value == nil { 117 | return ok, nil 118 | } 119 | 120 | err = s.encoder.Decode(data, value) 121 | if err != nil { 122 | return false, fmt.Errorf("decode data: %w", err) 123 | } 124 | 125 | return true, nil 126 | } 127 | 128 | // Delete removes a key and its associated value from the memory. The boolean 129 | // return value indicates if the key existed or not. 130 | func (s *Storage) Delete(key string) (bool, error) { 131 | s.mu.Lock() 132 | s.logger.Debug("Deleting data from memory", zap.String("key", key)) 133 | ok, err := s.memory.Delete(key) 134 | s.mu.Unlock() 135 | 136 | return ok, err 137 | } 138 | 139 | // Close closes the Memory that is managed by this Storage. 140 | func (s *Storage) Close() error { 141 | s.mu.Lock() 142 | err := s.memory.Close() 143 | s.mu.Unlock() 144 | return err 145 | } 146 | 147 | func newInMemory() *inMemory { 148 | return &inMemory{data: map[string][]byte{}} 149 | } 150 | 151 | func (m *inMemory) Set(key string, value []byte) error { 152 | m.data[key] = value 153 | return nil 154 | } 155 | 156 | func (m *inMemory) Get(key string) ([]byte, bool, error) { 157 | value, ok := m.data[key] 158 | return value, ok, nil 159 | } 160 | 161 | func (m *inMemory) Delete(key string) (bool, error) { 162 | _, ok := m.data[key] 163 | delete(m.data, key) 164 | return ok, nil 165 | } 166 | 167 | func (m *inMemory) Keys() ([]string, error) { 168 | keys := make([]string, 0, len(m.data)) 169 | for k := range m.data { 170 | keys = append(keys, k) 171 | } 172 | 173 | return keys, nil 174 | } 175 | 176 | func (m *inMemory) Close() error { 177 | m.data = map[string][]byte{} 178 | return nil 179 | } 180 | 181 | func (jsonEncoder) Encode(value interface{}) ([]byte, error) { 182 | return json.Marshal(value) 183 | } 184 | 185 | func (jsonEncoder) Decode(data []byte, target interface{}) error { 186 | return json.Unmarshal(data, target) 187 | } 188 | -------------------------------------------------------------------------------- /storage_test.go: -------------------------------------------------------------------------------- 1 | package joe 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "errors" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | "go.uber.org/zap/zaptest" 12 | ) 13 | 14 | func TestStorage(t *testing.T) { 15 | logger := zaptest.NewLogger(t) 16 | store := NewStorage(logger) 17 | 18 | ok, err := store.Get("test", nil) 19 | assert.NoError(t, err) 20 | assert.False(t, ok) 21 | 22 | err = store.Set("test", "foo") 23 | assert.NoError(t, err) 24 | 25 | err = store.Set("test", "foo") 26 | assert.NoError(t, err, "setting a key more than once should not error") 27 | 28 | err = store.Set("test-2", "bar") 29 | assert.NoError(t, err) 30 | 31 | keys, err := store.Keys() 32 | assert.NoError(t, err) 33 | assert.Equal(t, []string{"test", "test-2"}, keys) 34 | 35 | ok, err = store.Get("test", nil) 36 | assert.NoError(t, err, "getting a key without a target to unmarshal to should not fail") 37 | assert.True(t, ok) 38 | 39 | var val string 40 | ok, err = store.Get("test", &val) 41 | assert.NoError(t, err) 42 | assert.True(t, ok) 43 | assert.Equal(t, "foo", val) 44 | 45 | ok, err = store.Delete("does-not-exist") 46 | assert.NoError(t, err) 47 | assert.False(t, ok) 48 | 49 | ok, err = store.Delete("test") 50 | assert.NoError(t, err) 51 | assert.True(t, ok) 52 | 53 | ok, err = store.Get("test", nil) 54 | assert.NoError(t, err) 55 | assert.False(t, ok) 56 | 57 | assert.NoError(t, store.Close()) 58 | } 59 | 60 | func TestStorage_Encoder(t *testing.T) { 61 | logger := zaptest.NewLogger(t) 62 | enc := new(gobEncoder) 63 | store := NewStorage(logger) 64 | store.SetMemoryEncoder(enc) 65 | 66 | val := []string{"foo", "bar"} 67 | err := store.Set("test", val) 68 | require.NoError(t, err) 69 | 70 | var actual []string 71 | ok, err := store.Get("test", &actual) 72 | require.NoError(t, err) 73 | assert.True(t, ok) 74 | assert.Equal(t, val, actual) 75 | } 76 | 77 | func TestStorage_EncoderErrors(t *testing.T) { 78 | logger := zaptest.NewLogger(t) 79 | enc := new(gobEncoder) 80 | 81 | store := NewStorage(logger) 82 | store.SetMemoryEncoder(enc) 83 | 84 | err := store.Set("test", "ok") 85 | require.NoError(t, err, "should insert the first value without an error") 86 | 87 | enc.encodeErr = errors.New("something went wrong") 88 | err = store.Set("test", "foo") 89 | assert.EqualError(t, err, "encode data: something went wrong") 90 | 91 | var actual []string 92 | enc.decodeErr = errors.New("this did not work") 93 | ok, err := store.Get("test", &actual) 94 | assert.EqualError(t, err, "decode data: this did not work") 95 | assert.False(t, ok) 96 | } 97 | 98 | // gobEncoder is an example of a different encoder. This is not part of joe to 99 | // avoid the extra import in production code. 100 | type gobEncoder struct { 101 | encodeErr error 102 | decodeErr error 103 | } 104 | 105 | func (e gobEncoder) Encode(value interface{}) ([]byte, error) { 106 | if err := e.encodeErr; err != nil { 107 | return nil, err 108 | } 109 | 110 | data := new(bytes.Buffer) 111 | enc := gob.NewEncoder(data) 112 | err := enc.Encode(value) 113 | return data.Bytes(), err 114 | } 115 | 116 | func (e gobEncoder) Decode(data []byte, target interface{}) error { 117 | if err := e.decodeErr; err != nil { 118 | return err 119 | } 120 | 121 | enc := gob.NewDecoder(bytes.NewBuffer(data)) 122 | return enc.Decode(target) 123 | } 124 | -------------------------------------------------------------------------------- /user.go: -------------------------------------------------------------------------------- 1 | package joe 2 | 3 | // User contains all the information about a user. 4 | type User struct { 5 | ID string 6 | Name string 7 | RealName string 8 | } 9 | --------------------------------------------------------------------------------