[docs] Move documentation from Wiki to docs-site (#49)

This commit is contained in:
Knut Ahlers 2023-08-14 15:44:23 +02:00 committed by GitHub
parent 28cc95f3d9
commit ad3162e263
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1067 additions and 344 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
docs/static/* filter=lfs diff=lfs merge=lfs -text

57
.github/workflows/doc-generator.yml vendored Normal file
View File

@ -0,0 +1,57 @@
---
name: doc-generator
on: push
jobs:
doc-generator:
defaults:
run:
shell: bash
container:
image: luzifer/archlinux
env:
CGO_ENABLED: 0
GOPATH: /go
permissions:
contents: read
pages: write
id-token: write
runs-on: ubuntu-latest
steps:
- name: Install required packages
run: |
pacman -Syy --noconfirm \
curl \
git \
git-lfs \
make \
tar
- uses: actions/checkout@v3
with:
lfs: true
submodules: true
- name: Marking workdir safe
run: git config --global --add safe.directory /__w/twitch-bot/twitch-bot
- name: Generate documentation
run: make render_docs
- name: Upload GitHub Pages artifact
if: github.ref == 'refs/heads/master'
uses: actions/upload-pages-artifact@v1
with:
path: .rendered-docs
- name: Deploy artifact
if: github.ref == 'refs/heads/master'
uses: actions/deploy-pages@v1
...

View File

@ -1,38 +0,0 @@
---
name: wiki-update
on:
push:
branches:
- master
jobs:
wiki-update:
defaults:
run:
shell: bash
container:
image: luzifer/archlinux
env:
CGO_ENABLED: 0
GOPATH: /go
runs-on: ubuntu-latest
steps:
- name: Install required packages
run: |
pacman -Syy --noconfirm \
git
- uses: actions/checkout@v3
- name: Marking workdir safe
run: git config --global --add safe.directory /__w/twitch-bot/twitch-bot
- name: Update Wiki
run: 'git push https://github.com/Luzifer/twitch-bot.wiki.git $(git subtree split --prefix wiki HEAD):refs/heads/master --force'
...

4
.gitignore vendored
View File

@ -1,11 +1,15 @@
config
config.hcl
config.yaml
docs/resources/_gen
editor/app.css
editor/app.js
editor/bundle.*
.env
hugo_*
.hugo_build.lock
node_modules
.rendered-docs
storage.db
storage.db-journal
storage.json.gz

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "docs/themes/ace-documentation"]
path = docs/themes/ace-documentation
url = https://github.com/vantagedesign/ace-documentation.git

View File

@ -1,5 +1,13 @@
HUGO_VERSION:=0.117.0
default: lint frontend_lint test
build_prod: frontend_prod
go build \
-trimpath \
-mod=readonly \
-ldflags "-X main.version=$(shell git describe --tags --always || echo dev)"
lint:
golangci-lint run
@ -27,17 +35,6 @@ frontend_lint: node_modules
node_modules:
npm ci
# --- Wiki Updates
actor_docs:
go run . --storage-conn-string $(shell mktemp).db actor-docs >wiki/Actors.md
pull_wiki:
git subtree pull --prefix=wiki https://github.com/Luzifer/twitch-bot.wiki.git master --squash
push_wiki:
git subtree push --prefix=wiki https://github.com/Luzifer/twitch-bot.wiki.git master
# --- Tools
update_ua_list:
@ -56,3 +53,19 @@ trivy:
--scanners config,license,secret,vuln \
--severity HIGH,CRITICAL \
--skip-dirs docs
# -- Documentation Site --
actor_docs:
go run . --storage-conn-string $(shell mktemp).db actor-docs >docs/content/configuration/actors.md
eventclient_docs:
echo -e "---\ntitle: EventClient\nweight: 10000\n---\n" >docs/content/overlays/eventclient.md
docker run --rm -i -v $(CURDIR):$(CURDIR) -w $(CURDIR) node:18-alpine sh -ec 'npx --yes jsdoc-to-markdown --files ./internal/apimodules/overlays/default/eventclient.js' >>docs/content/overlays/eventclient.md
render_docs: hugo_$(HUGO_VERSION)
./hugo_$(HUGO_VERSION) --cleanDestinationDir --gc --source docs
hugo_$(HUGO_VERSION):
curl -sSfL https://github.com/gohugoio/hugo/releases/download/v$(HUGO_VERSION)/hugo_extended_$(HUGO_VERSION)_linux-amd64.tar.gz | tar -xz hugo
mv hugo $@

View File

@ -1,4 +1,10 @@
# Available Actions
---
title: "Available Actions"
---
{{ "{{< lead >}}" }}
All these actions can be executed by your bot as soon as you add them to rules. Read their documentation to learn how to master them.
{{ "{{< /lead >}}" }}
{{ range .Actors }}
## {{ .Name }}

View File

@ -0,0 +1,6 @@
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
---

19
docs/content/_index.md Normal file
View File

@ -0,0 +1,19 @@
---
title: Twitch-Bot Documentation
---
{{< lead >}}
You are tired of all those cloud-bots working only sometimes, messing up at random times (always when you need them most) and limiting you to very little functionality while forcing you to give them every single possible permission available? Twitch-Bot is a **fully open-source** bot you can host yourself giving you **full control** over its functions and being **extensible** using custom scripts and commands any developer can build for you. Additionally you are in control which **permissions** to give to the bot and which you rather not want it to have.
{{< /lead >}}
## Features
**Open-Source:** This means you (or any developer you trust) can look up how things work inside the bot, can modify the bot yourself and be sure the bot does not use the access you are granting it to do stuff you don't want it to do. Also you are not dependent on some company to keep the bot running for you but are in control over it. In case I'm no longer willing to develop the bot, it will not cease to exist but can be developed further by anyone.
**Overlays:** The bot contains a web-server to host custom overlays which can be built like any website with **HTML and Javascript**. Some default overlays are included ready to use and for everything not available in the default distribution there is a helper library available to connect to the bot and work with events and bot state.
**YAML Configuration:** The whole configuration is stored in a single YAML file, not in any proprietary format. You can simply create a backup of that file and even if some mistake or broken server happens you simply put back the configuration file and all of your rules, auto-messages, API-keys are instantly back again. (What's not in the configuration file is the data the bot stores like counters, events and variables.)
**Common Database Formats:** All the data mentioned in the last point is stored in a common database format like **SQLite**, **MySQL** or **PostgreSQL**. With exception of the credentials all the data is stored in a plain format which means you can use well-known database tooling to create backups. This also means you can use custom tooling to do with the data what you want! (Though a warning for this point: The database schema is not guaranteed to be stable! While it's possible I do not recommend directly accessing the data in the database for other tools.)
**API-First Design:** The bot is built to have an API and an included documentation for this API. Most of its functionality is exposed through the API and you can easily build tooling against that API to make to bot do your bidding.

View File

@ -0,0 +1,15 @@
---
title: "Configuration"
weight: 2
---
{{< lead >}}
As the bot is capable of doing a lot of things, the configuration needs to cover all of this and therefore isn't simple to master. This section is to help you mastering your configuration and to help you to get your bot to speed.
{{< /lead >}}
## What to do here?
- Have a look at the [Config-File Syntax]({{< ref "config-file.md" >}}) to get an overview what you can do in your `config.yaml`
- Read about the [Available Actions]({{< ref "actors.md" >}}) to see what the bot is capable of
- See [Available Events]({{< ref "events.md" >}}) to get an overview on which events you can react with those actions
- Learn about [Templating]({{< ref "templating.md" >}}) for knowledge about the template syntax and available functions

View File

@ -1,4 +1,10 @@
# Available Actions
---
title: "Available Actions"
---
{{< lead >}}
All these actions can be executed by your bot as soon as you add them to rules. Read their documentation to learn how to master them.
{{< /lead >}}
## Add Fields to Event
@ -396,7 +402,11 @@ Scans for links in the message and adds the "links" field to the event data
```yaml
- type: linkdetector
# Does not have configuration attributes
attributes:
# Enable heuristic scans to find links with spaces or other means of obfuscation in them
# Optional: true
# Type: bool
heuristic: false
```
## Send RAW Message

View File

@ -1,4 +1,10 @@
## Configuration
---
title: "Config-File Syntax"
---
{{< lead >}}
The YAML configuration file is the heart of the bot configuration. You can configure every aspect of the bot using the configuration file. The web-interface afterwards allows to modify the configuration file to assist you with the configuration.
{{< /lead >}}
```yaml
---
@ -139,49 +145,3 @@ rules: # See below for examples
...
```
## Command executions
Your command will get a JSON object passed through `stdin` you can parse to gain details about the message. It is expected to yield an array of actions on `stdout` and exit with status `0`. If it does not the action will be marked failed. In case you need to output debug output you can use `stderr` which is directly piped to the bots `stderr`.
This is an example input you might get on `stdin`:
```json
{
"badges": {
"glhf-pledge": 1,
"moderator": 1
},
"channel": "#tezrian",
"message": "!test",
"tags": {
"badge-info": "",
"badges": "moderator/1,glhf-pledge/1",
"client-nonce": "6801c82a341f728dbbaad87ef30eae49",
"color": "#A72920",
"display-name": "Luziferus",
"emotes": "",
"flags": "",
"id": "dca06466-3741-4b22-8339-4cb5b07a02cc",
"mod": "1",
"room-id": "485884564",
"subscriber": "0",
"tmi-sent-ts": "1610313040489",
"turbo": "0",
"user-id": "69699328",
"user-type": "mod"
},
"username": "luziferus"
}
```
The example was dumped using this action:
```yaml
- actions:
- type: script
attributes:
command: [/usr/bin/bash, -c, "jq . >&2"]
match_channels: ['#tezrian']
match_message: '^!test'
```

View File

@ -1,4 +1,6 @@
# Available Events
---
title: "Available Events"
---
## `ban`

View File

@ -1,42 +1,58 @@
# Rule examples
---
title: "Rule Examples"
---
## Chat-addable generic text-respond-commands
```yaml
# Respond with variable content if set
- actions:
- uuid: 688e631f-08a8-5544-b4b2-1737ea71ce00
description: Trigger Generic Command
actions:
- type: respond
attributes:
message: '{{ variable (list "genericcmd" .channel (group 1) | join ":") }}'
disable_on_template: '{{ eq (variable (list "genericcmd" .channel (group 1) | joih ":")) "" }}'
match_channels: ['#mychannel']
match_message: '^!([^\s]+)(?: |$)'
cooldown: 1m0s
match_channels:
- '#luziferus'
- '#tezrian'
match_message: '^!([^\s]+)(?: |$)'
disable_on_template: '{{ eq (variable (list "genericcmd" .channel (group 1) | join ":")) "" }}'
# Set variable content to content of chat command
- actions:
- uuid: ba4f7bb3-af39-5c57-bb97-216a8af69246
description: Set Generic Command
actions:
- type: setvariable
attributes:
variable: '{{ list "genericcmd" .channel (group 1) | join ":" }}'
set: '{{ group 2 }}'
variable: '{{ list "genericcmd" .channel (group 1) | join ":" }}'
- type: respond
attributes:
message: '[Admin] Set command !{{ group 1 }} to "{{ group 2 }}"'
enable_on: [broadcaster, moderator]
match_channels: ['#mychannel']
match_message: '^!setcmd ([^\s]+) (.*)'
match_channels:
- '#luziferus'
- '#tezrian'
match_message: ^!setcmd ([^\s]+) (.*)
enable_on:
- broadcaster
- moderator
# Remove variable and therefore delete command
- actions:
- uuid: 21619e80-2c6a-536e-8b83-e5fe6c580356
description: Clear Generic Command
actions:
- type: setvariable
attributes:
variable: '{{ list "genericcmd" .channel (group 1) | join ":" }}'
clear: true
variable: '{{ list "genericcmd" .channel (group 1) | join ":" }}'
- type: respond
attributes:
message: '[Admin] Deleted command !{{ group 1 }}'
enable_on: [broadcaster, moderator]
match_channels: ['#mychannel']
match_message: '^!clearcmd ([^\s]+)'
match_channels:
- '#luziferus'
- '#tezrian'
match_message: ^!clearcmd ([^\s]+)
enable_on:
- broadcaster
- moderator
```
## Game death counter with dynamic name
@ -57,24 +73,6 @@
match_message: '^!death'
```
## Link-protection while allowing Twitch clips
```yaml
- actions:
- type: timeout
attributes:
duration: 1s
- type: respond
attributes:
message: '@{{ .username }}, please ask for permission before posting links.'
disable_on: [broadcaster, moderator, subscriber, vip]
disable_on_match_messages:
- '^(?:https?://)?clips\.twitch\.tv/[a-zA-Z0-9-]+$'
disable_on_permit: true
match_channels: ['#mychannel']
match_message: '(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]'
```
## Post follow date for an user
```yaml
@ -108,9 +106,7 @@
- actions:
- type: respond
attributes:
message: >-
@{{ fixUsername (arg 1) }}, you will not get timed out
for the next {{ .permitTimeout }} seconds.
message: '{{ mention .to }}, you will not get timed out for the next {{ .permitTimeout }} seconds.'
match_channels: ['#mychannel']
match_event: 'permit'
```

View File

@ -0,0 +1,51 @@
---
title: "Script Actions"
---
{{< lead >}}
In order to maximize the flexibility of the bot you can trigger external scripts / commands in rules. These scripts are provided with extensive data to act on.
{{< /lead >}}
Your command will get a JSON object passed through `stdin` you can parse to gain details about the message. It is expected to yield an array of actions on `stdout` and exit with status `0`. If it does not the action will be marked failed. In case you need to output debug output you can use `stderr` which is directly piped to the bots `stderr`.
This is an example input you might get on `stdin`:
```json
{
"badges": {
"glhf-pledge": 1,
"moderator": 1
},
"channel": "#tezrian",
"message": "!test",
"tags": {
"badge-info": "",
"badges": "moderator/1,glhf-pledge/1",
"client-nonce": "6801c82a341f728dbbaad87ef30eae49",
"color": "#A72920",
"display-name": "Luziferus",
"emotes": "",
"flags": "",
"id": "dca06466-3741-4b22-8339-4cb5b07a02cc",
"mod": "1",
"room-id": "485884564",
"subscriber": "0",
"tmi-sent-ts": "1610313040489",
"turbo": "0",
"user-id": "69699328",
"user-type": "mod"
},
"username": "luziferus"
}
```
The example was dumped using this action:
```yaml
- actions:
- type: script
attributes:
command: [/usr/bin/bash, -c, "jq . >&2"]
match_channels: ['#tezrian']
match_message: '^!test'
```

View File

@ -1,8 +1,12 @@
## Templating
---
title: "Templating"
---
{{< lead >}}
Generally speaking the templating uses [Golang `text/template`](https://pkg.go.dev/text/template) template syntax. All fields with templating enabled do support the full synax from the `text/template` package.
{{< /lead >}}
### Variables
## Variables
There are certain variables available in the strings with templating enabled:
@ -12,7 +16,7 @@ There are certain variables available in the strings with templating enabled:
- `username` - The username of the message author
### Functions
## Functions
Within templates following functions can be used:
@ -29,7 +33,7 @@ Examples below are using this syntax in the code block:
< Output from the template
```
#### `arg`
### `arg`
Takes the message sent to the channel, splits by space and returns the Nth element
@ -43,7 +47,7 @@ Example:
< @tester please refrain from BSG
```
#### `botHasBadge`
### `botHasBadge`
Checks whether bot has the given badge in the current channel
@ -56,7 +60,7 @@ Example:
< true
```
#### `channelCounter`
### `channelCounter`
Wraps the counter name into a channel specific counter name including the channel name
@ -69,7 +73,7 @@ Example:
< 5
```
#### `counterValue`
### `counterValue`
Returns the current value of the counter which identifier was supplied
@ -82,7 +86,7 @@ Example:
< 5
```
#### `counterValueAdd`
### `counterValueAdd`
Adds the given value (or 1 if no value) to the counter and returns its new value
@ -94,7 +98,7 @@ Example:
< 1 6
```
#### `displayName`
### `displayName`
Returns the display name the specified user set for themselves
@ -107,7 +111,7 @@ Example:
< Luziferus - foobar
```
#### `doesFollow`
### `doesFollow`
Returns whether `from` follows `to`
@ -120,7 +124,7 @@ Example:
< true
```
#### `doesFollowLongerThan`
### `doesFollowLongerThan`
Returns whether `from` follows `to` for more than `duration`
@ -133,7 +137,7 @@ Example:
< true
```
#### `fixUsername`
### `fixUsername`
Ensures the username no longer contains the `@` or `#` prefix
@ -146,7 +150,7 @@ Example:
< luziferus - luziferus
```
#### `formatDuration`
### `formatDuration`
Returns a formated duration. Pass empty strings to leave out the specific duration part.
@ -159,7 +163,7 @@ Example:
< 5 hours, 33 minutes, 12 seconds - 5 hours, 33 minutes
```
#### `followAge`
### `followAge`
Looks up when `from` followed `to` and returns the duration between then and now
@ -172,7 +176,7 @@ Example:
< 15004h14m59.116620989s
```
#### `followDate`
### `followDate`
Looks up when `from` followed `to`
@ -185,7 +189,7 @@ Example:
< 2021-04-10 16:07:07 +0000 UTC
```
#### `group`
### `group`
Gets matching group specified by index from `match_message` regular expression, when `fallback` is defined, it is used when group has an empty match
@ -200,7 +204,7 @@ Example:
< test - oops
```
#### `inList`
### `inList`
Tests whether a string is in a given list of strings (for conditional templates).
@ -215,7 +219,7 @@ Example:
< true
```
#### `jsonAPI`
### `jsonAPI`
Fetches remote URL and applies jq-like query to it returning the result as string. (Remote API needs to return status 200 within 5 seconds.)
@ -230,7 +234,7 @@ Example:
< example string
```
#### `lastPoll`
### `lastPoll`
Gets the last (currently running or archived) poll for the given channel (the channel must have given extended permission for poll access!)
@ -245,7 +249,7 @@ Example:
See schema of returned object in [`pkg/twitch/polls.go#L13`](https://github.com/Luzifer/twitch-bot/blob/master/pkg/twitch/polls.go#L13)
#### `lastQuoteIndex`
### `lastQuoteIndex`
Gets the last quote index in the quote database for the current channel
@ -258,7 +262,7 @@ Example:
< Last Quote: #32
```
#### `mention`
### `mention`
Strips username and converts into a mention
@ -271,7 +275,7 @@ Example:
< @user @user @user
```
#### `pow`
### `pow`
Returns float from calculation: `float1 ** float2`
@ -284,7 +288,7 @@ Example:
< 10000
```
#### `randomString`
### `randomString`
Randomly picks a string from a list of strings
@ -297,7 +301,7 @@ Example:
< a
```
#### `recentGame`
### `recentGame`
Returns the last played game name of the specified user (see shoutout example) or the `fallback` if the game could not be fetched. If no fallback was supplied the message will fail and not be sent.
@ -311,7 +315,7 @@ Example:
```
#### `recentTitle`
### `recentTitle`
Returns the last stream title of the specified user or the `fallback` if the title could not be fetched. If no fallback was supplied the message will fail and not be sent.
@ -324,7 +328,7 @@ Example:
< Die Oper haben wir überlebt, mal sehen was uns sonst noch alles töten möchte - none
```
#### `seededRandom`
### `seededRandom`
Returns a float value stable for the given seed
@ -337,7 +341,7 @@ Example:
< Your int this hour: 17%
```
#### `streamUptime`
### `streamUptime`
Returns the duration the stream is online (causes an error if no current stream is found)
@ -350,7 +354,7 @@ Example:
< 3 hours, 56 minutes
```
#### `subCount`
### `subCount`
Returns the number of subscribers (accounts) currently subscribed to the given channel
@ -363,7 +367,7 @@ Example:
< 26
```
#### `subPoints`
### `subPoints`
Returns the number of sub-points currently given through the T1 / T2 / T3 subscriptions to the given channel
@ -376,7 +380,7 @@ Example:
< 26
```
#### `tag`
### `tag`
Takes the message sent to the channel, returns the value of the tag specified
@ -389,7 +393,7 @@ Example:
< luziferus
```
#### `textAPI`
### `textAPI`
Fetches remote URL and returns the result as string. (Remote API needs to return status 200 within 5 seconds.)
@ -404,7 +408,7 @@ Example:
< Weather for Hamburg, DE: Few clouds with a temperature of 22 C (71.6 F). [...]
```
#### `variable`
### `variable`
Returns the variable value or default in case it is empty
@ -417,7 +421,7 @@ Example:
< test - fallback
```
### Upgrade from `v2.x` to `v3.x`
## Upgrade from `v2.x` to `v3.x`
When adding [sprig](https://masterminds.github.io/sprig/) function collection some functions collided and needed replacement. You need to adapt your templates accordingly:

View File

@ -0,0 +1,14 @@
---
title: "Getting started"
weight: 1
---
{{< lead >}}
Get started with the installation of the Twitch-Bot and get to the point you do have a working bot inside your channel.
{{< /lead >}}
The majority of this sections assumes you are working on a **Linux Server** (systemd based, like Debian / Ubuntu) with direct internet access. If you are doing your setup on another system you might need to adapt steps or even replace them. The basic setup should be the same but some steps will differ.
If you don't have a server yet you could get a [Hetzner Cloud server (referral link)](https://hetzner.cloud/?ref=843sgmhTGlwR) for little more than 5 Euro per month which is perfectly capable to drive your bot!
Installing the bot on your **local computer** is not advisable as your bot will miss events occurring when you're offline / your computer is turned off. Also it will suffer from instabilities in your local internet connection as datacenter-connections are typically way more stable than your local domestical internet connection. (Though it is possible to use the bot on your local computer, I'd just advice against it.)

View File

@ -0,0 +1,27 @@
---
title: "Configuration"
weight: 5
---
{{< lead >}}
After you finally can access your bot through the [External Access]({{< ref "external-access.md" >}}) you can start to configure it!
{{< /lead >}}
In order to gain access to the bot you need to add yourself as an editor to the configuration. To do so edit the configuration created during the [Service Setup]({{< ref "setup.md" >}}) (in our example `/var/lib/twitch-bot/config.yaml`) and modify the bot editors:
```yaml
# List of strings: Either Twitch user-ids or nicknames (best to stick
# with IDs as they can't change while nicknames can be changed every
# 60 days). Those users are able to use the config editor web-interface.
bot_editors: [ 'luziferus' ]
```
Of course you should not enter my nickname but yours into the list. Don't worry about the hint IDs should be used. Using the nickname is fine for now and the bot will automatically adjust the entry when you start configuring the bot through the web-interface.
The confiuration change is automatically loaded by the bot (you should see that in the service log) and you should now be able to log into the web-interface using the **Login with Twitch** button.
At this point you've fully set up the bot to run and you've gained access to it and with this state the "Getting Started" guide ends. You can now
- add a channel in the web-interface, configure rules with [actions]({{< ref "../configuration/actors.md" >}}) and [templates]({{< ref "../configuration/templating.md" >}})
- have a look on all the other options in the [Config-File Syntax]({{< ref "../configuration/config-file.md" >}})
- discover what the bot can do in the [Rule Examples]({{< ref "../configuration/rule-examples.md" >}})

View File

@ -0,0 +1,56 @@
---
title: "External Access"
weight: 4
---
{{< lead >}}
In order to be able to access the bot through a web-browser and make configuration changes using the web-interface we need to securely expose the interface to the internet.
{{< /lead >}}
{{< alert style="info" >}}
In case you did the installation of the bot on your **local machine**, skip this part. You can access the web-interface at `http://localhost:3000/` after you've continued with the [Configuration]({{< ref "configuration.md" >}}).
{{< /alert >}}
In order not to make this a quite long and extensive tutorial we'll use two tutorials of DigitalOcean to aid us:
- [DigitalOcean: How To Install Nginx on Ubuntu 20.04](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-20-04)
- [DigitalOcean: How To Secure Nginx with Let's Encrypt on Ubuntu 20.04](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-20-04)
We will follow the first one up to step 4, and omit step 5 and 6. These are not required for us as they are configuring a locally hosted website which we don't want to do for the bot. Instead of the suggested `/etc/nginx/sites-available/your_domain` file in the first tutorial we will create a `/etc/nginx/sites-available/twitch-bot.conf` file with the following content:
```nginx
server {
listen 80;
listen [::]:80;
server_name twitch-bot.mydomain.com;
location / {
add_header X-Robots-Tag noindex;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:3000/;
}
}
```
Make sure to replace the `server_name` directive with the (sub-)domain you want the bot to be available at.
After creating the file we'll enable it similar to the tutorial:
```console
# Link the configuration to the enabled ones
$ sudo ln -s /etc/nginx/sites-available/twitch-bot.conf /etc/nginx/sites-enabled/
# Check whether there are any errors
$ sudo nginx -t
# If there were no errors, restart the nginx service
$ sudo systemctl restart nginx
```

View File

@ -0,0 +1,50 @@
---
title: "Installation"
weight: 1
---
{{< lead >}}
Installation is the first step you need to take, therefore choose how you want to proceed:
{{< /lead >}}
## Downloading a pre-compiled Binary
You can always find the latest pre-compiled binary on the [Releases-Page](https://github.com/Luzifer/twitch-bot/releases) of the Github repository. The latest release contains binaries for MacOS (`darwin`), Linux (`amd64` / `arm`) and Windows.
This way of installation is the easiest one:
- Download the archive for your system
- Unzip / untar the archive
- Find a binary in the location you unpacked the archive to
The binary you get from this is everything you need: It contains the bot as well as the web-interface to configure the bot.
Move this binary to a location you will find it again later.
## Using a pre-built Docker image
The Docker image is automatically built from the source and provided in two different variants:
| Image | Description |
| ----- | ----------- |
| <span style="white-space:nowrap">`luzifer/twitch-bot:latest`</span> | The latest development version, not recommended for use as your main bot, perfect for testing the latest changes not yet released. |
| <span style="white-space:nowrap">`luzifer/twitch-bot:stable`</span> | Automatically updated on every versioned release, you just can use this tag to always have the latest stable version. Pay attention: This automatically switches over on major / breaking releases! |
| <span style="white-space:nowrap">`luzifer/twitch-bot:<version>`</span> | If you don't want to auto-update you can use the tagged version in the Docker image (i.e. tag `v3.15.0` would be available as `luzifer/twitch-bot:v3.15.0`) |
## Building the Binary yourself
If you want to do customizations or just don't trust my build system you can execute the whole build yourself which requires you to have the [latest Golang release](https://go.dev/dl/) and [LTS NodeJS release](https://nodejs.org/en) available on your machine.
```console
# First checkout the code
$ git clone https://github.com/Luzifer/twitch-bot.git
$ cd twitch-bot
# Optionally switch to a release tag
$ git checkout v3.15.0
# Second build the binary
$ make build_prod
```
Move this binary to a location you will find it again later.

View File

@ -0,0 +1,19 @@
---
title: "Preparations"
weight: 2
---
{{< lead >}}
In order to communicate with Twitch you need to set up an application in the Twitch developers console.
{{< /lead >}}
{{< alert style="warning" >}}
In case you are setting up multiple Twitch-Bot instances, create **one application per instance**! Re-using an existing application even for a test-instance will lead to unexpected results like randomly breaking bot-authorizations!
{{< /alert >}}
Registering your application is a relatively straight-forward process:
- Go to https://dev.twitch.tv/console/apps/create
- Fill out the form you are presented with. You can choose any **Name** you want for your bot. I'd recommend using one you later will recognize your bot under. For the **OAuth Redirect URL** choose the URL you want to have the bot available under later. If you want to have it running locally you can choose `http://localhost:3000/` for this field.
![](/screen-twitch-console-register-app.png)
- After registering your application go into the application you've just created and click the **New Secret** button. Note down the **Client-Id** and **Client-Secret** in a safe place. You will need them in the [Configuration]({{< ref "configuration.md" >}}) step.

View File

@ -0,0 +1,163 @@
---
title: "Service Setup"
weight: 3
---
{{< lead >}}
In order to have your bot started automatically on system start we should set it up as a service.
{{< /lead >}}
## General preparation
{{< alert style="info" >}}
In order not to put confidential information into the configuration file we want to create a configuration file to hold these secrets. Also we will create a folder to put stored data and overlay files into. You can use other folders for those files, just remember to adjust the paths in all places. For this page I will assume the binary is placed in `/usr/local/bin/twitch-bot_linux_amd64`.
{{< /alert >}}
```console
# First create a folder to hold our secret file(s)
$ mkdir /etc/twitch-bot
# Second create a folder to hold the data
$ mkdir -p /var/lib/twitch-bot/overlays
# Create the secrets file and secure access to it
$ touch /etc/twitch-bot/environment
$ chown root:root /etc/twitch-bot/environment
$ chmod 0600 /etc/twitch-bot/environment
# Edit the file to hold the secrets (use your favourite editor instead of nano)
$ nano /etc/twitch-bot/environment
```
Lets put this into the file we've just edited (replace the brackets, don't leave them inside the file!):
```env
BASE_URL=http://localhost:3000/
CONFIG=/var/lib/twitch-bot/config.yaml
LOG_LEVEL=info
OVERLAYS_DIR=/var/lib/twitch-bot/overlays
STORAGE_CONN_STRING=/var/lib/twitch-bot/storage.db
STORAGE_CONN_TYPE=sqlite
STORAGE_ENCRYPTION_PASS=[put a random secure password here]
TWITCH_CLIENT=[put the client-id from the preparations step here]
TWITCH_CLIENT_SECRET=[put the client-secret from the preparations step here]
```
If you want to use a database server like MariaDB or PostgreSQL adjust the `STORAGE_CONN_*` variables like described in the project [README](https://github.com/Luzifer/twitch-bot/blob/master/README.md#database-connection-strings).
## Option 1: Using a downloaded or compiled Binary
We will create a systemd service to start the binary using the environment variables file we've created in the preparation step. This file will be placed into `/etc/systemd/system/twitch-bot.service`:
```service
[Unit]
Description=Twitch-Bot Service
After=network-online.target
Requires=network-online.target
[Service]
EnvironmentFile=/etc/twitch-bot/environment
ExecStart=/usr/local/bin/twitch-bot_linux_amd64
Restart=Always
RestartSecs=5
[Install]
WantedBy=multi-user.target
```
To enable and start the service which makes it automatically start on every server boot execute these commands:
```console
$ systemctl daemon-reload
$ systemctl enable --now twitch-bot
```
After the first start a configuration file has been created at `/var/lib/twitch-bot/config.yaml`. You want to change the port in the `http_listen` line in this file:
```yaml
# IP/Port to start the web-interface on. Format: IP:Port
# The default is 127.0.0.1:0 - Listen on localhost with random port
http_listen: "127.0.0.1:3000"
```
After changing the port restart the service once:
```console
$ systemctl restart twitch-bot
```
To update the bot first stop the bot, then replace the binary and start the bot again:
```console
$ systemctl stop twitch-bot
$ mv [path to new binary] /usr/local/bin/twitch-bot_linux_amd64
$ systemctl start twitch-bot
```
## Option 2: Using a Docker image
We will create a systemd service to start the binary using the environment variables file we've created in the preparation step. This file will be placed into `/etc/systemd/system/twitch-bot.service`:
```service
[Unit]
Description=Twitch-Bot Service
After=network-online.target
Requires=network-online.target
[Service]
EnvironmentFile=/etc/twitch-bot/environment
ExecStartPre=-/usr/bin/docker rm -f %n
ExecStartPre=/usr/bin/docker pull luzifer/twitch-bot:stable
ExecStart=/usr/bin/docker run --rm --name %n \
--env-file /etc/twitch-bot/environment \
-v /var/lib/twitch-bot:/var/lib/twitch-bot \
-p 127.0.0.1:3000:3000 \
luzifer/twitch-bot:stable
Restart=Always
RestartSecs=5
[Install]
WantedBy=multi-user.target
```
To enable and start the service which makes it automatically start on every server boot execute these commands:
```console
$ systemctl daemon-reload
$ systemctl enable --now twitch-bot
```
After the first start a configuration file has been created at `/var/lib/twitch-bot/config.yaml`. You want to change IP and port in the `http_listen` line in this file:
```yaml
# IP/Port to start the web-interface on. Format: IP:Port
# The default is 127.0.0.1:0 - Listen on localhost with random port
http_listen: "0.0.0.0:3000"
```
After changing the port restart the service once:
```console
$ systemctl restart twitch-bot
```
To update the bot just restart the service:
```console
$ systemctl restart twitch-bot
```
## Debugging the Service
In both options you created a service which is running as a system user. You can do the same things for both options to see what the bot is doing:
```console
$ journalctl -fu twitch-bot
```
This will stream the logs of the bots into your terminal and you can read what the bot is doing.
In case the bot behaves unexpectedly you can increase the `LOG_LEVEL` from `info` to `debug` within the `/etc/twitch-bot/environment` file and restart the bot to get more verbose logs.

View File

@ -0,0 +1,42 @@
---
title: Overlays
---
{{< lead >}}
Overlays in OBS are added as a **Browser Source** and while often graphical tooling to build them is available it's possible to build custom overlays with well-known web-technology like **HTML, Javascript, CSS**.
{{< /lead >}}
{{< alert style="info" >}}
In the [Service Setup]({{< ref "../getting-started/setup.md" >}}) we configured an `OVERLAYS_DIR` which is used to serve the files for the overlays. Every file you put into that directory is available to the public at `https://your-bot.example.com/overlays/`. Therefore pay attention not to put any secrets into that directory!
{{< /alert >}}
The bot includes some files which are merged with the files you put into that directory. If you put a file with the same name as the included, your file will overwrite the included one! Therefore you can modify included templates by just copying them into your overlay directory and changing the stuff you want changed.
Currently the following files are available in the default distribution:
- `debug.html` - The Debug Overlay (see below for an example how to use)
- `eventclient.js` - The [EventClient]({{< ref "eventclient.md" >}}) Javascript library to aid you in developing overlays and communicating with the bot
- `sounds.html` / `sounds.js` - The [Sound-Alerts Overlay]({{< ref "soundalerts.md" >}})
- `template.html` - A very simple example overlay without external dependencies
You can see the sources for these included files in the [project repository](https://github.com/Luzifer/twitch-bot/tree/master/internal/apimodules/overlays/default).
## Configuring Secrets and Parameters
As you shouldn't put secrets in a public place the [EventClient]({{< ref "eventclient.md" >}}) library provides a mechanism to fetch secrets from the URL hash through the `paramOptionFallback` method. This also is used for all configuration you can pass into the class options. Especially for the token you should use this mechaism.
As an example lets have a look at this URL path:
`/overlays/debug.html#token=55cdb1e4-c776-4467-8560-a47a4abc55de&replay=true&channel=%23luziferus&maxReplayAge=720&hide=join,part`
Here you can see the Debug Overlay configured with:
- `token` - A token configured in the bot having access to the `overlays` permission
- `replay` - Enabled to retrieve past events
- `channel` - Set to `#luziferus` to retrieve events from my channel
- `maxReplayAge` - Set to 720h (30d) not to retrieve events from the infinite past
- `hide` - Set to omit `join` and `part` events (custom parameter for the Debug Overlay)
As those parameters are configured through the URL hash (`#...`) they are never sent to the server, therefore are not logged in any access-logs and exist only in the local URL. So with a custom overlay you would put `https://your-bot.example.com/overlays/myoverlay.html#token=55cdb1e4-c776-4467-8560-a47a4abc55de` into your OBS browser source and your overlay would be able to communicate with the bot.
The debug-overlay can be used to view all events received within the bot you can react on in overlays and bot rules.

View File

@ -0,0 +1,92 @@
---
title: EventClient
weight: 10000
---
## Classes
<dl>
<dt><a href="#EventClient">EventClient</a></dt>
<dd><p>EventClient abstracts the connection to the bot websocket for events</p>
</dd>
</dl>
## Typedefs
<dl>
<dt><a href="#Options">Options</a> : <code>Object</code></dt>
<dd><p>Options to pass to the EventClient constructor</p>
</dd>
</dl>
<a name="EventClient"></a>
## EventClient
EventClient abstracts the connection to the bot websocket for events
**Kind**: global class
* [EventClient](#EventClient)
* [new EventClient(opts)](#new_EventClient_new)
* [.apiBase()](#EventClient+apiBase) ⇒ <code>string</code>
* [.paramOptionFallback(key, [fallback])](#EventClient+paramOptionFallback) ⇒ <code>\*</code>
* [.renderTemplate(template)](#EventClient+renderTemplate) ⇒ <code>Promise</code>
<a name="new_EventClient_new"></a>
### new EventClient(opts)
Creates, initializes and connects the EventClient
| Param | Type | Description |
| --- | --- | --- |
| opts | [<code>Options</code>](#Options) | Options for the EventClient |
<a name="EventClient+apiBase"></a>
### eventClient.apiBase() ⇒ <code>string</code>
Returns the API base URL without trailing slash
**Kind**: instance method of [<code>EventClient</code>](#EventClient)
**Returns**: <code>string</code> - API base URL
<a name="EventClient+paramOptionFallback"></a>
### eventClient.paramOptionFallback(key, [fallback]) ⇒ <code>\*</code>
Resolves the given key through url hash parameters with fallback to constructor options
**Kind**: instance method of [<code>EventClient</code>](#EventClient)
**Returns**: <code>\*</code> - Value of the key or `null`
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| key | <code>string</code> | | The key to resolve |
| [fallback] | <code>\*</code> | <code></code> | Fallback to return if neither params nor options contained that key |
<a name="EventClient+renderTemplate"></a>
### eventClient.renderTemplate(template) ⇒ <code>Promise</code>
Renders a given template using the bots msgformat API (supports all templating you can use in bot messages). To use this function the token passed through the constructor or the URL hash must have the `msgformat` permission in addition to the `overlays` permission.
**Kind**: instance method of [<code>EventClient</code>](#EventClient)
**Returns**: <code>Promise</code> - Promise resolving to the rendered output of the template
| Param | Type | Description |
| --- | --- | --- |
| template | <code>string</code> | The template to render |
<a name="Options"></a>
## Options : <code>Object</code>
Options to pass to the EventClient constructor
**Kind**: global typedef
**Properties**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| [channel] | <code>string</code> | | Filter for specific channel events (format: `#channel`) |
| [handlers] | <code>Object</code> | <code>{}</code> | Map event types to callback functions `(event, fields, time, live) => {...}` |
| [maxReplayAge] | <code>number</code> | <code>-1</code> | Number of hours to replay the events for (-1 = infinite) |
| [replay] | <code>boolean</code> | <code>false</code> | Request a replay at connect (requires channel to be set to a channel name) |
| [token] | <code>string</code> | | API access token to use to connect to the WebSocket (if not set, must be provided through URL hash) |

View File

@ -0,0 +1,105 @@
---
title: Sound-Alerts
weight: 50
---
{{< lead >}}
The Sound-Alerts overlay provides you with a way to let your viewers trigger sounds through channel-points and chat commands.
{{< /lead >}}
This overlay utilizes the custom events actor to generate events specific to the overlay in order to trigger sound alerts.
## OBS Setup
To use it
- generate an API token with the `overlays` permission and note it in a secure place
- add a Browser-Source to your OBS scenes with at least 1×1 px in size
- set the URL to `https://your-bot.example.com/overlays/sounds.html?token=[your-token]&channel=[your-channel]`
After you've done this you're already done with the setup inside your OBS.
## Add the sound-files to the overlays directory
In order to be able to play those sounds the sounds need to be added to overlays directory. You cannot use remote sounds hosted somewhere else because of security limitations.
The Linux OBS browser-source does not allow for proprietary codecs to be used so the sounds should be converted to sound files containing the Opus Codec. (Though this isn't strictly required for Windows OBS it might be a good idea to use this codec in general as it is a non-proprietary codec.)
If you got a sound file in `mp3`, `wav`, `aac` or whatever format you can use [`ffmpeg`](https://ffmpeg.org/) to convert it to a `webm` file which automatically will use the Opus codec:
```console
ffmpeg mysoundfile.mp3 mysoundfile.webm
```
In the following example rules I'll assume you created a `sounds` directory inside your overlays directory and stored some sound files in there.
## Creating Rules to trigger Sounds
### Using Channel-Points
```yaml
- description: Channelpoint SoundAlert
actions:
- type: customevent
attributes:
fields: |
{{
$sound := get ( dict
"b8afa5fa-2441-43ef-a634-77d0ad4f2691" "applaus"
"63c8d002-89f4-41fc-b6c9-03b339f439cd" "badum"
"39c34f65-85e3-4792-9e16-ff3d186aaf6f" "bonk"
"b9f41ea4-3fcb-41d8-9072-f0923089d097" "tick"
"f67c792e-2555-486e-b441-8247cb8ac97e" "typewriter"
"c9ca69a6-9d2e-4b39-90c9-26a4b62064fc" "zirp"
) .reward_id
}}
{
"type": "soundalert",
"soundUrl": "sounds/{{ $sound }}.webm"
}
match_channels:
- '#luziferus'
match_event: channelpoint_redeem
disable_on_offline: true
disable_on_template: '{{ not (hasPrefix "Sound: " .reward_title) }}'
```
On the first glimps this seems rather complicated so lets look at it step-by-step:
- We defined a new rule for our Channel-Points Sound-Alerts
- This rule reacts on redeemed Channel-Points: `match_event: channelpoint_redeem`
- It does not trigger when the channel is offline and it does not trigger when the Channel-Points reward is not named `Sound: [...]`
- It only triggers in my channel: `match_channels: ['#luziferus']`
- When all the conditions mentioned above are met the rule will create a `customevent` containing the `"type": "soundalert"` which is used by the overlay we've set up in OBS before
- It gives the `"soundUrl": "sounds/{{ $sound }}.webm"`, therefore telling the overlay to play the file `sounds/{{ $sound }}.webm` when the event is received
- Now to the "complicated" part: In order not to define a rule for each single sound I've fused all the rewards together into one rule with a mapping of the `.reward_id` to the name of the sound file to be played. This is done through creating a `dict` with the reward id and the name of the file and a `get` function around it selecting the `.reward_id` from the given `dict`.
Before adding this to your bot create at least one **Custom Reward** in the channel-points section in your streamer dashboard. The **Reward Name** should be `Sound: [...]` in order to be matched by the rule above. The **Reward Description** is optional, the viewer should not enter a text, the **Cost** is something you can freely define (I'm using 250 points), the **Reward Icon** too. I'd advice to enable **Skip Reward Requests Queue** as the bot will automatically trigger and disabling this would clutter your Reward Requests Queue. Also you might want to enable the **Cooldown**: I'm using a 5min cooldown on each sound for them not to be spammed permanently.
You now can simply copy the rule into your bot configuration and just adjust the channel, the reward-ids and the sound file names. To get the `reward_id` of the event have a look into the Debug-Overlay described on the [Overlays]({{< ref "_index.md" >}}) page and trigger the reward once. (If you're doing this being offline just disable the cooldown for a moment to be able to trigger the reward while being offline.)
### Using Chat-Commands
```yaml
- description: Command SoundAlert
actions:
- type: customevent
attributes:
fields: |
{
"type": "soundalert",
"soundUrl": "sounds/{{ group 1 }}.webm"
}
cooldown: 5m
match_channels:
- '#luziferus'
match_message: '(?i)^!sound (applaus|badum|bonk|tick)$'
disable_on_offline: true
```
As we don't have to deal with the Twitch reward-id stuff and just define a command this rule is rather simple but lets have a quick walkthrough too:
- We defined a rule for the `!sound` command: `match_message: '(?i)^!sound (applaus|badum|bonk|tick)$'`
- To add a little bit of security here the command also defines which sounds can be triggered: `(applaus|badum|bonk|tick)`
- Also this rule does not trigger when the channel is offline and defines a 5 minute cooldown (this is not per sound but for all sounds so triggering `!sound applaus` will set the cooldown also for `!sound tick`)
- Finally the rule also creates the `customevent` of `"type": "soundalert"` passing a `soundUrl` where to find the sound

35
docs/hugo.toml Normal file
View File

@ -0,0 +1,35 @@
baseURL = '/'
languageCode = 'en-us'
pygmentsCodeFences = true
pygmentsStyle = "monokailight"
theme = "ace-documentation"
title = 'Twitch-Bot Documentation'
defaultContentLanguage = "en"
defaultContentLanguageInSubdir= false
enableMissingTranslationPlaceholders = false
cleanDestinationDir = true
disableKinds = ["RSS"]
publishDir = "../.rendered-docs"
[params]
disableReadmoreNav = true # set true to hide prev/next navigation, default is false
disableSearch = false # default is false
highlightClientSide = false # set true to use highlight.pack.js instead of the default hugo chroma highlighter
menushortcutsnewtab = true # set true to open shortcuts links to a new tab/window
ordersectionsby = "weight" # ordersectionsby = "title"
[markup]
[markup.goldmark]
[markup.goldmark.renderer]
unsafe = true
[outputs]
home = [ "HTML", "JSON"]
[[menu.shortcuts]]
name = "<i class='fab fa-github'></i>"
url = "https://github.com/Luzifer/twitch-bot"
weight = 2

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6ba856c767a8fe0de2520eb6a64cddfb7b01ecb8a7812524a85626e388dbf634
size 62344

1
docs/themes/ace-documentation vendored Submodule

@ -0,0 +1 @@
Subproject commit 241592496471791045d36500cd9add18caa6ae43

View File

@ -46,7 +46,7 @@
<script src="https://cdn.jsdelivr.net/combine/npm/vue@2,npm/moment@2"></script>
<script type="module">
import EventClient from './eventclient.mjs'
import EventClient from './eventclient.js'
new Vue({
computed: {

View File

@ -0,0 +1,173 @@
/**
* Options to pass to the EventClient constructor
* @typedef {Object} Options
* @prop {string} [channel] - Filter for specific channel events (format: `#channel`)
* @prop {Object} [handlers={}] - Map event types to callback functions `(event, fields, time, live) => {...}`
* @prop {number} [maxReplayAge=-1] - Number of hours to replay the events for (-1 = infinite)
* @prop {boolean} [replay=false] - Request a replay at connect (requires channel to be set to a channel name)
* @prop {string} [token] - API access token to use to connect to the WebSocket (if not set, must be provided through URL hash)
*/
const HOUR = 3600 * 1000
const initialSocketBackoff = 500
const maxSocketBackoff = 10000
const socketBackoffMultiplier = 1.25
/**
* @class EventClient abstracts the connection to the bot websocket for events
*/
class EventClient {
/**
* Creates, initializes and connects the EventClient
*
* @param {Options} opts Options for the EventClient
*/
constructor(opts) {
this.params = new URLSearchParams(window.location.hash.substr(1))
this.handlers = { ...opts.handlers || {} }
this.options = { ...opts }
this.token = this.paramOptionFallback('token')
if (!this.token) {
throw new Error('token for socket not present in hash or opts')
}
this.socketBackoff = initialSocketBackoff
this.connect()
// If reply is enabled and channel is provided, fetch the replay
if (this.paramOptionFallback('replay', false) && this.paramOptionFallback('channel')) {
this.fetchReplayForChannel(
this.paramOptionFallback('channel'),
Number(this.paramOptionFallback('maxReplayAge', -1)),
)
}
}
/**
* Returns the API base URL without trailing slash
*
* @returns {string} API base URL
*/
apiBase() {
return window.location.href.substr(0, window.location.href.indexOf('/overlays/'))
}
/**
* Connects the EventClient to the socket
*
* @private
*/
connect() {
if (this.socket) {
this.socket.close()
this.socket = null
}
this.socket = new WebSocket(this.socketAddr())
this.socket.onclose = () => {
this.socketBackoff = Math.min(this.socketBackoff * socketBackoffMultiplier, maxSocketBackoff)
window.setTimeout(() => this.connect(), this.socketBackoff)
}
this.socket.onmessage = evt => {
const data = JSON.parse(evt.data)
if (data.type === '_auth') {
// Special handling for auth confirmation
this.socketBackoff = initialSocketBackoff
return
}
if (this.paramOptionFallback('channel') && !data.fields?.channel?.match(this.paramOptionFallback('channel'))) {
// Channel filter is active and channel does not match
return
}
for (const fn of [this.handlers[data.type], this.handlers._].filter(fn => fn)) {
fn(data.type, data.fields, new Date(data.time), data.is_live)
}
}
this.socket.onopen = () => {
this.socket.send(JSON.stringify({
fields: { token: this.token },
type: '_auth',
}))
}
}
/**
* Requests past events from the API and feed them through the registered handlers
*
* @param {string} channel The channel to fetch the events for
* @param {number} hours The amount of hours to fetch into the past (-1 = infinite)
* @private
* @returns {Promise} Can be listened for failures using `.catch`
*/
fetchReplayForChannel(channel, hours = -1) {
const params = new URLSearchParams()
if (hours > -1) {
params.set('since', new Date(new Date().getTime() - hours * HOUR).toISOString())
}
return fetch(`${this.apiBase()}/overlays/events/${encodeURIComponent(channel)}?${params.toString()}`, {
headers: {
authorization: this.paramOptionFallback('token'),
},
})
.then(resp => resp.json())
.then(data => {
const handlers = []
for (const msg of data) {
for (const fn of [this.handlers[msg.type], this.handlers._].filter(fn => fn)) {
handlers.push(fn(msg.type, msg.fields, new Date(msg.time), msg.is_live))
}
}
return Promise.all(handlers)
})
}
/**
* Resolves the given key through url hash parameters with fallback to constructor options
*
* @param {string} key The key to resolve
* @param {*} [fallback=null] Fallback to return if neither params nor options contained that key
* @returns {*} Value of the key or `null`
*/
paramOptionFallback(key, fallback = null) {
return this.params.get(key) || this.options[key] || fallback
}
/**
* Renders a given template using the bots msgformat API (supports all templating you can use in bot messages). To use this function the token passed through the constructor or the URL hash must have the `msgformat` permission in addition to the `overlays` permission.
*
* @param {string} template The template to render
* @returns {Promise} Promise resolving to the rendered output of the template
*/
renderTemplate(template) {
return fetch(`${this.apiBase()}/msgformat/format?template=${encodeURIComponent(template)}`, {
headers: {
authorization: this.paramOptionFallback('token'),
},
})
.then(resp => resp.text())
}
/**
* Modifies the overlay address to the websocket address the bot listens to
*
* @private
* @returns {string} Websocket address in form ws://... or wss://...
*/
socketAddr() {
return `${this.apiBase().replace(/^http/, 'ws')}/overlays/events.sock`
}
}
export default EventClient

View File

@ -1,170 +1,4 @@
/**
* Options to pass to the EventClient constructor
* @typedef {Object} EventClient~Options
* @prop {string} [channel] - Filter for specific channel events (format: `#channel`)
* @prop {Object} handlers - Map event types to callback functions `(event, fields, time, live) => {...}`
* @prop {number} [maxReplayAge=-1] - Number of hours to replay the events for (-1 = infinite)
* @prop {boolean} replay - Request a replay at connect (requires channel to be set to a channel name)
* @prop {string} [token] - API access token to use to connect to the WebSocket
*/
/* Deprecated: Kept here for backwards compatibility */
const HOUR = 3600 * 1000
const initialSocketBackoff = 500
const maxSocketBackoff = 10000
const socketBackoffMultiplier = 1.25
/**
* @class EventClient abstracts the connection to the bot websocket for events
*/
export default class EventClient {
/**
* Creates, initializes and connects the EventClient
*
* @param {EventClient~Options} opts {@link EventClient~Options} for the EventClient
*/
constructor(opts) {
this.params = new URLSearchParams(window.location.hash.substr(1))
this.handlers = { ...opts.handlers || {} }
this.options = { ...opts }
this.token = this.paramOptionFallback('token')
if (!this.token) {
throw new Error('token for socket not present in hash or opts')
}
this.socketBackoff = initialSocketBackoff
this.connect()
// If reply is enabled and channel is provided, fetch the replay
if (this.paramOptionFallback('replay', false) && this.paramOptionFallback('channel')) {
this.fetchReplayForChannel(
this.paramOptionFallback('channel'),
Number(this.paramOptionFallback('maxReplayAge', -1)),
)
}
}
/**
* Returns the API base URL without trailing slash
*
* @returns {string} API base URL
*/
apiBase() {
return window.location.href.substr(0, window.location.href.indexOf('/overlays/'))
}
/**
* Connects the EventClient to the socket
*
* @private
*/
connect() {
if (this.socket) {
this.socket.close()
this.socket = null
}
this.socket = new WebSocket(this.socketAddr())
this.socket.onclose = () => {
this.socketBackoff = Math.min(this.socketBackoff * socketBackoffMultiplier, maxSocketBackoff)
window.setTimeout(() => this.connect(), this.socketBackoff)
}
this.socket.onmessage = evt => {
const data = JSON.parse(evt.data)
if (data.type === '_auth') {
// Special handling for auth confirmation
this.socketBackoff = initialSocketBackoff
return
}
if (this.paramOptionFallback('channel') && !data.fields?.channel?.match(this.paramOptionFallback('channel'))) {
// Channel filter is active and channel does not match
return
}
for (const fn of [this.handlers[data.type], this.handlers._].filter(fn => fn)) {
fn(data.type, data.fields, new Date(data.time), data.is_live)
}
}
this.socket.onopen = () => {
this.socket.send(JSON.stringify({
fields: { token: this.token },
type: '_auth',
}))
}
}
/*
* Requests past events from the API and feed them through the registered handlers
*
* @params {string} channel The channel to fetch the events for
* @params {number} hours The amount of hours to fetch into the past (-1 = infinite)
* @returns {Promise} Can be listened for failures using `.catch`
*/
fetchReplayForChannel(channel, hours = -1) {
const params = new URLSearchParams()
if (hours > -1) {
params.set('since', new Date(new Date().getTime() - hours * HOUR).toISOString())
}
return fetch(`${this.apiBase()}/overlays/events/${encodeURIComponent(channel)}?${params.toString()}`, {
headers: {
authorization: this.paramOptionFallback('token'),
},
})
.then(resp => resp.json())
.then(data => {
const handlers = []
for (const msg of data) {
for (const fn of [this.handlers[msg.type], this.handlers._].filter(fn => fn)) {
handlers.push(fn(msg.type, msg.fields, new Date(msg.time), msg.is_live))
}
}
return Promise.all(handlers)
})
}
/**
* Resolves the given key through url hash parameters with fallback to constructor options
*
* @params {string} key The key to resolve
* @params {*=} fallback=null Fallback to return if neither params nor options contained that key
* @returns {*} Value of the key or null
*/
paramOptionFallback(key, fallback = null) {
return this.params.get(key) || this.options[key] || fallback
}
/**
* Renders a given template using the bots msgformat API (supports all templating you can use in bot messages)
*
* @params {string} template The template to render
* @returns {Promise} Promise resolving to the rendered output of the template
*/
renderTemplate(template) {
return fetch(`${this.apiBase()}/msgformat/format?template=${encodeURIComponent(template)}`, {
headers: {
authorization: this.paramOptionFallback('token'),
},
})
.then(resp => resp.text())
}
/**
* Modifies the overlay address to the websocket address the bot listens to
*
* @private
* @returns {string} Websocket address in form ws://... or wss://...
*/
socketAddr() {
return `${this.apiBase().replace(/^http/, 'ws')}/overlays/events.sock`
}
}
import EventClient from './eventclient.js'
export default EventClient

View File

@ -1,6 +1,6 @@
/* global Vue */
import EventClient from './eventclient.mjs'
import EventClient from './eventclient.js'
new Vue({
created() {

View File

@ -9,7 +9,7 @@
<div id="content" />
<script type="module">
import EventClient from './eventclient.mjs'
import EventClient from './eventclient.js'
function updateTemplate(botClient) {
return botClient.renderTemplate(botClient.paramOptionFallback('template', ''))