mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2025-01-09 21:01:50 +00:00
Compare commits
24 commits
4dd903288c
...
932879c8df
Author | SHA1 | Date | |
---|---|---|---|
932879c8df | |||
97dbc74ebc | |||
2c9d8ef33c | |||
de3a4941ed | |||
8dd3d7db0c | |||
2c17ef58fa | |||
8154a50351 | |||
8c2c4e7c62 | |||
7737d939f4 | |||
acf96c31ad | |||
afe2963d33 | |||
293a7d9e30 | |||
94b040ed81 | |||
b131a7be5f | |||
6c941fb330 | |||
264eef4130 | |||
35b47bca65 | |||
e7e9877c05 | |||
a49a1844ba | |||
dc8f645f24 | |||
a9984b2df2 | |||
30482591a7 | |||
262742603c | |||
f76cbebda3 |
102 changed files with 1926 additions and 1753 deletions
57
.github/workflows/doc-generator.yml
vendored
57
.github/workflows/doc-generator.yml
vendored
|
@ -1,57 +0,0 @@
|
||||||
---
|
|
||||||
|
|
||||||
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 DOCS_BASE_URL=https://luzifer.github.io/twitch-bot/
|
|
||||||
|
|
||||||
- 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
|
|
||||||
|
|
||||||
...
|
|
36
.github/workflows/docker-publish.yml
vendored
36
.github/workflows/docker-publish.yml
vendored
|
@ -1,36 +0,0 @@
|
||||||
---
|
|
||||||
|
|
||||||
name: docker-publish
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ['master']
|
|
||||||
tags: ['v*']
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
docker-publish:
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
lfs: true
|
|
||||||
show-progress: false
|
|
||||||
|
|
||||||
- name: Log into registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Docker Build & Publish
|
|
||||||
run: bash ci/docker-publish.sh
|
|
||||||
|
|
||||||
...
|
|
274
.github/workflows/generated_workflow.yml
vendored
Normal file
274
.github/workflows/generated_workflow.yml
vendored
Normal file
|
@ -0,0 +1,274 @@
|
||||||
|
name: CI Workflow
|
||||||
|
on: push
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
jobs:
|
||||||
|
doc-generator:
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
needs:
|
||||||
|
- test
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
container:
|
||||||
|
image: luzifer/gh-arch-env
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOPATH: /go
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
lfs: true
|
||||||
|
show-progress: false
|
||||||
|
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 DOCS_BASE_URL=https://luzifer.github.io/twitch-bot/
|
||||||
|
- name: Upload GitHub Pages artifact
|
||||||
|
uses: actions/upload-pages-artifact@v1
|
||||||
|
with:
|
||||||
|
path: .rendered-docs
|
||||||
|
- name: Deploy artifact
|
||||||
|
uses: actions/deploy-pages@v1
|
||||||
|
docker-publish:
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master' }}
|
||||||
|
needs:
|
||||||
|
- test
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- name: Log into registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Docker Build & Publish
|
||||||
|
run: bash ci/docker-publish.sh
|
||||||
|
integration-crdb:
|
||||||
|
if: ${{ github.ref == 'refs/heads/master' }}
|
||||||
|
needs:
|
||||||
|
- test
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
container:
|
||||||
|
image: luzifer/gh-arch-env
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOPATH: /go
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
crdb:
|
||||||
|
image: luzifer/crdb-gh-service
|
||||||
|
options: --health-cmd "curl -sSf http://localhost:8080/health" --health-interval 10s --health-timeout 5s --health-retries 5
|
||||||
|
steps:
|
||||||
|
- name: Install required packages
|
||||||
|
run: |
|
||||||
|
pacman -Syy --noconfirm \
|
||||||
|
cockroachdb-bin
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- name: Marking workdir safe
|
||||||
|
run: git config --global --add safe.directory /__w/twitch-bot/twitch-bot
|
||||||
|
- name: Set up CRDB service
|
||||||
|
run: |
|
||||||
|
cockroach sql --host crdb --insecure <<EOF
|
||||||
|
CREATE DATABASE integration;
|
||||||
|
CREATE USER "twitch_bot" WITH PASSWORD NULL;
|
||||||
|
ALTER DATABASE integration OWNER to "twitch_bot";
|
||||||
|
EOF
|
||||||
|
- name: Run tests against CRDB
|
||||||
|
env:
|
||||||
|
GO_TEST_DB_ENGINE: postgres
|
||||||
|
GO_TEST_DB_DSN: host=crdb user=twitch_bot dbname=integration port=26257 sslmode=disable timezone=UTC
|
||||||
|
run: make short_test
|
||||||
|
integration-mariadb:
|
||||||
|
if: ${{ github.ref == 'refs/heads/master' }}
|
||||||
|
needs:
|
||||||
|
- test
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
container:
|
||||||
|
image: luzifer/gh-arch-env
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOPATH: /go
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
mariadb:
|
||||||
|
image: mariadb:11
|
||||||
|
env:
|
||||||
|
MYSQL_PASSWORD: twitch-bot-pass
|
||||||
|
MYSQL_ROOT_PASSWORD: root-pass
|
||||||
|
MYSQL_USER: twitch-bot
|
||||||
|
options: --health-cmd "healthcheck.sh --connect --innodb_initialized" --health-interval 10s --health-timeout 5s --health-retries 5
|
||||||
|
steps:
|
||||||
|
- name: Install required packages
|
||||||
|
run: |
|
||||||
|
pacman -Syy --noconfirm \
|
||||||
|
mariadb-clients
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- name: Marking workdir safe
|
||||||
|
run: git config --global --add safe.directory /__w/twitch-bot/twitch-bot
|
||||||
|
- name: Set up MariaDB service
|
||||||
|
run: |
|
||||||
|
mariadb -h mariadb -u root --password=root-pass <<EOF
|
||||||
|
CREATE DATABASE integration DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||||
|
GRANT ALL ON integration.* TO 'twitch-bot'@'%';
|
||||||
|
EOF
|
||||||
|
- name: Run tests against MariaDB
|
||||||
|
env:
|
||||||
|
GO_TEST_DB_ENGINE: mysql
|
||||||
|
GO_TEST_DB_DSN: twitch-bot:twitch-bot-pass@tcp(mariadb:3306)/integration?charset=utf8mb4&parseTime=True
|
||||||
|
run: make short_test
|
||||||
|
integration-mysql:
|
||||||
|
if: ${{ github.ref == 'refs/heads/master' }}
|
||||||
|
needs:
|
||||||
|
- test
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
container:
|
||||||
|
image: luzifer/gh-arch-env
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOPATH: /go
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
image: mysql:8
|
||||||
|
env:
|
||||||
|
MYSQL_PASSWORD: twitch-bot-pass
|
||||||
|
MYSQL_ROOT_PASSWORD: root-pass
|
||||||
|
MYSQL_USER: twitch-bot
|
||||||
|
options: --health-cmd "mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
||||||
|
steps:
|
||||||
|
- name: Install required packages
|
||||||
|
run: |
|
||||||
|
pacman -Syy --noconfirm \
|
||||||
|
mariadb-clients
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- name: Marking workdir safe
|
||||||
|
run: git config --global --add safe.directory /__w/twitch-bot/twitch-bot
|
||||||
|
- name: Set up MySQL service
|
||||||
|
run: |
|
||||||
|
mariadb -h mysql -u root --password=root-pass <<EOF
|
||||||
|
CREATE DATABASE integration DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||||
|
GRANT ALL ON integration.* TO 'twitch-bot'@'%';
|
||||||
|
EOF
|
||||||
|
- name: Run tests against MySQL
|
||||||
|
env:
|
||||||
|
GO_TEST_DB_ENGINE: mysql
|
||||||
|
GO_TEST_DB_DSN: twitch-bot:twitch-bot-pass@tcp(mysql:3306)/integration?charset=utf8mb4&parseTime=True
|
||||||
|
run: make short_test
|
||||||
|
integration-postgres:
|
||||||
|
if: ${{ github.ref == 'refs/heads/master' }}
|
||||||
|
needs:
|
||||||
|
- test
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
container:
|
||||||
|
image: luzifer/gh-arch-env
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOPATH: /go
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15
|
||||||
|
env:
|
||||||
|
POSTGRES_PASSWORD: twitch-bot-pass
|
||||||
|
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- name: Marking workdir safe
|
||||||
|
run: git config --global --add safe.directory /__w/twitch-bot/twitch-bot
|
||||||
|
- name: Run tests against PostgreSQL
|
||||||
|
env:
|
||||||
|
GO_TEST_DB_ENGINE: postgres
|
||||||
|
GO_TEST_DB_DSN: host=postgres user=postgres password=twitch-bot-pass dbname=postgres port=5432 sslmode=disable timezone=UTC
|
||||||
|
run: make short_test
|
||||||
|
release:
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
needs:
|
||||||
|
- test
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
container:
|
||||||
|
image: luzifer/gh-arch-env
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOPATH: /go
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- name: Marking workdir safe
|
||||||
|
run: |
|
||||||
|
git config --global --add safe.directory /__w/twitch-bot/twitch-bot
|
||||||
|
- name: Build release
|
||||||
|
run: make publish
|
||||||
|
- name: Extract changelog
|
||||||
|
run: awk "/^#/ && ++c==2{exit}; /^#/f" "History.md" | tail -n +2 >release_changelog.md
|
||||||
|
- name: Update stable branch
|
||||||
|
run: |
|
||||||
|
git branch -f stable ${GITHUB_SHA}
|
||||||
|
git push -f origin stable
|
||||||
|
- name: Release
|
||||||
|
uses: ncipollo/release-action@v1
|
||||||
|
with:
|
||||||
|
artifacts: .build/*
|
||||||
|
bodyFile: release_changelog.md
|
||||||
|
draft: false
|
||||||
|
generateReleaseNotes: false
|
||||||
|
test:
|
||||||
|
if: ${{ github.ref != 'refs/heads/stable' }}
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
container:
|
||||||
|
image: luzifer/gh-arch-env
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOPATH: /go
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- name: Marking workdir safe
|
||||||
|
run: |
|
||||||
|
git config --global --add safe.directory /__w/twitch-bot/twitch-bot
|
||||||
|
- name: Lint and test code
|
||||||
|
run: make lint test frontend_lint
|
||||||
|
- name: Build release (quick local for compile)
|
||||||
|
run: make build_prod
|
||||||
|
- name: Execute Trivy scan
|
||||||
|
run: make trivy
|
61
.github/workflows/integration-crdb.yml
vendored
61
.github/workflows/integration-crdb.yml
vendored
|
@ -1,61 +0,0 @@
|
||||||
---
|
|
||||||
|
|
||||||
name: integration-crdb
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
container:
|
|
||||||
image: luzifer/archlinux
|
|
||||||
env:
|
|
||||||
CGO_ENABLED: 0
|
|
||||||
GOPATH: /go
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
services:
|
|
||||||
crdb:
|
|
||||||
image: luzifer/crdb-gh-service
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Enable custom AUR package repo
|
|
||||||
run: echo -e "[luzifer]\nSigLevel = Never\nServer = https://archrepo.hub.luzifer.io/\$arch" >>/etc/pacman.conf
|
|
||||||
|
|
||||||
- name: Install required packages
|
|
||||||
run: |
|
|
||||||
pacman -Syy --noconfirm \
|
|
||||||
cockroachdb-bin \
|
|
||||||
git \
|
|
||||||
go \
|
|
||||||
make
|
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Marking workdir safe
|
|
||||||
run: git config --global --add safe.directory /__w/twitch-bot/twitch-bot
|
|
||||||
|
|
||||||
- name: Set up CRDB service
|
|
||||||
run: |
|
|
||||||
cockroach sql --host crdb --insecure <<EOF
|
|
||||||
CREATE DATABASE integration;
|
|
||||||
CREATE USER "twitch_bot" WITH PASSWORD NULL;
|
|
||||||
ALTER DATABASE integration OWNER to "twitch_bot";
|
|
||||||
EOF
|
|
||||||
|
|
||||||
- name: Run tests against CRDB
|
|
||||||
env:
|
|
||||||
GO_TEST_DB_ENGINE: postgres
|
|
||||||
GO_TEST_DB_DSN: host=crdb user=twitch_bot dbname=integration port=26257 sslmode=disable timezone=UTC
|
|
||||||
run: make test
|
|
||||||
|
|
||||||
...
|
|
64
.github/workflows/integration-mariadb.yml
vendored
64
.github/workflows/integration-mariadb.yml
vendored
|
@ -1,64 +0,0 @@
|
||||||
---
|
|
||||||
|
|
||||||
name: integration-mariadb
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
container:
|
|
||||||
image: luzifer/archlinux
|
|
||||||
env:
|
|
||||||
CGO_ENABLED: 0
|
|
||||||
GOPATH: /go
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
services:
|
|
||||||
mariadb:
|
|
||||||
image: mariadb:11
|
|
||||||
env:
|
|
||||||
MYSQL_PASSWORD: twitch-bot-pass
|
|
||||||
MYSQL_ROOT_PASSWORD: root-pass
|
|
||||||
MYSQL_USER: twitch-bot
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Enable custom AUR package repo
|
|
||||||
run: echo -e "[luzifer]\nSigLevel = Never\nServer = https://archrepo.hub.luzifer.io/\$arch" >>/etc/pacman.conf
|
|
||||||
|
|
||||||
- name: Install required packages
|
|
||||||
run: |
|
|
||||||
pacman -Syy --noconfirm \
|
|
||||||
git \
|
|
||||||
go \
|
|
||||||
make \
|
|
||||||
mariadb-clients
|
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Marking workdir safe
|
|
||||||
run: git config --global --add safe.directory /__w/twitch-bot/twitch-bot
|
|
||||||
|
|
||||||
- name: Set up MariaDB service
|
|
||||||
run: |
|
|
||||||
mariadb -h mariadb -u root --password=root-pass <<EOF
|
|
||||||
CREATE DATABASE integration DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
|
||||||
GRANT ALL ON integration.* TO 'twitch-bot'@'%';
|
|
||||||
EOF
|
|
||||||
|
|
||||||
- name: Run tests against MariaDB
|
|
||||||
env:
|
|
||||||
GO_TEST_DB_ENGINE: mysql
|
|
||||||
GO_TEST_DB_DSN: twitch-bot:twitch-bot-pass@tcp(mariadb:3306)/integration?charset=utf8mb4&parseTime=True
|
|
||||||
run: make test
|
|
||||||
|
|
||||||
...
|
|
64
.github/workflows/integration-mysql.yml
vendored
64
.github/workflows/integration-mysql.yml
vendored
|
@ -1,64 +0,0 @@
|
||||||
---
|
|
||||||
|
|
||||||
name: integration-mysql
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
container:
|
|
||||||
image: luzifer/archlinux
|
|
||||||
env:
|
|
||||||
CGO_ENABLED: 0
|
|
||||||
GOPATH: /go
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
services:
|
|
||||||
mysql:
|
|
||||||
image: mysql:8
|
|
||||||
env:
|
|
||||||
MYSQL_PASSWORD: twitch-bot-pass
|
|
||||||
MYSQL_ROOT_PASSWORD: root-pass
|
|
||||||
MYSQL_USER: twitch-bot
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Enable custom AUR package repo
|
|
||||||
run: echo -e "[luzifer]\nSigLevel = Never\nServer = https://archrepo.hub.luzifer.io/\$arch" >>/etc/pacman.conf
|
|
||||||
|
|
||||||
- name: Install required packages
|
|
||||||
run: |
|
|
||||||
pacman -Syy --noconfirm \
|
|
||||||
git \
|
|
||||||
go \
|
|
||||||
make \
|
|
||||||
mariadb-clients
|
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Marking workdir safe
|
|
||||||
run: git config --global --add safe.directory /__w/twitch-bot/twitch-bot
|
|
||||||
|
|
||||||
- name: Set up MySQL service
|
|
||||||
run: |
|
|
||||||
mariadb -h mysql -u root --password=root-pass <<EOF
|
|
||||||
CREATE DATABASE integration DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
|
||||||
GRANT ALL ON integration.* TO 'twitch-bot'@'%';
|
|
||||||
EOF
|
|
||||||
|
|
||||||
- name: Run tests against MySQL
|
|
||||||
env:
|
|
||||||
GO_TEST_DB_ENGINE: mysql
|
|
||||||
GO_TEST_DB_DSN: twitch-bot:twitch-bot-pass@tcp(mysql:3306)/integration?charset=utf8mb4&parseTime=True
|
|
||||||
run: make test
|
|
||||||
|
|
||||||
...
|
|
54
.github/workflows/integration-postgres.yml
vendored
54
.github/workflows/integration-postgres.yml
vendored
|
@ -1,54 +0,0 @@
|
||||||
---
|
|
||||||
|
|
||||||
name: integration-postgres
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
container:
|
|
||||||
image: luzifer/archlinux
|
|
||||||
env:
|
|
||||||
CGO_ENABLED: 0
|
|
||||||
GOPATH: /go
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:15
|
|
||||||
env:
|
|
||||||
POSTGRES_PASSWORD: twitch-bot-pass
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Enable custom AUR package repo
|
|
||||||
run: echo -e "[luzifer]\nSigLevel = Never\nServer = https://archrepo.hub.luzifer.io/\$arch" >>/etc/pacman.conf
|
|
||||||
|
|
||||||
- name: Install required packages
|
|
||||||
run: |
|
|
||||||
pacman -Syy --noconfirm \
|
|
||||||
git \
|
|
||||||
go \
|
|
||||||
make
|
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Marking workdir safe
|
|
||||||
run: git config --global --add safe.directory /__w/twitch-bot/twitch-bot
|
|
||||||
|
|
||||||
- name: Run tests against PostgreSQL
|
|
||||||
env:
|
|
||||||
GO_TEST_DB_ENGINE: postgres
|
|
||||||
GO_TEST_DB_DSN: host=postgres user=postgres password=twitch-bot-pass dbname=postgres port=5432 sslmode=disable timezone=UTC
|
|
||||||
run: make test
|
|
||||||
|
|
||||||
...
|
|
87
.github/workflows/test-and-build.yml
vendored
87
.github/workflows/test-and-build.yml
vendored
|
@ -1,87 +0,0 @@
|
||||||
---
|
|
||||||
|
|
||||||
name: test-and-build
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches-ignore:
|
|
||||||
- stable
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-and-build:
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
container:
|
|
||||||
image: luzifer/archlinux
|
|
||||||
env:
|
|
||||||
CGO_ENABLED: 0
|
|
||||||
GOPATH: /go
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Enable custom AUR package repo
|
|
||||||
run: echo -e "[luzifer]\nSigLevel = Never\nServer = https://archrepo.hub.luzifer.io/\$arch" >>/etc/pacman.conf
|
|
||||||
|
|
||||||
- name: Install required packages
|
|
||||||
run: |
|
|
||||||
pacman -Syy --noconfirm \
|
|
||||||
awk \
|
|
||||||
curl \
|
|
||||||
diffutils \
|
|
||||||
git \
|
|
||||||
git-lfs \
|
|
||||||
go \
|
|
||||||
golangci-lint-bin \
|
|
||||||
make \
|
|
||||||
nodejs-lts-hydrogen \
|
|
||||||
npm \
|
|
||||||
tar \
|
|
||||||
trivy \
|
|
||||||
unzip \
|
|
||||||
which \
|
|
||||||
zip
|
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
lfs: true
|
|
||||||
show-progress: false
|
|
||||||
|
|
||||||
- name: Marking workdir safe
|
|
||||||
run: |
|
|
||||||
git config --global --add safe.directory /__w/twitch-bot/twitch-bot
|
|
||||||
|
|
||||||
- name: Lint and test code
|
|
||||||
run: make lint test frontend_lint
|
|
||||||
|
|
||||||
- name: Build release
|
|
||||||
run: make publish
|
|
||||||
|
|
||||||
- name: Execute Trivy scan
|
|
||||||
run: make trivy
|
|
||||||
|
|
||||||
- name: Extract changelog
|
|
||||||
run: 'awk "/^#/ && ++c==2{exit}; /^#/f" "History.md" | tail -n +2 >release_changelog.md'
|
|
||||||
|
|
||||||
- name: Update stable branch
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
run: |
|
|
||||||
git branch -f stable ${GITHUB_SHA}
|
|
||||||
git push -f origin stable
|
|
||||||
|
|
||||||
- name: Release
|
|
||||||
uses: ncipollo/release-action@v1
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
with:
|
|
||||||
artifacts: '.build/*'
|
|
||||||
bodyFile: release_changelog.md
|
|
||||||
draft: false
|
|
||||||
generateReleaseNotes: false
|
|
||||||
|
|
||||||
...
|
|
63
History.md
63
History.md
|
@ -1,3 +1,66 @@
|
||||||
|
# 3.29.2 / 2024-04-13
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> This release introduces a new configuration validation which might lead to your bot not starting as of stronger type checking of actor settings. To validate the config is fine run a validation against the config once before replacing the bot binary / Docker image:
|
||||||
|
>
|
||||||
|
> `./twitch-bot --storage-conn-string "file::memory:?cache=shared" -c path/to/config.yaml validate-config`
|
||||||
|
>
|
||||||
|
> Using the connection string shown above will use a non-persistent database and can be used while the existing bot is running.
|
||||||
|
|
||||||
|
* New Features
|
||||||
|
* [eventsub] Add support for suspicious user events
|
||||||
|
|
||||||
|
* Improvements
|
||||||
|
* [core] Enforce attribute type schema validation on config
|
||||||
|
* [core] Remove deprecated fallback token / token migration
|
||||||
|
* [counter] Allow `counterTopList` to specify how to sort
|
||||||
|
* [counter] Record first seen and last updated on counters
|
||||||
|
* [counter] Revise template parsing logic
|
||||||
|
* [docs] Add field-type annotations to events
|
||||||
|
* [spotify] Improve error handling / documentation
|
||||||
|
* [spotify] Switch to PKCE flow, remove need for clientSecret
|
||||||
|
|
||||||
|
* Bugfixes
|
||||||
|
* [core] Fix: Do not retry core-kv query when it's not set
|
||||||
|
* [core] Fix: Don't initialize twitch client before start checks
|
||||||
|
* [eventsub] Fix: Fetching existing subscriptions broken
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> In case you're using the DockerHub Docker images and rely on the presence of the `stable` tag please switch to the [Github Registry](https://github.com/Luzifer/twitch-bot/pkgs/container/twitch-bot) and use the `latest` tag. Development releases are published as `develop`. The `stable` tag will not be updated beyond `v3.28.1`, DockerHub images are currently still supported but will be faded out.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Re-release of v3.29.0 as of broken tests in that release, no functional changes.
|
||||||
|
|
||||||
|
# 3.29.0 / 2024-04-13
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> This release introduces a new configuration validation which might lead to your bot not starting as of stronger type checking of actor settings. To validate the config is fine run a validation against the config once before replacing the bot binary / Docker image:
|
||||||
|
>
|
||||||
|
> `./twitch-bot --storage-conn-string "file::memory:?cache=shared" -c path/to/config.yaml validate-config`
|
||||||
|
>
|
||||||
|
> Using the connection string shown above will use a non-persistent database and can be used while the existing bot is running.
|
||||||
|
|
||||||
|
* New Features
|
||||||
|
* [eventsub] Add support for suspicious user events
|
||||||
|
|
||||||
|
* Improvements
|
||||||
|
* [core] Enforce attribute type schema validation on config
|
||||||
|
* [core] Remove deprecated fallback token / token migration
|
||||||
|
* [counter] Allow `counterTopList` to specify how to sort
|
||||||
|
* [counter] Record first seen and last updated on counters
|
||||||
|
* [counter] Revise template parsing logic
|
||||||
|
* [docs] Add field-type annotations to events
|
||||||
|
* [spotify] Improve error handling / documentation
|
||||||
|
* [spotify] Switch to PKCE flow, remove need for clientSecret
|
||||||
|
|
||||||
|
* Bugfixes
|
||||||
|
* [core] Fix: Do not retry core-kv query when it's not set
|
||||||
|
* [core] Fix: Don't initialize twitch client before start checks
|
||||||
|
* [eventsub] Fix: Fetching existing subscriptions broken
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> In case you're using the DockerHub Docker images and rely on the presence of the `stable` tag please switch to the [Github Registry](https://github.com/Luzifer/twitch-bot/pkgs/container/twitch-bot) and use the `latest` tag. Development releases are published as `develop`. The `stable` tag will not be updated beyond `v3.28.1`, DockerHub images are currently still supported but will be faded out.
|
||||||
|
|
||||||
# 3.28.1 / 2024-04-02
|
# 3.28.1 / 2024-04-02
|
||||||
|
|
||||||
* New Features
|
* New Features
|
||||||
|
|
8
Makefile
8
Makefile
|
@ -15,6 +15,9 @@ lint:
|
||||||
publish: frontend_prod
|
publish: frontend_prod
|
||||||
bash ./ci/build.sh
|
bash ./ci/build.sh
|
||||||
|
|
||||||
|
short_test:
|
||||||
|
go test -cover -test.short -v ./...
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test -cover -v ./...
|
go test -cover -v ./...
|
||||||
|
|
||||||
|
@ -33,7 +36,7 @@ frontend_lint: node_modules
|
||||||
src
|
src
|
||||||
|
|
||||||
node_modules:
|
node_modules:
|
||||||
npm ci
|
npm ci --include dev
|
||||||
|
|
||||||
# --- Tools
|
# --- Tools
|
||||||
|
|
||||||
|
@ -41,6 +44,9 @@ update_ua_list:
|
||||||
# User-Agents provided by https://www.useragents.me/
|
# User-Agents provided by https://www.useragents.me/
|
||||||
curl -sSf https://www.useragents.me/api | jq -r '.data[].ua' | grep -v 'Trident' >internal/linkcheck/user-agents.txt
|
curl -sSf https://www.useragents.me/api | jq -r '.data[].ua' | grep -v 'Trident' >internal/linkcheck/user-agents.txt
|
||||||
|
|
||||||
|
gh-workflow:
|
||||||
|
bash ci/create-workflow.sh
|
||||||
|
|
||||||
# -- Vulnerability scanning --
|
# -- Vulnerability scanning --
|
||||||
|
|
||||||
trivy:
|
trivy:
|
||||||
|
|
|
@ -4,12 +4,14 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
@ -49,7 +51,7 @@ func init() {
|
||||||
type ActorScript struct{}
|
type ActorScript struct{}
|
||||||
|
|
||||||
// Execute implements actor interface
|
// Execute implements actor interface
|
||||||
func (ActorScript) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (ActorScript) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
command, err := attrs.StringSlice("command")
|
command, err := attrs.StringSlice("command")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "getting command")
|
return false, errors.Wrap(err, "getting command")
|
||||||
|
@ -130,13 +132,16 @@ func (ActorScript) IsAsync() bool { return false }
|
||||||
func (ActorScript) Name() string { return "script" }
|
func (ActorScript) Name() string { return "script" }
|
||||||
|
|
||||||
// Validate implements actor interface
|
// Validate implements actor interface
|
||||||
func (ActorScript) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (ActorScript) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
cmd, err := attrs.StringSlice("command")
|
if err = attrs.ValidateSchema(
|
||||||
if err != nil || len(cmd) == 0 {
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "command", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeStringSlice}),
|
||||||
return errors.New("command must be slice of strings with length > 0")
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "skip_cooldown_on_error", Type: fieldcollection.SchemaFieldTypeBool}),
|
||||||
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, el := range cmd {
|
for i, el := range attrs.MustStringSlice("command", nil) {
|
||||||
if err = tplValidator(el); err != nil {
|
if err = tplValidator(el); err != nil {
|
||||||
return errors.Wrapf(err, "validating cmd template (element %d)", i)
|
return errors.Wrapf(err, "validating cmd template (element %d)", i)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -41,7 +42,7 @@ func registerAction(name string, acf plugins.ActorCreationFunc) {
|
||||||
availableActions[name] = acf
|
availableActions[name] = acf
|
||||||
}
|
}
|
||||||
|
|
||||||
func triggerAction(c *irc.Client, m *irc.Message, rule *plugins.Rule, ra *plugins.RuleAction, eventData *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func triggerAction(c *irc.Client, m *irc.Message, rule *plugins.Rule, ra *plugins.RuleAction, eventData *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
availableActionsLock.RLock()
|
availableActionsLock.RLock()
|
||||||
defer availableActionsLock.RUnlock()
|
defer availableActionsLock.RUnlock()
|
||||||
|
|
||||||
|
@ -65,7 +66,7 @@ func triggerAction(c *irc.Client, m *irc.Message, rule *plugins.Rule, ra *plugin
|
||||||
return apc, errors.Wrap(err, "execute action")
|
return apc, errors.Wrap(err, "execute action")
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleMessage(c *irc.Client, m *irc.Message, event *string, eventData *plugins.FieldCollection) {
|
func handleMessage(c *irc.Client, m *irc.Message, event *string, eventData *fieldcollection.FieldCollection) {
|
||||||
// Send events to registered handlers
|
// Send events to registered handlers
|
||||||
if event != nil {
|
if event != nil {
|
||||||
go notifyEventHandlers(*event, eventData)
|
go notifyEventHandlers(*event, eventData)
|
||||||
|
@ -77,9 +78,9 @@ func handleMessage(c *irc.Client, m *irc.Message, event *string, eventData *plug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleMessageRuleExecution(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection) {
|
func handleMessageRuleExecution(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection) {
|
||||||
var (
|
var (
|
||||||
ruleEventData = plugins.NewFieldCollection()
|
ruleEventData = fieldcollection.NewFieldCollection()
|
||||||
preventCooldown bool
|
preventCooldown bool
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -13,8 +13,8 @@ import (
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/go_helpers/v2/str"
|
"github.com/Luzifer/go_helpers/v2/str"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var cronParser = cron.NewParser(cron.SecondOptional | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
|
var cronParser = cron.NewParser(cron.SecondOptional | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
|
||||||
|
@ -174,7 +174,7 @@ func (a *autoMessage) allowExecuteDisableOnTemplate() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := plugins.NewFieldCollection()
|
fields := fieldcollection.NewFieldCollection()
|
||||||
fields.Set("channel", a.Channel)
|
fields.Set("channel", a.Channel)
|
||||||
|
|
||||||
res, err := formatMessage(*a.DisableOnTemplate, nil, nil, fields)
|
res, err := formatMessage(*a.DisableOnTemplate, nil, nil, fields)
|
||||||
|
|
15
ci/create-workflow.sh
Normal file
15
ci/create-workflow.sh
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
target_file=.github/workflows/generated_workflow.yml
|
||||||
|
source_files=($(find ci/workflow-parts -name 'part_*'))
|
||||||
|
base=ci/workflow-parts/index.yaml
|
||||||
|
|
||||||
|
cp ${base} ${target_file}
|
||||||
|
|
||||||
|
for source_file in "${source_files[@]}"; do
|
||||||
|
job_name=${source_file##*/part_}
|
||||||
|
job_name=${job_name%%.*}
|
||||||
|
yq -P ".jobs.${job_name} |= load(\"${source_file}\")" ${target_file} >${target_file}.new
|
||||||
|
mv ${target_file}.new ${target_file}
|
||||||
|
done
|
|
@ -19,8 +19,8 @@ branch)
|
||||||
tags+=(develop)
|
tags+=(develop)
|
||||||
;;
|
;;
|
||||||
tag)
|
tag)
|
||||||
# Build to latest & stable: Older tags are not intended to rebuild
|
# Build to latest: Older tags are not intended to rebuild
|
||||||
tags+=(latest stable ${GITHUB_REF_NAME})
|
tags+=(develop latest ${GITHUB_REF_NAME})
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
log "ERR: The ref type ${GITHUB_REF_TYPE} is not handled."
|
log "ERR: The ref type ${GITHUB_REF_TYPE} is not handled."
|
||||||
|
|
7
ci/workflow-parts/index.yaml
Normal file
7
ci/workflow-parts/index.yaml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
name: CI Workflow
|
||||||
|
on: push
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs: {}
|
44
ci/workflow-parts/part_doc-generator.yml
Normal file
44
ci/workflow-parts/part_doc-generator.yml
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
needs: [test]
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: luzifer/gh-arch-env
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOPATH: /go
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
lfs: true
|
||||||
|
show-progress: false
|
||||||
|
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 DOCS_BASE_URL=https://luzifer.github.io/twitch-bot/
|
||||||
|
|
||||||
|
- name: Upload GitHub Pages artifact
|
||||||
|
uses: actions/upload-pages-artifact@v1
|
||||||
|
with:
|
||||||
|
path: .rendered-docs
|
||||||
|
|
||||||
|
- name: Deploy artifact
|
||||||
|
uses: actions/deploy-pages@v1
|
||||||
|
|
||||||
|
...
|
30
ci/workflow-parts/part_docker-publish.yml
Normal file
30
ci/workflow-parts/part_docker-publish.yml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master' }}
|
||||||
|
needs: [test]
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Log into registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Docker Build & Publish
|
||||||
|
run: bash ci/docker-publish.sh
|
||||||
|
|
||||||
|
...
|
54
ci/workflow-parts/part_integration-crdb.yml
Normal file
54
ci/workflow-parts/part_integration-crdb.yml
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
if: ${{ github.ref == 'refs/heads/master' }}
|
||||||
|
needs: [test]
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: luzifer/gh-arch-env
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOPATH: /go
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
services:
|
||||||
|
crdb:
|
||||||
|
image: luzifer/crdb-gh-service
|
||||||
|
options: >-
|
||||||
|
--health-cmd "curl -sSf http://localhost:8080/health"
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Install required packages
|
||||||
|
run: |
|
||||||
|
pacman -Syy --noconfirm \
|
||||||
|
cockroachdb-bin
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Marking workdir safe
|
||||||
|
run: git config --global --add safe.directory /__w/twitch-bot/twitch-bot
|
||||||
|
|
||||||
|
- name: Set up CRDB service
|
||||||
|
run: |
|
||||||
|
cockroach sql --host crdb --insecure <<EOF
|
||||||
|
CREATE DATABASE integration;
|
||||||
|
CREATE USER "twitch_bot" WITH PASSWORD NULL;
|
||||||
|
ALTER DATABASE integration OWNER to "twitch_bot";
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Run tests against CRDB
|
||||||
|
env:
|
||||||
|
GO_TEST_DB_ENGINE: postgres
|
||||||
|
GO_TEST_DB_DSN: host=crdb user=twitch_bot dbname=integration port=26257 sslmode=disable timezone=UTC
|
||||||
|
run: make short_test
|
||||||
|
|
||||||
|
...
|
57
ci/workflow-parts/part_integration-mariadb.yml
Normal file
57
ci/workflow-parts/part_integration-mariadb.yml
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
if: ${{ github.ref == 'refs/heads/master' }}
|
||||||
|
needs: [test]
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: luzifer/gh-arch-env
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOPATH: /go
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
services:
|
||||||
|
mariadb:
|
||||||
|
image: mariadb:11
|
||||||
|
env:
|
||||||
|
MYSQL_PASSWORD: twitch-bot-pass
|
||||||
|
MYSQL_ROOT_PASSWORD: root-pass
|
||||||
|
MYSQL_USER: twitch-bot
|
||||||
|
options: >-
|
||||||
|
--health-cmd "healthcheck.sh --connect --innodb_initialized"
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Install required packages
|
||||||
|
run: |
|
||||||
|
pacman -Syy --noconfirm \
|
||||||
|
mariadb-clients
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Marking workdir safe
|
||||||
|
run: git config --global --add safe.directory /__w/twitch-bot/twitch-bot
|
||||||
|
|
||||||
|
- name: Set up MariaDB service
|
||||||
|
run: |
|
||||||
|
mariadb -h mariadb -u root --password=root-pass <<EOF
|
||||||
|
CREATE DATABASE integration DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||||
|
GRANT ALL ON integration.* TO 'twitch-bot'@'%';
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Run tests against MariaDB
|
||||||
|
env:
|
||||||
|
GO_TEST_DB_ENGINE: mysql
|
||||||
|
GO_TEST_DB_DSN: twitch-bot:twitch-bot-pass@tcp(mariadb:3306)/integration?charset=utf8mb4&parseTime=True
|
||||||
|
run: make short_test
|
||||||
|
|
||||||
|
...
|
57
ci/workflow-parts/part_integration-mysql.yml
Normal file
57
ci/workflow-parts/part_integration-mysql.yml
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
if: ${{ github.ref == 'refs/heads/master' }}
|
||||||
|
needs: [test]
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: luzifer/gh-arch-env
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOPATH: /go
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
image: mysql:8
|
||||||
|
env:
|
||||||
|
MYSQL_PASSWORD: twitch-bot-pass
|
||||||
|
MYSQL_ROOT_PASSWORD: root-pass
|
||||||
|
MYSQL_USER: twitch-bot
|
||||||
|
options: >-
|
||||||
|
--health-cmd "mysqladmin ping"
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Install required packages
|
||||||
|
run: |
|
||||||
|
pacman -Syy --noconfirm \
|
||||||
|
mariadb-clients
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Marking workdir safe
|
||||||
|
run: git config --global --add safe.directory /__w/twitch-bot/twitch-bot
|
||||||
|
|
||||||
|
- name: Set up MySQL service
|
||||||
|
run: |
|
||||||
|
mariadb -h mysql -u root --password=root-pass <<EOF
|
||||||
|
CREATE DATABASE integration DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||||
|
GRANT ALL ON integration.* TO 'twitch-bot'@'%';
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Run tests against MySQL
|
||||||
|
env:
|
||||||
|
GO_TEST_DB_ENGINE: mysql
|
||||||
|
GO_TEST_DB_DSN: twitch-bot:twitch-bot-pass@tcp(mysql:3306)/integration?charset=utf8mb4&parseTime=True
|
||||||
|
run: make short_test
|
||||||
|
|
||||||
|
...
|
43
ci/workflow-parts/part_integration-postgres.yml
Normal file
43
ci/workflow-parts/part_integration-postgres.yml
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
if: ${{ github.ref == 'refs/heads/master' }}
|
||||||
|
needs: [test]
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: luzifer/gh-arch-env
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOPATH: /go
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15
|
||||||
|
env:
|
||||||
|
POSTGRES_PASSWORD: twitch-bot-pass
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Marking workdir safe
|
||||||
|
run: git config --global --add safe.directory /__w/twitch-bot/twitch-bot
|
||||||
|
|
||||||
|
- name: Run tests against PostgreSQL
|
||||||
|
env:
|
||||||
|
GO_TEST_DB_ENGINE: postgres
|
||||||
|
GO_TEST_DB_DSN: host=postgres user=postgres password=twitch-bot-pass dbname=postgres port=5432 sslmode=disable timezone=UTC
|
||||||
|
run: make short_test
|
||||||
|
|
||||||
|
...
|
49
ci/workflow-parts/part_release.yml
Normal file
49
ci/workflow-parts/part_release.yml
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
needs: [test]
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: luzifer/gh-arch-env
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOPATH: /go
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Marking workdir safe
|
||||||
|
run: |
|
||||||
|
git config --global --add safe.directory /__w/twitch-bot/twitch-bot
|
||||||
|
|
||||||
|
- name: Build release
|
||||||
|
run: make publish
|
||||||
|
|
||||||
|
- name: Extract changelog
|
||||||
|
run: 'awk "/^#/ && ++c==2{exit}; /^#/f" "History.md" | tail -n +2 >release_changelog.md'
|
||||||
|
|
||||||
|
- name: Update stable branch
|
||||||
|
run: |
|
||||||
|
git branch -f stable ${GITHUB_SHA}
|
||||||
|
git push -f origin stable
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
uses: ncipollo/release-action@v1
|
||||||
|
with:
|
||||||
|
artifacts: '.build/*'
|
||||||
|
bodyFile: release_changelog.md
|
||||||
|
draft: false
|
||||||
|
generateReleaseNotes: false
|
||||||
|
|
||||||
|
...
|
35
ci/workflow-parts/part_test.yml
Normal file
35
ci/workflow-parts/part_test.yml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
if: ${{ github.ref != 'refs/heads/stable' }}
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: luzifer/gh-arch-env
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOPATH: /go
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Marking workdir safe
|
||||||
|
run: |
|
||||||
|
git config --global --add safe.directory /__w/twitch-bot/twitch-bot
|
||||||
|
|
||||||
|
- name: Lint and test code
|
||||||
|
run: make lint test frontend_lint
|
||||||
|
|
||||||
|
- name: Build release (quick local for compile)
|
||||||
|
run: make build_prod
|
||||||
|
|
||||||
|
- name: Execute Trivy scan
|
||||||
|
run: make trivy
|
||||||
|
|
||||||
|
...
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/go_helpers/v2/str"
|
"github.com/Luzifer/go_helpers/v2/str"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
@ -303,7 +304,7 @@ func (c *configFile) CloseRawMessageWriter() (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c configFile) GetMatchingRules(m *irc.Message, event *string, eventData *plugins.FieldCollection) []*plugins.Rule {
|
func (c configFile) GetMatchingRules(m *irc.Message, event *string, eventData *fieldcollection.FieldCollection) []*plugins.Rule {
|
||||||
configLock.RLock()
|
configLock.RLock()
|
||||||
defer configLock.RUnlock()
|
defer configLock.RUnlock()
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,10 @@ Ad-break has begun and ads are playing now in mentioned channel.
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `duration` - Duration of the ads in seconds
|
- `duration` _int64_ - Duration of the ads in seconds
|
||||||
- `is_automatic` - Were the ads started by the ad-manager?
|
- `is_automatic` _bool_ - Were the ads started by the ad-manager?
|
||||||
- `started_at` - When did the ad-break start
|
- `started_at` _time.Time_ - When did the ad-break start
|
||||||
|
|
||||||
## `ban`
|
## `ban`
|
||||||
|
|
||||||
|
@ -21,9 +21,9 @@ Note: This event does **not** contain the acting user! You cannot use the `{{.us
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `target_id` - The ID of the user being banned
|
- `target_id` _string_ - The ID of the user being banned
|
||||||
- `target_name` - The login-name of the user being banned
|
- `target_name` _string_ - The login-name of the user being banned
|
||||||
|
|
||||||
## `bits`
|
## `bits`
|
||||||
|
|
||||||
|
@ -31,9 +31,9 @@ User spent bits in the channel. The full message is available like in a normal c
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `bits` - Total amount of bits spent in the message
|
- `bits` _int64_ - Total amount of bits spent in the message
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `username` - The login-name of the user who spent the bits
|
- `username` _string_ - The login-name of the user who spent the bits
|
||||||
|
|
||||||
## `category_update`
|
## `category_update`
|
||||||
|
|
||||||
|
@ -41,8 +41,8 @@ The current category for the channel was changed. (This event has some delay to
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `category` - The name of the new game / category
|
- `category` _string_ - The name of the new game / category
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
|
|
||||||
## `channelpoint_redeem`
|
## `channelpoint_redeem`
|
||||||
|
|
||||||
|
@ -50,14 +50,14 @@ A custom channel-point reward was redeemed in the given channel. (Only available
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `reward_cost` - Number of points the user paid for the reward
|
- `reward_cost` _int64_ - Number of points the user paid for the reward
|
||||||
- `reward_id` - ID of the reward the user redeemed
|
- `reward_id` _string_ - ID of the reward the user redeemed
|
||||||
- `reward_title` - Title of the reward the user redeemed
|
- `reward_title` _string_ - Title of the reward the user redeemed
|
||||||
- `status` - Status of the reward (one of `unknown`, `unfulfilled`, `fulfilled`, and `canceled`)
|
- `status` _string_ - Status of the reward (one of `unknown`, `unfulfilled`, `fulfilled`, and `canceled`)
|
||||||
- `user_id` - The ID of the user who redeemed the reward
|
- `user_id` _string_ - The ID of the user who redeemed the reward
|
||||||
- `user_input` - The text the user entered into the input for the reward
|
- `user_input` _string_ - The text the user entered into the input for the reward
|
||||||
- `user` - The login-name of the user who redeemed the reward
|
- `user` _string_ - The login-name of the user who redeemed the reward
|
||||||
|
|
||||||
## `clearchat`
|
## `clearchat`
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ Note: This event does **not** contain the acting user! You cannot use the `{{.us
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
|
|
||||||
## `delete`
|
## `delete`
|
||||||
|
|
||||||
|
@ -77,9 +77,9 @@ Note: This event does **not** contain the acting user! You cannot use the `{{.us
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `message_id` - The UUID of the message being deleted
|
- `message_id` _string_ - The UUID of the message being deleted
|
||||||
- `target_name` - Login name of the author of the deleted message
|
- `target_name` _string_ - Login name of the author of the deleted message
|
||||||
|
|
||||||
## `follow`
|
## `follow`
|
||||||
|
|
||||||
|
@ -87,10 +87,10 @@ User followed the channel. This event is not de-duplicated and therefore might b
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `followed_at` - Time object of the follow date
|
- `followed_at` _time.Time_ - Time object of the follow date
|
||||||
- `user_id` - ID of the newly following user
|
- `user_id` _string_ - ID of the newly following user
|
||||||
- `user` - The login-name of the user who followed
|
- `user` _string_ - The login-name of the user who followed
|
||||||
|
|
||||||
## `giftpaidupgrade`
|
## `giftpaidupgrade`
|
||||||
|
|
||||||
|
@ -98,9 +98,9 @@ User upgraded their gifted subscription into a paid one. This event does not con
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `gifter` - The login-name of the user who gifted the subscription
|
- `gifter` _string_ - The login-name of the user who gifted the subscription
|
||||||
- `username` - The login-name of the user who upgraded their subscription
|
- `username` _string_ - The login-name of the user who upgraded their subscription
|
||||||
|
|
||||||
## `hypetrain_begin`, `hypetrain_end`, `hypetrain_progress`
|
## `hypetrain_begin`, `hypetrain_end`, `hypetrain_progress`
|
||||||
|
|
||||||
|
@ -108,10 +108,10 @@ An Hype-Train has begun, ended or progressed in the given channel.
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `level` - The current level of the Hype-Train
|
- `level` _int64_ - The current level of the Hype-Train
|
||||||
- `levelProgress` - Percentage of reached "points" in the current level to complete the level (not available on `hypetrain_end`)
|
- `levelProgress` _float64_ - Percentage of reached "points" in the current level to complete the level (not available on `hypetrain_end`)
|
||||||
- `event` - Raw Hype-Train event, see schema in [`pkg/twitch/eventsub.go#L92`](https://github.com/Luzifer/twitch-bot/blob/master/pkg/twitch/eventsub.go#L121)
|
- `event` _EventSubEventHypetrain_ - Raw Hype-Train event, see schema in [`pkg/twitch/eventsub.go#L92`](https://github.com/Luzifer/twitch-bot/blob/master/pkg/twitch/eventsub.go#L121)
|
||||||
|
|
||||||
## `join`
|
## `join`
|
||||||
|
|
||||||
|
@ -119,8 +119,8 @@ User joined the channel-chat. This is **NOT** an indicator they are viewing, the
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `user` - The login-name of the user who joined
|
- `user` _string_ - The login-name of the user who joined
|
||||||
|
|
||||||
## `kofi_donation`
|
## `kofi_donation`
|
||||||
|
|
||||||
|
@ -128,14 +128,14 @@ A Ko-fi donation was received through the API-Webhook.
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred for
|
- `channel` _string_ - The channel the event occurred for
|
||||||
- `from` - The name submitted by Ko-fi (can be arbitrarily entered)
|
- `from` _string_ - The name submitted by Ko-fi (can be arbitrarily entered)
|
||||||
- `amount` - The amount donated as submitted by Ko-fi (i.e. 27.95)
|
- `amount` _float64_ - The amount donated as submitted by Ko-fi (i.e. 27.95)
|
||||||
- `currency` - The currency of the amount (i.e. USD)
|
- `currency` _string_ - The currency of the amount (i.e. USD)
|
||||||
- `isSubscription` - Boolean, true on monthly subscriptions, false on single-donations
|
- `isSubscription` _bool_ - true on monthly subscriptions, false on single-donations
|
||||||
- `isFirstSubPayment` - Boolean, true on first montly payment, false otherwise
|
- `isFirstSubPayment` _bool_ - true on first montly payment, false otherwise
|
||||||
- `message` - The message entered by the donator (**not** present when donation was marked as private!)
|
- `message` _string_ - The message entered by the donator (**not** present when donation was marked as private!)
|
||||||
- `tier` - The tier the subscriber subscribed to (seems not to be filled on the first transaction?)
|
- `tier` _string_ - The tier the subscriber subscribed to (seems not to be filled on the first transaction?)
|
||||||
|
|
||||||
## `outbound_raid`
|
## `outbound_raid`
|
||||||
|
|
||||||
|
@ -143,10 +143,10 @@ The channel has raided another channel. (The event is issued in the moment the r
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the raid originated at
|
- `channel` _string_ - The channel the raid originated at
|
||||||
- `to` - The login-name of the channel the viewers are sent to
|
- `to` _string_ - The login-name of the channel the viewers are sent to
|
||||||
- `to_id` - The ID of the channel the viewers are sent to
|
- `to_id` _string_ - The ID of the channel the viewers are sent to
|
||||||
- `viewers` - The number of viewers included in the raid
|
- `viewers` _int64_ - The number of viewers included in the raid
|
||||||
|
|
||||||
## `part`
|
## `part`
|
||||||
|
|
||||||
|
@ -154,8 +154,8 @@ User left the channel-chat. This is **NOT** an indicator they are no longer view
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `user` - The login-name of the user who left
|
- `user` _string_ - The login-name of the user who left
|
||||||
|
|
||||||
## `permit`
|
## `permit`
|
||||||
|
|
||||||
|
@ -163,9 +163,9 @@ User received a permit, which means they are no longer affected by rules which a
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `user` - The login-name of the user who **gave** the permit
|
- `user` _string_ - The login-name of the user who **gave** the permit
|
||||||
- `to` - The username who got the permit
|
- `to` _string_ - The username who got the permit
|
||||||
|
|
||||||
## `poll_begin` / `poll_end` / `poll_progress`
|
## `poll_begin` / `poll_end` / `poll_progress`
|
||||||
|
|
||||||
|
@ -173,10 +173,10 @@ A poll was started / was ended / had changes in the given channel.
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `poll` - The poll object describing the poll, see schema in [`pkg/twitch/eventsub.go#L92`](https://github.com/Luzifer/twitch-bot/blob/master/pkg/twitch/eventsub.go#L152)
|
- `poll` _EventSubEventPoll_ - The poll object describing the poll, see schema in [`pkg/twitch/eventsub.go#L92`](https://github.com/Luzifer/twitch-bot/blob/master/pkg/twitch/eventsub.go#L152)
|
||||||
- `status` - The status of the poll (one of `completed`, `terminated` or `archived`) - only available in `poll_end`
|
- `status` _string_ - The status of the poll (one of `completed`, `terminated` or `archived`) - only available in `poll_end`
|
||||||
- `title` - The title of the poll the event was generated for
|
- `title` _string_ - The title of the poll the event was generated for
|
||||||
|
|
||||||
## `raid`
|
## `raid`
|
||||||
|
|
||||||
|
@ -184,9 +184,9 @@ The channel was raided by another user.
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `username` - The login-name of the user who raided the channel
|
- `username` _string_ - The login-name of the user who raided the channel
|
||||||
- `viewercount` - The amount of users who have been raided (this number is not fully accurate)
|
- `viewercount` _int64_ - The amount of users who have been raided (this number is not fully accurate)
|
||||||
|
|
||||||
## `resub`
|
## `resub`
|
||||||
|
|
||||||
|
@ -194,10 +194,10 @@ The user shared their resubscription. (This event is triggered manually by the u
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `plan` - The sub-plan they are using (`1000` = T1, `2000` = T2, `3000` = T3, `Prime`)
|
- `plan` _string_ - The sub-plan they are using (`1000` = T1, `2000` = T2, `3000` = T3, `Prime`)
|
||||||
- `subscribed_months` - How long have they been subscribed
|
- `subscribed_months` _int64_ - How long have they been subscribed
|
||||||
- `username` - The login-name of the user who resubscribed
|
- `username` _string_ - The login-name of the user who resubscribed
|
||||||
|
|
||||||
## `shoutout_created`
|
## `shoutout_created`
|
||||||
|
|
||||||
|
@ -205,10 +205,10 @@ The channel gave another streamer a (Twitch native) shoutout
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `to_id` - The ID of the channel who received the shoutout
|
- `to_id` _string_ - The ID of the channel who received the shoutout
|
||||||
- `to` - The login-name of the channel who received the shoutout
|
- `to` _string_ - The login-name of the channel who received the shoutout
|
||||||
- `viewers` - The amount of viewers the shoutout was shown to
|
- `viewers` _int64_ - The amount of viewers the shoutout was shown to
|
||||||
|
|
||||||
## `shoutout_received`
|
## `shoutout_received`
|
||||||
|
|
||||||
|
@ -216,10 +216,10 @@ The channel received a (Twitch native) shoutout by another channel.
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `from_id` - The ID of the channel who issued the shoutout
|
- `from_id` _string_ - The ID of the channel who issued the shoutout
|
||||||
- `from` - The login-name of the channel who issued the shoutout
|
- `from` _string_ - The login-name of the channel who issued the shoutout
|
||||||
- `viewers` - The amount of viewers the shoutout was shown to
|
- `viewers` _int64_ - The amount of viewers the shoutout was shown to
|
||||||
|
|
||||||
## `stream_offline`
|
## `stream_offline`
|
||||||
|
|
||||||
|
@ -227,7 +227,7 @@ The channels stream went offline. (This event has some delay to the real categor
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
|
|
||||||
## `stream_online`
|
## `stream_online`
|
||||||
|
|
||||||
|
@ -235,7 +235,7 @@ The channels stream went offline. (This event has some delay to the real categor
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
|
|
||||||
## `sub`
|
## `sub`
|
||||||
|
|
||||||
|
@ -243,9 +243,9 @@ The user newly subscribed on their own. (This event is triggered automatically a
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `plan` - The sub-plan they are using (`1000` = T1, `2000` = T2, `3000` = T3, `Prime`)
|
- `plan` _string_ - The sub-plan they are using (`1000` = T1, `2000` = T2, `3000` = T3, `Prime`)
|
||||||
- `username` - The login-name of the user who subscribed
|
- `username` _string_ - The login-name of the user who subscribed
|
||||||
|
|
||||||
## `subgift`
|
## `subgift`
|
||||||
|
|
||||||
|
@ -253,13 +253,14 @@ The user gifted the subscription to a specific user. (This event **DOES** occur
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `gifted_months` - Number of months the user gifted
|
- `gifted_months` _int64_ - Number of months the user gifted
|
||||||
- `origin_id` - ID unique to the gift-event (can be used to match `subgift` events to corresponding `submysterygift` event)
|
- `origin_id` _string_ - ID unique to the gift-event (can be used to match `subgift` events to corresponding `submysterygift` event)
|
||||||
- `plan` - The sub-plan they are using (`1000` = T1, `2000` = T2, `3000` = T3, `Prime`)
|
- `plan` _string_ - The sub-plan they are using (`1000` = T1, `2000` = T2, `3000` = T3, `Prime`)
|
||||||
- `subscribed_months` - How long the recipient has been subscribed
|
- `subscribed_months` _int64_ - How long the recipient has been subscribed
|
||||||
- `to` - The user who received the sub
|
- `to` _string_ - The user who received the sub
|
||||||
- `username` - The login-name of the user who gifted the subscription
|
- `total_gifted` _int64_ - How many subs has the user given in total (might be zero due to users preferences)
|
||||||
|
- `username` _string_ - The login-name of the user who gifted the subscription
|
||||||
|
|
||||||
## `submysterygift`
|
## `submysterygift`
|
||||||
|
|
||||||
|
@ -267,11 +268,34 @@ The user gifted multiple subs to the community. (This event is followed by `numb
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `number` - The amount of gifted subs
|
- `number` _int64_ - The amount of gifted subs
|
||||||
- `origin_id` - ID unique to the gift-event (can be used to match `subgift` events to corresponding `submysterygift` event)
|
- `origin_id` _string_ - ID unique to the gift-event (can be used to match `subgift` events to corresponding `submysterygift` event)
|
||||||
- `plan` - The sub-plan they are using (`1000` = T1, `2000` = T2, `3000` = T3, `Prime`)
|
- `plan` _string_ - The sub-plan they are using (`1000` = T1, `2000` = T2, `3000` = T3, `Prime`)
|
||||||
- `username` - The login-name of the user who gifted the subscription
|
- `username` _string_ - The login-name of the user who gifted the subscription
|
||||||
|
|
||||||
|
## `sus_user_message`
|
||||||
|
|
||||||
|
A suspicious (monitored / restricted) user sent a message in the given channel
|
||||||
|
|
||||||
|
- `ban_evasion` _string_ - Status of the ban-evasion detection: `unknown`, `possible`, `likely`
|
||||||
|
- `channel` _string_ - The channel in which the event occurred
|
||||||
|
- `message` _string_ - The message the user sent in plain text
|
||||||
|
- `shared_ban_channels` _[]string_ - IDs of channels with shared ban-info in which the user is also banned
|
||||||
|
- `status` _string_ - Restriction status: `active_monitoring`, `restricted`
|
||||||
|
- `user_id` _string_ - ID of the user sending the message
|
||||||
|
- `user_type` _[]string_ - How the user ended being on the naughty-list: `manually_added`, `ban_evader_detector`, or `shared_channel_ban`
|
||||||
|
- `username` _string_ - The login-name of the user sending the message
|
||||||
|
|
||||||
|
## `sus_user_update`
|
||||||
|
|
||||||
|
The status of suspicious user was changed by a moderator
|
||||||
|
|
||||||
|
- `channel` _string_ - The channel in which the event occurred
|
||||||
|
- `moderator` _string_ - The login-name of the moderator changing the status
|
||||||
|
- `status` _string_ - Restriction status: `no_treatment`, `active_monitoring`, `restricted`
|
||||||
|
- `user_id` _string_ - ID of the suspicious user
|
||||||
|
- `username` _string_ - Login-name of the suspicious user
|
||||||
|
|
||||||
## `timeout`
|
## `timeout`
|
||||||
|
|
||||||
|
@ -281,11 +305,11 @@ Note: This event does **not** contain the acting user! You cannot use the `{{.us
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `duration` - The timeout duration (`time.Duration`, nanoseconds)
|
- `duration` _time.Duration_ - The timeout duration (nanoseconds)
|
||||||
- `seconds` - The timeout duration (`int`, seconds)
|
- `seconds` _int_ - The timeout duration (seconds)
|
||||||
- `target_id` - The ID of the user being timed out
|
- `target_id` _string_ - The ID of the user being timed out
|
||||||
- `target_name` - The login-name of the user being timed out
|
- `target_name` _string_ - The login-name of the user being timed out
|
||||||
|
|
||||||
## `title_update`
|
## `title_update`
|
||||||
|
|
||||||
|
@ -293,8 +317,8 @@ The current title for the channel was changed. (This event has some delay to the
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` _string_ - The channel the event occurred in
|
||||||
- `title` - The title of the stream
|
- `title` _string_ - The title of the stream
|
||||||
|
|
||||||
## `whisper`
|
## `whisper`
|
||||||
|
|
||||||
|
@ -302,4 +326,4 @@ The bot received a whisper message. (You can use `(.*)` as message match and `{{
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
|
|
||||||
- `username` - The login-name of the user who sent the message
|
- `username` _string_ - The login-name of the user who sent the message
|
||||||
|
|
|
@ -128,9 +128,9 @@ Example:
|
||||||
|
|
||||||
### `counterTopList`
|
### `counterTopList`
|
||||||
|
|
||||||
Returns the top n counters for the given prefix as objects with Name and Value fields
|
Returns the top n counters for the given prefix as objects with Name and Value fields. Can be ordered by `name` / `value` / `first_seen` / `last_modified` ascending (`ASC`) or descending (`DESC`): i.e. `last_modified DESC` defaults to `value DESC`
|
||||||
|
|
||||||
Syntax: `counterTopList <prefix> <n>`
|
Syntax: `counterTopList <prefix> <n> [orderBy]`
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
@ -467,7 +467,7 @@ Example:
|
||||||
|
|
||||||
```
|
```
|
||||||
# Your int this hour: {{ printf "%.0f" (mulf (seededRandom (list "int" .username (now | date "2006-01-02 15") | join ":")) 100) }}%
|
# Your int this hour: {{ printf "%.0f" (mulf (seededRandom (list "int" .username (now | date "2006-01-02 15") | join ":")) 100) }}%
|
||||||
< Your int this hour: 70%
|
< Your int this hour: 88%
|
||||||
```
|
```
|
||||||
|
|
||||||
### `spotifyCurrentPlaying`
|
### `spotifyCurrentPlaying`
|
||||||
|
|
|
@ -17,25 +17,29 @@ Start with going to the [Spotify for Developers Dashboard](https://developer.spo
|
||||||
|
|
||||||
- "App name" is something you can choose yourself
|
- "App name" is something you can choose yourself
|
||||||
- "App description" is also required, choose yourself
|
- "App description" is also required, choose yourself
|
||||||
- "Redirect URI" must be `https://example.com/spotify/<channel>` so for exmaple `https://example.com/spotify/luziferus`
|
- "Redirect URI" must be `https://example.com/spotify/<channel>` so for example `https://example.com/spotify/luziferus`
|
||||||
- Select "Web API" for the "API/SDKs you are planning to use"
|
- Select "Web API" for the "API/SDKs you are planning to use"
|
||||||
- Check the ToS box (of course after reading those!)
|
- Check the ToS box (of course after reading those!)
|
||||||
- Click "Save"
|
- Click "Save"
|
||||||
- From the "Settings" button of your app get the "Client ID" and "Client secret" and note them down
|
- From the "Settings" button of your app get the "Client ID" and note it down
|
||||||
- Optional: If you need to authorize multiple channels (i.e. for multiple users of the bot instance) you can edit the "Redirect URIs" on the "Settings" page and add more.
|
- Optional: If you need to authorize multiple channels (i.e. for multiple users of the bot instance) you can edit the "Redirect URIs" on the "Settings" page and add more.
|
||||||
|
|
||||||
|
{{< alert style="info" >}}If you are managing a bot instance for multiple persons having their own Spotify accounts you need to invite them to the Spotify app as long as it is in development-mode. You can do that in the Spotify Developer Dashboard under "User Management" (up to 25 users). As an alternative every person can create an own Spotify app and you can enter their `clientId` into the config for their respective channel.{{< /alert >}}
|
||||||
|
|
||||||
Now head into the configuration file and configure the Spotify module:
|
Now head into the configuration file and configure the Spotify module:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# Module configuration by channel or defining bot-wide defaults. See
|
# Module configuration by channel or defining bot-wide defaults. See
|
||||||
# module specific documentation for options to configure in this
|
# module specific documentation for options to configure in this
|
||||||
# section. All modules come with internal defaults so there is no
|
# section.
|
||||||
# need to configure this but you can overwrite the internal defaults.
|
|
||||||
module_config:
|
module_config:
|
||||||
spotify:
|
spotify:
|
||||||
|
# Use one client-id for all channels (invite users)
|
||||||
default:
|
default:
|
||||||
clientId: 'put the client ID you noted down here'
|
clientId: 'put the client ID you noted down here'
|
||||||
clientSecret: 'put the secret here'
|
# Use one client-id per channel (have each user create an app)
|
||||||
|
anotherttvuser:
|
||||||
|
clientId: 'put the client ID they sent you here'
|
||||||
```
|
```
|
||||||
|
|
||||||
Now send the user which currently playing track should be displayed to the `https://example.com/spotify/<channel>` URL. So I for example would visit `https://example.com/spotify/luziferus`. They are redirected to Spotify, need to authorize the app and if everything went well the bot tells them "Spotify is now authorized for this channel, you can close this page".
|
Now send the user which currently playing track should be displayed to the `https://example.com/spotify/<channel>` URL. So I for example would visit `https://example.com/spotify/luziferus`. They are redirected to Spotify, need to authorize the app and if everything went well the bot tells them "Spotify is now authorized for this channel, you can close this page".
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -44,6 +45,8 @@ var (
|
||||||
eventTypeSubgift = ptrStr("subgift")
|
eventTypeSubgift = ptrStr("subgift")
|
||||||
eventTypeSubmysterygift = ptrStr("submysterygift")
|
eventTypeSubmysterygift = ptrStr("submysterygift")
|
||||||
eventTypeSub = ptrStr("sub")
|
eventTypeSub = ptrStr("sub")
|
||||||
|
eventTypeSusUserMessage = ptrStr("sus_user_message")
|
||||||
|
eventTypeSusUserUpdate = ptrStr("sus_user_update")
|
||||||
eventTypeTimeout = ptrStr("timeout")
|
eventTypeTimeout = ptrStr("timeout")
|
||||||
eventTypeWatchStreak = ptrStr("watch_streak")
|
eventTypeWatchStreak = ptrStr("watch_streak")
|
||||||
eventTypeWhisper = ptrStr("whisper")
|
eventTypeWhisper = ptrStr("whisper")
|
||||||
|
@ -82,6 +85,8 @@ var (
|
||||||
eventTypeSub,
|
eventTypeSub,
|
||||||
eventTypeSubgift,
|
eventTypeSubgift,
|
||||||
eventTypeSubmysterygift,
|
eventTypeSubmysterygift,
|
||||||
|
eventTypeSusUserMessage,
|
||||||
|
eventTypeSusUserUpdate,
|
||||||
eventTypeTimeout,
|
eventTypeTimeout,
|
||||||
eventTypeWatchStreak,
|
eventTypeWatchStreak,
|
||||||
eventTypeWhisper,
|
eventTypeWhisper,
|
||||||
|
@ -93,7 +98,7 @@ var (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func notifyEventHandlers(event string, eventData *plugins.FieldCollection) {
|
func notifyEventHandlers(event string, eventData *fieldcollection.FieldCollection) {
|
||||||
registeredEventHandlersLock.Lock()
|
registeredEventHandlersLock.Lock()
|
||||||
defer registeredEventHandlersLock.Unlock()
|
defer registeredEventHandlersLock.Unlock()
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/go_helpers/v2/str"
|
"github.com/Luzifer/go_helpers/v2/str"
|
||||||
korvike "github.com/Luzifer/korvike/functions"
|
korvike "github.com/Luzifer/korvike/functions"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
|
@ -37,7 +38,7 @@ func newTemplateFuncProvider() *templateFuncProvider {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *templateFuncProvider) GetFuncMap(m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) template.FuncMap {
|
func (t *templateFuncProvider) GetFuncMap(m *irc.Message, r *plugins.Rule, fields *fieldcollection.FieldCollection) template.FuncMap {
|
||||||
t.lock.RLock()
|
t.lock.RLock()
|
||||||
defer t.lock.RUnlock()
|
defer t.lock.RUnlock()
|
||||||
|
|
||||||
|
|
|
@ -6,12 +6,13 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
tplFuncs.Register("arg", func(m *irc.Message, _ *plugins.Rule, _ *plugins.FieldCollection) interface{} {
|
tplFuncs.Register("arg", func(m *irc.Message, _ *plugins.Rule, _ *fieldcollection.FieldCollection) interface{} {
|
||||||
return func(arg int) (string, error) {
|
return func(arg int) (string, error) {
|
||||||
msgParts := strings.Split(m.Trailing(), " ")
|
msgParts := strings.Split(m.Trailing(), " ")
|
||||||
if len(msgParts) <= arg {
|
if len(msgParts) <= arg {
|
||||||
|
@ -30,7 +31,7 @@ func init() {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
tplFuncs.Register("chatterHasBadge", func(m *irc.Message, _ *plugins.Rule, _ *plugins.FieldCollection) interface{} {
|
tplFuncs.Register("chatterHasBadge", func(m *irc.Message, _ *plugins.Rule, _ *fieldcollection.FieldCollection) interface{} {
|
||||||
return func(badge string) bool {
|
return func(badge string) bool {
|
||||||
badges := twitch.ParseBadgeLevels(m)
|
badges := twitch.ParseBadgeLevels(m)
|
||||||
return badges.Has(badge)
|
return badges.Has(badge)
|
||||||
|
@ -57,7 +58,7 @@ func init() {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
tplFuncs.Register("group", func(m *irc.Message, r *plugins.Rule, _ *plugins.FieldCollection) interface{} {
|
tplFuncs.Register("group", func(m *irc.Message, r *plugins.Rule, _ *fieldcollection.FieldCollection) interface{} {
|
||||||
return func(idx int, fallback ...string) (string, error) {
|
return func(idx int, fallback ...string) (string, error) {
|
||||||
fields := r.GetMatchMessage().FindStringSubmatch(m.Trailing())
|
fields := r.GetMatchMessage().FindStringSubmatch(m.Trailing())
|
||||||
if len(fields) <= idx {
|
if len(fields) <= idx {
|
||||||
|
@ -94,7 +95,7 @@ func init() {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
tplFuncs.Register("tag", func(m *irc.Message, _ *plugins.Rule, _ *plugins.FieldCollection) interface{} {
|
tplFuncs.Register("tag", func(m *irc.Message, _ *plugins.Rule, _ *fieldcollection.FieldCollection) interface{} {
|
||||||
return func(tag string) string { return m.Tags[tag] }
|
return func(tag string) string { return m.Tags[tag] }
|
||||||
}, plugins.TemplateFuncDocumentation{
|
}, plugins.TemplateFuncDocumentation{
|
||||||
Description: "Takes the message sent to the channel, returns the value of the tag specified",
|
Description: "Takes the message sent to the channel, returns the value of the tag specified",
|
||||||
|
|
12
go.mod
12
go.mod
|
@ -4,19 +4,19 @@ go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Luzifer/go-openssl/v4 v4.2.2
|
github.com/Luzifer/go-openssl/v4 v4.2.2
|
||||||
github.com/Luzifer/go_helpers/v2 v2.23.0
|
github.com/Luzifer/go_helpers/v2 v2.24.0
|
||||||
github.com/Luzifer/korvike/functions v0.11.0
|
github.com/Luzifer/korvike/functions v0.11.0
|
||||||
github.com/Luzifer/rconfig/v2 v2.5.0
|
github.com/Luzifer/rconfig/v2 v2.5.0
|
||||||
github.com/Masterminds/sprig/v3 v3.2.3
|
github.com/Masterminds/sprig/v3 v3.2.3
|
||||||
github.com/getsentry/sentry-go v0.27.0
|
github.com/getsentry/sentry-go v0.27.0
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/go-git/go-git/v5 v5.11.0
|
github.com/go-git/go-git/v5 v5.12.0
|
||||||
github.com/go-sql-driver/mysql v1.8.1
|
github.com/go-sql-driver/mysql v1.8.1
|
||||||
github.com/gofrs/uuid v4.4.0+incompatible
|
github.com/gofrs/uuid v4.4.0+incompatible
|
||||||
github.com/gofrs/uuid/v3 v3.1.2
|
github.com/gofrs/uuid/v3 v3.1.2
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/gorilla/websocket v1.5.1
|
github.com/gorilla/websocket v1.5.1
|
||||||
github.com/itchyny/gojq v0.12.14
|
github.com/itchyny/gojq v0.12.15
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||||
github.com/orandin/sentrus v1.0.0
|
github.com/orandin/sentrus v1.0.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
|
@ -31,7 +31,7 @@ require (
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gorm.io/driver/mysql v1.5.6
|
gorm.io/driver/mysql v1.5.6
|
||||||
gorm.io/driver/postgres v1.5.7
|
gorm.io/driver/postgres v1.5.7
|
||||||
gorm.io/gorm v1.25.8
|
gorm.io/gorm v1.25.9
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
@ -87,7 +87,7 @@ require (
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||||
github.com/sergi/go-diff v1.3.1 // indirect
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||||
github.com/shopspring/decimal v1.3.1 // indirect
|
github.com/shopspring/decimal v1.3.1 // indirect
|
||||||
github.com/skeema/knownhosts v1.2.2 // indirect
|
github.com/skeema/knownhosts v1.2.2 // indirect
|
||||||
github.com/spf13/cast v1.6.0 // indirect
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
|
@ -105,6 +105,6 @@ require (
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
modernc.org/libc v1.49.0 // indirect
|
modernc.org/libc v1.49.0 // indirect
|
||||||
modernc.org/mathutil v1.6.0 // indirect
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
modernc.org/memory v1.7.2 // indirect
|
modernc.org/memory v1.8.0 // indirect
|
||||||
modernc.org/sqlite v1.29.5 // indirect
|
modernc.org/sqlite v1.29.5 // indirect
|
||||||
)
|
)
|
||||||
|
|
28
go.sum
28
go.sum
|
@ -6,8 +6,8 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/Luzifer/go-openssl/v4 v4.2.2 h1:wKF/GhSKGJtHFQYTkN61wXig7mPvDj/oPpW6MmnBpjc=
|
github.com/Luzifer/go-openssl/v4 v4.2.2 h1:wKF/GhSKGJtHFQYTkN61wXig7mPvDj/oPpW6MmnBpjc=
|
||||||
github.com/Luzifer/go-openssl/v4 v4.2.2/go.mod h1:+kAwI4NpyYXoWil85gKSCEJNoCQlMeFikEMn2f+5ffc=
|
github.com/Luzifer/go-openssl/v4 v4.2.2/go.mod h1:+kAwI4NpyYXoWil85gKSCEJNoCQlMeFikEMn2f+5ffc=
|
||||||
github.com/Luzifer/go_helpers/v2 v2.23.0 h1:VowDwOCl6nOt+GVqKUX/do6a94pEeqNTRHb29MsoGX4=
|
github.com/Luzifer/go_helpers/v2 v2.24.0 h1:abACOhsn6a6c6X22jq42mZM1wuOM0Ihfa6yzssrjrOg=
|
||||||
github.com/Luzifer/go_helpers/v2 v2.23.0/go.mod h1:BSGkJ/dxqs7AxsfZt8zjJb4R6YB5dONS+/ad7foLUrk=
|
github.com/Luzifer/go_helpers/v2 v2.24.0/go.mod h1:KSVUdAJAav5cWGyB5oKGxmC27HrKULVTOxwPS/Kr+pc=
|
||||||
github.com/Luzifer/korvike/functions v0.11.0 h1:2hr3nnt9hy8Esu1W3h50+RggcLRXvrw92kVQLvxzd2Q=
|
github.com/Luzifer/korvike/functions v0.11.0 h1:2hr3nnt9hy8Esu1W3h50+RggcLRXvrw92kVQLvxzd2Q=
|
||||||
github.com/Luzifer/korvike/functions v0.11.0/go.mod h1:osumwH64mWgbwZIfE7rE0BB7Y5HXxrzyO4JfO7fhduU=
|
github.com/Luzifer/korvike/functions v0.11.0/go.mod h1:osumwH64mWgbwZIfE7rE0BB7Y5HXxrzyO4JfO7fhduU=
|
||||||
github.com/Luzifer/rconfig/v2 v2.5.0 h1:zx5lfQbNX3za4VegID97IeY+M+BmfgHxWJTYA94sxok=
|
github.com/Luzifer/rconfig/v2 v2.5.0 h1:zx5lfQbNX3za4VegID97IeY+M+BmfgHxWJTYA94sxok=
|
||||||
|
@ -62,8 +62,8 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec
|
||||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||||
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
|
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
|
||||||
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
|
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
|
||||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
|
@ -72,8 +72,8 @@ github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+
|
||||||
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
|
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||||
github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
|
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
|
||||||
github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
|
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
|
||||||
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
|
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
|
||||||
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||||
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
|
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
|
||||||
|
@ -161,8 +161,8 @@ github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
|
||||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||||
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||||
github.com/itchyny/gojq v0.12.14 h1:6k8vVtsrhQSYgSGg827AD+PVVaB1NLXEdX+dda2oZCc=
|
github.com/itchyny/gojq v0.12.15 h1:WC1Nxbx4Ifw5U2oQWACYz32JK8G9qxNtHzrvW4KEcqI=
|
||||||
github.com/itchyny/gojq v0.12.14/go.mod h1:y1G7oO7XkcR1LPZO59KyoCRy08T3j9vDYRV0GgYSS+s=
|
github.com/itchyny/gojq v0.12.15/go.mod h1:uWAHCbCIla1jiNxmeT5/B5mOjSdfkCq6p8vxWg+BM10=
|
||||||
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
|
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
|
||||||
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
|
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
@ -245,8 +245,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
||||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
||||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
|
@ -422,8 +422,8 @@ gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkD
|
||||||
gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
|
gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
|
||||||
gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
|
gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
|
||||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
gorm.io/gorm v1.25.8 h1:WAGEZ/aEcznN4D03laj8DKnehe1e9gYQAjW8xyPRdeo=
|
gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8=
|
||||||
gorm.io/gorm v1.25.8/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
modernc.org/cc/v4 v4.19.5 h1:QlsZyQ1zf78DGeqnQ9ILi9hXyMdoC5e1qoGNUyBjHQw=
|
modernc.org/cc/v4 v4.19.5 h1:QlsZyQ1zf78DGeqnQ9ILi9hXyMdoC5e1qoGNUyBjHQw=
|
||||||
|
@ -438,8 +438,8 @@ modernc.org/libc v1.49.0 h1:/kkNBuCXvlTbOGwrQdgR67eK1Y9+kR+fhdBd89C64VM=
|
||||||
modernc.org/libc v1.49.0/go.mod h1:DNz0lgQgT6FPIPm8rHtjFj0FL5/YOr/NYFXWYBcSxMw=
|
modernc.org/libc v1.49.0/go.mod h1:DNz0lgQgT6FPIPm8rHtjFj0FL5/YOr/NYFXWYBcSxMw=
|
||||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||||
|
|
|
@ -14,14 +14,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
botTwitchClient *twitch.Client
|
botTwitchClient func() *twitch.Client
|
||||||
|
|
||||||
announceChatcommandRegex = regexp.MustCompile(`^/announce(|blue|green|orange|purple) +(.+)$`)
|
announceChatcommandRegex = regexp.MustCompile(`^/announce(|blue|green|orange|purple) +(.+)$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
func Register(args plugins.RegistrationArguments) error {
|
func Register(args plugins.RegistrationArguments) error {
|
||||||
botTwitchClient = args.GetTwitchClient()
|
botTwitchClient = args.GetTwitchClient
|
||||||
|
|
||||||
args.RegisterMessageModFunc("/announce", handleChatCommand)
|
args.RegisterMessageModFunc("/announce", handleChatCommand)
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ func handleChatCommand(m *irc.Message) error {
|
||||||
return errors.New("announce message does not match required format")
|
return errors.New("announce message does not match required format")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := botTwitchClient.SendChatAnnouncement(context.Background(), channel, matches[1], matches[2]); err != nil {
|
if err := botTwitchClient().SendChatAnnouncement(context.Background(), channel, matches[1], matches[2]); err != nil {
|
||||||
return errors.Wrap(err, "sending announcement")
|
return errors.Wrap(err, "sending announcement")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,8 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
@ -18,7 +20,7 @@ import (
|
||||||
const actorName = "ban"
|
const actorName = "ban"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
botTwitchClient *twitch.Client
|
botTwitchClient func() *twitch.Client
|
||||||
formatMessage plugins.MsgFormatter
|
formatMessage plugins.MsgFormatter
|
||||||
|
|
||||||
banChatcommandRegex = regexp.MustCompile(`^/ban +([^\s]+) +(.+)$`)
|
banChatcommandRegex = regexp.MustCompile(`^/ban +([^\s]+) +(.+)$`)
|
||||||
|
@ -26,7 +28,7 @@ var (
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
func Register(args plugins.RegistrationArguments) (err error) {
|
func Register(args plugins.RegistrationArguments) (err error) {
|
||||||
botTwitchClient = args.GetTwitchClient()
|
botTwitchClient = args.GetTwitchClient
|
||||||
formatMessage = args.FormatMessage
|
formatMessage = args.FormatMessage
|
||||||
|
|
||||||
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
||||||
|
@ -87,7 +89,7 @@ func Register(args plugins.RegistrationArguments) (err error) {
|
||||||
|
|
||||||
type actor struct{}
|
type actor struct{}
|
||||||
|
|
||||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
ptrStringEmpty := func(v string) *string { return &v }("")
|
ptrStringEmpty := func(v string) *string { return &v }("")
|
||||||
|
|
||||||
reason, err := formatMessage(attrs.MustString("reason", ptrStringEmpty), m, r, eventData)
|
reason, err := formatMessage(attrs.MustString("reason", ptrStringEmpty), m, r, eventData)
|
||||||
|
@ -96,7 +98,7 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, errors.Wrap(
|
return false, errors.Wrap(
|
||||||
botTwitchClient.BanUser(
|
botTwitchClient().BanUser(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
plugins.DeriveChannel(m, eventData),
|
plugins.DeriveChannel(m, eventData),
|
||||||
plugins.DeriveUser(m, eventData),
|
plugins.DeriveUser(m, eventData),
|
||||||
|
@ -110,14 +112,13 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
func (actor) IsAsync() bool { return false }
|
func (actor) IsAsync() bool { return false }
|
||||||
func (actor) Name() string { return actorName }
|
func (actor) Name() string { return actorName }
|
||||||
|
|
||||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
reasonTemplate, err := attrs.String("reason")
|
if err = attrs.ValidateSchema(
|
||||||
if err != nil || reasonTemplate == "" {
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "reason", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
return errors.New("reason must be non-empty string")
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
}
|
helpers.SchemaValidateTemplateField(tplValidator, "reason"),
|
||||||
|
); err != nil {
|
||||||
if err = tplValidator(reasonTemplate); err != nil {
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
return errors.Wrap(err, "validating reason template")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -131,7 +132,7 @@ func handleAPIBan(w http.ResponseWriter, r *http.Request) {
|
||||||
reason = r.FormValue("reason")
|
reason = r.FormValue("reason")
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := botTwitchClient.BanUser(r.Context(), channel, user, 0, reason); err != nil {
|
if err := botTwitchClient().BanUser(r.Context(), channel, user, 0, reason); err != nil {
|
||||||
http.Error(w, errors.Wrap(err, "issuing ban").Error(), http.StatusInternalServerError)
|
http.Error(w, errors.Wrap(err, "issuing ban").Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -147,7 +148,7 @@ func handleChatCommand(m *irc.Message) error {
|
||||||
return errors.New("ban message does not match required format")
|
return errors.New("ban message does not match required format")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := botTwitchClient.BanUser(context.Background(), channel, matches[1], 0, matches[2]); err != nil {
|
if err := botTwitchClient().BanUser(context.Background(), channel, matches[1], 0, matches[2]); err != nil {
|
||||||
return errors.Wrap(err, "executing ban")
|
return errors.Wrap(err, "executing ban")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,8 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
@ -19,9 +21,6 @@ var (
|
||||||
formatMessage plugins.MsgFormatter
|
formatMessage plugins.MsgFormatter
|
||||||
hasPerm plugins.ChannelPermissionCheckFunc
|
hasPerm plugins.ChannelPermissionCheckFunc
|
||||||
tcGetter func(string) (*twitch.Client, error)
|
tcGetter func(string) (*twitch.Client, error)
|
||||||
|
|
||||||
ptrBoolFalse = func(v bool) *bool { return &v }(false)
|
|
||||||
ptrStringEmpty = func(s string) *string { return &s }("")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
|
@ -71,7 +70,7 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
|
|
||||||
type actor struct{}
|
type actor struct{}
|
||||||
|
|
||||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
channel := plugins.DeriveChannel(m, eventData)
|
channel := plugins.DeriveChannel(m, eventData)
|
||||||
if channel, err = formatMessage(attrs.MustString("channel", &channel), m, r, eventData); err != nil {
|
if channel, err = formatMessage(attrs.MustString("channel", &channel), m, r, eventData); err != nil {
|
||||||
return false, errors.Wrap(err, "parsing channel")
|
return false, errors.Wrap(err, "parsing channel")
|
||||||
|
@ -96,7 +95,7 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
return false, errors.Wrapf(err, "getting Twitch client for %q", creator)
|
return false, errors.Wrapf(err, "getting Twitch client for %q", creator)
|
||||||
}
|
}
|
||||||
|
|
||||||
clipInfo, err := tc.CreateClip(context.TODO(), channel, attrs.MustBool("add_delay", ptrBoolFalse))
|
clipInfo, err := tc.CreateClip(context.TODO(), channel, attrs.MustBool("add_delay", helpers.Ptr(false)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "creating clip")
|
return false, errors.Wrap(err, "creating clip")
|
||||||
}
|
}
|
||||||
|
@ -110,11 +109,15 @@ func (actor) IsAsync() bool { return false }
|
||||||
|
|
||||||
func (actor) Name() string { return actorName }
|
func (actor) Name() string { return actorName }
|
||||||
|
|
||||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
for _, field := range []string{"channel", "creator"} {
|
if err = attrs.ValidateSchema(
|
||||||
if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil {
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "channel", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
return errors.Wrapf(err, "validating %s template", field)
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "creator", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
}
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "add_delay", Type: fieldcollection.SchemaFieldTypeBool}),
|
||||||
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
|
helpers.SchemaValidateTemplateField(tplValidator, "channel", "creator"),
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/actors/linkdetector"
|
"github.com/Luzifer/twitch-bot/v3/internal/actors/linkdetector"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
|
@ -17,13 +18,13 @@ import (
|
||||||
const actorName = "clipdetector"
|
const actorName = "clipdetector"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
botTwitchClient *twitch.Client
|
botTwitchClient func() *twitch.Client
|
||||||
clipIDScanner = regexp.MustCompile(`(?:clips\.twitch\.tv|www\.twitch\.tv/[^/]*/clip)/([A-Za-z0-9_-]+)`)
|
clipIDScanner = regexp.MustCompile(`(?:clips\.twitch\.tv|www\.twitch\.tv/[^/]*/clip)/([A-Za-z0-9_-]+)`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
func Register(args plugins.RegistrationArguments) error {
|
func Register(args plugins.RegistrationArguments) error {
|
||||||
botTwitchClient = args.GetTwitchClient()
|
botTwitchClient = args.GetTwitchClient
|
||||||
|
|
||||||
args.RegisterActor(actorName, func() plugins.Actor { return &Actor{} })
|
args.RegisterActor(actorName, func() plugins.Actor { return &Actor{} })
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
type Actor struct{}
|
type Actor struct{}
|
||||||
|
|
||||||
// Execute implements the actor interface
|
// Execute implements the actor interface
|
||||||
func (Actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (Actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
if eventData.HasAll("clips") {
|
if eventData.HasAll("clips") {
|
||||||
// We already detected clips, lets not do it again
|
// We already detected clips, lets not do it again
|
||||||
return false, nil
|
return false, nil
|
||||||
|
@ -63,7 +64,7 @@ func (Actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
clipInfo, err := botTwitchClient.GetClipByID(context.Background(), clipIDMatch[1])
|
clipInfo, err := botTwitchClient().GetClipByID(context.Background(), clipIDMatch[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "getting clip info")
|
return false, errors.Wrap(err, "getting clip info")
|
||||||
}
|
}
|
||||||
|
@ -82,4 +83,6 @@ func (Actor) IsAsync() bool { return false }
|
||||||
func (Actor) Name() string { return actorName }
|
func (Actor) Name() string { return actorName }
|
||||||
|
|
||||||
// Validate implements the actor interface
|
// Validate implements the actor interface
|
||||||
func (Actor) Validate(plugins.TemplateValidatorFunc, *plugins.FieldCollection) error { return nil }
|
func (Actor) Validate(plugins.TemplateValidatorFunc, *fieldcollection.FieldCollection) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package commercial
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -10,6 +11,8 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
@ -61,7 +64,7 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
|
|
||||||
type actor struct{}
|
type actor struct{}
|
||||||
|
|
||||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
ptrStringEmpty := func(v string) *string { return &v }("")
|
ptrStringEmpty := func(v string) *string { return &v }("")
|
||||||
|
|
||||||
durationStr, err := formatMessage(attrs.MustString("duration", ptrStringEmpty), m, r, eventData)
|
durationStr, err := formatMessage(attrs.MustString("duration", ptrStringEmpty), m, r, eventData)
|
||||||
|
@ -75,14 +78,13 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
func (actor) IsAsync() bool { return false }
|
func (actor) IsAsync() bool { return false }
|
||||||
func (actor) Name() string { return actorName }
|
func (actor) Name() string { return actorName }
|
||||||
|
|
||||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
durationTemplate, err := attrs.String("duration")
|
if err = attrs.ValidateSchema(
|
||||||
if err != nil || durationTemplate == "" {
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "duration", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
return errors.New("duration must be non-empty string")
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
}
|
helpers.SchemaValidateTemplateField(tplValidator, "duration"),
|
||||||
|
); err != nil {
|
||||||
if err = tplValidator(durationTemplate); err != nil {
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
return errors.Wrap(err, "validating duration template")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -7,12 +7,15 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
@ -21,7 +24,7 @@ var (
|
||||||
db database.Connector
|
db database.Connector
|
||||||
formatMessage plugins.MsgFormatter
|
formatMessage plugins.MsgFormatter
|
||||||
|
|
||||||
ptrStringEmpty = func(s string) *string { return &s }("")
|
errNotAValue = fmt.Errorf("not a value")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
|
@ -135,7 +138,7 @@ func Register(args plugins.RegistrationArguments) (err error) {
|
||||||
return fmt.Errorf("registering API route: %w", err)
|
return fmt.Errorf("registering API route: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
args.RegisterTemplateFunction("channelCounter", func(_ *irc.Message, _ *plugins.Rule, fields *plugins.FieldCollection) interface{} {
|
args.RegisterTemplateFunction("channelCounter", func(_ *irc.Message, _ *plugins.Rule, fields *fieldcollection.FieldCollection) interface{} {
|
||||||
return func(name string) (string, error) {
|
return func(name string) (string, error) {
|
||||||
channel, err := fields.String("channel")
|
channel, err := fields.String("channel")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -165,11 +168,11 @@ func Register(args plugins.RegistrationArguments) (err error) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
args.RegisterTemplateFunction("counterTopList", plugins.GenericTemplateFunctionGetter(func(prefix string, n int) ([]counter, error) {
|
args.RegisterTemplateFunction("counterTopList", plugins.GenericTemplateFunctionGetter(func(prefix string, n int, orderBy string) ([]counter, error) {
|
||||||
return getCounterTopList(db, prefix, n)
|
return getCounterTopList(db, prefix, n, orderBy)
|
||||||
}), plugins.TemplateFuncDocumentation{
|
}), plugins.TemplateFuncDocumentation{
|
||||||
Description: "Returns the top n counters for the given prefix as objects with Name and Value fields",
|
Description: "Returns the top n counters for the given prefix as objects with Name and Value fields. Can be ordered by `name` / `value` / `first_seen` / `last_modified` ascending (`ASC`) or descending (`DESC`): i.e. `last_modified DESC` defaults to `value DESC`",
|
||||||
Syntax: `counterTopList <prefix> <n>`,
|
Syntax: `counterTopList <prefix> <n> [orderBy]`,
|
||||||
Example: &plugins.TemplateFuncDocumentationExample{
|
Example: &plugins.TemplateFuncDocumentationExample{
|
||||||
Template: `{{ range (counterTopList (list .channel "test" "" | join ":") 3) }}{{ .Name }}: {{ .Value }} - {{ end }}`,
|
Template: `{{ range (counterTopList (list .channel "test" "" | join ":") 3) }}{{ .Name }}: {{ .Value }} - {{ end }}`,
|
||||||
FakedOutput: "#example:test:foo: 5 - #example:test:bar: 4 - ",
|
FakedOutput: "#example:test:foo: 5 - #example:test:bar: 4 - ",
|
||||||
|
@ -193,7 +196,7 @@ func Register(args plugins.RegistrationArguments) (err error) {
|
||||||
mod = val[0]
|
mod = val[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := updateCounter(db, name, mod, false); err != nil {
|
if err := updateCounter(db, name, mod, false, time.Now()); err != nil {
|
||||||
return 0, errors.Wrap(err, "updating counter")
|
return 0, errors.Wrap(err, "updating counter")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,65 +215,114 @@ func Register(args plugins.RegistrationArguments) (err error) {
|
||||||
|
|
||||||
type actorCounter struct{}
|
type actorCounter struct{}
|
||||||
|
|
||||||
func (actorCounter) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (a actorCounter) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
counterName, err := formatMessage(attrs.MustString("counter", nil), m, r, eventData)
|
counterName, err := formatMessage(attrs.MustString("counter", nil), m, r, eventData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "preparing response")
|
return false, errors.Wrap(err, "preparing response")
|
||||||
}
|
}
|
||||||
|
|
||||||
if counterSet := attrs.MustString("counter_set", ptrStringEmpty); counterSet != "" {
|
// First lets look whether we shall set the counter (counter_set is
|
||||||
parseValue, err := formatMessage(counterSet, m, r, eventData)
|
// defined and the template evaluates into something which is not
|
||||||
if err != nil {
|
// an empty string)
|
||||||
return false, errors.Wrap(err, "execute counter value template")
|
counterSet, err := a.parseAttributeTemplateToNumber(m, r, eventData, attrs, "counter_set", 0)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
// Nice, we got a value to set
|
||||||
|
if err = updateCounter(db, counterName, counterSet, true, time.Now()); err != nil {
|
||||||
|
return false, fmt.Errorf("setting counter: %w", err)
|
||||||
}
|
}
|
||||||
|
return false, nil
|
||||||
|
|
||||||
counterValue, err := strconv.ParseInt(parseValue, 10, 64)
|
case errors.Is(err, errNotAValue):
|
||||||
if err != nil {
|
// Nope, not a set but that's fine, we just go to step-adjustment
|
||||||
return false, errors.Wrap(err, "parse counter value")
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, errors.Wrap(
|
default:
|
||||||
updateCounter(db, counterName, counterValue, true),
|
// B0rked
|
||||||
"set counter",
|
return false, fmt.Errorf("parsing counter-set: %w", err)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var counterStep int64 = 1
|
// Second check whether we do have a template in counter_step and it
|
||||||
if s := attrs.MustString("counter_step", ptrStringEmpty); s != "" {
|
// evaluates into a non-empty string and then adjust the counter
|
||||||
parseStep, err := formatMessage(s, m, r, eventData)
|
// accordingly
|
||||||
if err != nil {
|
counterStep, err := a.parseAttributeTemplateToNumber(m, r, eventData, attrs, "counter_step", 1)
|
||||||
return false, errors.Wrap(err, "execute counter step template")
|
switch {
|
||||||
|
case err == nil, errors.Is(err, errNotAValue):
|
||||||
|
// Either got a value or there was none, therefore the default was
|
||||||
|
// returned which is 1 and we can apply this
|
||||||
|
if err = updateCounter(db, counterName, counterStep, false, time.Now()); err != nil {
|
||||||
|
return false, fmt.Errorf("updating counter: %w", err)
|
||||||
}
|
}
|
||||||
|
return false, nil
|
||||||
|
|
||||||
counterStep, err = strconv.ParseInt(parseStep, 10, 64)
|
default:
|
||||||
if err != nil {
|
// B0rked
|
||||||
return false, errors.Wrap(err, "parse counter step")
|
return false, fmt.Errorf("parsing counter-step: %w", err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, errors.Wrap(
|
|
||||||
updateCounter(db, counterName, counterStep, false),
|
|
||||||
"update counter",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (actorCounter) IsAsync() bool { return false }
|
func (actorCounter) IsAsync() bool { return false }
|
||||||
func (actorCounter) Name() string { return "counter" }
|
func (actorCounter) Name() string { return "counter" }
|
||||||
|
|
||||||
func (actorCounter) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actorCounter) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
if cn, err := attrs.String("counter"); err != nil || cn == "" {
|
if err = attrs.ValidateSchema(
|
||||||
return errors.New("counter name must be non-empty string")
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "counter", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
}
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "counter_step", Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "counter_set", Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
for _, field := range []string{"counter", "counter_step", "counter_set"} {
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil {
|
helpers.SchemaValidateTemplateField(tplValidator, "counter", "counter_step", "counter_set"),
|
||||||
return errors.Wrapf(err, "validating %s template", field)
|
); err != nil {
|
||||||
}
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (actorCounter) parseAttributeTemplateToNumber(
|
||||||
|
m *irc.Message,
|
||||||
|
r *plugins.Rule,
|
||||||
|
eventData *fieldcollection.FieldCollection,
|
||||||
|
attrs *fieldcollection.FieldCollection,
|
||||||
|
field string,
|
||||||
|
defaultValue int64,
|
||||||
|
) (v int64, err error) {
|
||||||
|
// Get the string
|
||||||
|
sv, err := attrs.String(field)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
// We got a string and continue below
|
||||||
|
|
||||||
|
case errors.Is(err, fieldcollection.ErrValueNotSet):
|
||||||
|
// That's fine, the string is not available, we report that and
|
||||||
|
// return the default value
|
||||||
|
return defaultValue, errNotAValue
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Not sure what brought us here but we should report that
|
||||||
|
return defaultValue, fmt.Errorf("getting string value: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we need to evaluate the template
|
||||||
|
sv, err = formatMessage(sv, m, r, eventData)
|
||||||
|
if err != nil {
|
||||||
|
return defaultValue, fmt.Errorf("executing template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The template evaluated into an empty string, we don't try to
|
||||||
|
// parse that and report it as a missing value with default
|
||||||
|
if sv == "" {
|
||||||
|
return defaultValue, errNotAValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// The template was not empty, we need to parse the resulting int
|
||||||
|
// and return it
|
||||||
|
v, err = strconv.ParseInt(sv, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return defaultValue, fmt.Errorf("parsing to int: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
func routeActorCounterGetValue(w http.ResponseWriter, r *http.Request) {
|
func routeActorCounterGetValue(w http.ResponseWriter, r *http.Request) {
|
||||||
template := r.FormValue("template")
|
template := r.FormValue("template")
|
||||||
if template == "" {
|
if template == "" {
|
||||||
|
@ -299,7 +351,7 @@ func routeActorCounterSetValue(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = updateCounter(db, mux.Vars(r)["name"], value, absolute); err != nil {
|
if err = updateCounter(db, mux.Vars(r)["name"], value, absolute, time.Now()); err != nil {
|
||||||
http.Error(w, errors.Wrap(err, "updating value").Error(), http.StatusInternalServerError)
|
http.Error(w, errors.Wrap(err, "updating value").Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,25 @@
|
||||||
package counter
|
package counter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/str"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
counter struct {
|
counter struct {
|
||||||
Name string `gorm:"primaryKey"`
|
Name string `gorm:"primaryKey"`
|
||||||
Value int64
|
Value int64
|
||||||
|
FirstSeen time.Time
|
||||||
|
LastModified time.Time
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,7 +39,7 @@ func getCounterValue(db database.Connector, counterName string) (int64, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
//revive:disable-next-line:flag-parameter
|
//revive:disable-next-line:flag-parameter
|
||||||
func updateCounter(db database.Connector, counterName string, value int64, absolute bool) error {
|
func updateCounter(db database.Connector, counterName string, value int64, absolute bool, atTime time.Time) error {
|
||||||
if !absolute {
|
if !absolute {
|
||||||
cv, err := getCounterValue(db, counterName)
|
cv, err := getCounterValue(db, counterName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -46,8 +53,8 @@ func updateCounter(db database.Connector, counterName string, value int64, absol
|
||||||
helpers.RetryTransaction(db.DB(), func(tx *gorm.DB) error {
|
helpers.RetryTransaction(db.DB(), func(tx *gorm.DB) error {
|
||||||
return tx.Clauses(clause.OnConflict{
|
return tx.Clauses(clause.OnConflict{
|
||||||
Columns: []clause.Column{{Name: "name"}},
|
Columns: []clause.Column{{Name: "name"}},
|
||||||
DoUpdates: clause.AssignmentColumns([]string{"value"}),
|
DoUpdates: clause.AssignmentColumns([]string{"last_modified", "value"}),
|
||||||
}).Create(counter{Name: counterName, Value: value}).Error
|
}).Create(counter{Name: counterName, Value: value, FirstSeen: atTime.UTC(), LastModified: atTime.UTC()}).Error
|
||||||
}),
|
}),
|
||||||
"storing counter value",
|
"storing counter value",
|
||||||
)
|
)
|
||||||
|
@ -75,12 +82,39 @@ func getCounterRank(db database.Connector, prefix, name string) (rank, count int
|
||||||
return rank, count, nil
|
return rank, count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCounterTopList(db database.Connector, prefix string, n int) ([]counter, error) {
|
func getCounterTopList(db database.Connector, prefix string, n int, orderBy ...string) ([]counter, error) {
|
||||||
var cc []counter
|
var (
|
||||||
|
cc []counter
|
||||||
|
|
||||||
|
order string
|
||||||
|
validOrderCols = []string{"first_seen", "last_modified", "name", "value"}
|
||||||
|
validOrderDirs = []string{"ASC", "DESC"}
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(orderBy) == 0 || orderBy[0] == "" {
|
||||||
|
order = "value DESC"
|
||||||
|
} else {
|
||||||
|
order = orderBy[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
col, dir, _ := strings.Cut(order, " ")
|
||||||
|
if col == "" {
|
||||||
|
col = "value"
|
||||||
|
}
|
||||||
|
if dir == "" {
|
||||||
|
dir = "ASC"
|
||||||
|
}
|
||||||
|
|
||||||
|
if !str.StringInSlice(col, validOrderCols) {
|
||||||
|
return nil, fmt.Errorf("invalid orderBy column")
|
||||||
|
}
|
||||||
|
if !str.StringInSlice(dir, validOrderDirs) {
|
||||||
|
return nil, fmt.Errorf("invalid orderBy direction")
|
||||||
|
}
|
||||||
|
|
||||||
err := helpers.Retry(func() error {
|
err := helpers.Retry(func() error {
|
||||||
return db.DB().
|
return db.DB().
|
||||||
Order("value DESC").
|
Order(strings.Join([]string{col, dir}, " ")).
|
||||||
Limit(n).
|
Limit(n).
|
||||||
Find(&cc, "name LIKE ?", prefix+"%").
|
Find(&cc, "name LIKE ?", prefix+"%").
|
||||||
Error
|
Error
|
||||||
|
|
|
@ -3,6 +3,7 @@ package counter
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -20,12 +21,19 @@ func TestCounterStoreLoop(t *testing.T) {
|
||||||
assert.NoError(t, err, "reading non-existent counter")
|
assert.NoError(t, err, "reading non-existent counter")
|
||||||
assert.Equal(t, int64(0), v, "expecting 0 counter value on non-existent counter")
|
assert.Equal(t, int64(0), v, "expecting 0 counter value on non-existent counter")
|
||||||
|
|
||||||
err = updateCounter(dbc, counterName, 5, true)
|
err = updateCounter(dbc, counterName, 5, true, time.Now())
|
||||||
assert.NoError(t, err, "inserting counter")
|
assert.NoError(t, err, "inserting counter")
|
||||||
|
|
||||||
err = updateCounter(dbc, counterName, 1, false)
|
var rawCounter counter
|
||||||
|
assert.NoError(t, dbc.DB().First(&rawCounter, "name = ?", counterName).Error)
|
||||||
|
assert.Equal(t, rawCounter.FirstSeen, rawCounter.LastModified)
|
||||||
|
|
||||||
|
err = updateCounter(dbc, counterName, 1, false, time.Now())
|
||||||
assert.NoError(t, err, "updating counter")
|
assert.NoError(t, err, "updating counter")
|
||||||
|
|
||||||
|
assert.NoError(t, dbc.DB().First(&rawCounter, "name = ?", counterName).Error)
|
||||||
|
assert.NotEqual(t, rawCounter.FirstSeen, rawCounter.LastModified)
|
||||||
|
|
||||||
v, err = getCounterValue(dbc, counterName)
|
v, err = getCounterValue(dbc, counterName)
|
||||||
assert.NoError(t, err, "reading existent counter")
|
assert.NoError(t, err, "reading existent counter")
|
||||||
assert.Equal(t, int64(6), v, "expecting counter value on existing counter")
|
assert.Equal(t, int64(6), v, "expecting counter value on existing counter")
|
||||||
|
@ -35,11 +43,13 @@ func TestCounterTopListAndRank(t *testing.T) {
|
||||||
dbc := database.GetTestDatabase(t)
|
dbc := database.GetTestDatabase(t)
|
||||||
require.NoError(t, dbc.DB().AutoMigrate(&counter{}))
|
require.NoError(t, dbc.DB().AutoMigrate(&counter{}))
|
||||||
|
|
||||||
|
testTime := time.Now().UTC()
|
||||||
|
|
||||||
counterTemplate := `#example:test:%v`
|
counterTemplate := `#example:test:%v`
|
||||||
for i := 0; i < 6; i++ {
|
for i := 0; i < 6; i++ {
|
||||||
require.NoError(
|
require.NoError(
|
||||||
t,
|
t,
|
||||||
updateCounter(dbc, fmt.Sprintf(counterTemplate, i), int64(i), true),
|
updateCounter(dbc, fmt.Sprintf(counterTemplate, i), int64(i), true, testTime),
|
||||||
"inserting counter %d", i,
|
"inserting counter %d", i,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -49,11 +59,40 @@ func TestCounterTopListAndRank(t *testing.T) {
|
||||||
assert.Len(t, cc, 3)
|
assert.Len(t, cc, 3)
|
||||||
|
|
||||||
assert.Equal(t, []counter{
|
assert.Equal(t, []counter{
|
||||||
{Name: "#example:test:5", Value: 5},
|
{Name: "#example:test:5", Value: 5, FirstSeen: testTime, LastModified: testTime},
|
||||||
{Name: "#example:test:4", Value: 4},
|
{Name: "#example:test:4", Value: 4, FirstSeen: testTime, LastModified: testTime},
|
||||||
{Name: "#example:test:3", Value: 3},
|
{Name: "#example:test:3", Value: 3, FirstSeen: testTime, LastModified: testTime},
|
||||||
}, cc)
|
}, cc)
|
||||||
|
|
||||||
|
cc, err = getCounterTopList(dbc, fmt.Sprintf(counterTemplate, ""), 3, "name DESC")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, cc, 3)
|
||||||
|
|
||||||
|
assert.Equal(t, []counter{
|
||||||
|
{Name: "#example:test:5", Value: 5, FirstSeen: testTime, LastModified: testTime},
|
||||||
|
{Name: "#example:test:4", Value: 4, FirstSeen: testTime, LastModified: testTime},
|
||||||
|
{Name: "#example:test:3", Value: 3, FirstSeen: testTime, LastModified: testTime},
|
||||||
|
}, cc)
|
||||||
|
|
||||||
|
cc, err = getCounterTopList(dbc, fmt.Sprintf(counterTemplate, ""), 3, "name")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, cc, 3)
|
||||||
|
|
||||||
|
assert.Equal(t, []counter{
|
||||||
|
{Name: "#example:test:0", Value: 0, FirstSeen: testTime, LastModified: testTime},
|
||||||
|
{Name: "#example:test:1", Value: 1, FirstSeen: testTime, LastModified: testTime},
|
||||||
|
{Name: "#example:test:2", Value: 2, FirstSeen: testTime, LastModified: testTime},
|
||||||
|
}, cc)
|
||||||
|
|
||||||
|
_, err = getCounterTopList(dbc, fmt.Sprintf(counterTemplate, ""), 3, "foobar")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
_, err = getCounterTopList(dbc, fmt.Sprintf(counterTemplate, ""), 3, "name foo")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
_, err = getCounterTopList(dbc, fmt.Sprintf(counterTemplate, ""), 3, "name ASC; DROP TABLE counters;")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
rank, count, err := getCounterRank(dbc,
|
rank, count, err := getCounterRank(dbc,
|
||||||
fmt.Sprintf(counterTemplate, ""),
|
fmt.Sprintf(counterTemplate, ""),
|
||||||
fmt.Sprintf(counterTemplate, 4))
|
fmt.Sprintf(counterTemplate, 4))
|
||||||
|
|
|
@ -2,11 +2,14 @@
|
||||||
package delay
|
package delay
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -48,11 +51,10 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
|
|
||||||
type actor struct{}
|
type actor struct{}
|
||||||
|
|
||||||
func (actor) Execute(_ *irc.Client, _ *irc.Message, _ *plugins.Rule, _ *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actor) Execute(_ *irc.Client, _ *irc.Message, _ *plugins.Rule, _ *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
var (
|
var (
|
||||||
ptrZeroDuration = func(v time.Duration) *time.Duration { return &v }(0)
|
delay = attrs.MustDuration("delay", helpers.Ptr(time.Duration(0)))
|
||||||
delay = attrs.MustDuration("delay", ptrZeroDuration)
|
jitter = attrs.MustDuration("jitter", helpers.Ptr(time.Duration(0)))
|
||||||
jitter = attrs.MustDuration("jitter", ptrZeroDuration)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if delay == 0 && jitter == 0 {
|
if delay == 0 && jitter == 0 {
|
||||||
|
@ -71,6 +73,13 @@ func (actor) Execute(_ *irc.Client, _ *irc.Message, _ *plugins.Rule, _ *plugins.
|
||||||
func (actor) IsAsync() bool { return false }
|
func (actor) IsAsync() bool { return false }
|
||||||
func (actor) Name() string { return actorName }
|
func (actor) Name() string { return actorName }
|
||||||
|
|
||||||
func (actor) Validate(plugins.TemplateValidatorFunc, *plugins.FieldCollection) (err error) {
|
func (actor) Validate(_ plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
|
if err = attrs.ValidateSchema(
|
||||||
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "delay", Type: fieldcollection.SchemaFieldTypeDuration}),
|
||||||
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "jitter", Type: fieldcollection.SchemaFieldTypeDuration}),
|
||||||
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,17 +7,18 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
const actorName = "delete"
|
const actorName = "delete"
|
||||||
|
|
||||||
var botTwitchClient *twitch.Client
|
var botTwitchClient func() *twitch.Client
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
func Register(args plugins.RegistrationArguments) error {
|
func Register(args plugins.RegistrationArguments) error {
|
||||||
botTwitchClient = args.GetTwitchClient()
|
botTwitchClient = args.GetTwitchClient
|
||||||
|
|
||||||
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
||||||
|
|
||||||
|
@ -32,14 +33,14 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
|
|
||||||
type actor struct{}
|
type actor struct{}
|
||||||
|
|
||||||
func (actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData *plugins.FieldCollection, _ *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData *fieldcollection.FieldCollection, _ *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
msgID, ok := m.Tags["id"]
|
msgID, ok := m.Tags["id"]
|
||||||
if !ok || msgID == "" {
|
if !ok || msgID == "" {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, errors.Wrap(
|
return false, errors.Wrap(
|
||||||
botTwitchClient.DeleteMessage(
|
botTwitchClient().DeleteMessage(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
plugins.DeriveChannel(m, eventData),
|
plugins.DeriveChannel(m, eventData),
|
||||||
msgID,
|
msgID,
|
||||||
|
@ -51,6 +52,6 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData *
|
||||||
func (actor) IsAsync() bool { return false }
|
func (actor) IsAsync() bool { return false }
|
||||||
func (actor) Name() string { return actorName }
|
func (actor) Name() string { return actorName }
|
||||||
|
|
||||||
func (actor) Validate(plugins.TemplateValidatorFunc, *plugins.FieldCollection) (err error) {
|
func (actor) Validate(plugins.TemplateValidatorFunc, *fieldcollection.FieldCollection) (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,13 @@ package eventmod
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -44,7 +47,7 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
|
|
||||||
type actor struct{}
|
type actor struct{}
|
||||||
|
|
||||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
ptrStringEmpty := func(v string) *string { return &v }("")
|
ptrStringEmpty := func(v string) *string { return &v }("")
|
||||||
|
|
||||||
fd, err := formatMessage(attrs.MustString("fields", ptrStringEmpty), m, r, eventData)
|
fd, err := formatMessage(attrs.MustString("fields", ptrStringEmpty), m, r, eventData)
|
||||||
|
@ -69,14 +72,13 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
func (actor) IsAsync() bool { return false }
|
func (actor) IsAsync() bool { return false }
|
||||||
func (actor) Name() string { return actorName }
|
func (actor) Name() string { return actorName }
|
||||||
|
|
||||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
fieldsTemplate, err := attrs.String("fields")
|
if err = attrs.ValidateSchema(
|
||||||
if err != nil || fieldsTemplate == "" {
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "fields", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
return errors.New("fields must be non-empty string")
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
}
|
helpers.SchemaValidateTemplateField(tplValidator, "fields"),
|
||||||
|
); err != nil {
|
||||||
if err = tplValidator(fieldsTemplate); err != nil {
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
return errors.Wrap(err, "validating fields template")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -5,6 +5,7 @@ package filesay
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
@ -13,6 +14,8 @@ import (
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -57,7 +60,7 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
|
|
||||||
type actor struct{}
|
type actor struct{}
|
||||||
|
|
||||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
ptrStringEmpty := func(v string) *string { return &v }("")
|
ptrStringEmpty := func(v string) *string { return &v }("")
|
||||||
|
|
||||||
source, err := formatMessage(attrs.MustString("source", ptrStringEmpty), m, r, eventData)
|
source, err := formatMessage(attrs.MustString("source", ptrStringEmpty), m, r, eventData)
|
||||||
|
@ -114,14 +117,13 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
func (actor) IsAsync() bool { return true }
|
func (actor) IsAsync() bool { return true }
|
||||||
func (actor) Name() string { return actorName }
|
func (actor) Name() string { return actorName }
|
||||||
|
|
||||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) error {
|
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
sourceTpl, err := attrs.String("source")
|
if err = attrs.ValidateSchema(
|
||||||
if err != nil || sourceTpl == "" {
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "source", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
return errors.New("source is expected to be non-empty string")
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
}
|
helpers.SchemaValidateTemplateField(tplValidator, "source"),
|
||||||
|
); err != nil {
|
||||||
if err = tplValidator(sourceTpl); err != nil {
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
return errors.Wrap(err, "validating source template")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -3,16 +3,18 @@
|
||||||
package linkdetector
|
package linkdetector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/linkcheck"
|
"github.com/Luzifer/twitch-bot/v3/internal/linkcheck"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
const actorName = "linkdetector"
|
const actorName = "linkdetector"
|
||||||
|
|
||||||
var ptrFalse = func(v bool) *bool { return &v }(false)
|
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
func Register(args plugins.RegistrationArguments) error {
|
func Register(args plugins.RegistrationArguments) error {
|
||||||
args.RegisterActor(actorName, func() plugins.Actor { return &Actor{} })
|
args.RegisterActor(actorName, func() plugins.Actor { return &Actor{} })
|
||||||
|
@ -42,13 +44,13 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
type Actor struct{}
|
type Actor struct{}
|
||||||
|
|
||||||
// Execute implements the actor interface
|
// Execute implements the actor interface
|
||||||
func (Actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (Actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
if eventData.HasAll("links") {
|
if eventData.HasAll("links") {
|
||||||
// We already detected links, lets not do it again
|
// We already detected links, lets not do it again
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if attrs.MustBool("heuristic", ptrFalse) {
|
if attrs.MustBool("heuristic", helpers.Ptr(false)) {
|
||||||
eventData.Set("links", linkcheck.New().HeuristicScanForLinks(m.Trailing()))
|
eventData.Set("links", linkcheck.New().HeuristicScanForLinks(m.Trailing()))
|
||||||
} else {
|
} else {
|
||||||
eventData.Set("links", linkcheck.New().ScanForLinks(m.Trailing()))
|
eventData.Set("links", linkcheck.New().ScanForLinks(m.Trailing()))
|
||||||
|
@ -64,4 +66,13 @@ func (Actor) IsAsync() bool { return false }
|
||||||
func (Actor) Name() string { return actorName }
|
func (Actor) Name() string { return actorName }
|
||||||
|
|
||||||
// Validate implements the actor interface
|
// Validate implements the actor interface
|
||||||
func (Actor) Validate(plugins.TemplateValidatorFunc, *plugins.FieldCollection) error { return nil }
|
func (Actor) Validate(_ plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
|
if err = attrs.ValidateSchema(
|
||||||
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "heuristic", Type: fieldcollection.SchemaFieldTypeBool}),
|
||||||
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ package linkprotect
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -11,7 +12,9 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/actors/clipdetector"
|
"github.com/Luzifer/twitch-bot/v3/internal/actors/clipdetector"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
@ -19,15 +22,13 @@ import (
|
||||||
const actorName = "linkprotect"
|
const actorName = "linkprotect"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
botTwitchClient *twitch.Client
|
botTwitchClient func() *twitch.Client
|
||||||
clipLink = regexp.MustCompile(`.*(?:clips\.twitch\.tv|www\.twitch\.tv/[^/]*/clip)/.*`)
|
clipLink = regexp.MustCompile(`.*(?:clips\.twitch\.tv|www\.twitch\.tv/[^/]*/clip)/.*`)
|
||||||
ptrBoolFalse = func(v bool) *bool { return &v }(false)
|
|
||||||
ptrStringEmpty = func(v string) *string { return &v }("")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
func Register(args plugins.RegistrationArguments) error {
|
func Register(args plugins.RegistrationArguments) error {
|
||||||
botTwitchClient = args.GetTwitchClient()
|
botTwitchClient = args.GetTwitchClient
|
||||||
|
|
||||||
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
||||||
|
|
||||||
|
@ -127,7 +128,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:gocyclo // Minimum over the limit, makes no sense to split
|
//nolint:gocyclo // Minimum over the limit, makes no sense to split
|
||||||
func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
// In case the clip detector did not run before, lets run it now
|
// In case the clip detector did not run before, lets run it now
|
||||||
if preventCooldown, err = (clipdetector.Actor{}).Execute(c, m, r, eventData, attrs); err != nil {
|
if preventCooldown, err = (clipdetector.Actor{}).Execute(c, m, r, eventData, attrs); err != nil {
|
||||||
return preventCooldown, errors.Wrap(err, "detecting links / clips")
|
return preventCooldown, errors.Wrap(err, "detecting links / clips")
|
||||||
|
@ -141,13 +142,13 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
||||||
if len(links) == 0 {
|
if len(links) == 0 {
|
||||||
// If there are no links there is nothing to protect and there
|
// If there are no links there is nothing to protect and there
|
||||||
// are also no clips as they are parsed from the links
|
// are also no clips as they are parsed from the links
|
||||||
if attrs.MustBool("stop_on_no_action", ptrBoolFalse) {
|
if attrs.MustBool("stop_on_no_action", helpers.Ptr(false)) {
|
||||||
return false, plugins.ErrStopRuleExecution
|
return false, plugins.ErrStopRuleExecution
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
clipsInterface, err := eventData.Any("clips")
|
clipsInterface, err := eventData.Get("clips")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return preventCooldown, errors.Wrap(err, "getting clips from event")
|
return preventCooldown, errors.Wrap(err, "getting clips from event")
|
||||||
}
|
}
|
||||||
|
@ -157,21 +158,21 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.check(links, clips, attrs) == verdictAllFine {
|
if a.check(links, clips, attrs) == verdictAllFine {
|
||||||
if attrs.MustBool("stop_on_no_action", ptrBoolFalse) {
|
if attrs.MustBool("stop_on_no_action", helpers.Ptr(false)) {
|
||||||
return false, plugins.ErrStopRuleExecution
|
return false, plugins.ErrStopRuleExecution
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// That message misbehaved so we need to punish them
|
// That message misbehaved so we need to punish them
|
||||||
switch lt := attrs.MustString("action", ptrStringEmpty); lt {
|
switch lt := attrs.MustString("action", helpers.Ptr("")); lt {
|
||||||
case "ban":
|
case "ban":
|
||||||
if err = botTwitchClient.BanUser(
|
if err = botTwitchClient().BanUser(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
plugins.DeriveChannel(m, eventData),
|
plugins.DeriveChannel(m, eventData),
|
||||||
strings.TrimLeft(plugins.DeriveUser(m, eventData), "@"),
|
strings.TrimLeft(plugins.DeriveUser(m, eventData), "@"),
|
||||||
0,
|
0,
|
||||||
attrs.MustString("reason", ptrStringEmpty),
|
attrs.MustString("reason", helpers.Ptr("")),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return false, errors.Wrap(err, "executing user ban")
|
return false, errors.Wrap(err, "executing user ban")
|
||||||
}
|
}
|
||||||
|
@ -182,7 +183,7 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
||||||
return false, errors.New("found no mesage id")
|
return false, errors.New("found no mesage id")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = botTwitchClient.DeleteMessage(
|
if err = botTwitchClient().DeleteMessage(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
plugins.DeriveChannel(m, eventData),
|
plugins.DeriveChannel(m, eventData),
|
||||||
msgID,
|
msgID,
|
||||||
|
@ -196,18 +197,18 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
||||||
return false, errors.Wrap(err, "parsing punishment level")
|
return false, errors.Wrap(err, "parsing punishment level")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = botTwitchClient.BanUser(
|
if err = botTwitchClient().BanUser(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
plugins.DeriveChannel(m, eventData),
|
plugins.DeriveChannel(m, eventData),
|
||||||
strings.TrimLeft(plugins.DeriveUser(m, eventData), "@"),
|
strings.TrimLeft(plugins.DeriveUser(m, eventData), "@"),
|
||||||
to,
|
to,
|
||||||
attrs.MustString("reason", ptrStringEmpty),
|
attrs.MustString("reason", helpers.Ptr("")),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return false, errors.Wrap(err, "executing user ban")
|
return false, errors.Wrap(err, "executing user ban")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if attrs.MustBool("stop_on_action", ptrBoolFalse) {
|
if attrs.MustBool("stop_on_action", helpers.Ptr(false)) {
|
||||||
return false, plugins.ErrStopRuleExecution
|
return false, plugins.ErrStopRuleExecution
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,41 +219,49 @@ func (actor) IsAsync() bool { return false }
|
||||||
|
|
||||||
func (actor) Name() string { return actorName }
|
func (actor) Name() string { return actorName }
|
||||||
|
|
||||||
func (actor) Validate(_ plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) error {
|
func (actor) Validate(_ plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
if v, err := attrs.String("action"); err != nil || v == "" {
|
if err = attrs.ValidateSchema(
|
||||||
return errors.New("action must be non-empty string")
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "action", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
}
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "reason", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "allowed_links", Type: fieldcollection.SchemaFieldTypeStringSlice}),
|
||||||
if v, err := attrs.String("reason"); err != nil || v == "" {
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "disallowed_links", Type: fieldcollection.SchemaFieldTypeStringSlice}),
|
||||||
return errors.New("reason must be non-empty string")
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "allowed_clip_channels", Type: fieldcollection.SchemaFieldTypeStringSlice}),
|
||||||
}
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "disallowed_clip_channels", Type: fieldcollection.SchemaFieldTypeStringSlice}),
|
||||||
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "stop_on_action", Type: fieldcollection.SchemaFieldTypeBool}),
|
||||||
if len(attrs.MustStringSlice("allowed_links"))+
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "stop_on_no_action", Type: fieldcollection.SchemaFieldTypeBool}),
|
||||||
len(attrs.MustStringSlice("disallowed_links"))+
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
len(attrs.MustStringSlice("allowed_clip_channels"))+
|
func(attrs, _ *fieldcollection.FieldCollection) error {
|
||||||
len(attrs.MustStringSlice("disallowed_clip_channels")) == 0 {
|
if len(attrs.MustStringSlice("allowed_links", helpers.Ptr([]string{})))+
|
||||||
return errors.New("no conditions are provided")
|
len(attrs.MustStringSlice("disallowed_links", helpers.Ptr([]string{})))+
|
||||||
|
len(attrs.MustStringSlice("allowed_clip_channels", helpers.Ptr([]string{})))+
|
||||||
|
len(attrs.MustStringSlice("disallowed_clip_channels", helpers.Ptr([]string{}))) == 0 {
|
||||||
|
return errors.New("no conditions are provided")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a actor) check(links []string, clips []twitch.ClipInfo, attrs *plugins.FieldCollection) (v verdict) {
|
func (a actor) check(links []string, clips []twitch.ClipInfo, attrs *fieldcollection.FieldCollection) (v verdict) {
|
||||||
hasClipDefinition := len(attrs.MustStringSlice("allowed_clip_channels"))+len(attrs.MustStringSlice("disallowed_clip_channels")) > 0
|
hasClipDefinition := len(attrs.MustStringSlice("allowed_clip_channels", helpers.Ptr([]string{})))+len(attrs.MustStringSlice("disallowed_clip_channels", helpers.Ptr([]string{}))) > 0
|
||||||
|
|
||||||
if v = a.checkLinkDenied(attrs.MustStringSlice("disallowed_links"), links, hasClipDefinition); v == verdictMisbehave {
|
if v = a.checkLinkDenied(attrs.MustStringSlice("disallowed_links", helpers.Ptr([]string{})), links, hasClipDefinition); v == verdictMisbehave {
|
||||||
return verdictMisbehave
|
return verdictMisbehave
|
||||||
}
|
}
|
||||||
|
|
||||||
if v = a.checkAllLinksAllowed(attrs.MustStringSlice("allowed_links"), links, hasClipDefinition); v == verdictMisbehave {
|
if v = a.checkAllLinksAllowed(attrs.MustStringSlice("allowed_links", helpers.Ptr([]string{})), links, hasClipDefinition); v == verdictMisbehave {
|
||||||
return verdictMisbehave
|
return verdictMisbehave
|
||||||
}
|
}
|
||||||
|
|
||||||
if v = a.checkClipChannelDenied(attrs.MustStringSlice("disallowed_clip_channels"), clips); v == verdictMisbehave {
|
if v = a.checkClipChannelDenied(attrs.MustStringSlice("disallowed_clip_channels", helpers.Ptr([]string{})), clips); v == verdictMisbehave {
|
||||||
return verdictMisbehave
|
return verdictMisbehave
|
||||||
}
|
}
|
||||||
|
|
||||||
if v = a.checkAllClipChannelsAllowed(attrs.MustStringSlice("allowed_clip_channels"), clips); v == verdictMisbehave {
|
if v = a.checkAllClipChannelsAllowed(attrs.MustStringSlice("allowed_clip_channels", helpers.Ptr([]string{})), clips); v == verdictMisbehave {
|
||||||
return verdictMisbehave
|
return verdictMisbehave
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,18 +2,19 @@
|
||||||
package log
|
package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var formatMessage plugins.MsgFormatter
|
||||||
formatMessage plugins.MsgFormatter
|
|
||||||
ptrStringEmpty = func(v string) *string { return &v }("")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
func Register(args plugins.RegistrationArguments) error {
|
func Register(args plugins.RegistrationArguments) error {
|
||||||
|
@ -44,8 +45,8 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
|
|
||||||
type actor struct{}
|
type actor struct{}
|
||||||
|
|
||||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
message, err := formatMessage(attrs.MustString("message", ptrStringEmpty), m, r, eventData)
|
message, err := formatMessage(attrs.MustString("message", helpers.Ptr("")), m, r, eventData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "executing message template")
|
return false, errors.Wrap(err, "executing message template")
|
||||||
}
|
}
|
||||||
|
@ -61,13 +62,13 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
func (actor) IsAsync() bool { return true }
|
func (actor) IsAsync() bool { return true }
|
||||||
func (actor) Name() string { return "log" }
|
func (actor) Name() string { return "log" }
|
||||||
|
|
||||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
if v, err := attrs.String("message"); err != nil || v == "" {
|
if err = attrs.ValidateSchema(
|
||||||
return errors.New("message must be non-empty string")
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "message", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
}
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
|
helpers.SchemaValidateTemplateField(tplValidator, "message"),
|
||||||
if err = tplValidator(attrs.MustString("message", ptrStringEmpty)); err != nil {
|
); err != nil {
|
||||||
return errors.Wrap(err, "validating message template")
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -20,12 +20,7 @@ const (
|
||||||
postTimeout = 5 * time.Second
|
postTimeout = 5 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var formatMessage plugins.MsgFormatter
|
||||||
formatMessage plugins.MsgFormatter
|
|
||||||
|
|
||||||
ptrBoolFalse = func(v bool) *bool { return &v }(false)
|
|
||||||
ptrStringEmpty = func(s string) *string { return &s }("")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
func Register(args plugins.RegistrationArguments) error {
|
func Register(args plugins.RegistrationArguments) error {
|
||||||
|
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -50,18 +52,18 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d discordActor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (d discordActor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
var payload discordPayload
|
var payload discordPayload
|
||||||
|
|
||||||
if payload.Content, err = formatMessage(attrs.MustString("content", ptrStringEmpty), m, r, eventData); err != nil {
|
if payload.Content, err = formatMessage(attrs.MustString("content", helpers.Ptr("")), m, r, eventData); err != nil {
|
||||||
return false, errors.Wrap(err, "parsing content")
|
return false, errors.Wrap(err, "parsing content")
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.Username, err = formatMessage(attrs.MustString("username", ptrStringEmpty), m, r, eventData); err != nil {
|
if payload.Username, err = formatMessage(attrs.MustString("username", helpers.Ptr("")), m, r, eventData); err != nil {
|
||||||
return false, errors.Wrap(err, "parsing username")
|
return false, errors.Wrap(err, "parsing username")
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.AvatarURL, err = formatMessage(attrs.MustString("avatar_url", ptrStringEmpty), m, r, eventData); err != nil {
|
if payload.AvatarURL, err = formatMessage(attrs.MustString("avatar_url", helpers.Ptr("")), m, r, eventData); err != nil {
|
||||||
return false, errors.Wrap(err, "parsing avatar_url")
|
return false, errors.Wrap(err, "parsing avatar_url")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,14 +71,14 @@ func (d discordActor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, ev
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendPayload(attrs.MustString("hook_url", ptrStringEmpty), payload, http.StatusNoContent)
|
return sendPayload(attrs.MustString("hook_url", helpers.Ptr("")), payload, http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (discordActor) IsAsync() bool { return false }
|
func (discordActor) IsAsync() bool { return false }
|
||||||
|
|
||||||
func (discordActor) Name() string { return "discordhook" }
|
func (discordActor) Name() string { return "discordhook" }
|
||||||
|
|
||||||
func (d discordActor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (d discordActor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
if err = d.ValidateRequireNonEmpty(attrs, "hook_url"); err != nil {
|
if err = d.ValidateRequireNonEmpty(attrs, "hook_url"); err != nil {
|
||||||
return err //nolint:wrapcheck
|
return err //nolint:wrapcheck
|
||||||
}
|
}
|
||||||
|
@ -89,7 +91,7 @@ func (d discordActor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs
|
||||||
return err //nolint:wrapcheck
|
return err //nolint:wrapcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
if !attrs.MustBool("add_embed", ptrBoolFalse) {
|
if !attrs.MustBool("add_embed", helpers.Ptr(false)) {
|
||||||
// We're not validating the rest if embeds are disabled but in
|
// We're not validating the rest if embeds are disabled but in
|
||||||
// this case the content is mandatory
|
// this case the content is mandatory
|
||||||
return d.ValidateRequireNonEmpty(attrs, "content") //nolint:wrapcheck
|
return d.ValidateRequireNonEmpty(attrs, "content") //nolint:wrapcheck
|
||||||
|
@ -111,8 +113,8 @@ func (d discordActor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo // It's complex but just a bunch of converters
|
//nolint:gocyclo // It's complex but just a bunch of converters
|
||||||
func (discordActor) addEmbed(payload *discordPayload, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (err error) {
|
func (discordActor) addEmbed(payload *discordPayload, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
if !attrs.MustBool("add_embed", ptrBoolFalse) {
|
if !attrs.MustBool("add_embed", helpers.Ptr(false)) {
|
||||||
// No embed? No problem!
|
// No embed? No problem!
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -122,45 +124,45 @@ func (discordActor) addEmbed(payload *discordPayload, m *irc.Message, r *plugins
|
||||||
sv string
|
sv string
|
||||||
)
|
)
|
||||||
|
|
||||||
if embed.Title, err = formatMessage(attrs.MustString("embed_title", ptrStringEmpty), m, r, eventData); err != nil {
|
if embed.Title, err = formatMessage(attrs.MustString("embed_title", helpers.Ptr("")), m, r, eventData); err != nil {
|
||||||
return errors.Wrap(err, "parsing embed_title")
|
return errors.Wrap(err, "parsing embed_title")
|
||||||
}
|
}
|
||||||
|
|
||||||
if embed.Description, err = formatMessage(attrs.MustString("embed_description", ptrStringEmpty), m, r, eventData); err != nil {
|
if embed.Description, err = formatMessage(attrs.MustString("embed_description", helpers.Ptr("")), m, r, eventData); err != nil {
|
||||||
return errors.Wrap(err, "parsing embed_description")
|
return errors.Wrap(err, "parsing embed_description")
|
||||||
}
|
}
|
||||||
|
|
||||||
if embed.URL, err = formatMessage(attrs.MustString("embed_url", ptrStringEmpty), m, r, eventData); err != nil {
|
if embed.URL, err = formatMessage(attrs.MustString("embed_url", helpers.Ptr("")), m, r, eventData); err != nil {
|
||||||
return errors.Wrap(err, "parsing embed_url")
|
return errors.Wrap(err, "parsing embed_url")
|
||||||
}
|
}
|
||||||
|
|
||||||
if sv, err = formatMessage(attrs.MustString("embed_image", ptrStringEmpty), m, r, eventData); err != nil {
|
if sv, err = formatMessage(attrs.MustString("embed_image", helpers.Ptr("")), m, r, eventData); err != nil {
|
||||||
return errors.Wrap(err, "parsing embed_image")
|
return errors.Wrap(err, "parsing embed_image")
|
||||||
} else if sv != "" {
|
} else if sv != "" {
|
||||||
embed.Image = &discordPayloadEmbedImage{URL: sv}
|
embed.Image = &discordPayloadEmbedImage{URL: sv}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sv, err = formatMessage(attrs.MustString("embed_thumbnail", ptrStringEmpty), m, r, eventData); err != nil {
|
if sv, err = formatMessage(attrs.MustString("embed_thumbnail", helpers.Ptr("")), m, r, eventData); err != nil {
|
||||||
return errors.Wrap(err, "parsing embed_thumbnail")
|
return errors.Wrap(err, "parsing embed_thumbnail")
|
||||||
} else if sv != "" {
|
} else if sv != "" {
|
||||||
embed.Thumbnail = &discordPayloadEmbedImage{URL: sv}
|
embed.Thumbnail = &discordPayloadEmbedImage{URL: sv}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sv, err = formatMessage(attrs.MustString("embed_author_name", ptrStringEmpty), m, r, eventData); err != nil {
|
if sv, err = formatMessage(attrs.MustString("embed_author_name", helpers.Ptr("")), m, r, eventData); err != nil {
|
||||||
return errors.Wrap(err, "parsing embed_author_name")
|
return errors.Wrap(err, "parsing embed_author_name")
|
||||||
} else if sv != "" {
|
} else if sv != "" {
|
||||||
embed.Author = &discordPayloadEmbedAuthor{Name: sv}
|
embed.Author = &discordPayloadEmbedAuthor{Name: sv}
|
||||||
|
|
||||||
if embed.Author.URL, err = formatMessage(attrs.MustString("embed_author_url", ptrStringEmpty), m, r, eventData); err != nil {
|
if embed.Author.URL, err = formatMessage(attrs.MustString("embed_author_url", helpers.Ptr("")), m, r, eventData); err != nil {
|
||||||
return errors.Wrap(err, "parsing embed_author_url")
|
return errors.Wrap(err, "parsing embed_author_url")
|
||||||
}
|
}
|
||||||
|
|
||||||
if embed.Author.IconURL, err = formatMessage(attrs.MustString("embed_author_icon_url", ptrStringEmpty), m, r, eventData); err != nil {
|
if embed.Author.IconURL, err = formatMessage(attrs.MustString("embed_author_icon_url", helpers.Ptr("")), m, r, eventData); err != nil {
|
||||||
return errors.Wrap(err, "parsing embed_author_icon_url")
|
return errors.Wrap(err, "parsing embed_author_icon_url")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sv, err = formatMessage(attrs.MustString("embed_fields", ptrStringEmpty), m, r, eventData); err != nil {
|
if sv, err = formatMessage(attrs.MustString("embed_fields", helpers.Ptr("")), m, r, eventData); err != nil {
|
||||||
return errors.Wrap(err, "parsing embed_fields")
|
return errors.Wrap(err, "parsing embed_fields")
|
||||||
} else if sv != "" {
|
} else if sv != "" {
|
||||||
var flds []discordPayloadEmbedField
|
var flds []discordPayloadEmbedField
|
||||||
|
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -14,14 +16,14 @@ type slackCompatibleActor struct {
|
||||||
plugins.ActorKit
|
plugins.ActorKit
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s slackCompatibleActor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (s slackCompatibleActor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
text, err := formatMessage(attrs.MustString("text", nil), m, r, eventData)
|
text, err := formatMessage(attrs.MustString("text", nil), m, r, eventData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "parsing text")
|
return false, errors.Wrap(err, "parsing text")
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendPayload(
|
return sendPayload(
|
||||||
s.fixHookURL(attrs.MustString("hook_url", ptrStringEmpty)),
|
s.fixHookURL(attrs.MustString("hook_url", helpers.Ptr(""))),
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"text": text,
|
"text": text,
|
||||||
},
|
},
|
||||||
|
@ -33,7 +35,7 @@ func (slackCompatibleActor) IsAsync() bool { return false }
|
||||||
|
|
||||||
func (slackCompatibleActor) Name() string { return "slackhook" }
|
func (slackCompatibleActor) Name() string { return "slackhook" }
|
||||||
|
|
||||||
func (s slackCompatibleActor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (s slackCompatibleActor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
if err = s.ValidateRequireNonEmpty(attrs, "hook_url", "text"); err != nil {
|
if err = s.ValidateRequireNonEmpty(attrs, "hook_url", "text"); err != nil {
|
||||||
return err //nolint:wrapcheck
|
return err //nolint:wrapcheck
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,14 @@ package modchannel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
@ -18,8 +21,6 @@ const actorName = "modchannel"
|
||||||
var (
|
var (
|
||||||
formatMessage plugins.MsgFormatter
|
formatMessage plugins.MsgFormatter
|
||||||
tcGetter func(string) (*twitch.Client, error)
|
tcGetter func(string) (*twitch.Client, error)
|
||||||
|
|
||||||
ptrStringEmpty = func(s string) *string { return &s }("")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
|
@ -70,10 +71,10 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
|
|
||||||
type actor struct{}
|
type actor struct{}
|
||||||
|
|
||||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
var (
|
var (
|
||||||
game = attrs.MustString("game", ptrStringEmpty)
|
game = attrs.MustString("game", helpers.Ptr(""))
|
||||||
title = attrs.MustString("title", ptrStringEmpty)
|
title = attrs.MustString("title", helpers.Ptr(""))
|
||||||
)
|
)
|
||||||
|
|
||||||
if game == "" && title == "" {
|
if game == "" && title == "" {
|
||||||
|
@ -119,15 +120,15 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
func (actor) IsAsync() bool { return false }
|
func (actor) IsAsync() bool { return false }
|
||||||
func (actor) Name() string { return actorName }
|
func (actor) Name() string { return actorName }
|
||||||
|
|
||||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
if v, err := attrs.String("channel"); err != nil || v == "" {
|
if err = attrs.ValidateSchema(
|
||||||
return errors.New("channel must be non-empty string")
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "channel", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
}
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "game", Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "title", Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
for _, field := range []string{"channel", "game", "title"} {
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil {
|
helpers.SchemaValidateTemplateField(tplValidator, "channel", "game", "title"),
|
||||||
return errors.Wrapf(err, "validating %s template", field)
|
); err != nil {
|
||||||
}
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -14,7 +14,7 @@ type (
|
||||||
|
|
||||||
func actionBan(channel, match, _, user string) error {
|
func actionBan(channel, match, _, user string) error {
|
||||||
return errors.Wrap(
|
return errors.Wrap(
|
||||||
botTwitchClient.BanUser(
|
botTwitchClient().BanUser(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
channel,
|
channel,
|
||||||
user,
|
user,
|
||||||
|
@ -27,7 +27,7 @@ func actionBan(channel, match, _, user string) error {
|
||||||
|
|
||||||
func actionDelete(channel, _, msgid, _ string) (err error) {
|
func actionDelete(channel, _, msgid, _ string) (err error) {
|
||||||
return errors.Wrap(
|
return errors.Wrap(
|
||||||
botTwitchClient.DeleteMessage(
|
botTwitchClient().DeleteMessage(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
channel,
|
channel,
|
||||||
msgid,
|
msgid,
|
||||||
|
@ -39,7 +39,7 @@ func actionDelete(channel, _, msgid, _ string) (err error) {
|
||||||
func getActionTimeout(duration time.Duration) actionFn {
|
func getActionTimeout(duration time.Duration) actionFn {
|
||||||
return func(channel, match, _, user string) error {
|
return func(channel, match, _, user string) error {
|
||||||
return errors.Wrap(
|
return errors.Wrap(
|
||||||
botTwitchClient.BanUser(
|
botTwitchClient().BanUser(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
channel,
|
channel,
|
||||||
user,
|
user,
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
package nuke
|
package nuke
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -13,7 +14,9 @@ import (
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/go_helpers/v2/str"
|
"github.com/Luzifer/go_helpers/v2/str"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
@ -24,20 +27,16 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
botTwitchClient *twitch.Client
|
botTwitchClient func() *twitch.Client
|
||||||
formatMessage plugins.MsgFormatter
|
formatMessage plugins.MsgFormatter
|
||||||
|
|
||||||
messageStore = map[string][]*storedMessage{}
|
messageStore = map[string][]*storedMessage{}
|
||||||
messageStoreLock sync.RWMutex
|
messageStoreLock sync.RWMutex
|
||||||
|
|
||||||
ptrStringDelete = func(v string) *string { return &v }("delete")
|
|
||||||
ptrStringEmpty = func(s string) *string { return &s }("")
|
|
||||||
ptrString10m = func(v string) *string { return &v }("10m")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
func Register(args plugins.RegistrationArguments) error {
|
func Register(args plugins.RegistrationArguments) error {
|
||||||
botTwitchClient = args.GetTwitchClient()
|
botTwitchClient = args.GetTwitchClient
|
||||||
formatMessage = args.FormatMessage
|
formatMessage = args.FormatMessage
|
||||||
|
|
||||||
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
||||||
|
@ -150,14 +149,14 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
rawMatch, err := formatMessage(attrs.MustString("match", nil), m, r, eventData)
|
rawMatch, err := formatMessage(attrs.MustString("match", nil), m, r, eventData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "formatting match")
|
return false, errors.Wrap(err, "formatting match")
|
||||||
}
|
}
|
||||||
match := regexp.MustCompile(rawMatch)
|
match := regexp.MustCompile(rawMatch)
|
||||||
|
|
||||||
rawScan, err := formatMessage(attrs.MustString("scan", ptrString10m), m, r, eventData)
|
rawScan, err := formatMessage(attrs.MustString("scan", helpers.Ptr("10m")), m, r, eventData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "formatting scan duration")
|
return false, errors.Wrap(err, "formatting scan duration")
|
||||||
}
|
}
|
||||||
|
@ -171,7 +170,7 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
action actionFn
|
action actionFn
|
||||||
actionName string
|
actionName string
|
||||||
)
|
)
|
||||||
rawAction, err := formatMessage(attrs.MustString("action", ptrStringDelete), m, r, eventData)
|
rawAction, err := formatMessage(attrs.MustString("action", helpers.Ptr("delete")), m, r, eventData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "formatting action")
|
return false, errors.Wrap(err, "formatting action")
|
||||||
}
|
}
|
||||||
|
@ -235,15 +234,15 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
func (actor) IsAsync() bool { return false }
|
func (actor) IsAsync() bool { return false }
|
||||||
func (actor) Name() string { return actorName }
|
func (actor) Name() string { return actorName }
|
||||||
|
|
||||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
if v, err := attrs.String("match"); err != nil || v == "" {
|
if err = attrs.ValidateSchema(
|
||||||
return errors.New("match must be non-empty string")
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "match", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
}
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "action", Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "scan", Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
for _, field := range []string{"scan", "action", "match"} {
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil {
|
helpers.SchemaValidateTemplateField(tplValidator, "scan", "action", "match"),
|
||||||
return errors.Wrapf(err, "validating %s template", field)
|
); err != nil {
|
||||||
}
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -4,6 +4,7 @@ package punish
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -12,6 +13,8 @@ import (
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
|
@ -25,11 +28,9 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
botTwitchClient *twitch.Client
|
botTwitchClient func() *twitch.Client
|
||||||
db database.Connector
|
db database.Connector
|
||||||
formatMessage plugins.MsgFormatter
|
formatMessage plugins.MsgFormatter
|
||||||
ptrDefaultCooldown = func(v time.Duration) *time.Duration { return &v }(oneWeek)
|
|
||||||
ptrStringEmpty = func(v string) *string { return &v }("")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
|
@ -43,7 +44,7 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
return database.CopyObjects(src, target, &punishLevel{})
|
return database.CopyObjects(src, target, &punishLevel{})
|
||||||
})
|
})
|
||||||
|
|
||||||
botTwitchClient = args.GetTwitchClient()
|
botTwitchClient = args.GetTwitchClient
|
||||||
formatMessage = args.FormatMessage
|
formatMessage = args.FormatMessage
|
||||||
|
|
||||||
args.RegisterActor(actorNamePunish, func() plugins.Actor { return &actorPunish{} })
|
args.RegisterActor(actorNamePunish, func() plugins.Actor { return &actorPunish{} })
|
||||||
|
@ -146,12 +147,12 @@ type (
|
||||||
|
|
||||||
// Punish
|
// Punish
|
||||||
|
|
||||||
func (actorPunish) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actorPunish) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
var (
|
var (
|
||||||
cooldown = attrs.MustDuration("cooldown", ptrDefaultCooldown)
|
cooldown = attrs.MustDuration("cooldown", helpers.Ptr(oneWeek))
|
||||||
reason = attrs.MustString("reason", ptrStringEmpty)
|
reason = attrs.MustString("reason", helpers.Ptr(""))
|
||||||
user = attrs.MustString("user", nil)
|
user = attrs.MustString("user", nil)
|
||||||
uuid = attrs.MustString("uuid", ptrStringEmpty)
|
uuid = attrs.MustString("uuid", helpers.Ptr(""))
|
||||||
)
|
)
|
||||||
|
|
||||||
levels, err := attrs.StringSlice("levels")
|
levels, err := attrs.StringSlice("levels")
|
||||||
|
@ -171,7 +172,7 @@ func (actorPunish) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, event
|
||||||
|
|
||||||
switch lt := levels[nLvl]; lt {
|
switch lt := levels[nLvl]; lt {
|
||||||
case "ban":
|
case "ban":
|
||||||
if err = botTwitchClient.BanUser(
|
if err = botTwitchClient().BanUser(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
plugins.DeriveChannel(m, eventData),
|
plugins.DeriveChannel(m, eventData),
|
||||||
strings.TrimLeft(user, "@"),
|
strings.TrimLeft(user, "@"),
|
||||||
|
@ -187,7 +188,7 @@ func (actorPunish) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, event
|
||||||
return false, errors.New("found no mesage id")
|
return false, errors.New("found no mesage id")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = botTwitchClient.DeleteMessage(
|
if err = botTwitchClient().DeleteMessage(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
plugins.DeriveChannel(m, eventData),
|
plugins.DeriveChannel(m, eventData),
|
||||||
msgID,
|
msgID,
|
||||||
|
@ -201,7 +202,7 @@ func (actorPunish) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, event
|
||||||
return false, errors.Wrap(err, "parsing punishment level")
|
return false, errors.Wrap(err, "parsing punishment level")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = botTwitchClient.BanUser(
|
if err = botTwitchClient().BanUser(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
plugins.DeriveChannel(m, eventData),
|
plugins.DeriveChannel(m, eventData),
|
||||||
strings.TrimLeft(user, "@"),
|
strings.TrimLeft(user, "@"),
|
||||||
|
@ -225,17 +226,17 @@ func (actorPunish) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, event
|
||||||
func (actorPunish) IsAsync() bool { return false }
|
func (actorPunish) IsAsync() bool { return false }
|
||||||
func (actorPunish) Name() string { return actorNamePunish }
|
func (actorPunish) Name() string { return actorNamePunish }
|
||||||
|
|
||||||
func (actorPunish) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actorPunish) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
if v, err := attrs.String("user"); err != nil || v == "" {
|
if err = attrs.ValidateSchema(
|
||||||
return errors.New("user must be non-empty string")
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "levels", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeStringSlice}),
|
||||||
}
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "user", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "cooldown", Type: fieldcollection.SchemaFieldTypeDuration}),
|
||||||
if v, err := attrs.StringSlice("levels"); err != nil || len(v) == 0 {
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "reason", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
return errors.New("levels must be slice of strings with length > 0")
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "uuid", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
}
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
|
helpers.SchemaValidateTemplateField(tplValidator, "user"),
|
||||||
if err = tplValidator(attrs.MustString("user", ptrStringEmpty)); err != nil {
|
); err != nil {
|
||||||
return errors.Wrap(err, "validating user template")
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -243,10 +244,10 @@ func (actorPunish) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *p
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
|
|
||||||
func (actorResetPunish) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actorResetPunish) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
var (
|
var (
|
||||||
user = attrs.MustString("user", nil)
|
user = attrs.MustString("user", nil)
|
||||||
uuid = attrs.MustString("uuid", ptrStringEmpty)
|
uuid = attrs.MustString("uuid", helpers.Ptr(""))
|
||||||
)
|
)
|
||||||
|
|
||||||
if user, err = formatMessage(user, m, r, eventData); err != nil {
|
if user, err = formatMessage(user, m, r, eventData); err != nil {
|
||||||
|
@ -262,13 +263,14 @@ func (actorResetPunish) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule,
|
||||||
func (actorResetPunish) IsAsync() bool { return false }
|
func (actorResetPunish) IsAsync() bool { return false }
|
||||||
func (actorResetPunish) Name() string { return actorNameResetPunish }
|
func (actorResetPunish) Name() string { return actorNameResetPunish }
|
||||||
|
|
||||||
func (actorResetPunish) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actorResetPunish) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
if v, err := attrs.String("user"); err != nil || v == "" {
|
if err = attrs.ValidateSchema(
|
||||||
return errors.New("user must be non-empty string")
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "user", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
}
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "uuid", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
if err = tplValidator(attrs.MustString("user", ptrStringEmpty)); err != nil {
|
helpers.SchemaValidateTemplateField(tplValidator, "user"),
|
||||||
return errors.Wrap(err, "validating user template")
|
); err != nil {
|
||||||
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -10,6 +10,8 @@ import (
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
@ -23,9 +25,9 @@ var (
|
||||||
formatMessage plugins.MsgFormatter
|
formatMessage plugins.MsgFormatter
|
||||||
send plugins.SendMessageFunc
|
send plugins.SendMessageFunc
|
||||||
|
|
||||||
ptrStringEmpty = func(v string) *string { return &v }("")
|
// ptrStringEmpty = func(v string) *string { return &v }("")
|
||||||
ptrStringOutFormat = func(v string) *string { return &v }("Quote #{{ .index }}: {{ .quote }}")
|
// ptrStringOutFormat = func(v string) *string { return &v }("Quote #{{ .index }}: {{ .quote }}")
|
||||||
ptrStringZero = func(v string) *string { return &v }("0")
|
// ptrStringZero = func(v string) *string { return &v }("0")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
|
@ -93,7 +95,7 @@ func Register(args plugins.RegistrationArguments) (err error) {
|
||||||
return fmt.Errorf("registering API: %w", err)
|
return fmt.Errorf("registering API: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
args.RegisterTemplateFunction("lastQuoteIndex", func(m *irc.Message, _ *plugins.Rule, _ *plugins.FieldCollection) interface{} {
|
args.RegisterTemplateFunction("lastQuoteIndex", func(m *irc.Message, _ *plugins.Rule, _ *fieldcollection.FieldCollection) interface{} {
|
||||||
return func() (int, error) {
|
return func() (int, error) {
|
||||||
return getMaxQuoteIdx(db, plugins.DeriveChannel(m, nil))
|
return getMaxQuoteIdx(db, plugins.DeriveChannel(m, nil))
|
||||||
}
|
}
|
||||||
|
@ -113,11 +115,11 @@ type (
|
||||||
actor struct{}
|
actor struct{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
var (
|
var (
|
||||||
action = attrs.MustString("action", ptrStringEmpty)
|
action = attrs.MustString("action", helpers.Ptr(""))
|
||||||
indexStr = attrs.MustString("index", ptrStringZero)
|
indexStr = attrs.MustString("index", helpers.Ptr("0"))
|
||||||
quote = attrs.MustString("quote", ptrStringEmpty)
|
quote = attrs.MustString("quote", helpers.Ptr(""))
|
||||||
)
|
)
|
||||||
|
|
||||||
if indexStr == "" {
|
if indexStr == "" {
|
||||||
|
@ -166,7 +168,7 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
fields.Set("index", idx)
|
fields.Set("index", idx)
|
||||||
fields.Set("quote", quote)
|
fields.Set("quote", quote)
|
||||||
|
|
||||||
format := attrs.MustString("format", ptrStringOutFormat)
|
format := attrs.MustString("format", helpers.Ptr("Quote #{{ .index }}: {{ .quote }}"))
|
||||||
msg, err := formatMessage(format, m, r, fields)
|
msg, err := formatMessage(format, m, r, fields)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "formatting output message")
|
return false, errors.Wrap(err, "formatting output message")
|
||||||
|
@ -190,31 +192,36 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
func (actor) IsAsync() bool { return false }
|
func (actor) IsAsync() bool { return false }
|
||||||
func (actor) Name() string { return actorName }
|
func (actor) Name() string { return actorName }
|
||||||
|
|
||||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
action := attrs.MustString("action", ptrStringEmpty)
|
if err = attrs.ValidateSchema(
|
||||||
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "action", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "quote", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "index", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "format", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
|
helpers.SchemaValidateTemplateField(tplValidator, "index", "quote", "format"),
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
action := attrs.MustString("action", helpers.Ptr(""))
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case "add":
|
case "add":
|
||||||
if v, err := attrs.String("quote"); err != nil || v == "" {
|
if v, err := attrs.String("quote"); err != nil || v == "" {
|
||||||
return errors.New("quote must be non-empty string for action add")
|
return fmt.Errorf("quote must be non-empty string for action add")
|
||||||
}
|
}
|
||||||
|
|
||||||
case "del":
|
case "del":
|
||||||
if v, err := attrs.String("index"); err != nil || v == "" {
|
if v, err := attrs.String("index"); err != nil || v == "" {
|
||||||
return errors.New("index must be non-empty string for adction del")
|
return fmt.Errorf("index must be non-empty string for adction del")
|
||||||
}
|
}
|
||||||
|
|
||||||
case "get":
|
case "get":
|
||||||
// No requirements
|
// No requirements
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return errors.New("action must be one of add, del or get")
|
return fmt.Errorf("action must be one of add, del or get")
|
||||||
}
|
|
||||||
|
|
||||||
for _, field := range []string{"index", "quote", "format"} {
|
|
||||||
if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil {
|
|
||||||
return errors.Wrapf(err, "validating %s template", field)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -2,9 +2,13 @@
|
||||||
package raw
|
package raw
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -13,8 +17,6 @@ const actorName = "raw"
|
||||||
var (
|
var (
|
||||||
formatMessage plugins.MsgFormatter
|
formatMessage plugins.MsgFormatter
|
||||||
send plugins.SendMessageFunc
|
send plugins.SendMessageFunc
|
||||||
|
|
||||||
ptrStringEmpty = func(s string) *string { return &s }("")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
|
@ -47,7 +49,7 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
|
|
||||||
type actor struct{}
|
type actor struct{}
|
||||||
|
|
||||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
rawMsg, err := formatMessage(attrs.MustString("message", nil), m, r, eventData)
|
rawMsg, err := formatMessage(attrs.MustString("message", nil), m, r, eventData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "preparing raw message")
|
return false, errors.Wrap(err, "preparing raw message")
|
||||||
|
@ -67,13 +69,13 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
func (actor) IsAsync() bool { return false }
|
func (actor) IsAsync() bool { return false }
|
||||||
func (actor) Name() string { return actorName }
|
func (actor) Name() string { return actorName }
|
||||||
|
|
||||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
if v, err := attrs.String("message"); err != nil || v == "" {
|
if err = attrs.ValidateSchema(
|
||||||
return errors.New("message must be non-empty string")
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "message", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
}
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
|
helpers.SchemaValidateTemplateField(tplValidator, "message"),
|
||||||
if err = tplValidator(attrs.MustString("message", ptrStringEmpty)); err != nil {
|
); err != nil {
|
||||||
return errors.Wrap(err, "validating message template")
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -12,6 +12,8 @@ import (
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,9 +22,6 @@ const actorName = "respond"
|
||||||
var (
|
var (
|
||||||
formatMessage plugins.MsgFormatter
|
formatMessage plugins.MsgFormatter
|
||||||
send plugins.SendMessageFunc
|
send plugins.SendMessageFunc
|
||||||
|
|
||||||
ptrBoolFalse = func(v bool) *bool { return &v }(false)
|
|
||||||
ptrStringEmpty = func(s string) *string { return &s }("")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
|
@ -102,7 +101,7 @@ func Register(args plugins.RegistrationArguments) (err error) {
|
||||||
|
|
||||||
type actor struct{}
|
type actor struct{}
|
||||||
|
|
||||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
msg, err := formatMessage(attrs.MustString("message", nil), m, r, eventData)
|
msg, err := formatMessage(attrs.MustString("message", nil), m, r, eventData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !attrs.CanString("fallback") || attrs.MustString("fallback", nil) == "" {
|
if !attrs.CanString("fallback") || attrs.MustString("fallback", nil) == "" {
|
||||||
|
@ -127,7 +126,7 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if attrs.MustBool("as_reply", ptrBoolFalse) {
|
if attrs.MustBool("as_reply", helpers.Ptr(false)) {
|
||||||
id, ok := m.Tags["id"]
|
id, ok := m.Tags["id"]
|
||||||
if ok {
|
if ok {
|
||||||
if ircMessage.Tags == nil {
|
if ircMessage.Tags == nil {
|
||||||
|
@ -146,15 +145,16 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
func (actor) IsAsync() bool { return false }
|
func (actor) IsAsync() bool { return false }
|
||||||
func (actor) Name() string { return actorName }
|
func (actor) Name() string { return actorName }
|
||||||
|
|
||||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
if v, err := attrs.String("message"); err != nil || v == "" {
|
if err = attrs.ValidateSchema(
|
||||||
return errors.New("message must be non-empty string")
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "message", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
}
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "fallback", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "as_reply", Type: fieldcollection.SchemaFieldTypeBool}),
|
||||||
for _, field := range []string{"message", "fallback"} {
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "to_channel", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil {
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
return errors.Wrapf(err, "validating %s template", field)
|
helpers.SchemaValidateTemplateField(tplValidator, "message", "fallback"),
|
||||||
}
|
); err != nil {
|
||||||
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -4,21 +4,24 @@ package shield
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
const actorName = "shield"
|
const actorName = "shield"
|
||||||
|
|
||||||
var botTwitchClient *twitch.Client
|
var botTwitchClient func() *twitch.Client
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
func Register(args plugins.RegistrationArguments) error {
|
func Register(args plugins.RegistrationArguments) error {
|
||||||
botTwitchClient = args.GetTwitchClient()
|
botTwitchClient = args.GetTwitchClient
|
||||||
|
|
||||||
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
||||||
|
|
||||||
|
@ -45,14 +48,12 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
|
|
||||||
type actor struct{}
|
type actor struct{}
|
||||||
|
|
||||||
func (actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
ptrBoolFalse := func(v bool) *bool { return &v }(false)
|
|
||||||
|
|
||||||
return false, errors.Wrap(
|
return false, errors.Wrap(
|
||||||
botTwitchClient.UpdateShieldMode(
|
botTwitchClient().UpdateShieldMode(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
plugins.DeriveChannel(m, eventData),
|
plugins.DeriveChannel(m, eventData),
|
||||||
attrs.MustBool("enable", ptrBoolFalse),
|
attrs.MustBool("enable", helpers.Ptr(false)),
|
||||||
),
|
),
|
||||||
"configuring shield mode",
|
"configuring shield mode",
|
||||||
)
|
)
|
||||||
|
@ -61,9 +62,12 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData *
|
||||||
func (actor) IsAsync() bool { return false }
|
func (actor) IsAsync() bool { return false }
|
||||||
func (actor) Name() string { return actorName }
|
func (actor) Name() string { return actorName }
|
||||||
|
|
||||||
func (actor) Validate(_ plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actor) Validate(_ plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
if _, err = attrs.Bool("enable"); err != nil {
|
if err = attrs.ValidateSchema(
|
||||||
return errors.New("enable must be boolean")
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "enable", Type: fieldcollection.SchemaFieldTypeBool}),
|
||||||
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -4,11 +4,14 @@ package shoutout
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
@ -16,7 +19,7 @@ import (
|
||||||
const actorName = "shoutout"
|
const actorName = "shoutout"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
botTwitchClient *twitch.Client
|
botTwitchClient func() *twitch.Client
|
||||||
formatMessage plugins.MsgFormatter
|
formatMessage plugins.MsgFormatter
|
||||||
ptrStringEmpty = func(v string) *string { return &v }("")
|
ptrStringEmpty = func(v string) *string { return &v }("")
|
||||||
|
|
||||||
|
@ -25,7 +28,7 @@ var (
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
func Register(args plugins.RegistrationArguments) error {
|
func Register(args plugins.RegistrationArguments) error {
|
||||||
botTwitchClient = args.GetTwitchClient()
|
botTwitchClient = args.GetTwitchClient
|
||||||
formatMessage = args.FormatMessage
|
formatMessage = args.FormatMessage
|
||||||
|
|
||||||
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
||||||
|
@ -55,14 +58,14 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
|
|
||||||
type actor struct{}
|
type actor struct{}
|
||||||
|
|
||||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
user, err := formatMessage(attrs.MustString("user", ptrStringEmpty), m, r, eventData)
|
user, err := formatMessage(attrs.MustString("user", ptrStringEmpty), m, r, eventData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "executing user template")
|
return false, errors.Wrap(err, "executing user template")
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, errors.Wrap(
|
return false, errors.Wrap(
|
||||||
botTwitchClient.SendShoutout(
|
botTwitchClient().SendShoutout(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
plugins.DeriveChannel(m, eventData),
|
plugins.DeriveChannel(m, eventData),
|
||||||
user,
|
user,
|
||||||
|
@ -74,13 +77,13 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
func (actor) IsAsync() bool { return false }
|
func (actor) IsAsync() bool { return false }
|
||||||
func (actor) Name() string { return actorName }
|
func (actor) Name() string { return actorName }
|
||||||
|
|
||||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
if v, err := attrs.String("user"); err != nil || v == "" {
|
if err = attrs.ValidateSchema(
|
||||||
return errors.New("user must be non-empty string")
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "user", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
}
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
|
helpers.SchemaValidateTemplateField(tplValidator, "user"),
|
||||||
if err = tplValidator(attrs.MustString("user", ptrStringEmpty)); err != nil {
|
); err != nil {
|
||||||
return errors.Wrap(err, "validating user template")
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -94,7 +97,7 @@ func handleChatCommand(m *irc.Message) error {
|
||||||
return errors.New("shoutout message does not match required format")
|
return errors.New("shoutout message does not match required format")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := botTwitchClient.SendShoutout(context.Background(), channel, matches[1]); err != nil {
|
if err := botTwitchClient().SendShoutout(context.Background(), channel, matches[1]); err != nil {
|
||||||
return errors.Wrap(err, "executing shoutout")
|
return errors.Wrap(err, "executing shoutout")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -24,6 +25,12 @@ func getCurrentTrackForChannel(channel string) (track currentPlayingTrackRespons
|
||||||
return track, fmt.Errorf("loading oauth token: %w", err)
|
return track, fmt.Errorf("loading oauth token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := db.StoreEncryptedCoreMeta(strings.Join([]string{"spotify-auth", channel}, ":"), token); err != nil {
|
||||||
|
logrus.WithError(err).Error("storing back Spotify auth token")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), spotifyRequestTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), spotifyRequestTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
@ -42,14 +49,34 @@ func getCurrentTrackForChannel(channel string) (track currentPlayingTrackRespons
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
defer func() {
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err := db.StoreEncryptedCoreMeta(strings.Join([]string{"spotify-auth", channel}, ":"), token); err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Error("storing back Spotify auth token")
|
return track, fmt.Errorf("reading response body: %w", err)
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
if err = json.NewDecoder(resp.Body).Decode(&track); err != nil {
|
switch resp.StatusCode {
|
||||||
return track, fmt.Errorf("decoding response: %w", err)
|
case http.StatusOK:
|
||||||
|
// This is perfect, continue below
|
||||||
|
|
||||||
|
case http.StatusUnauthorized:
|
||||||
|
// The token is FUBAR
|
||||||
|
return track, fmt.Errorf("token expired (HTTP 401 - unauthorized)")
|
||||||
|
|
||||||
|
case http.StatusForbidden:
|
||||||
|
// The request is FUBAR
|
||||||
|
return track, fmt.Errorf("bad oAuth request, report this to dev (HTTP 403 - forbidden): %q", body)
|
||||||
|
|
||||||
|
case http.StatusTooManyRequests:
|
||||||
|
// We asked too often
|
||||||
|
return track, fmt.Errorf("rate-limited (HTTP 429 - too many requests)")
|
||||||
|
|
||||||
|
default:
|
||||||
|
// WTF?
|
||||||
|
return track, fmt.Errorf("unexpected HTTP status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(body, &track); err != nil {
|
||||||
|
return track, fmt.Errorf("decoding response (%q): %w", body, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return track, nil
|
return track, nil
|
||||||
|
|
|
@ -2,21 +2,33 @@ package spotify
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/crypto/pbkdf2"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const spotifyRequestTimeout = 2 * time.Second
|
const (
|
||||||
|
spotifyRequestTimeout = 2 * time.Second
|
||||||
|
|
||||||
|
pkcePBKDFIter = 210000
|
||||||
|
pkcePBKDFLen = 64
|
||||||
|
)
|
||||||
|
|
||||||
|
var instanceSalt = uuid.Must(uuid.NewV4()).String()
|
||||||
|
|
||||||
func handleStartAuth(w http.ResponseWriter, r *http.Request) {
|
func handleStartAuth(w http.ResponseWriter, r *http.Request) {
|
||||||
channel := mux.Vars(r)["channel"]
|
channel := mux.Vars(r)["channel"]
|
||||||
|
pkceVerifier := hex.EncodeToString(pbkdf2.Key([]byte(channel), []byte(instanceSalt), pkcePBKDFIter, pkcePBKDFLen, sha512.New))
|
||||||
|
|
||||||
redirURL := baseURL.ResolveReference(&url.URL{Path: r.URL.Path})
|
redirURL := baseURL.ResolveReference(&url.URL{Path: r.URL.Path})
|
||||||
conf, err := oauthConfig(channel, strings.Split(redirURL.String(), "?")[0])
|
conf, err := oauthConfig(channel, strings.Split(redirURL.String(), "?")[0])
|
||||||
|
@ -30,13 +42,20 @@ func handleStartAuth(w http.ResponseWriter, r *http.Request) {
|
||||||
if code == "" {
|
if code == "" {
|
||||||
http.Redirect(
|
http.Redirect(
|
||||||
w, r,
|
w, r,
|
||||||
conf.AuthCodeURL(fmt.Sprintf("%x", sha256.Sum256(append([]byte(conf.ClientID), []byte(channel)...)))),
|
conf.AuthCodeURL(
|
||||||
|
fmt.Sprintf("%x", sha256.Sum256(append([]byte(conf.ClientID), []byte(channel)...))),
|
||||||
|
oauth2.S256ChallengeOption(pkceVerifier),
|
||||||
|
),
|
||||||
http.StatusFound,
|
http.StatusFound,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := conf.Exchange(r.Context(), r.URL.Query().Get("code"))
|
token, err := conf.Exchange(
|
||||||
|
r.Context(),
|
||||||
|
r.URL.Query().Get("code"),
|
||||||
|
oauth2.VerifierOption(pkceVerifier),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Error("getting Spotify oauth token")
|
logrus.WithError(err).Error("getting Spotify oauth token")
|
||||||
http.Error(w, "unable to get Spotify auth token", http.StatusInternalServerError)
|
http.Error(w, "unable to get Spotify auth token", http.StatusInternalServerError)
|
||||||
|
@ -58,14 +77,8 @@ func oauthConfig(channel, redirectURL string) (conf *oauth2.Config, err error) {
|
||||||
return nil, fmt.Errorf("getting clientId for channel: %w", err)
|
return nil, fmt.Errorf("getting clientId for channel: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
clientSecret, err := getModuleConfig(actorName, channel).String("clientSecret")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("getting clientSecret for channel: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &oauth2.Config{
|
return &oauth2.Config{
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
ClientSecret: clientSecret,
|
|
||||||
Endpoint: oauth2.Endpoint{
|
Endpoint: oauth2.Endpoint{
|
||||||
AuthURL: "https://accounts.spotify.com/authorize",
|
AuthURL: "https://accounts.spotify.com/authorize",
|
||||||
TokenURL: "https://accounts.spotify.com/api/token",
|
TokenURL: "https://accounts.spotify.com/api/token",
|
||||||
|
|
|
@ -3,9 +3,13 @@
|
||||||
package stopexec
|
package stopexec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -42,10 +46,8 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
|
|
||||||
type actor struct{}
|
type actor struct{}
|
||||||
|
|
||||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
ptrStringEmpty := func(v string) *string { return &v }("")
|
when, err := formatMessage(attrs.MustString("when", helpers.Ptr("")), m, r, eventData)
|
||||||
|
|
||||||
when, err := formatMessage(attrs.MustString("when", ptrStringEmpty), m, r, eventData)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "executing when template")
|
return false, errors.Wrap(err, "executing when template")
|
||||||
}
|
}
|
||||||
|
@ -60,14 +62,13 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
func (actor) IsAsync() bool { return false }
|
func (actor) IsAsync() bool { return false }
|
||||||
func (actor) Name() string { return actorName }
|
func (actor) Name() string { return actorName }
|
||||||
|
|
||||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
whenTemplate, err := attrs.String("when")
|
if err = attrs.ValidateSchema(
|
||||||
if err != nil || whenTemplate == "" {
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "when", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
return errors.New("when must be non-empty string")
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
}
|
helpers.SchemaValidateTemplateField(tplValidator, "when"),
|
||||||
|
); err != nil {
|
||||||
if err = tplValidator(whenTemplate); err != nil {
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
return errors.Wrap(err, "validating when template")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -3,6 +3,7 @@ package timeout
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
@ -10,6 +11,8 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
@ -17,7 +20,7 @@ import (
|
||||||
const actorName = "timeout"
|
const actorName = "timeout"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
botTwitchClient *twitch.Client
|
botTwitchClient func() *twitch.Client
|
||||||
formatMessage plugins.MsgFormatter
|
formatMessage plugins.MsgFormatter
|
||||||
ptrStringEmpty = func(v string) *string { return &v }("")
|
ptrStringEmpty = func(v string) *string { return &v }("")
|
||||||
|
|
||||||
|
@ -26,7 +29,7 @@ var (
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
func Register(args plugins.RegistrationArguments) error {
|
func Register(args plugins.RegistrationArguments) error {
|
||||||
botTwitchClient = args.GetTwitchClient()
|
botTwitchClient = args.GetTwitchClient
|
||||||
formatMessage = args.FormatMessage
|
formatMessage = args.FormatMessage
|
||||||
|
|
||||||
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
||||||
|
@ -65,14 +68,14 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
|
|
||||||
type actor struct{}
|
type actor struct{}
|
||||||
|
|
||||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
reason, err := formatMessage(attrs.MustString("reason", ptrStringEmpty), m, r, eventData)
|
reason, err := formatMessage(attrs.MustString("reason", ptrStringEmpty), m, r, eventData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "executing reason template")
|
return false, errors.Wrap(err, "executing reason template")
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, errors.Wrap(
|
return false, errors.Wrap(
|
||||||
botTwitchClient.BanUser(
|
botTwitchClient().BanUser(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
plugins.DeriveChannel(m, eventData),
|
plugins.DeriveChannel(m, eventData),
|
||||||
plugins.DeriveUser(m, eventData),
|
plugins.DeriveUser(m, eventData),
|
||||||
|
@ -86,17 +89,18 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
func (actor) IsAsync() bool { return false }
|
func (actor) IsAsync() bool { return false }
|
||||||
func (actor) Name() string { return actorName }
|
func (actor) Name() string { return actorName }
|
||||||
|
|
||||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
if v, err := attrs.Duration("duration"); err != nil || v < time.Second {
|
if err = attrs.ValidateSchema(
|
||||||
return errors.New("duration must be of type duration greater or equal one second")
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "duration", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeDuration}),
|
||||||
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "reason", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
|
helpers.SchemaValidateTemplateField(tplValidator, "reason"),
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, err := attrs.String("reason"); err != nil || v == "" {
|
if attrs.MustDuration("duration", nil) < time.Second {
|
||||||
return errors.New("reason must be non-empty string")
|
return errors.New("duration must be greater or equal one second")
|
||||||
}
|
|
||||||
|
|
||||||
if err = tplValidator(attrs.MustString("reason", ptrStringEmpty)); err != nil {
|
|
||||||
return errors.Wrap(err, "validating reason template")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -115,7 +119,7 @@ func handleChatCommand(m *irc.Message) error {
|
||||||
return errors.Wrap(err, "parsing timeout duration")
|
return errors.Wrap(err, "parsing timeout duration")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = botTwitchClient.BanUser(context.Background(), channel, matches[1], time.Duration(duration)*time.Second, matches[3]); err != nil {
|
if err = botTwitchClient().BanUser(context.Background(), channel, matches[1], time.Duration(duration)*time.Second, matches[3]); err != nil {
|
||||||
return errors.Wrap(err, "executing timeout")
|
return errors.Wrap(err, "executing timeout")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,8 @@ import (
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
@ -144,7 +146,7 @@ func Register(args plugins.RegistrationArguments) (err error) {
|
||||||
|
|
||||||
type actorSetVariable struct{}
|
type actorSetVariable struct{}
|
||||||
|
|
||||||
func (actorSetVariable) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actorSetVariable) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
varName, err := formatMessage(attrs.MustString("variable", nil), m, r, eventData)
|
varName, err := formatMessage(attrs.MustString("variable", nil), m, r, eventData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "preparing variable name")
|
return false, errors.Wrap(err, "preparing variable name")
|
||||||
|
@ -171,15 +173,15 @@ func (actorSetVariable) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule,
|
||||||
func (actorSetVariable) IsAsync() bool { return false }
|
func (actorSetVariable) IsAsync() bool { return false }
|
||||||
func (actorSetVariable) Name() string { return "setvariable" }
|
func (actorSetVariable) Name() string { return "setvariable" }
|
||||||
|
|
||||||
func (actorSetVariable) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actorSetVariable) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
if v, err := attrs.String("variable"); err != nil || v == "" {
|
if err = attrs.ValidateSchema(
|
||||||
return errors.New("variable name must be non-empty string")
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "variable", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
}
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "clear", Type: fieldcollection.SchemaFieldTypeBool}),
|
||||||
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "set", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
for _, field := range []string{"set", "variable"} {
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil {
|
helpers.SchemaValidateTemplateField(tplValidator, "set", "variable"),
|
||||||
return errors.Wrapf(err, "validating %s template", field)
|
); err != nil {
|
||||||
}
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -3,11 +3,14 @@ package vip
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
@ -16,8 +19,6 @@ var (
|
||||||
formatMessage plugins.MsgFormatter
|
formatMessage plugins.MsgFormatter
|
||||||
permCheckFn plugins.ChannelPermissionCheckFunc
|
permCheckFn plugins.ChannelPermissionCheckFunc
|
||||||
tcGetter func(string) (*twitch.Client, error)
|
tcGetter func(string) (*twitch.Client, error)
|
||||||
|
|
||||||
ptrStringEmpty = func(s string) *string { return &s }("")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
|
@ -98,21 +99,20 @@ type (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (actor) IsAsync() bool { return false }
|
func (actor) IsAsync() bool { return false }
|
||||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
for _, field := range []string{"channel", "user"} {
|
if err = attrs.ValidateSchema(
|
||||||
if v, err := attrs.String(field); err != nil || v == "" {
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "channel", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
return errors.Errorf("%s must be non-empty string", field)
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "user", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
}
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
|
helpers.SchemaValidateTemplateField(tplValidator, "channel", "user"),
|
||||||
if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil {
|
); err != nil {
|
||||||
return errors.Wrapf(err, "validating %s template", field)
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (actor) getParams(m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (channel, user string, err error) {
|
func (actor) getParams(m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (channel, user string, err error) {
|
||||||
if channel, err = formatMessage(attrs.MustString("channel", nil), m, r, eventData); err != nil {
|
if channel, err = formatMessage(attrs.MustString("channel", nil), m, r, eventData); err != nil {
|
||||||
return "", "", errors.Wrap(err, "parsing channel")
|
return "", "", errors.Wrap(err, "parsing channel")
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,7 @@ func (actor) getParams(m *irc.Message, r *plugins.Rule, eventData *plugins.Field
|
||||||
return strings.TrimLeft(channel, "#"), user, nil
|
return strings.TrimLeft(channel, "#"), user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u unvipActor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (u unvipActor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
channel, user, err := u.getParams(m, r, eventData, attrs)
|
channel, user, err := u.getParams(m, r, eventData, attrs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "getting parameters")
|
return false, errors.Wrap(err, "getting parameters")
|
||||||
|
@ -140,7 +140,7 @@ func (u unvipActor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, even
|
||||||
|
|
||||||
func (unvipActor) Name() string { return "unvip" }
|
func (unvipActor) Name() string { return "unvip" }
|
||||||
|
|
||||||
func (v vipActor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (v vipActor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
channel, user, err := v.getParams(m, r, eventData, attrs)
|
channel, user, err := v.getParams(m, r, eventData, attrs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "getting parameters")
|
return false, errors.Wrap(err, "getting parameters")
|
||||||
|
|
|
@ -3,10 +3,13 @@ package whisper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
@ -14,21 +17,19 @@ import (
|
||||||
const actorName = "whisper"
|
const actorName = "whisper"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
botTwitchClient *twitch.Client
|
botTwitchClient func() *twitch.Client
|
||||||
formatMessage plugins.MsgFormatter
|
formatMessage plugins.MsgFormatter
|
||||||
|
|
||||||
ptrStringEmpty = func(s string) *string { return &s }("")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register provides the plugins.RegisterFunc
|
// Register provides the plugins.RegisterFunc
|
||||||
func Register(args plugins.RegistrationArguments) error {
|
func Register(args plugins.RegistrationArguments) error {
|
||||||
botTwitchClient = args.GetTwitchClient()
|
botTwitchClient = args.GetTwitchClient
|
||||||
formatMessage = args.FormatMessage
|
formatMessage = args.FormatMessage
|
||||||
|
|
||||||
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
||||||
|
|
||||||
args.RegisterActorDocumentation(plugins.ActionDocumentation{
|
args.RegisterActorDocumentation(plugins.ActionDocumentation{
|
||||||
Description: "Send a whisper (requires a verified bot!)",
|
Description: "Send a whisper",
|
||||||
Name: "Send Whisper",
|
Name: "Send Whisper",
|
||||||
Type: "whisper",
|
Type: "whisper",
|
||||||
|
|
||||||
|
@ -59,7 +60,7 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
|
|
||||||
type actor struct{}
|
type actor struct{}
|
||||||
|
|
||||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
to, err := formatMessage(attrs.MustString("to", nil), m, r, eventData)
|
to, err := formatMessage(attrs.MustString("to", nil), m, r, eventData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "preparing whisper receiver")
|
return false, errors.Wrap(err, "preparing whisper receiver")
|
||||||
|
@ -71,7 +72,7 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, errors.Wrap(
|
return false, errors.Wrap(
|
||||||
botTwitchClient.SendWhisper(context.Background(), to, msg),
|
botTwitchClient().SendWhisper(context.Background(), to, msg),
|
||||||
"sending whisper",
|
"sending whisper",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -79,19 +80,14 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
func (actor) IsAsync() bool { return false }
|
func (actor) IsAsync() bool { return false }
|
||||||
func (actor) Name() string { return actorName }
|
func (actor) Name() string { return actorName }
|
||||||
|
|
||||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
if v, err := attrs.String("to"); err != nil || v == "" {
|
if err = attrs.ValidateSchema(
|
||||||
return errors.New("to must be non-empty string")
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "message", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
}
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "to", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
if v, err := attrs.String("message"); err != nil || v == "" {
|
helpers.SchemaValidateTemplateField(tplValidator, "message", "to"),
|
||||||
return errors.New("message must be non-empty string")
|
); err != nil {
|
||||||
}
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
|
|
||||||
for _, field := range []string{"message", "to"} {
|
|
||||||
if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil {
|
|
||||||
return errors.Wrapf(err, "validating %s template", field)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
package customevent
|
package customevent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
type actor struct{}
|
type actor struct{}
|
||||||
|
|
||||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
fd, err := formatMessage(attrs.MustString("fields", ptrStringEmpty), m, r, eventData)
|
fd, err := formatMessage(attrs.MustString("fields", ptrStringEmpty), m, r, eventData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "executing fields template")
|
return false, errors.Wrap(err, "executing fields template")
|
||||||
|
@ -35,15 +38,14 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
||||||
func (actor) IsAsync() bool { return false }
|
func (actor) IsAsync() bool { return false }
|
||||||
func (actor) Name() string { return actorName }
|
func (actor) Name() string { return actorName }
|
||||||
|
|
||||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
if v, err := attrs.String("fields"); err != nil || v == "" {
|
if err = attrs.ValidateSchema(
|
||||||
return errors.New("fields is expected to be non-empty string")
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "fields", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
}
|
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "schedule_in", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
for _, field := range []string{"fields", "schedule_in"} {
|
helpers.SchemaValidateTemplateField(tplValidator, "fields", "schedule_in"),
|
||||||
if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil {
|
); err != nil {
|
||||||
return errors.Wrapf(err, "validating %s template", field)
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
@ -130,14 +131,14 @@ func handleCreateEvent(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseEvent(channel string, fieldData io.Reader) (*plugins.FieldCollection, error) {
|
func parseEvent(channel string, fieldData io.Reader) (*fieldcollection.FieldCollection, error) {
|
||||||
payload := make(map[string]any)
|
payload := make(map[string]any)
|
||||||
|
|
||||||
if err := json.NewDecoder(fieldData).Decode(&payload); err != nil {
|
if err := json.NewDecoder(fieldData).Decode(&payload); err != nil {
|
||||||
return nil, errors.Wrap(err, "parsing event payload")
|
return nil, errors.Wrap(err, "parsing event payload")
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := plugins.FieldCollectionFromData(payload)
|
fields := fieldcollection.FieldCollectionFromData(payload)
|
||||||
fields.Set("channel", "#"+strings.TrimLeft(channel, "#"))
|
fields.Set("channel", "#"+strings.TrimLeft(channel, "#"))
|
||||||
|
|
||||||
return fields, nil
|
return fields, nil
|
||||||
|
|
|
@ -9,9 +9,9 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const cleanupTimeout = 15 * time.Minute
|
const cleanupTimeout = 15 * time.Minute
|
||||||
|
@ -48,7 +48,7 @@ func getFutureEvents(db database.Connector) (out []storedCustomEvent, err error)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func storeEvent(db database.Connector, scheduleAt time.Time, channel string, fields *plugins.FieldCollection) error {
|
func storeEvent(db database.Connector, scheduleAt time.Time, channel string, fields *fieldcollection.FieldCollection) error {
|
||||||
fieldBuf := new(bytes.Buffer)
|
fieldBuf := new(bytes.Buffer)
|
||||||
if err := json.NewEncoder(fieldBuf).Encode(fields); err != nil {
|
if err := json.NewEncoder(fieldBuf).Encode(fields); err != nil {
|
||||||
return errors.Wrap(err, "marshalling fields")
|
return errors.Wrap(err, "marshalling fields")
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
@ -87,7 +88,7 @@ func handleKoFiPost(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := plugins.NewFieldCollection()
|
fields := fieldcollection.NewFieldCollection()
|
||||||
fields.Set("channel", "#"+strings.TrimLeft(channel, "#"))
|
fields.Set("channel", "#"+strings.TrimLeft(channel, "#"))
|
||||||
|
|
||||||
switch payload.Type {
|
switch payload.Type {
|
||||||
|
|
|
@ -10,9 +10,9 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/Luzifer/go_helpers/v2/backoff"
|
"github.com/Luzifer/go_helpers/v2/backoff"
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -86,7 +86,7 @@ func getEventByID(db database.Connector, eventID uint64) (socketMessage, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o overlaysEvent) ToSocketMessage() (socketMessage, error) {
|
func (o overlaysEvent) ToSocketMessage() (socketMessage, error) {
|
||||||
fields := new(plugins.FieldCollection)
|
fields := new(fieldcollection.FieldCollection)
|
||||||
if err := json.NewDecoder(strings.NewReader(o.Fields)).Decode(fields); err != nil {
|
if err := json.NewDecoder(strings.NewReader(o.Fields)).Decode(fields); err != nil {
|
||||||
return socketMessage{}, errors.Wrap(err, "decoding fields")
|
return socketMessage{}, errors.Wrap(err, "decoding fields")
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,8 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEventDatabaseRoundtrip(t *testing.T) {
|
func TestEventDatabaseRoundtrip(t *testing.T) {
|
||||||
|
@ -30,7 +30,7 @@ func TestEventDatabaseRoundtrip(t *testing.T) {
|
||||||
IsLive: true,
|
IsLive: true,
|
||||||
Time: tEvent2,
|
Time: tEvent2,
|
||||||
Type: "event 2",
|
Type: "event 2",
|
||||||
Fields: plugins.FieldCollectionFromData(map[string]any{"foo": "bar"}),
|
Fields: fieldcollection.FieldCollectionFromData(map[string]any{"foo": "bar"}),
|
||||||
})
|
})
|
||||||
assert.Equal(t, uint64(1), evtID)
|
assert.Equal(t, uint64(1), evtID)
|
||||||
assert.NoError(t, err, "adding second event")
|
assert.NoError(t, err, "adding second event")
|
||||||
|
@ -39,7 +39,7 @@ func TestEventDatabaseRoundtrip(t *testing.T) {
|
||||||
IsLive: true,
|
IsLive: true,
|
||||||
Time: tEvent1,
|
Time: tEvent1,
|
||||||
Type: "event 1",
|
Type: "event 1",
|
||||||
Fields: plugins.FieldCollectionFromData(map[string]any{"foo": "bar"}),
|
Fields: fieldcollection.FieldCollectionFromData(map[string]any{"foo": "bar"}),
|
||||||
})
|
})
|
||||||
assert.Equal(t, uint64(2), evtID)
|
assert.Equal(t, uint64(2), evtID)
|
||||||
assert.NoError(t, err, "adding first event")
|
assert.NoError(t, err, "adding first event")
|
||||||
|
@ -48,7 +48,7 @@ func TestEventDatabaseRoundtrip(t *testing.T) {
|
||||||
IsLive: true,
|
IsLive: true,
|
||||||
Time: tEvent1,
|
Time: tEvent1,
|
||||||
Type: "event",
|
Type: "event",
|
||||||
Fields: plugins.FieldCollectionFromData(map[string]any{"foo": "bar"}),
|
Fields: fieldcollection.FieldCollectionFromData(map[string]any{"foo": "bar"}),
|
||||||
})
|
})
|
||||||
assert.Equal(t, uint64(3), evtID)
|
assert.Equal(t, uint64(3), evtID)
|
||||||
assert.NoError(t, err, "adding other channel event")
|
assert.NoError(t, err, "adding other channel event")
|
||||||
|
@ -66,6 +66,6 @@ func TestEventDatabaseRoundtrip(t *testing.T) {
|
||||||
IsLive: false,
|
IsLive: false,
|
||||||
Time: tEvent1,
|
Time: tEvent1,
|
||||||
Type: "event 1",
|
Type: "event 1",
|
||||||
Fields: plugins.FieldCollectionFromData(map[string]any{"foo": "bar"}),
|
Fields: fieldcollection.FieldCollectionFromData(map[string]any{"foo": "bar"}),
|
||||||
}, evt)
|
}, evt)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/go_helpers/v2/str"
|
"github.com/Luzifer/go_helpers/v2/str"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
|
@ -41,12 +42,12 @@ type (
|
||||||
|
|
||||||
// socketMessage represents the message overlay sockets will receive
|
// socketMessage represents the message overlay sockets will receive
|
||||||
socketMessage struct {
|
socketMessage struct {
|
||||||
EventID uint64 `json:"event_id"`
|
EventID uint64 `json:"event_id"`
|
||||||
IsLive bool `json:"is_live"`
|
IsLive bool `json:"is_live"`
|
||||||
Reason sendReason `json:"reason"`
|
Reason sendReason `json:"reason"`
|
||||||
Time time.Time `json:"time"`
|
Time time.Time `json:"time"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Fields *plugins.FieldCollection `json:"fields"`
|
Fields *fieldcollection.FieldCollection `json:"fields"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -180,7 +181,7 @@ func Register(args plugins.RegistrationArguments) (err error) {
|
||||||
return fmt.Errorf("registering API route: %w", err)
|
return fmt.Errorf("registering API route: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = args.RegisterEventHandler(func(event string, eventData *plugins.FieldCollection) (err error) {
|
if err = args.RegisterEventHandler(func(event string, eventData *fieldcollection.FieldCollection) (err error) {
|
||||||
subscribersLock.RLock()
|
subscribersLock.RLock()
|
||||||
defer subscribersLock.RUnlock()
|
defer subscribersLock.RUnlock()
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package raffle
|
package raffle
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
@ -17,7 +19,7 @@ var ptrStrEmpty = ptrStr("")
|
||||||
|
|
||||||
func ptrStr(v string) *string { return &v }
|
func ptrStr(v string) *string { return &v }
|
||||||
|
|
||||||
func (enterRaffleActor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, evtData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
func (enterRaffleActor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, evtData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
|
||||||
if m != nil || evtData.MustString("reward_id", ptrStrEmpty) == "" {
|
if m != nil || evtData.MustString("reward_id", ptrStrEmpty) == "" {
|
||||||
return false, errors.New("enter-raffle actor is only supposed to act on channelpoint redeems")
|
return false, errors.New("enter-raffle actor is only supposed to act on channelpoint redeems")
|
||||||
}
|
}
|
||||||
|
@ -43,7 +45,7 @@ func (enterRaffleActor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule,
|
||||||
EnteredAt: time.Now().UTC(),
|
EnteredAt: time.Now().UTC(),
|
||||||
}
|
}
|
||||||
|
|
||||||
raffleEventFields := plugins.FieldCollectionFromData(map[string]any{
|
raffleEventFields := fieldcollection.FieldCollectionFromData(map[string]any{
|
||||||
"user_id": re.UserID,
|
"user_id": re.UserID,
|
||||||
"user": re.UserLogin,
|
"user": re.UserLogin,
|
||||||
})
|
})
|
||||||
|
@ -70,10 +72,12 @@ func (enterRaffleActor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule,
|
||||||
func (enterRaffleActor) IsAsync() bool { return false }
|
func (enterRaffleActor) IsAsync() bool { return false }
|
||||||
func (enterRaffleActor) Name() string { return "enter-raffle" }
|
func (enterRaffleActor) Name() string { return "enter-raffle" }
|
||||||
|
|
||||||
func (enterRaffleActor) Validate(_ plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
func (enterRaffleActor) Validate(_ plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||||
keyword, err := attrs.String("keyword")
|
if err = attrs.ValidateSchema(
|
||||||
if err != nil || keyword == "" {
|
fieldcollection.MustHaveField(fieldcollection.SchemaField{Name: "keyword", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||||
return errors.New("keyword must be non-empty string")
|
fieldcollection.MustHaveNoUnknowFields,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("validating attributes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -9,9 +9,9 @@ import (
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -374,7 +374,7 @@ func (d *dbClient) PickWinner(raffleID uint64) error {
|
||||||
d.speakUp[strings.Join([]string{r.Channel, winner.UserLogin}, ":")] = &speakUpWait{RaffleEntryID: winner.ID, Until: speakUpUntil}
|
d.speakUp[strings.Join([]string{r.Channel, winner.UserLogin}, ":")] = &speakUpWait{RaffleEntryID: winner.ID, Until: speakUpUntil}
|
||||||
d.lock.Unlock()
|
d.lock.Unlock()
|
||||||
|
|
||||||
fields := plugins.FieldCollectionFromData(map[string]any{
|
fields := fieldcollection.FieldCollectionFromData(map[string]any{
|
||||||
"user_id": winner.UserID,
|
"user_id": winner.UserID,
|
||||||
"user": winner.UserLogin,
|
"user": winner.UserLogin,
|
||||||
"winner": winner,
|
"winner": winner,
|
||||||
|
@ -636,9 +636,9 @@ func (d *dbClient) Update(r raffle) error {
|
||||||
|
|
||||||
// SendEvent processes the text template and sends the message if
|
// SendEvent processes the text template and sends the message if
|
||||||
// enabled given through the event
|
// enabled given through the event
|
||||||
func (r raffle) SendEvent(evt raffleMessageEvent, fields *plugins.FieldCollection) (err error) {
|
func (r raffle) SendEvent(evt raffleMessageEvent, fields *fieldcollection.FieldCollection) (err error) {
|
||||||
if fields == nil {
|
if fields == nil {
|
||||||
fields = plugins.NewFieldCollection()
|
fields = fieldcollection.NewFieldCollection()
|
||||||
}
|
}
|
||||||
|
|
||||||
fields.Set("raffle", r) // Make raffle available to templating
|
fields.Set("raffle", r) // Make raffle available to templating
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
@ -95,7 +96,7 @@ func handleRaffleEntry(m *irc.Message, channel, user string) error {
|
||||||
re.UserDisplayName = re.UserLogin
|
re.UserDisplayName = re.UserLogin
|
||||||
}
|
}
|
||||||
|
|
||||||
raffleEventFields := plugins.FieldCollectionFromData(map[string]any{
|
raffleEventFields := fieldcollection.FieldCollectionFromData(map[string]any{
|
||||||
"user_id": m.Tags["user-id"],
|
"user_id": m.Tags["user-id"],
|
||||||
"user": user,
|
"user": user,
|
||||||
})
|
})
|
||||||
|
|
4
internal/helpers/ptr.go
Normal file
4
internal/helpers/ptr.go
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
package helpers
|
||||||
|
|
||||||
|
// Ptr creates a pointer to any given type
|
||||||
|
func Ptr[T any](v T) *T { return &v }
|
21
internal/helpers/validateHelper.go
Normal file
21
internal/helpers/validateHelper.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SchemaValidateTemplateField contains a ValidateOpt for the
|
||||||
|
// fieldcollection schema validator to validate template fields
|
||||||
|
func SchemaValidateTemplateField(tplValidator func(string) error, fields ...string) fieldcollection.ValidateOpt {
|
||||||
|
return func(f, _ *fieldcollection.FieldCollection) (err error) {
|
||||||
|
for _, field := range fields {
|
||||||
|
if err = tplValidator(f.MustString(field, Ptr(""))); err != nil {
|
||||||
|
return fmt.Errorf("validating %s: %w", field, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -179,8 +179,7 @@ func TestScanForLinks(t *testing.T) {
|
||||||
},
|
},
|
||||||
// Case: false positives
|
// Case: false positives
|
||||||
{Heuristic: true, Message: "game dot exe has stopped working", ExpectedLinks: nil},
|
{Heuristic: true, Message: "game dot exe has stopped working", ExpectedLinks: nil},
|
||||||
{Heuristic: false, Message: "You're following since 12.12.2020 DogChamp", ExpectedLinks: nil},
|
{Heuristic: true, Message: "You are following since 12.12.2020 DogChamp", ExpectedLinks: nil},
|
||||||
{Heuristic: true, Message: "You're following since 12.12.2020 DogChamp", ExpectedLinks: []string{"http://You.re"}},
|
|
||||||
{Heuristic: false, Message: "Hey btw. es kann sein, dass", ExpectedLinks: nil},
|
{Heuristic: false, Message: "Hey btw. es kann sein, dass", ExpectedLinks: nil},
|
||||||
} {
|
} {
|
||||||
t.Run(fmt.Sprintf("h:%v lc:%d m:%s", testCase.Heuristic, len(testCase.ExpectedLinks), testCase.Message), func(t *testing.T) {
|
t.Run(fmt.Sprintf("h:%v lc:%d m:%s", testCase.Heuristic, len(testCase.ExpectedLinks), testCase.Message), func(t *testing.T) {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
package access
|
package access
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -29,8 +29,6 @@ type (
|
||||||
TwitchClient string
|
TwitchClient string
|
||||||
TwitchClientSecret string
|
TwitchClientSecret string
|
||||||
|
|
||||||
FallbackToken string // DEPRECATED
|
|
||||||
|
|
||||||
TokenUpdateHook func()
|
TokenUpdateHook func()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,72 +91,11 @@ func (s Service) GetChannelPermissions(channel string) ([]string, error) {
|
||||||
// bot user
|
// bot user
|
||||||
func (s Service) GetBotTwitchClient(cfg ClientConfig) (*twitch.Client, error) {
|
func (s Service) GetBotTwitchClient(cfg ClientConfig) (*twitch.Client, error) {
|
||||||
botUsername, err := s.GetBotUsername()
|
botUsername, err := s.GetBotUsername()
|
||||||
switch {
|
|
||||||
case errors.Is(err, nil):
|
|
||||||
// This is fine, we have a username
|
|
||||||
return s.GetTwitchClientForChannel(botUsername, cfg)
|
|
||||||
|
|
||||||
case errors.Is(err, database.ErrCoreMetaNotFound):
|
|
||||||
// The bot has no username stored, we try to auto-migrate below
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, errors.Wrap(err, "getting bot username from database")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bot username is not set, either we're running from fallback token
|
|
||||||
// or did not yet execute the v3.5.0 migration
|
|
||||||
|
|
||||||
var botAccessToken, botRefreshToken string
|
|
||||||
err = s.db.ReadEncryptedCoreMeta(coreMetaKeyBotToken, &botAccessToken)
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, nil):
|
|
||||||
// This is fine, we do have a pre-v3.5.0 config, lets do the migration
|
|
||||||
|
|
||||||
case errors.Is(err, database.ErrCoreMetaNotFound):
|
|
||||||
// We're don't have a stored pre-v3.5.0 token either, so we're
|
|
||||||
// running from the fallback token (which might be empty)
|
|
||||||
return twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, cfg.FallbackToken, ""), nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, errors.Wrap(err, "getting bot access token from database")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = s.db.ReadEncryptedCoreMeta(coreMetaKeyBotRefreshToken, &botRefreshToken); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "getting bot refresh token from database")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we do have (hopefully valid) tokens for the bot and therefore
|
|
||||||
// can determine who the bot is. That means we can set the username
|
|
||||||
// for later reference and afterwards delete the duplicated tokens.
|
|
||||||
|
|
||||||
_, botUser, err := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, botAccessToken, botRefreshToken).GetAuthorizedUser(context.Background())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "validating stored access token")
|
return nil, fmt.Errorf("getting bot username: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = s.db.StoreCoreMeta(coreMetaKeyBotUsername, botUser); err != nil {
|
return s.GetTwitchClientForChannel(botUsername, cfg)
|
||||||
return nil, errors.Wrap(err, "setting bot username")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = s.GetTwitchClientForChannel(botUser, cfg); errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
// There is no extended permission for that channel, we probably
|
|
||||||
// are in a state created by the v2 migrator. Lets just store the
|
|
||||||
// token without any permissions as we cannot know the permissions
|
|
||||||
// assigned to that token
|
|
||||||
if err = s.SetExtendedTwitchCredentials(botUser, botAccessToken, botRefreshToken, nil); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "moving bot access token")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = s.db.DeleteCoreMeta(coreMetaKeyBotToken); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "deleting deprecated bot token")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = s.db.DeleteCoreMeta(coreMetaKeyBotRefreshToken); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "deleting deprecated bot refresh-token")
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.GetTwitchClientForChannel(botUser, cfg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTwitchClientForChannel returns a twitch.Client configured to act
|
// GetTwitchClientForChannel returns a twitch.Client configured to act
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
package userstate
|
package userstate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
@ -16,7 +17,7 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
return errors.Wrap(err, "registering raw message handler")
|
return errors.Wrap(err, "registering raw message handler")
|
||||||
}
|
}
|
||||||
|
|
||||||
args.RegisterTemplateFunction("botHasBadge", func(m *irc.Message, _ *plugins.Rule, fields *plugins.FieldCollection) interface{} {
|
args.RegisterTemplateFunction("botHasBadge", func(m *irc.Message, _ *plugins.Rule, fields *fieldcollection.FieldCollection) interface{} {
|
||||||
return func(badge string) bool {
|
return func(badge string) bool {
|
||||||
state := userState.Get(plugins.DeriveChannel(m, fields))
|
state := userState.Get(plugins.DeriveChannel(m, fields))
|
||||||
if state == nil {
|
if state == nil {
|
||||||
|
|
15
irc.go
15
irc.go
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
@ -226,7 +227,7 @@ func (i ircHandler) handleClearChat(m *irc.Message) {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
evt *string
|
evt *string
|
||||||
fields = plugins.NewFieldCollection()
|
fields = fieldcollection.NewFieldCollection()
|
||||||
)
|
)
|
||||||
|
|
||||||
fields.Set(eventFieldChannel, i.getChannel(m)) // Compatibility to plugins.DeriveChannel
|
fields.Set(eventFieldChannel, i.getChannel(m)) // Compatibility to plugins.DeriveChannel
|
||||||
|
@ -258,7 +259,7 @@ func (i ircHandler) handleClearChat(m *irc.Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i ircHandler) handleClearMessage(m *irc.Message) {
|
func (i ircHandler) handleClearMessage(m *irc.Message) {
|
||||||
fields := plugins.FieldCollectionFromData(map[string]interface{}{
|
fields := fieldcollection.FieldCollectionFromData(map[string]interface{}{
|
||||||
eventFieldChannel: i.getChannel(m), // Compatibility to plugins.DeriveChannel
|
eventFieldChannel: i.getChannel(m), // Compatibility to plugins.DeriveChannel
|
||||||
"message_id": m.Tags["target-msg-id"],
|
"message_id": m.Tags["target-msg-id"],
|
||||||
"target_name": m.Tags["login"],
|
"target_name": m.Tags["login"],
|
||||||
|
@ -270,7 +271,7 @@ func (i ircHandler) handleClearMessage(m *irc.Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i ircHandler) handleJoin(m *irc.Message) {
|
func (i ircHandler) handleJoin(m *irc.Message) {
|
||||||
fields := plugins.FieldCollectionFromData(map[string]interface{}{
|
fields := fieldcollection.FieldCollectionFromData(map[string]interface{}{
|
||||||
eventFieldChannel: i.getChannel(m), // Compatibility to plugins.DeriveChannel
|
eventFieldChannel: i.getChannel(m), // Compatibility to plugins.DeriveChannel
|
||||||
eventFieldUserName: m.User, // Compatibility to plugins.DeriveUser
|
eventFieldUserName: m.User, // Compatibility to plugins.DeriveUser
|
||||||
})
|
})
|
||||||
|
@ -278,7 +279,7 @@ func (i ircHandler) handleJoin(m *irc.Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i ircHandler) handlePart(m *irc.Message) {
|
func (i ircHandler) handlePart(m *irc.Message) {
|
||||||
fields := plugins.FieldCollectionFromData(map[string]interface{}{
|
fields := fieldcollection.FieldCollectionFromData(map[string]interface{}{
|
||||||
eventFieldChannel: i.getChannel(m), // Compatibility to plugins.DeriveChannel
|
eventFieldChannel: i.getChannel(m), // Compatibility to plugins.DeriveChannel
|
||||||
eventFieldUserName: m.User, // Compatibility to plugins.DeriveUser
|
eventFieldUserName: m.User, // Compatibility to plugins.DeriveUser
|
||||||
})
|
})
|
||||||
|
@ -299,7 +300,7 @@ func (i ircHandler) handlePermit(m *irc.Message) {
|
||||||
|
|
||||||
username := msgParts[1]
|
username := msgParts[1]
|
||||||
|
|
||||||
fields := plugins.FieldCollectionFromData(map[string]interface{}{
|
fields := fieldcollection.FieldCollectionFromData(map[string]interface{}{
|
||||||
eventFieldChannel: i.getChannel(m), // Compatibility to plugins.DeriveChannel
|
eventFieldChannel: i.getChannel(m), // Compatibility to plugins.DeriveChannel
|
||||||
eventFieldUserName: m.User, // Compatibility to plugins.DeriveUser
|
eventFieldUserName: m.User, // Compatibility to plugins.DeriveUser
|
||||||
eventFieldUserID: m.Tags["user-id"],
|
eventFieldUserID: m.Tags["user-id"],
|
||||||
|
@ -356,7 +357,7 @@ func (i ircHandler) handleTwitchPrivmsg(m *irc.Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if bits := i.tagToNumeric(m, "bits", 0); bits > 0 {
|
if bits := i.tagToNumeric(m, "bits", 0); bits > 0 {
|
||||||
fields := plugins.FieldCollectionFromData(map[string]interface{}{
|
fields := fieldcollection.FieldCollectionFromData(map[string]interface{}{
|
||||||
"bits": bits,
|
"bits": bits,
|
||||||
eventFieldChannel: i.getChannel(m), // Compatibility to plugins.DeriveChannel
|
eventFieldChannel: i.getChannel(m), // Compatibility to plugins.DeriveChannel
|
||||||
"message": m.Trailing(),
|
"message": m.Trailing(),
|
||||||
|
@ -380,7 +381,7 @@ func (i ircHandler) handleTwitchUsernotice(m *irc.Message) {
|
||||||
"trailing": m.Trailing(),
|
"trailing": m.Trailing(),
|
||||||
}).Trace("IRC USERNOTICE event")
|
}).Trace("IRC USERNOTICE event")
|
||||||
|
|
||||||
evtData := plugins.FieldCollectionFromData(map[string]any{
|
evtData := fieldcollection.FieldCollectionFromData(map[string]any{
|
||||||
eventFieldChannel: i.getChannel(m), // Compatibility to plugins.DeriveChannel
|
eventFieldChannel: i.getChannel(m), // Compatibility to plugins.DeriveChannel
|
||||||
eventFieldUserName: m.Tags["login"], // Compatibility to plugins.DeriveUser
|
eventFieldUserName: m.Tags["login"], // Compatibility to plugins.DeriveUser
|
||||||
eventFieldUserID: m.Tags["user-id"],
|
eventFieldUserID: m.Tags["user-id"],
|
||||||
|
|
51
main.go
51
main.go
|
@ -54,7 +54,6 @@ var (
|
||||||
StorageEncryptionPass string `flag:"storage-encryption-pass" default:"" description:"Passphrase to encrypt secrets inside storage (defaults to twitch-client:twitch-client-secret)"`
|
StorageEncryptionPass string `flag:"storage-encryption-pass" default:"" description:"Passphrase to encrypt secrets inside storage (defaults to twitch-client:twitch-client-secret)"`
|
||||||
TwitchClient string `flag:"twitch-client" default:"" description:"Client ID to act as"`
|
TwitchClient string `flag:"twitch-client" default:"" description:"Client ID to act as"`
|
||||||
TwitchClientSecret string `flag:"twitch-client-secret" default:"" description:"Secret for the Client ID"`
|
TwitchClientSecret string `flag:"twitch-client-secret" default:"" description:"Secret for the Client ID"`
|
||||||
TwitchToken string `flag:"twitch-token" default:"" description:"OAuth token valid for client (fallback if no token was set in interface) -- DEPRECATED"`
|
|
||||||
ValidateConfig bool `flag:"validate-config,v" default:"false" description:"Loads the config, logs any errors and quits with status 0 on success"`
|
ValidateConfig bool `flag:"validate-config,v" default:"false" description:"Loads the config, logs any errors and quits with status 0 on success"`
|
||||||
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
||||||
WaitForSelfcheck time.Duration `flag:"wait-for-selfcheck" default:"60s" description:"Maximum time to wait for the self-check to respond when behind load-balancers"`
|
WaitForSelfcheck time.Duration `flag:"wait-for-selfcheck" default:"60s" description:"Maximum time to wait for the self-check to respond when behind load-balancers"`
|
||||||
|
@ -117,10 +116,6 @@ func initApp() error {
|
||||||
}, ":")
|
}, ":")
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.TwitchToken != "" {
|
|
||||||
log.Warn("You are using the DEPRECATED --twitch-token flag / TWITCH_TOKEN env variable, please switch to web-based auth! - This flag will be removed in a later release!")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,29 +146,6 @@ func main() {
|
||||||
log.WithError(err).Fatal("applying timer migration")
|
log.WithError(err).Fatal("applying timer migration")
|
||||||
}
|
}
|
||||||
|
|
||||||
if twitchClient, err = accessService.GetBotTwitchClient(access.ClientConfig{
|
|
||||||
TwitchClient: cfg.TwitchClient,
|
|
||||||
TwitchClientSecret: cfg.TwitchClientSecret,
|
|
||||||
FallbackToken: cfg.TwitchToken,
|
|
||||||
TokenUpdateHook: func() {
|
|
||||||
// make frontend reload its state as of token change
|
|
||||||
frontendNotifyHooks.Ping(frontendNotifyTypeReload)
|
|
||||||
},
|
|
||||||
}); err != nil {
|
|
||||||
if !errors.Is(err, access.ErrChannelNotAuthorized) {
|
|
||||||
log.WithError(err).Fatal("initializing Twitch client")
|
|
||||||
}
|
|
||||||
twitchClient = twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, cfg.TwitchToken, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
twitchWatch := newTwitchWatcher()
|
|
||||||
// Query may run that often as the twitchClient has an internal
|
|
||||||
// cache but shouldn't run more often as EventSub subscriptions
|
|
||||||
// are retried on error each time
|
|
||||||
if _, err = cronService.AddFunc("@every 30s", twitchWatch.Check); err != nil {
|
|
||||||
log.WithError(err).Fatal("registering twitchWatch cron")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow config to subscribe to external rules
|
// Allow config to subscribe to external rules
|
||||||
updCron := updateConfigCron()
|
updCron := updateConfigCron()
|
||||||
if _, err = cronService.AddFunc(updCron, updateConfigFromRemote); err != nil {
|
if _, err = cronService.AddFunc(updCron, updateConfigFromRemote); err != nil {
|
||||||
|
@ -257,6 +229,29 @@ func main() {
|
||||||
log.WithError(err).Fatal("Missing required parameters")
|
log.WithError(err).Fatal("Missing required parameters")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if twitchClient, err = accessService.GetBotTwitchClient(access.ClientConfig{
|
||||||
|
TwitchClient: cfg.TwitchClient,
|
||||||
|
TwitchClientSecret: cfg.TwitchClientSecret,
|
||||||
|
TokenUpdateHook: func() {
|
||||||
|
// make frontend reload its state as of token change
|
||||||
|
frontendNotifyHooks.Ping(frontendNotifyTypeReload)
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
if !errors.Is(err, access.ErrChannelNotAuthorized) {
|
||||||
|
log.WithError(err).Fatal("initializing Twitch client")
|
||||||
|
}
|
||||||
|
twitchClient = twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
twitchWatch := newTwitchWatcher()
|
||||||
|
|
||||||
|
// Query may run that often as the twitchClient has an internal
|
||||||
|
// cache but shouldn't run more often as EventSub subscriptions
|
||||||
|
// are retried on error each time
|
||||||
|
if _, err = cronService.AddFunc("@every 30s", twitchWatch.Check); err != nil {
|
||||||
|
log.WithError(err).Fatal("registering twitchWatch cron")
|
||||||
|
}
|
||||||
|
|
||||||
fsEvents := make(chan configChangeEvent, 1)
|
fsEvents := make(chan configChangeEvent, 1)
|
||||||
go watchConfigChanges(cfg.Config, fsEvents)
|
go watchConfigChanges(cfg.Config, fsEvents)
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,7 +20,7 @@ var (
|
||||||
|
|
||||||
stripNewline = regexp.MustCompile(`(?m)\s*\n\s*`)
|
stripNewline = regexp.MustCompile(`(?m)\s*\n\s*`)
|
||||||
|
|
||||||
formatMessageFieldSetters = []func(compiledFields *plugins.FieldCollection, m *irc.Message, fields *plugins.FieldCollection){
|
formatMessageFieldSetters = []func(compiledFields *fieldcollection.FieldCollection, m *irc.Message, fields *fieldcollection.FieldCollection){
|
||||||
formatMessageFieldChannel,
|
formatMessageFieldChannel,
|
||||||
formatMessageFieldMessage,
|
formatMessageFieldMessage,
|
||||||
formatMessageFieldUserID,
|
formatMessageFieldUserID,
|
||||||
|
@ -27,8 +28,8 @@ var (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func formatMessage(tplString string, m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) (string, error) {
|
func formatMessage(tplString string, m *irc.Message, r *plugins.Rule, fields *fieldcollection.FieldCollection) (string, error) {
|
||||||
compiledFields := plugins.NewFieldCollection()
|
compiledFields := fieldcollection.NewFieldCollection()
|
||||||
|
|
||||||
if config != nil {
|
if config != nil {
|
||||||
configLock.RLock()
|
configLock.RLock()
|
||||||
|
@ -61,11 +62,11 @@ func formatMessage(tplString string, m *irc.Message, r *plugins.Rule, fields *pl
|
||||||
return strings.TrimSpace(buf.String()), errors.Wrap(err, "execute template")
|
return strings.TrimSpace(buf.String()), errors.Wrap(err, "execute template")
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatMessageFieldChannel(compiledFields *plugins.FieldCollection, m *irc.Message, fields *plugins.FieldCollection) {
|
func formatMessageFieldChannel(compiledFields *fieldcollection.FieldCollection, m *irc.Message, fields *fieldcollection.FieldCollection) {
|
||||||
compiledFields.Set(eventFieldChannel, plugins.DeriveChannel(m, fields))
|
compiledFields.Set(eventFieldChannel, plugins.DeriveChannel(m, fields))
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatMessageFieldMessage(compiledFields *plugins.FieldCollection, m *irc.Message, _ *plugins.FieldCollection) {
|
func formatMessageFieldMessage(compiledFields *fieldcollection.FieldCollection, m *irc.Message, _ *fieldcollection.FieldCollection) {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -73,7 +74,7 @@ func formatMessageFieldMessage(compiledFields *plugins.FieldCollection, m *irc.M
|
||||||
compiledFields.Set("msg", m)
|
compiledFields.Set("msg", m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatMessageFieldUserID(compiledFields *plugins.FieldCollection, m *irc.Message, _ *plugins.FieldCollection) {
|
func formatMessageFieldUserID(compiledFields *fieldcollection.FieldCollection, m *irc.Message, _ *fieldcollection.FieldCollection) {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -83,7 +84,7 @@ func formatMessageFieldUserID(compiledFields *plugins.FieldCollection, m *irc.Me
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatMessageFieldUsername(compiledFields *plugins.FieldCollection, m *irc.Message, fields *plugins.FieldCollection) {
|
func formatMessageFieldUsername(compiledFields *fieldcollection.FieldCollection, m *irc.Message, fields *fieldcollection.FieldCollection) {
|
||||||
compiledFields.Set("username", plugins.DeriveUser(m, fields))
|
compiledFields.Set("username", plugins.DeriveUser(m, fields))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,7 +94,7 @@ func validateTemplate(tplString string) error {
|
||||||
|
|
||||||
_, err := template.
|
_, err := template.
|
||||||
New(tplString).
|
New(tplString).
|
||||||
Funcs(tplFuncs.GetFuncMap(nil, nil, plugins.NewFieldCollection())).
|
Funcs(tplFuncs.GetFuncMap(nil, nil, fieldcollection.NewFieldCollection())).
|
||||||
Parse(tplString)
|
Parse(tplString)
|
||||||
return errors.Wrap(err, "parsing template")
|
return errors.Wrap(err, "parsing template")
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,7 +119,7 @@ func (c connector) readCoreMeta(key string, value any, processor func(string) (s
|
||||||
if err = helpers.Retry(func() error {
|
if err = helpers.Retry(func() error {
|
||||||
err = c.db.First(&data, "name = ?", key).Error
|
err = c.db.First(&data, "name = ?", key).Error
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return ErrCoreMetaNotFound
|
return backoff.NewErrCannotRetry(ErrCoreMetaNotFound)
|
||||||
}
|
}
|
||||||
return errors.Wrap(err, "querying core meta table")
|
return errors.Wrap(err, "querying core meta table")
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
|
|
@ -29,6 +29,8 @@ const (
|
||||||
EventSubEventTypeChannelPollBegin = "channel.poll.begin"
|
EventSubEventTypeChannelPollBegin = "channel.poll.begin"
|
||||||
EventSubEventTypeChannelPollEnd = "channel.poll.end"
|
EventSubEventTypeChannelPollEnd = "channel.poll.end"
|
||||||
EventSubEventTypeChannelPollProgress = "channel.poll.progress"
|
EventSubEventTypeChannelPollProgress = "channel.poll.progress"
|
||||||
|
EventSubEventTypeChannelSuspiciousUserMessage = "channel.suspicious_user.message"
|
||||||
|
EventSubEventTypeChannelSuspiciousUserUpdate = "channel.suspicious_user.update"
|
||||||
EventSubEventTypeStreamOffline = "stream.offline"
|
EventSubEventTypeStreamOffline = "stream.offline"
|
||||||
EventSubEventTypeStreamOnline = "stream.online"
|
EventSubEventTypeStreamOnline = "stream.online"
|
||||||
EventSubEventTypeUserAuthorizationRevoke = "user.authorization.revoke"
|
EventSubEventTypeUserAuthorizationRevoke = "user.authorization.revoke"
|
||||||
|
@ -235,6 +237,53 @@ type (
|
||||||
StartedAt time.Time `json:"started_at"`
|
StartedAt time.Time `json:"started_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EventSubEventSuspiciousUserMessage contains the payload for a
|
||||||
|
// channel.suspicious_user.message
|
||||||
|
EventSubEventSuspiciousUserMessage struct {
|
||||||
|
BroadcasterUserID string `json:"broadcaster_user_id"`
|
||||||
|
BroadcasterUserName string `json:"broadcaster_user_name"`
|
||||||
|
BroadcasterUserLogin string `json:"broadcaster_user_login"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
UserName string `json:"user_name"`
|
||||||
|
UserLogin string `json:"user_login"`
|
||||||
|
LowTrustStatus string `json:"low_trust_status"` // Can be the following: "none", "active_monitoring", or "restricted"
|
||||||
|
SharedBanChannelIDs []string `json:"shared_ban_channel_ids"`
|
||||||
|
Types []string `json:"types"` // can be "manual", "ban_evader_detector", or "shared_channel_ban"
|
||||||
|
BanEvasionEvaluation string `json:"ban_evasion_evaluation"` // can be "unknown", "possible", or "likely"
|
||||||
|
Message struct {
|
||||||
|
MessageID string `json:"message_id"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
Fragments []struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
Cheermote struct {
|
||||||
|
Prefix string `json:"prefix"`
|
||||||
|
Bits int `json:"bits"`
|
||||||
|
Tier int `json:"tier"`
|
||||||
|
} `json:"Cheermote"`
|
||||||
|
Emote struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
EmoteSetID string `json:"emote_set_id"`
|
||||||
|
} `json:"emote"`
|
||||||
|
} `json:"fragments"`
|
||||||
|
} `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventSubEventSuspiciousUserUpdated contains the payload for a
|
||||||
|
// channel.suspicious_user.update
|
||||||
|
EventSubEventSuspiciousUserUpdated struct {
|
||||||
|
BroadcasterUserID string `json:"broadcaster_user_id"`
|
||||||
|
BroadcasterUserName string `json:"broadcaster_user_name"`
|
||||||
|
BroadcasterUserLogin string `json:"broadcaster_user_login"`
|
||||||
|
ModeratorUserID string `json:"moderator_user_id"`
|
||||||
|
ModeratorUserName string `json:"moderator_user_name"`
|
||||||
|
ModeratorUserLogin string `json:"moderator_user_login"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
UserName string `json:"user_name"`
|
||||||
|
UserLogin string `json:"user_login"`
|
||||||
|
LowTrustStatus string `json:"low_trust_status"` // Can be the following: "none", "active_monitoring", or "restricted"
|
||||||
|
}
|
||||||
|
|
||||||
// EventSubEventUserAuthorizationRevoke contains the payload for an
|
// EventSubEventUserAuthorizationRevoke contains the payload for an
|
||||||
// authorization revoke event
|
// authorization revoke event
|
||||||
EventSubEventUserAuthorizationRevoke struct {
|
EventSubEventUserAuthorizationRevoke struct {
|
||||||
|
@ -324,7 +373,6 @@ func (c *Client) createEventSubSubscription(ctx context.Context, auth AuthType,
|
||||||
if mustFetchSubsctiption {
|
if mustFetchSubsctiption {
|
||||||
params := make(url.Values)
|
params := make(url.Values)
|
||||||
params.Set("status", "enabled")
|
params.Set("status", "enabled")
|
||||||
params.Set("type", sub.Type)
|
|
||||||
|
|
||||||
if err = c.Request(ctx, ClientRequestOpts{
|
if err = c.Request(ctx, ClientRequestOpts{
|
||||||
AuthType: auth,
|
AuthType: auth,
|
||||||
|
|
|
@ -3,35 +3,36 @@ package twitch
|
||||||
// Collection of known API scopes
|
// Collection of known API scopes
|
||||||
const (
|
const (
|
||||||
// API Scopes
|
// API Scopes
|
||||||
ScopeChannelBot = "channel:bot"
|
ScopeChannelBot = "channel:bot"
|
||||||
ScopeChannelEditCommercial = "channel:edit:commercial"
|
ScopeChannelEditCommercial = "channel:edit:commercial"
|
||||||
ScopeChannelManageAds = "channel:manage:ads"
|
ScopeChannelManageAds = "channel:manage:ads"
|
||||||
ScopeChannelManageBroadcast = "channel:manage:broadcast"
|
ScopeChannelManageBroadcast = "channel:manage:broadcast"
|
||||||
ScopeChannelManageModerators = "channel:manage:moderators"
|
ScopeChannelManageModerators = "channel:manage:moderators"
|
||||||
ScopeChannelManagePolls = "channel:manage:polls"
|
ScopeChannelManagePolls = "channel:manage:polls"
|
||||||
ScopeChannelManagePredictions = "channel:manage:predictions"
|
ScopeChannelManagePredictions = "channel:manage:predictions"
|
||||||
ScopeChannelManageRaids = "channel:manage:raids"
|
ScopeChannelManageRaids = "channel:manage:raids"
|
||||||
ScopeChannelManageRedemptions = "channel:manage:redemptions"
|
ScopeChannelManageRedemptions = "channel:manage:redemptions"
|
||||||
ScopeChannelManageVIPS = "channel:manage:vips"
|
ScopeChannelManageVIPS = "channel:manage:vips"
|
||||||
ScopeChannelManageWhispers = "user:manage:whispers"
|
ScopeChannelManageWhispers = "user:manage:whispers"
|
||||||
ScopeChannelReadAds = "channel:read:ads"
|
ScopeChannelReadAds = "channel:read:ads"
|
||||||
ScopeChannelReadHypetrain = "channel:read:hype_train"
|
ScopeChannelReadHypetrain = "channel:read:hype_train"
|
||||||
ScopeChannelReadPolls = "channel:read:polls"
|
ScopeChannelReadPolls = "channel:read:polls"
|
||||||
ScopeChannelReadRedemptions = "channel:read:redemptions"
|
ScopeChannelReadRedemptions = "channel:read:redemptions"
|
||||||
ScopeChannelReadSubscriptions = "channel:read:subscriptions"
|
ScopeChannelReadSubscriptions = "channel:read:subscriptions"
|
||||||
ScopeClipsEdit = "clips:edit"
|
ScopeClipsEdit = "clips:edit"
|
||||||
ScopeModeratorManageAnnoucements = "moderator:manage:announcements"
|
ScopeModeratorManageAnnoucements = "moderator:manage:announcements"
|
||||||
ScopeModeratorManageBannedUsers = "moderator:manage:banned_users"
|
ScopeModeratorManageBannedUsers = "moderator:manage:banned_users"
|
||||||
ScopeModeratorManageChatMessages = "moderator:manage:chat_messages"
|
ScopeModeratorManageChatMessages = "moderator:manage:chat_messages"
|
||||||
ScopeModeratorManageChatSettings = "moderator:manage:chat_settings"
|
ScopeModeratorManageChatSettings = "moderator:manage:chat_settings"
|
||||||
ScopeModeratorManageShieldMode = "moderator:manage:shield_mode"
|
ScopeModeratorManageShieldMode = "moderator:manage:shield_mode"
|
||||||
ScopeModeratorManageShoutouts = "moderator:manage:shoutouts"
|
ScopeModeratorManageShoutouts = "moderator:manage:shoutouts"
|
||||||
ScopeModeratorReadFollowers = "moderator:read:followers"
|
ScopeModeratorReadFollowers = "moderator:read:followers"
|
||||||
ScopeModeratorReadShoutouts = "moderator:read:shoutouts"
|
ScopeModeratorReadShoutouts = "moderator:read:shoutouts"
|
||||||
ScopeUserBot = "user:bot"
|
ScopeModeratorReadSuspiciousUsers = "moderator:read:suspicious_users"
|
||||||
ScopeUserManageChatColor = "user:manage:chat_color"
|
ScopeUserBot = "user:bot"
|
||||||
ScopeUserManageWhispers = "user:manage:whispers"
|
ScopeUserManageChatColor = "user:manage:chat_color"
|
||||||
ScopeUserReadChat = "user:read:chat"
|
ScopeUserManageWhispers = "user:manage:whispers"
|
||||||
|
ScopeUserReadChat = "user:read:chat"
|
||||||
|
|
||||||
// Deprecated v5 scope but used in chat
|
// Deprecated v5 scope but used in chat
|
||||||
ScopeV5ChannelEditor = "channel_editor"
|
ScopeV5ChannelEditor = "channel_editor"
|
||||||
|
|
|
@ -3,6 +3,7 @@ package plugins
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,9 +17,9 @@ type (
|
||||||
// (not returning ErrValueNotSet) and does not contain zero value
|
// (not returning ErrValueNotSet) and does not contain zero value
|
||||||
// recognized by reflect (to just check whether the field is set
|
// recognized by reflect (to just check whether the field is set
|
||||||
// but allow zero values use HasAll on the FieldCollection)
|
// but allow zero values use HasAll on the FieldCollection)
|
||||||
func (ActorKit) ValidateRequireNonEmpty(attrs *FieldCollection, fields ...string) error {
|
func (ActorKit) ValidateRequireNonEmpty(attrs *fieldcollection.FieldCollection, fields ...string) error {
|
||||||
for _, field := range fields {
|
for _, field := range fields {
|
||||||
v, err := attrs.Any(field)
|
v, err := attrs.Get(field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "getting field %s", field)
|
return errors.Wrapf(err, "getting field %s", field)
|
||||||
}
|
}
|
||||||
|
@ -34,7 +35,7 @@ func (ActorKit) ValidateRequireNonEmpty(attrs *FieldCollection, fields ...string
|
||||||
// ValidateRequireValidTemplate checks whether fields are gettable
|
// ValidateRequireValidTemplate checks whether fields are gettable
|
||||||
// as strings and do have a template which validates (this does not
|
// as strings and do have a template which validates (this does not
|
||||||
// check for empty strings as an empty template is indeed valid)
|
// check for empty strings as an empty template is indeed valid)
|
||||||
func (ActorKit) ValidateRequireValidTemplate(tplValidator TemplateValidatorFunc, attrs *FieldCollection, fields ...string) error {
|
func (ActorKit) ValidateRequireValidTemplate(tplValidator TemplateValidatorFunc, attrs *fieldcollection.FieldCollection, fields ...string) error {
|
||||||
for _, field := range fields {
|
for _, field := range fields {
|
||||||
v, err := attrs.String(field)
|
v, err := attrs.String(field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -52,11 +53,11 @@ func (ActorKit) ValidateRequireValidTemplate(tplValidator TemplateValidatorFunc,
|
||||||
// ValidateRequireValidTemplateIfSet checks whether the field is
|
// ValidateRequireValidTemplateIfSet checks whether the field is
|
||||||
// either not set or a valid template (this does not
|
// either not set or a valid template (this does not
|
||||||
// check for empty strings as an empty template is indeed valid)
|
// check for empty strings as an empty template is indeed valid)
|
||||||
func (ActorKit) ValidateRequireValidTemplateIfSet(tplValidator TemplateValidatorFunc, attrs *FieldCollection, fields ...string) error {
|
func (ActorKit) ValidateRequireValidTemplateIfSet(tplValidator TemplateValidatorFunc, attrs *fieldcollection.FieldCollection, fields ...string) error {
|
||||||
for _, field := range fields {
|
for _, field := range fields {
|
||||||
v, err := attrs.String(field)
|
v, err := attrs.String(field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ErrValueNotSet) {
|
if errors.Is(err, fieldcollection.ErrValueNotSet) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return errors.Wrapf(err, "getting string field %s", field)
|
return errors.Wrapf(err, "getting string field %s", field)
|
||||||
|
|
|
@ -4,11 +4,12 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestValidateRequireNonEmpty(t *testing.T) {
|
func TestValidateRequireNonEmpty(t *testing.T) {
|
||||||
attrs := FieldCollectionFromData(map[string]any{
|
attrs := fieldcollection.FieldCollectionFromData(map[string]any{
|
||||||
"str": "",
|
"str": "",
|
||||||
"str_v": "valid",
|
"str_v": "valid",
|
||||||
"int": 0,
|
"int": 0,
|
||||||
|
|
|
@ -1,407 +0,0 @@
|
||||||
package plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrValueNotSet is used to notify the value is not available in the FieldCollection
|
|
||||||
ErrValueNotSet = errors.New("specified value not found")
|
|
||||||
// ErrValueMismatch is used to notify the value does not match the requested type
|
|
||||||
ErrValueMismatch = errors.New("specified value has different format")
|
|
||||||
)
|
|
||||||
|
|
||||||
// FieldCollection holds an map[string]any with conversion functions attached
|
|
||||||
type FieldCollection struct {
|
|
||||||
data map[string]any
|
|
||||||
lock sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFieldCollection creates a new FieldCollection with empty data store
|
|
||||||
func NewFieldCollection() *FieldCollection {
|
|
||||||
return &FieldCollection{data: make(map[string]any)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FieldCollectionFromData is a wrapper around NewFieldCollection and SetFromData
|
|
||||||
func FieldCollectionFromData(data map[string]any) *FieldCollection {
|
|
||||||
o := NewFieldCollection()
|
|
||||||
o.SetFromData(data)
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
// CanBool tries to read key name as bool and checks whether error is nil
|
|
||||||
func (f *FieldCollection) CanBool(name string) bool {
|
|
||||||
_, err := f.Bool(name)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CanDuration tries to read key name as time.Duration and checks whether error is nil
|
|
||||||
func (f *FieldCollection) CanDuration(name string) bool {
|
|
||||||
_, err := f.Duration(name)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CanInt64 tries to read key name as int64 and checks whether error is nil
|
|
||||||
func (f *FieldCollection) CanInt64(name string) bool {
|
|
||||||
_, err := f.Int64(name)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CanString tries to read key name as string and checks whether error is nil
|
|
||||||
func (f *FieldCollection) CanString(name string) bool {
|
|
||||||
_, err := f.String(name)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone is a wrapper around n.SetFromData(o.Data())
|
|
||||||
func (f *FieldCollection) Clone() *FieldCollection {
|
|
||||||
out := new(FieldCollection)
|
|
||||||
out.SetFromData(f.Data())
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data creates a map-copy of the data stored inside the FieldCollection
|
|
||||||
func (f *FieldCollection) Data() map[string]any {
|
|
||||||
if f == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
out := make(map[string]any)
|
|
||||||
for k := range f.data {
|
|
||||||
out[k] = f.data[k]
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expect takes a list of keys and returns an error with all non-found names
|
|
||||||
func (f *FieldCollection) Expect(keys ...string) error {
|
|
||||||
if len(keys) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if f == nil || f.data == nil {
|
|
||||||
return errors.New("uninitialized field collection")
|
|
||||||
}
|
|
||||||
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
var missing []string
|
|
||||||
|
|
||||||
for _, k := range keys {
|
|
||||||
if _, ok := f.data[k]; !ok {
|
|
||||||
missing = append(missing, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(missing) > 0 {
|
|
||||||
return errors.Errorf("missing key(s) %s", strings.Join(missing, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasAll takes a list of keys and returns whether all of them exist inside the FieldCollection
|
|
||||||
func (f *FieldCollection) HasAll(keys ...string) bool {
|
|
||||||
return f.Expect(keys...) == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustBool is a wrapper around Bool and panics if an error was returned
|
|
||||||
func (f *FieldCollection) MustBool(name string, defVal *bool) bool {
|
|
||||||
v, err := f.Bool(name)
|
|
||||||
if err != nil {
|
|
||||||
if defVal != nil {
|
|
||||||
return *defVal
|
|
||||||
}
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustDuration is a wrapper around Duration and panics if an error was returned
|
|
||||||
func (f *FieldCollection) MustDuration(name string, defVal *time.Duration) time.Duration {
|
|
||||||
v, err := f.Duration(name)
|
|
||||||
if err != nil {
|
|
||||||
if defVal != nil {
|
|
||||||
return *defVal
|
|
||||||
}
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustInt64 is a wrapper around Int64 and panics if an error was returned
|
|
||||||
func (f *FieldCollection) MustInt64(name string, defVal *int64) int64 {
|
|
||||||
v, err := f.Int64(name)
|
|
||||||
if err != nil {
|
|
||||||
if defVal != nil {
|
|
||||||
return *defVal
|
|
||||||
}
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustString is a wrapper around String and panics if an error was returned
|
|
||||||
func (f *FieldCollection) MustString(name string, defVal *string) string {
|
|
||||||
v, err := f.String(name)
|
|
||||||
if err != nil {
|
|
||||||
if defVal != nil {
|
|
||||||
return *defVal
|
|
||||||
}
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustStringSlice is a wrapper around StringSlice and returns nil in case name is not set
|
|
||||||
func (f *FieldCollection) MustStringSlice(name string) []string {
|
|
||||||
v, err := f.StringSlice(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Any tries to read key name as any-type (interface)
|
|
||||||
func (f *FieldCollection) Any(name string) (any, error) {
|
|
||||||
if f == nil || f.data == nil {
|
|
||||||
return false, errors.New("uninitialized field collection")
|
|
||||||
}
|
|
||||||
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
v, ok := f.data[name]
|
|
||||||
if !ok {
|
|
||||||
return false, ErrValueNotSet
|
|
||||||
}
|
|
||||||
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bool tries to read key name as bool
|
|
||||||
func (f *FieldCollection) Bool(name string) (bool, error) {
|
|
||||||
if f == nil || f.data == nil {
|
|
||||||
return false, errors.New("uninitialized field collection")
|
|
||||||
}
|
|
||||||
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
v, ok := f.data[name]
|
|
||||||
if !ok {
|
|
||||||
return false, ErrValueNotSet
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := v.(type) {
|
|
||||||
case bool:
|
|
||||||
return v, nil
|
|
||||||
case string:
|
|
||||||
bv, err := strconv.ParseBool(v)
|
|
||||||
return bv, errors.Wrap(err, "parsing string to bool")
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, ErrValueMismatch
|
|
||||||
}
|
|
||||||
|
|
||||||
// Duration tries to read key name as time.Duration
|
|
||||||
func (f *FieldCollection) Duration(name string) (time.Duration, error) {
|
|
||||||
if f == nil || f.data == nil {
|
|
||||||
return 0, errors.New("uninitialized field collection")
|
|
||||||
}
|
|
||||||
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
v, err := f.String(name)
|
|
||||||
if err != nil {
|
|
||||||
return 0, errors.Wrap(err, "getting string value")
|
|
||||||
}
|
|
||||||
|
|
||||||
d, err := time.ParseDuration(v)
|
|
||||||
return d, errors.Wrap(err, "parsing value")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Int64 tries to read key name as int64
|
|
||||||
func (f *FieldCollection) Int64(name string) (int64, error) {
|
|
||||||
if f == nil || f.data == nil {
|
|
||||||
return 0, errors.New("uninitialized field collection")
|
|
||||||
}
|
|
||||||
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
v, ok := f.data[name]
|
|
||||||
if !ok {
|
|
||||||
return 0, ErrValueNotSet
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := v.(type) {
|
|
||||||
case float64:
|
|
||||||
return int64(v), nil
|
|
||||||
case int:
|
|
||||||
return int64(v), nil
|
|
||||||
case int16:
|
|
||||||
return int64(v), nil
|
|
||||||
case int32:
|
|
||||||
return int64(v), nil
|
|
||||||
case int64:
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, ErrValueMismatch
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set sets a single key to specified value
|
|
||||||
func (f *FieldCollection) Set(key string, value any) {
|
|
||||||
if f == nil {
|
|
||||||
f = NewFieldCollection()
|
|
||||||
}
|
|
||||||
|
|
||||||
f.lock.Lock()
|
|
||||||
defer f.lock.Unlock()
|
|
||||||
|
|
||||||
if f.data == nil {
|
|
||||||
f.data = make(map[string]any)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.data[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFromData takes a map of data and copies all data into the FieldCollection
|
|
||||||
func (f *FieldCollection) SetFromData(data map[string]any) {
|
|
||||||
if f == nil {
|
|
||||||
f = NewFieldCollection()
|
|
||||||
}
|
|
||||||
|
|
||||||
f.lock.Lock()
|
|
||||||
defer f.lock.Unlock()
|
|
||||||
|
|
||||||
if f.data == nil {
|
|
||||||
f.data = make(map[string]any)
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, value := range data {
|
|
||||||
f.data[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String tries to read key name as string
|
|
||||||
func (f *FieldCollection) String(name string) (string, error) {
|
|
||||||
if f == nil || f.data == nil {
|
|
||||||
return "", errors.New("uninitialized field collection")
|
|
||||||
}
|
|
||||||
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
v, ok := f.data[name]
|
|
||||||
if !ok {
|
|
||||||
return "", ErrValueNotSet
|
|
||||||
}
|
|
||||||
|
|
||||||
if sv, ok := v.(string); ok {
|
|
||||||
return sv, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if iv, ok := v.(fmt.Stringer); ok {
|
|
||||||
return iv.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%v", v), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// StringSlice tries to read key name as []string
|
|
||||||
func (f *FieldCollection) StringSlice(name string) ([]string, error) {
|
|
||||||
if f == nil || f.data == nil {
|
|
||||||
return nil, errors.New("uninitialized field collection")
|
|
||||||
}
|
|
||||||
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
v, ok := f.data[name]
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrValueNotSet
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := v.(type) {
|
|
||||||
case []string:
|
|
||||||
return v, nil
|
|
||||||
|
|
||||||
case []any:
|
|
||||||
var out []string
|
|
||||||
|
|
||||||
for _, iv := range v {
|
|
||||||
sv, ok := iv.(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("value in slice was not string")
|
|
||||||
}
|
|
||||||
out = append(out, sv)
|
|
||||||
}
|
|
||||||
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, ErrValueMismatch
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implement JSON marshalling to plain underlying map[string]any
|
|
||||||
|
|
||||||
// MarshalJSON implements the json.Marshaller interface
|
|
||||||
func (f *FieldCollection) MarshalJSON() ([]byte, error) {
|
|
||||||
if f == nil || f.data == nil {
|
|
||||||
return []byte("{}"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
data, err := json.Marshal(f.data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("marshalling data to json: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON implements the json.Unmarshaller interface
|
|
||||||
func (f *FieldCollection) UnmarshalJSON(raw []byte) error {
|
|
||||||
data := make(map[string]any)
|
|
||||||
if err := json.Unmarshal(raw, &data); err != nil {
|
|
||||||
return errors.Wrap(err, "unmarshalling from JSON")
|
|
||||||
}
|
|
||||||
|
|
||||||
f.SetFromData(data)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implement YAML marshalling to plain underlying map[string]any
|
|
||||||
|
|
||||||
// MarshalYAML implements the yaml.Marshaller interface
|
|
||||||
func (f *FieldCollection) MarshalYAML() (any, error) {
|
|
||||||
return f.Data(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalYAML implements the yaml.Unmarshaller interface
|
|
||||||
func (f *FieldCollection) UnmarshalYAML(unmarshal func(any) error) error {
|
|
||||||
data := make(map[string]any)
|
|
||||||
if err := unmarshal(&data); err != nil {
|
|
||||||
return errors.Wrap(err, "unmarshalling from YAML")
|
|
||||||
}
|
|
||||||
|
|
||||||
f.SetFromData(data)
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,93 +0,0 @@
|
||||||
package plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFieldCollectionJSONMarshal(t *testing.T) {
|
|
||||||
var (
|
|
||||||
buf = new(bytes.Buffer)
|
|
||||||
raw = `{"key1":"test1","key2":"test2"}`
|
|
||||||
f = NewFieldCollection()
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := json.NewDecoder(strings.NewReader(raw)).Decode(f); err != nil {
|
|
||||||
t.Fatalf("Unable to unmarshal: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewEncoder(buf).Encode(f); err != nil {
|
|
||||||
t.Fatalf("Unable to marshal: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if raw != strings.TrimSpace(buf.String()) {
|
|
||||||
t.Errorf("Marshalled JSON does not match expectation: res=%s exp=%s", buf.String(), raw)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFieldCollectionYAMLMarshal(t *testing.T) {
|
|
||||||
var (
|
|
||||||
buf = new(bytes.Buffer)
|
|
||||||
raw = "key1: test1\nkey2: test2"
|
|
||||||
f = NewFieldCollection()
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := yaml.NewDecoder(strings.NewReader(raw)).Decode(f); err != nil {
|
|
||||||
t.Fatalf("Unable to unmarshal: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := yaml.NewEncoder(buf).Encode(f); err != nil {
|
|
||||||
t.Fatalf("Unable to marshal: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if raw != strings.TrimSpace(buf.String()) {
|
|
||||||
t.Errorf("Marshalled YAML does not match expectation: res=%s exp=%s", buf.String(), raw)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFieldCollectionNilModify(_ *testing.T) {
|
|
||||||
var f *FieldCollection
|
|
||||||
|
|
||||||
f.Set("foo", "bar")
|
|
||||||
|
|
||||||
f = nil
|
|
||||||
f.SetFromData(map[string]interface{}{"foo": "bar"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFieldCollectionNilClone(_ *testing.T) {
|
|
||||||
var f *FieldCollection
|
|
||||||
|
|
||||||
f.Clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFieldCollectionNilDataGet(t *testing.T) {
|
|
||||||
var f *FieldCollection
|
|
||||||
|
|
||||||
for name, fn := range map[string]func(name string) bool{
|
|
||||||
"bool": f.CanBool,
|
|
||||||
"duration": f.CanDuration,
|
|
||||||
"int64": f.CanInt64,
|
|
||||||
"string": f.CanString,
|
|
||||||
} {
|
|
||||||
if fn("foo") {
|
|
||||||
t.Errorf("%s key is available", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFieldCollectionIntToString(t *testing.T) {
|
|
||||||
val := 123
|
|
||||||
fc := FieldCollectionFromData(map[string]interface{}{"test": val})
|
|
||||||
|
|
||||||
if !fc.CanString("test") {
|
|
||||||
t.Fatalf("cannot convert %T to string", val)
|
|
||||||
}
|
|
||||||
|
|
||||||
if v := fc.MustString("test", nil); v != "123" {
|
|
||||||
t.Errorf("unexpected value: 123 != %s", v)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,12 +4,13 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DeriveChannel takes an irc.Message and a FieldCollection and tries
|
// DeriveChannel takes an irc.Message and a FieldCollection and tries
|
||||||
// to extract from them the channel the event / message has taken place
|
// to extract from them the channel the event / message has taken place
|
||||||
func DeriveChannel(m *irc.Message, evtData *FieldCollection) string {
|
func DeriveChannel(m *irc.Message, evtData *fieldcollection.FieldCollection) string {
|
||||||
if m != nil && len(m.Params) > 0 && strings.HasPrefix(m.Params[0], "#") {
|
if m != nil && len(m.Params) > 0 && strings.HasPrefix(m.Params[0], "#") {
|
||||||
return m.Params[0]
|
return m.Params[0]
|
||||||
}
|
}
|
||||||
|
@ -23,7 +24,7 @@ func DeriveChannel(m *irc.Message, evtData *FieldCollection) string {
|
||||||
|
|
||||||
// DeriveUser takes an irc.Message and a FieldCollection and tries
|
// DeriveUser takes an irc.Message and a FieldCollection and tries
|
||||||
// to extract from them the user causing the event / message
|
// to extract from them the user causing the event / message
|
||||||
func DeriveUser(m *irc.Message, evtData *FieldCollection) string {
|
func DeriveUser(m *irc.Message, evtData *fieldcollection.FieldCollection) string {
|
||||||
if m != nil && m.User != "" {
|
if m != nil && m.User != "" {
|
||||||
return m.User
|
return m.User
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
)
|
)
|
||||||
|
@ -15,7 +16,7 @@ type (
|
||||||
// Actor defines an interface to implement in the plugin for actors
|
// Actor defines an interface to implement in the plugin for actors
|
||||||
Actor interface {
|
Actor interface {
|
||||||
// Execute will be called after the config was read into the Actor
|
// Execute will be called after the config was read into the Actor
|
||||||
Execute(c *irc.Client, m *irc.Message, r *Rule, evtData *FieldCollection, attrs *FieldCollection) (preventCooldown bool, err error)
|
Execute(c *irc.Client, m *irc.Message, r *Rule, evtData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error)
|
||||||
// IsAsync may return true if the Execute function is to be executed
|
// IsAsync may return true if the Execute function is to be executed
|
||||||
// in a Go routine as of long runtime. Normally it should return false
|
// in a Go routine as of long runtime. Normally it should return false
|
||||||
// except in very specific cases
|
// except in very specific cases
|
||||||
|
@ -26,7 +27,7 @@ type (
|
||||||
// Validate will be called to validate the loaded configuration. It should
|
// Validate will be called to validate the loaded configuration. It should
|
||||||
// return an error if required keys are missing from the AttributeStore
|
// return an error if required keys are missing from the AttributeStore
|
||||||
// or if keys contain broken configs
|
// or if keys contain broken configs
|
||||||
Validate(TemplateValidatorFunc, *FieldCollection) error
|
Validate(TemplateValidatorFunc, *fieldcollection.FieldCollection) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActorCreationFunc is a function to return a new instance of the
|
// ActorCreationFunc is a function to return a new instance of the
|
||||||
|
@ -62,7 +63,7 @@ type (
|
||||||
|
|
||||||
// EventHandlerFunc defines the type of function required to listen
|
// EventHandlerFunc defines the type of function required to listen
|
||||||
// for events
|
// for events
|
||||||
EventHandlerFunc func(evt string, eventData *FieldCollection) error
|
EventHandlerFunc func(evt string, eventData *fieldcollection.FieldCollection) error
|
||||||
// EventHandlerRegisterFunc is passed from the bot to the
|
// EventHandlerRegisterFunc is passed from the bot to the
|
||||||
// plugins RegisterFunc to register a new event handler function
|
// plugins RegisterFunc to register a new event handler function
|
||||||
// which is then fed with all events occurring in the bot
|
// which is then fed with all events occurring in the bot
|
||||||
|
@ -76,12 +77,12 @@ type (
|
||||||
// ModuleConfigGetterFunc is passed from the bot to the
|
// ModuleConfigGetterFunc is passed from the bot to the
|
||||||
// plugins RegisterFunc to fetch module generic or channel specific
|
// plugins RegisterFunc to fetch module generic or channel specific
|
||||||
// configuration from the module configuration
|
// configuration from the module configuration
|
||||||
ModuleConfigGetterFunc func(module, channel string) *FieldCollection
|
ModuleConfigGetterFunc func(module, channel string) *fieldcollection.FieldCollection
|
||||||
|
|
||||||
// MsgFormatter is passed from the bot to the
|
// MsgFormatter is passed from the bot to the
|
||||||
// plugins RegisterFunc to format messages using all registered and
|
// plugins RegisterFunc to format messages using all registered and
|
||||||
// available template functions
|
// available template functions
|
||||||
MsgFormatter func(tplString string, m *irc.Message, r *Rule, fields *FieldCollection) (string, error)
|
MsgFormatter func(tplString string, m *irc.Message, r *Rule, fields *fieldcollection.FieldCollection) (string, error)
|
||||||
|
|
||||||
// MsgModificationFunc can be used to modify messages between the
|
// MsgModificationFunc can be used to modify messages between the
|
||||||
// plugins generating them and the bot sending them to the Twitch
|
// plugins generating them and the bot sending them to the Twitch
|
||||||
|
@ -160,7 +161,7 @@ type (
|
||||||
|
|
||||||
// TemplateFuncGetter is the type of function to implement in the
|
// TemplateFuncGetter is the type of function to implement in the
|
||||||
// plugin to create a new template function on request of the bot
|
// plugin to create a new template function on request of the bot
|
||||||
TemplateFuncGetter func(*irc.Message, *Rule, *FieldCollection) any
|
TemplateFuncGetter func(*irc.Message, *Rule, *fieldcollection.FieldCollection) any
|
||||||
// TemplateFuncRegister is passed from the bot to the
|
// TemplateFuncRegister is passed from the bot to the
|
||||||
// plugins RegisterFunc to register a new TemplateFuncGetter
|
// plugins RegisterFunc to register a new TemplateFuncGetter
|
||||||
TemplateFuncRegister func(name string, fg TemplateFuncGetter, doc ...TemplateFuncDocumentation)
|
TemplateFuncRegister func(name string, fg TemplateFuncGetter, doc ...TemplateFuncDocumentation)
|
||||||
|
@ -184,5 +185,5 @@ var ErrSkipSendingMessage = errors.New("skip sending message")
|
||||||
// requiring access to the irc.Message, Rule or FieldCollection to
|
// requiring access to the irc.Message, Rule or FieldCollection to
|
||||||
// satisfy the TemplateFuncGetter interface
|
// satisfy the TemplateFuncGetter interface
|
||||||
func GenericTemplateFunctionGetter(f any) TemplateFuncGetter {
|
func GenericTemplateFunctionGetter(f any) TemplateFuncGetter {
|
||||||
return func(*irc.Message, *Rule, *FieldCollection) any { return f }
|
return func(*irc.Message, *Rule, *fieldcollection.FieldCollection) any { return f }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
package plugins
|
package plugins
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
)
|
||||||
|
|
||||||
// DefaultConfigName is the name the default configuration must have
|
// DefaultConfigName is the name the default configuration must have
|
||||||
// when defined
|
// when defined
|
||||||
|
@ -9,16 +13,16 @@ const DefaultConfigName = "default"
|
||||||
type (
|
type (
|
||||||
// ModuleConfig represents a mapping of configurations per channel
|
// ModuleConfig represents a mapping of configurations per channel
|
||||||
// and module
|
// and module
|
||||||
ModuleConfig map[string]map[string]*FieldCollection
|
ModuleConfig map[string]map[string]*fieldcollection.FieldCollection
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetChannelConfig reads the channel specific configuration for the
|
// GetChannelConfig reads the channel specific configuration for the
|
||||||
// given module. This is created by taking an empty FieldCollection,
|
// given module. This is created by taking an empty FieldCollection,
|
||||||
// merging in the default configuration and finally overwriting all
|
// merging in the default configuration and finally overwriting all
|
||||||
// existing channel configurations.
|
// existing channel configurations.
|
||||||
func (m ModuleConfig) GetChannelConfig(module, channel string) *FieldCollection {
|
func (m ModuleConfig) GetChannelConfig(module, channel string) *fieldcollection.FieldCollection {
|
||||||
channel = strings.TrimLeft(channel, "#@")
|
channel = strings.TrimLeft(channel, "#@")
|
||||||
composed := NewFieldCollection()
|
composed := fieldcollection.NewFieldCollection()
|
||||||
|
|
||||||
for _, i := range []string{DefaultConfigName, channel} {
|
for _, i := range []string{DefaultConfigName, channel} {
|
||||||
f := m[module][i]
|
f := m[module][i]
|
||||||
|
|
|
@ -3,6 +3,7 @@ package plugins
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -10,12 +11,12 @@ import (
|
||||||
func TestModuleConfigGet(t *testing.T) {
|
func TestModuleConfigGet(t *testing.T) {
|
||||||
strPtrEmpty := func(v string) *string { return &v }("")
|
strPtrEmpty := func(v string) *string { return &v }("")
|
||||||
m := ModuleConfig{
|
m := ModuleConfig{
|
||||||
"test": map[string]*FieldCollection{
|
"test": map[string]*fieldcollection.FieldCollection{
|
||||||
DefaultConfigName: FieldCollectionFromData(map[string]any{
|
DefaultConfigName: fieldcollection.FieldCollectionFromData(map[string]any{
|
||||||
"setindefault": DefaultConfigName,
|
"setindefault": DefaultConfigName,
|
||||||
"setinboth": DefaultConfigName,
|
"setinboth": DefaultConfigName,
|
||||||
}),
|
}),
|
||||||
"test": FieldCollectionFromData(map[string]any{
|
"test": fieldcollection.FieldCollectionFromData(map[string]any{
|
||||||
"setinchannel": "channel",
|
"setinchannel": "channel",
|
||||||
"setinboth": "channel",
|
"setinboth": "channel",
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/go_helpers/v2/str"
|
"github.com/Luzifer/go_helpers/v2/str"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
)
|
)
|
||||||
|
@ -73,8 +74,8 @@ type (
|
||||||
|
|
||||||
// RuleAction represents an action to be executed when running a Rule
|
// RuleAction represents an action to be executed when running a Rule
|
||||||
RuleAction struct {
|
RuleAction struct {
|
||||||
Type string `json:"type" yaml:"type,omitempty"`
|
Type string `json:"type" yaml:"type,omitempty"`
|
||||||
Attributes *FieldCollection `json:"attributes" yaml:"attributes,omitempty"`
|
Attributes *fieldcollection.FieldCollection `json:"attributes" yaml:"attributes,omitempty"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -89,7 +90,7 @@ func (r Rule) MatcherID() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matches checks whether the Rule should be executed for the given parameters
|
// Matches checks whether the Rule should be executed for the given parameters
|
||||||
func (r *Rule) Matches(m *irc.Message, event *string, timerStore TimerStore, msgFormatter MsgFormatter, twitchClient *twitch.Client, eventData *FieldCollection) bool {
|
func (r *Rule) Matches(m *irc.Message, event *string, timerStore TimerStore, msgFormatter MsgFormatter, twitchClient *twitch.Client, eventData *fieldcollection.FieldCollection) bool {
|
||||||
r.msgFormatter = msgFormatter
|
r.msgFormatter = msgFormatter
|
||||||
r.timerStore = timerStore
|
r.timerStore = timerStore
|
||||||
r.twitchClient = twitchClient
|
r.twitchClient = twitchClient
|
||||||
|
@ -102,7 +103,7 @@ func (r *Rule) Matches(m *irc.Message, event *string, timerStore TimerStore, msg
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, matcher := range []func(*logrus.Entry, *irc.Message, *string, twitch.BadgeCollection, *FieldCollection) bool{
|
for _, matcher := range []func(*logrus.Entry, *irc.Message, *string, twitch.BadgeCollection, *fieldcollection.FieldCollection) bool{
|
||||||
r.allowExecuteDisable,
|
r.allowExecuteDisable,
|
||||||
r.allowExecuteChannelWhitelist,
|
r.allowExecuteChannelWhitelist,
|
||||||
r.allowExecuteUserWhitelist,
|
r.allowExecuteUserWhitelist,
|
||||||
|
@ -144,7 +145,7 @@ func (r *Rule) GetMatchMessage() *regexp.Regexp {
|
||||||
|
|
||||||
// SetCooldown uses the given TimerStore to set the cooldowns for the
|
// SetCooldown uses the given TimerStore to set the cooldowns for the
|
||||||
// Rule after execution
|
// Rule after execution
|
||||||
func (r *Rule) SetCooldown(timerStore TimerStore, m *irc.Message, evtData *FieldCollection) {
|
func (r *Rule) SetCooldown(timerStore TimerStore, m *irc.Message, evtData *fieldcollection.FieldCollection) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if r.Cooldown != nil {
|
if r.Cooldown != nil {
|
||||||
|
@ -250,7 +251,7 @@ func (r Rule) Validate(tplValidate TemplateValidatorFunc) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rule) allowExecuteBadgeBlacklist(logger *logrus.Entry, _ *irc.Message, _ *string, badges twitch.BadgeCollection, _ *FieldCollection) bool {
|
func (r *Rule) allowExecuteBadgeBlacklist(logger *logrus.Entry, _ *irc.Message, _ *string, badges twitch.BadgeCollection, _ *fieldcollection.FieldCollection) bool {
|
||||||
for _, b := range r.DisableOn {
|
for _, b := range r.DisableOn {
|
||||||
if badges.Has(b) {
|
if badges.Has(b) {
|
||||||
logger.Tracef("Non-Match: Disable-Badge %s", b)
|
logger.Tracef("Non-Match: Disable-Badge %s", b)
|
||||||
|
@ -261,7 +262,7 @@ func (r *Rule) allowExecuteBadgeBlacklist(logger *logrus.Entry, _ *irc.Message,
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rule) allowExecuteBadgeWhitelist(_ *logrus.Entry, _ *irc.Message, _ *string, badges twitch.BadgeCollection, _ *FieldCollection) bool {
|
func (r *Rule) allowExecuteBadgeWhitelist(_ *logrus.Entry, _ *irc.Message, _ *string, badges twitch.BadgeCollection, _ *fieldcollection.FieldCollection) bool {
|
||||||
if len(r.EnableOn) == 0 {
|
if len(r.EnableOn) == 0 {
|
||||||
// No match criteria set, does not speak against matching
|
// No match criteria set, does not speak against matching
|
||||||
return true
|
return true
|
||||||
|
@ -276,7 +277,7 @@ func (r *Rule) allowExecuteBadgeWhitelist(_ *logrus.Entry, _ *irc.Message, _ *st
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rule) allowExecuteChannelCooldown(logger *logrus.Entry, m *irc.Message, _ *string, badges twitch.BadgeCollection, evtData *FieldCollection) bool {
|
func (r *Rule) allowExecuteChannelCooldown(logger *logrus.Entry, m *irc.Message, _ *string, badges twitch.BadgeCollection, evtData *fieldcollection.FieldCollection) bool {
|
||||||
if r.ChannelCooldown == nil || DeriveChannel(m, evtData) == "" {
|
if r.ChannelCooldown == nil || DeriveChannel(m, evtData) == "" {
|
||||||
// No match criteria set, does not speak against matching
|
// No match criteria set, does not speak against matching
|
||||||
return true
|
return true
|
||||||
|
@ -301,7 +302,7 @@ func (r *Rule) allowExecuteChannelCooldown(logger *logrus.Entry, m *irc.Message,
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rule) allowExecuteChannelWhitelist(logger *logrus.Entry, m *irc.Message, _ *string, _ twitch.BadgeCollection, evtData *FieldCollection) bool {
|
func (r *Rule) allowExecuteChannelWhitelist(logger *logrus.Entry, m *irc.Message, _ *string, _ twitch.BadgeCollection, evtData *fieldcollection.FieldCollection) bool {
|
||||||
if len(r.MatchChannels) == 0 {
|
if len(r.MatchChannels) == 0 {
|
||||||
// No match criteria set, does not speak against matching
|
// No match criteria set, does not speak against matching
|
||||||
return true
|
return true
|
||||||
|
@ -315,7 +316,7 @@ func (r *Rule) allowExecuteChannelWhitelist(logger *logrus.Entry, m *irc.Message
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rule) allowExecuteDisable(logger *logrus.Entry, _ *irc.Message, _ *string, _ twitch.BadgeCollection, _ *FieldCollection) bool {
|
func (r *Rule) allowExecuteDisable(logger *logrus.Entry, _ *irc.Message, _ *string, _ twitch.BadgeCollection, _ *fieldcollection.FieldCollection) bool {
|
||||||
if r.Disable == nil {
|
if r.Disable == nil {
|
||||||
// No match criteria set, does not speak against matching
|
// No match criteria set, does not speak against matching
|
||||||
return true
|
return true
|
||||||
|
@ -329,7 +330,7 @@ func (r *Rule) allowExecuteDisable(logger *logrus.Entry, _ *irc.Message, _ *stri
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rule) allowExecuteDisableOnOffline(logger *logrus.Entry, m *irc.Message, _ *string, _ twitch.BadgeCollection, evtData *FieldCollection) bool {
|
func (r *Rule) allowExecuteDisableOnOffline(logger *logrus.Entry, m *irc.Message, _ *string, _ twitch.BadgeCollection, evtData *fieldcollection.FieldCollection) bool {
|
||||||
if r.DisableOnOffline == nil || !*r.DisableOnOffline || DeriveChannel(m, evtData) == "" {
|
if r.DisableOnOffline == nil || !*r.DisableOnOffline || DeriveChannel(m, evtData) == "" {
|
||||||
// No match criteria set, does not speak against matching
|
// No match criteria set, does not speak against matching
|
||||||
return true
|
return true
|
||||||
|
@ -348,7 +349,7 @@ func (r *Rule) allowExecuteDisableOnOffline(logger *logrus.Entry, m *irc.Message
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rule) allowExecuteDisableOnPermit(logger *logrus.Entry, m *irc.Message, _ *string, _ twitch.BadgeCollection, evtData *FieldCollection) bool {
|
func (r *Rule) allowExecuteDisableOnPermit(logger *logrus.Entry, m *irc.Message, _ *string, _ twitch.BadgeCollection, evtData *fieldcollection.FieldCollection) bool {
|
||||||
hasPermit, err := r.timerStore.HasPermit(DeriveChannel(m, evtData), DeriveUser(m, evtData))
|
hasPermit, err := r.timerStore.HasPermit(DeriveChannel(m, evtData), DeriveUser(m, evtData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WithError(err).Error("checking permit")
|
logger.WithError(err).Error("checking permit")
|
||||||
|
@ -363,7 +364,7 @@ func (r *Rule) allowExecuteDisableOnPermit(logger *logrus.Entry, m *irc.Message,
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rule) allowExecuteDisableOnTemplate(logger *logrus.Entry, m *irc.Message, _ *string, _ twitch.BadgeCollection, evtData *FieldCollection) bool {
|
func (r *Rule) allowExecuteDisableOnTemplate(logger *logrus.Entry, m *irc.Message, _ *string, _ twitch.BadgeCollection, evtData *fieldcollection.FieldCollection) bool {
|
||||||
if r.DisableOnTemplate == nil || *r.DisableOnTemplate == "" {
|
if r.DisableOnTemplate == nil || *r.DisableOnTemplate == "" {
|
||||||
// No match criteria set, does not speak against matching
|
// No match criteria set, does not speak against matching
|
||||||
return true
|
return true
|
||||||
|
@ -384,7 +385,7 @@ func (r *Rule) allowExecuteDisableOnTemplate(logger *logrus.Entry, m *irc.Messag
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rule) allowExecuteEventMatch(logger *logrus.Entry, _ *irc.Message, event *string, _ twitch.BadgeCollection, _ *FieldCollection) bool {
|
func (r *Rule) allowExecuteEventMatch(logger *logrus.Entry, _ *irc.Message, event *string, _ twitch.BadgeCollection, _ *fieldcollection.FieldCollection) bool {
|
||||||
// The user defines either no event to match or they define an
|
// The user defines either no event to match or they define an
|
||||||
// event to match. We now need to ensure this match is valid for
|
// event to match. We now need to ensure this match is valid for
|
||||||
// the current execution:
|
// the current execution:
|
||||||
|
@ -430,7 +431,7 @@ func (r *Rule) allowExecuteEventMatch(logger *logrus.Entry, _ *irc.Message, even
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rule) allowExecuteMessageMatcherBlacklist(logger *logrus.Entry, m *irc.Message, _ *string, _ twitch.BadgeCollection, _ *FieldCollection) bool {
|
func (r *Rule) allowExecuteMessageMatcherBlacklist(logger *logrus.Entry, m *irc.Message, _ *string, _ twitch.BadgeCollection, _ *fieldcollection.FieldCollection) bool {
|
||||||
if len(r.DisableOnMatchMessages) == 0 {
|
if len(r.DisableOnMatchMessages) == 0 {
|
||||||
// No match criteria set, does not speak against matching
|
// No match criteria set, does not speak against matching
|
||||||
return true
|
return true
|
||||||
|
@ -459,7 +460,7 @@ func (r *Rule) allowExecuteMessageMatcherBlacklist(logger *logrus.Entry, m *irc.
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rule) allowExecuteMessageMatcherWhitelist(logger *logrus.Entry, m *irc.Message, _ *string, _ twitch.BadgeCollection, _ *FieldCollection) bool {
|
func (r *Rule) allowExecuteMessageMatcherWhitelist(logger *logrus.Entry, m *irc.Message, _ *string, _ twitch.BadgeCollection, _ *fieldcollection.FieldCollection) bool {
|
||||||
if r.MatchMessage == nil {
|
if r.MatchMessage == nil {
|
||||||
// No match criteria set, does not speak against matching
|
// No match criteria set, does not speak against matching
|
||||||
return true
|
return true
|
||||||
|
@ -484,7 +485,7 @@ func (r *Rule) allowExecuteMessageMatcherWhitelist(logger *logrus.Entry, m *irc.
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rule) allowExecuteRuleCooldown(logger *logrus.Entry, _ *irc.Message, _ *string, badges twitch.BadgeCollection, _ *FieldCollection) bool {
|
func (r *Rule) allowExecuteRuleCooldown(logger *logrus.Entry, _ *irc.Message, _ *string, badges twitch.BadgeCollection, _ *fieldcollection.FieldCollection) bool {
|
||||||
if r.Cooldown == nil {
|
if r.Cooldown == nil {
|
||||||
// No match criteria set, does not speak against matching
|
// No match criteria set, does not speak against matching
|
||||||
return true
|
return true
|
||||||
|
@ -509,7 +510,7 @@ func (r *Rule) allowExecuteRuleCooldown(logger *logrus.Entry, _ *irc.Message, _
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rule) allowExecuteUserCooldown(logger *logrus.Entry, m *irc.Message, _ *string, badges twitch.BadgeCollection, evtData *FieldCollection) bool {
|
func (r *Rule) allowExecuteUserCooldown(logger *logrus.Entry, m *irc.Message, _ *string, badges twitch.BadgeCollection, evtData *fieldcollection.FieldCollection) bool {
|
||||||
if r.UserCooldown == nil {
|
if r.UserCooldown == nil {
|
||||||
// No match criteria set, does not speak against matching
|
// No match criteria set, does not speak against matching
|
||||||
return true
|
return true
|
||||||
|
@ -534,7 +535,7 @@ func (r *Rule) allowExecuteUserCooldown(logger *logrus.Entry, m *irc.Message, _
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rule) allowExecuteUserWhitelist(logger *logrus.Entry, m *irc.Message, _ *string, _ twitch.BadgeCollection, evtData *FieldCollection) bool {
|
func (r *Rule) allowExecuteUserWhitelist(logger *logrus.Entry, m *irc.Message, _ *string, _ twitch.BadgeCollection, evtData *fieldcollection.FieldCollection) bool {
|
||||||
if len(r.MatchUsers) == 0 {
|
if len(r.MatchUsers) == 0 {
|
||||||
// No match criteria set, does not speak against matching
|
// No match criteria set, does not speak against matching
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -141,7 +142,7 @@ func TestAllowExecuteDisableOnTemplate(t *testing.T) {
|
||||||
} {
|
} {
|
||||||
// We don't test the message formatter here but only the disable functionality
|
// We don't test the message formatter here but only the disable functionality
|
||||||
// so we fake the result of the evaluation
|
// so we fake the result of the evaluation
|
||||||
r.msgFormatter = func(string, *irc.Message, *Rule, *FieldCollection) (string, error) {
|
r.msgFormatter = func(string, *irc.Message, *Rule, *fieldcollection.FieldCollection) (string, error) {
|
||||||
return msg, nil
|
return msg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"gopkg.in/irc.v4"
|
"gopkg.in/irc.v4"
|
||||||
|
|
||||||
"github.com/Luzifer/go_helpers/v2/backoff"
|
"github.com/Luzifer/go_helpers/v2/backoff"
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/go_helpers/v2/str"
|
"github.com/Luzifer/go_helpers/v2/str"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/actors/announce"
|
"github.com/Luzifer/twitch-bot/v3/internal/actors/announce"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/actors/ban"
|
"github.com/Luzifer/twitch-bot/v3/internal/actors/ban"
|
||||||
|
@ -181,12 +182,12 @@ func getRegistrationArguments() plugins.RegistrationArguments {
|
||||||
SendMessage: sendMessage,
|
SendMessage: sendMessage,
|
||||||
ValidateToken: authService.ValidateTokenFor,
|
ValidateToken: authService.ValidateTokenFor,
|
||||||
|
|
||||||
CreateEvent: func(evt string, eventData *plugins.FieldCollection) error {
|
CreateEvent: func(evt string, eventData *fieldcollection.FieldCollection) error {
|
||||||
handleMessage(ircHdl.Client(), nil, &evt, eventData)
|
handleMessage(ircHdl.Client(), nil, &evt, eventData)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|
||||||
GetModuleConfigForChannel: func(module, channel string) *plugins.FieldCollection {
|
GetModuleConfigForChannel: func(module, channel string) *fieldcollection.FieldCollection {
|
||||||
return config.ModuleConfig.GetChannelConfig(module, channel)
|
return config.ModuleConfig.GetChannelConfig(module, channel)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
29
scopes.go
29
scopes.go
|
@ -4,20 +4,21 @@ import "github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
channelExtendedScopes = map[string]string{
|
channelExtendedScopes = map[string]string{
|
||||||
twitch.ScopeChannelEditCommercial: "run commercial",
|
twitch.ScopeChannelEditCommercial: "run commercial",
|
||||||
twitch.ScopeChannelManageBroadcast: "modify category / title",
|
twitch.ScopeChannelManageBroadcast: "modify category / title",
|
||||||
twitch.ScopeChannelManagePolls: "manage polls",
|
twitch.ScopeChannelManagePolls: "manage polls",
|
||||||
twitch.ScopeChannelManagePredictions: "manage predictions",
|
twitch.ScopeChannelManagePredictions: "manage predictions",
|
||||||
twitch.ScopeChannelManageRaids: "start raids",
|
twitch.ScopeChannelManageRaids: "start raids",
|
||||||
twitch.ScopeChannelManageVIPS: "manage VIPs",
|
twitch.ScopeChannelManageVIPS: "manage VIPs",
|
||||||
twitch.ScopeChannelReadAds: "see when an ad-break starts",
|
twitch.ScopeChannelReadAds: "see when an ad-break starts",
|
||||||
twitch.ScopeChannelReadHypetrain: "see Hype-Train events",
|
twitch.ScopeChannelReadHypetrain: "see Hype-Train events",
|
||||||
twitch.ScopeChannelReadRedemptions: "see channel-point redemptions",
|
twitch.ScopeChannelReadRedemptions: "see channel-point redemptions",
|
||||||
twitch.ScopeChannelReadSubscriptions: "see subscribed users / sub count / points",
|
twitch.ScopeChannelReadSubscriptions: "see subscribed users / sub count / points",
|
||||||
twitch.ScopeClipsEdit: "create clips on behalf of this user",
|
twitch.ScopeClipsEdit: "create clips on behalf of this user",
|
||||||
twitch.ScopeModeratorReadFollowers: "see who follows this channel",
|
twitch.ScopeModeratorReadFollowers: "see who follows this channel",
|
||||||
twitch.ScopeModeratorReadShoutouts: "see shoutouts created / received",
|
twitch.ScopeModeratorReadShoutouts: "see shoutouts created / received",
|
||||||
twitch.ScopeUserManageWhispers: "send whispers on behalf of this user",
|
twitch.ScopeModeratorReadSuspiciousUsers: "see users marked suspicious / restricted",
|
||||||
|
twitch.ScopeUserManageWhispers: "send whispers on behalf of this user",
|
||||||
}
|
}
|
||||||
|
|
||||||
botDefaultScopes = []string{
|
botDefaultScopes = []string{
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue