Compare commits
280 commits
Author | SHA1 | Date | |
---|---|---|---|
4caa8d114e |
|||
313e42a6cb |
|||
428735b3fe |
|||
4b220a0553 |
|||
da1112a4f5 |
|||
6dec23718b |
|||
038fef397e |
|||
c629e37dad |
|||
9879782dfe |
|||
24e4378b7a |
|||
253ced8a68 |
|||
fe902b908e |
|||
0b2a0812d0 |
|||
16ef36687a |
|||
cd4ee2538b |
|||
01fdd6235e |
|||
21e990f94d |
|||
f743479af2 |
|||
6b2d42a7ee |
|||
2591d31045 |
|||
0cca5353dc |
|||
25b22b70e7 |
|||
e6aac58fbf |
|||
fc77212892 |
|||
718e1fa5f0 |
|||
590a5a53a7 |
|||
6b284d364c |
|||
ff713ea159 |
|||
f4a4d3d51c |
|||
51a1918522 |
|||
4931b3ea1a |
|||
906f9b374c |
|||
1bccf39bab |
|||
4aec4e8168 |
|||
4376fe6499 |
|||
0e83695c05 |
|||
cbb4bc713f |
|||
e71a04fc6c |
|||
2863f6d07b |
|||
fa2fa655c3 |
|||
0c3cfc7f26 |
|||
62866e3595 |
|||
21a0b3c453 |
|||
085c04642a |
|||
1064a28086 |
|||
b1958b9135 |
|||
6cbf195d4b |
|||
e50227ff92 |
|||
98d0f7561a |
|||
2a081c9a9d |
|||
9dcde1ede4 |
|||
45f261feb6 |
|||
a1e393a22b |
|||
86ece55ccf |
|||
d1a9dfda21 |
|||
a338471d89 |
|||
b0f463643d |
|||
85adc8c9a0 |
|||
2c17456e53 |
|||
37276ee335 |
|||
18b1587267 |
|||
cacf08158e |
|||
30eec0bcd9 |
|||
ceaa444b8a |
|||
a6110ea605 |
|||
3cfa35ba5e |
|||
c9fbcfcd87 |
|||
263137e947 |
|||
50195a0f33 |
|||
1d17cfff31 |
|||
98ee09ad87 |
|||
ed2657f981 |
|||
d3ad8e2dab |
|||
8f135dc79d |
|||
da5ba74aaa |
|||
aa17043566 |
|||
bb8d62cca2 |
|||
6e6ef5227c |
|||
0d866c702f |
|||
8d6f74d8eb |
|||
ebef4df8c2 |
|||
8ff90c1bf3 |
|||
0c3c066e25 |
|||
bf7385fab4 |
|||
efbd1f2e14 |
|||
01fa504216 |
|||
61a4a20395 |
|||
c261dba13a |
|||
a719e79962 |
|||
0c6bc3a844 |
|||
f5532cd86c |
|||
d0068c6b84 |
|||
a86e13960d |
|||
ebe66edc3c |
|||
f3a5fa3be0 |
|||
fa529a5936 |
|||
b7013c1fbb |
|||
f7e1509716 |
|||
24335e9451 |
|||
fd966855b4 |
|||
8a28f58b06 |
|||
74cd388928 |
|||
d0ed052a5a |
|||
df9c548e2d |
|||
7b837db5f6 |
|||
4e2241c7f4 |
|||
432d06535f |
|||
99775cb815 |
|||
9fcb6fca77 |
|||
a6c8f0aa15 |
|||
7fbc7b565b |
|||
0a45c0d92e |
|||
438297fc75 |
|||
74e647de27 |
|||
37cfc399e8 |
|||
51328629b1 |
|||
2e7a631e1c |
|||
0d42d5336a |
|||
07eebd47b7 |
|||
e3aa795604 |
|||
de782bae4e |
|||
91a41f53c6 |
|||
7a1eb5b9d0 |
|||
e337b47938 |
|||
5bc8f1fa23 |
|||
50190346d5 |
|||
beae543e0b |
|||
ebce29eb6a |
|||
59862ccb8d |
|||
eb1f57d253 |
|||
ed20672011 |
|||
183f5233e2 |
|||
5c86323bf5 |
|||
bfa50a7d1c |
|||
93ad13ce70 |
|||
59546e19b2 |
|||
d22132caac |
|||
c368fea4de |
|||
733bbbb6bf |
|||
702ef21be1 |
|||
60d71ce66b |
|||
5ec75f7a98 |
|||
fdcb02f7a9 |
|||
db873c9706 |
|||
5c6bf33914 |
|||
62d24e8fe1 |
|||
276d952be3 |
|||
b4a9a7fee0 |
|||
c29e82e3ab |
|||
a4d505f505 |
|||
6d175a801c |
|||
e9bf7b84a6 |
|||
992b7fe4af |
|||
e8f96f19d1 |
|||
790576ddb5 |
|||
6ec96f9daa |
|||
678a003307 |
|||
1fcc35524e |
|||
865e38125a |
|||
68a899cd79 |
|||
34abf9d564 |
|||
8ce0b5b294 |
|||
574ca67c69 |
|||
05d6c8bc50 |
|||
e4f0ec004d |
|||
3db82d9c7b |
|||
53c6aa07fa |
|||
8355e6306a |
|||
e0edb68d44 |
|||
5519e4de68 |
|||
e76358fc4f |
|||
04cc7762c2 |
|||
acfc5cbe12 |
|||
09cf117501 |
|||
facbdbe477 |
|||
aebe017b27 |
|||
fb628c24fc |
|||
68d24ccefc |
|||
05012ecc96 |
|||
92321e4054 |
|||
8119140c46 |
|||
ddded3decb |
|||
848c222233 |
|||
a7c238d67f |
|||
2ea0c3e746 |
|||
e46e7bd863 |
|||
870efae0b9 |
|||
2a01049812 |
|||
2104690965 |
|||
5e9929f7be |
|||
9c1718e090 |
|||
bab21223c1 |
|||
4843f97231 |
|||
d62e978c4e |
|||
1059f70468 |
|||
365e1c9fcd |
|||
1bd9cd578f |
|||
2719af4960 |
|||
cf7811591c |
|||
e86e1f9f91 |
|||
a97dc76e1a |
|||
14e8a5e2b7 |
|||
5da4196ba1 |
|||
b06b9d7645 |
|||
0be1a89019 |
|||
2748dfe82b |
|||
5271b50ae6 |
|||
0bb1948a82 |
|||
48b253d213 |
|||
5b977d3f31 |
|||
f7308345e0 |
|||
207ebd3e52 |
|||
12de1efef2 |
|||
0df88165c7 |
|||
6421c87ed0 |
|||
0eb8523939 |
|||
de973e7fdf |
|||
2959c79e7e |
|||
7bfa1e6166 |
|||
ed5768f80e |
|||
5a842c8160 |
|||
52e54017e4 |
|||
d3ba7c965a |
|||
8c0fed012c |
|||
7f045fa928 |
|||
bbe454ed4f |
|||
2543622abf |
|||
22325da84f |
|||
1bde118f9d |
|||
019d46817e |
|||
edc1d9da5b |
|||
a57aa101db |
|||
c3788e19ab |
|||
5998a07892 |
|||
2c4fccd56d |
|||
2393b2c4ab |
|||
a6b30aa6e7 |
|||
99eecd1631 |
|||
00320ba09c |
|||
db2d80642a |
|||
3cfee5ccc9 |
|||
9cac1686b8 |
|||
f26ce9b0da |
|||
dd80433cb0 |
|||
0d76c58ede |
|||
096657bcee |
|||
ff475f286b |
|||
06d7fcb019 |
|||
710783aaf7 |
|||
19038dbc6e |
|||
740a71a173 |
|||
e0a8ce3684 |
|||
5a8459cedc |
|||
8819b4031a |
|||
41535bc4df |
|||
150daf8a80 |
|||
1d192ad796 |
|||
b1ceb29bfb |
|||
26a57c379d |
|||
13bc753b7d |
|||
e8d60e2733 |
|||
4964ed25cf |
|||
014df155ae |
|||
c4be936c63 |
|||
b38ecc9d0b |
|||
621d266391 |
|||
f1d4c1a283 |
|||
0355713f7c |
|||
c63793be2d |
|||
2a64caec09 |
|||
8e8895d32e |
|||
0a37873241 |
|||
19a30d342a |
|||
30305600e7 |
|||
5dd6a5323c |
|||
a01ce9aa5f |
|||
eb37a75da8 |
|||
3cefd39960 |
|||
ebf734be40 |
|||
f56a7a3266 |
100 changed files with 3583 additions and 2158 deletions
.git_changerelease.yaml.gitattributesfunctions.gogo.modgo.sumirc.gomain.gopackage-lock.jsonpackage.json
.github/workflows
.golangci.ymlDockerfileHistory.mdMakefileactions.goauthMiddleware.goci/workflow-parts
index.yamlpart_doc-generator.ymlpart_docker-publish.ymlpart_integration-crdb.ymlpart_integration-mariadb.ymlpart_integration-mysql.ymlpart_integration-postgres.ymlpart_release.ymlpart_test.yml
cli.gocli_actorDocs.gocli_apiToken.gocli_migrateDatabase.gocli_resetSecrets.gocli_tplDocs.gocli_validateConfig.goconfig.goconfigEditor_automessage.goconfigEditor_general.goconfigEditor_rules.goconfigRemoteUpdate.godocs
content
static
internal
actors
counter
marker
quotedb
spotify
variables
vip
apimodules
kofi
msgformat
overlays
raffle
linkcheck
locker
service/timer
template
pkg
plugins
plugins_core.gorenovate.jsonscopes.gosrc
tools
twitchWatcher.go
19
.git_changerelease.yaml
Normal file
19
.git_changerelease.yaml
Normal file
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
|
||||
# Template to format the commit message containing the changelog change
|
||||
# which will be used to add the tag to.
|
||||
release_commit_message: "Release: Twitch-Bot {{.Version}}"
|
||||
|
||||
# Commands to run before committing the changelog and adding the tag.
|
||||
# Therefore these can add content to be included into the release-
|
||||
# commit. These commands have access to the `TAG_VERSION` variable
|
||||
# which contains the tag to be applied after the commit. If the
|
||||
# command specified here is prefixed with a `-` sign, the exit status
|
||||
# will not fail the release process. If it is not prefixed with a `-`
|
||||
# a non-zero exit status will terminate the release process. The
|
||||
# commands will be run from the repostory root, so sub-dirs MUST be
|
||||
# specified. All commands are run as `bash -ec "..."` so you can use
|
||||
# bash inside the commands.
|
||||
pre_commit_commands: []
|
||||
|
||||
...
|
1
.gitattributes
vendored
1
.gitattributes
vendored
|
@ -1 +0,0 @@
|
|||
docs/static/* filter=lfs diff=lfs merge=lfs -text
|
|
@ -1,17 +1,42 @@
|
|||
name: CI Workflow
|
||||
|
||||
on: push
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
doc-generator:
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
needs:
|
||||
- test
|
||||
test:
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
container:
|
||||
image: luzifer/gh-arch-env
|
||||
image: ghcr.io/luzifer-docker/action-env:master@sha256:33e747ae78dc2b8dc291a433f4c3c4d522cf565b449fe9c3e33a0c21f516a570
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOPATH: /go
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
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
|
||||
|
||||
doc-generator:
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
needs: [test]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
container:
|
||||
image: ghcr.io/luzifer-docker/action-env:master@sha256:33e747ae78dc2b8dc291a433f4c3c4d522cf565b449fe9c3e33a0c21f516a570
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOPATH: /go
|
||||
|
@ -21,9 +46,8 @@ jobs:
|
|||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
lfs: true
|
||||
show-progress: false
|
||||
submodules: true
|
||||
- name: Marking workdir safe
|
||||
|
@ -31,15 +55,15 @@ jobs:
|
|||
- 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
|
||||
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1
|
||||
with:
|
||||
path: .rendered-docs
|
||||
- name: Deploy artifact
|
||||
uses: actions/deploy-pages@v1
|
||||
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5
|
||||
|
||||
docker-publish:
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master' }}
|
||||
needs:
|
||||
- test
|
||||
needs: [test]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
@ -47,83 +71,48 @@ jobs:
|
|||
packages: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
show-progress: false
|
||||
- name: Log into registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
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
|
||||
needs: [test]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
container:
|
||||
image: luzifer/gh-arch-env
|
||||
image: ghcr.io/luzifer-docker/action-env:master@sha256:33e747ae78dc2b8dc291a433f4c3c4d522cf565b449fe9c3e33a0c21f516a570
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOPATH: /go
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
mariadb:
|
||||
image: mariadb:11
|
||||
image: mariadb:11.8.3@sha256:272084c2dec70619714df329c4ffcb336e3f8c723072c3f56f2e4015997bbf2c
|
||||
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
|
||||
options: >-
|
||||
--health-cmd "healthcheck.sh
|
||||
--connect
|
||||
--innodb_initialized"
|
||||
--health-interval 10s
|
||||
--health-retries 5
|
||||
--health-timeout 5s
|
||||
steps:
|
||||
- name: Install required packages
|
||||
run: |
|
||||
pacman -Syy --noconfirm \
|
||||
mariadb-clients
|
||||
- uses: actions/checkout@v4
|
||||
run: apt-get install -y --no-install-recommends mariadb-client
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
show-progress: false
|
||||
- name: Marking workdir safe
|
||||
|
@ -131,77 +120,83 @@ jobs:
|
|||
- 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'@'%';
|
||||
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
|
||||
needs: [test]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
container:
|
||||
image: luzifer/gh-arch-env
|
||||
image: ghcr.io/luzifer-docker/action-env:master@sha256:33e747ae78dc2b8dc291a433f4c3c4d522cf565b449fe9c3e33a0c21f516a570
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOPATH: /go
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8
|
||||
image: mysql:9.4.0@sha256:439bfb4044dc59ade76c4e5c4065c02e5ba4d4007db32c40ac58d55c03069916
|
||||
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
|
||||
options: >-
|
||||
--health-cmd "mysqladmin ping"
|
||||
--health-interval 10s
|
||||
--health-retries 5
|
||||
--health-timeout 5s
|
||||
steps:
|
||||
- name: Install required packages
|
||||
run: |
|
||||
pacman -Syy --noconfirm \
|
||||
mariadb-clients
|
||||
- uses: actions/checkout@v4
|
||||
run: apt-get install -y --no-install-recommends mariadb-client
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
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'@'%';
|
||||
mariadb --skip-ssl -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
|
||||
needs: [test]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
container:
|
||||
image: luzifer/gh-arch-env
|
||||
image: ghcr.io/luzifer-docker/action-env:master@sha256:33e747ae78dc2b8dc291a433f4c3c4d522cf565b449fe9c3e33a0c21f516a570
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOPATH: /go
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
image: postgres:17.6@sha256:29e0bb09c8e7e7fc265ea9f4367de9622e55bae6b0b97e7cce740c2d63c2ebc0
|
||||
env:
|
||||
POSTGRES_PASSWORD: twitch-bot-pass
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-retries 5
|
||||
--health-timeout 5s
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
show-progress: false
|
||||
- name: Marking workdir safe
|
||||
|
@ -209,17 +204,24 @@ jobs:
|
|||
- 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
|
||||
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
|
||||
needs: [test]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
container:
|
||||
image: luzifer/gh-arch-env
|
||||
image: ghcr.io/luzifer-docker/action-env:master@sha256:33e747ae78dc2b8dc291a433f4c3c4d522cf565b449fe9c3e33a0c21f516a570
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOPATH: /go
|
||||
|
@ -227,48 +229,19 @@ jobs:
|
|||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
show-progress: false
|
||||
- name: Marking workdir safe
|
||||
run: |
|
||||
git config --global --add safe.directory /__w/twitch-bot/twitch-bot
|
||||
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
|
||||
uses: ncipollo/release-action@bcfe5470707e8832e12347755757cec0eb3c22af # 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
|
296
.golangci.yml
296
.golangci.yml
|
@ -1,176 +1,146 @@
|
|||
# Derived from https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml
|
||||
|
||||
---
|
||||
version: '2'
|
||||
|
||||
run:
|
||||
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||
timeout: 5m
|
||||
# Force readonly modules usage for checking
|
||||
modules-download-mode: readonly
|
||||
|
||||
output:
|
||||
formats:
|
||||
- format: tab
|
||||
tab:
|
||||
path: stdout
|
||||
colors: false
|
||||
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- containedctx
|
||||
- contextcheck
|
||||
- copyloopvar
|
||||
- dogsled
|
||||
- durationcheck
|
||||
- errcheck
|
||||
- errchkjson
|
||||
- forbidigo
|
||||
- funlen
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godox
|
||||
- gosec
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- mnd
|
||||
- nakedret
|
||||
- nilerr
|
||||
- nilnil
|
||||
- noctx
|
||||
- nolintlint
|
||||
- revive
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- unused
|
||||
- wastedassign
|
||||
- wrapcheck
|
||||
|
||||
settings:
|
||||
funlen:
|
||||
lines: 100
|
||||
statements: 60
|
||||
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
|
||||
mnd:
|
||||
ignored-functions:
|
||||
- strconv.(?:Format|Parse)\B+
|
||||
|
||||
revive:
|
||||
rules:
|
||||
- name: atomic
|
||||
- name: banned-characters
|
||||
arguments:
|
||||
- ;
|
||||
- name: bare-return
|
||||
- name: blank-imports
|
||||
- name: bool-literal-in-expr
|
||||
- name: call-to-gc
|
||||
- name: confusing-naming
|
||||
- name: confusing-results
|
||||
- name: constant-logical-expr
|
||||
- name: context-as-argument
|
||||
- name: context-keys-type
|
||||
- name: deep-exit
|
||||
- name: defer
|
||||
- name: dot-imports
|
||||
- name: duplicated-imports
|
||||
- name: early-return
|
||||
- name: empty-block
|
||||
- name: empty-lines
|
||||
- name: errorf
|
||||
- name: error-naming
|
||||
- name: error-return
|
||||
- name: error-strings
|
||||
- name: exported
|
||||
arguments:
|
||||
- sayRepetitiveInsteadOfStutters
|
||||
- name: flag-parameter
|
||||
- name: get-return
|
||||
- name: identical-branches
|
||||
- name: if-return
|
||||
- name: import-shadowing
|
||||
- name: increment-decrement
|
||||
- name: indent-error-flow
|
||||
- name: modifies-parameter
|
||||
- name: modifies-value-receiver
|
||||
- name: optimize-operands-order
|
||||
- name: range
|
||||
- name: range-val-address
|
||||
- name: range-val-in-closure
|
||||
- name: receiver-naming
|
||||
- name: redefines-builtin-id
|
||||
- name: string-of-int
|
||||
- name: struct-tag
|
||||
- name: superfluous-else
|
||||
- name: time-equal
|
||||
- name: time-naming
|
||||
- name: unconditional-recursion
|
||||
- name: unexported-naming
|
||||
- name: unexported-return
|
||||
- name: unhandled-error
|
||||
arguments:
|
||||
- fmt.(Fp|P)rint(f|ln|)
|
||||
- name: unnecessary-stmt
|
||||
- name: unreachable-code
|
||||
- name: unused-parameter
|
||||
- name: unused-receiver
|
||||
- name: useless-break
|
||||
- name: var-declaration
|
||||
- name: var-naming
|
||||
- name: waitgroup-by-value
|
||||
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
issues:
|
||||
# This disables the included exclude-list in golangci-lint as that
|
||||
# list for example fully hides G304 gosec rule, errcheck, exported
|
||||
# rule of revive and other errors one really wants to see.
|
||||
# Smme detail: https://github.com/golangci/golangci-lint/issues/456
|
||||
exclude-use-default: false
|
||||
# Don't limit the number of shown issues: Report ALL of them
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
formatters:
|
||||
enable:
|
||||
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false]
|
||||
- bidichk # Checks for dangerous unicode character sequences [fast: true, auto-fix: false]
|
||||
- bodyclose # checks whether HTTP response body is closed successfully [fast: true, auto-fix: false]
|
||||
- containedctx # containedctx is a linter that detects struct contained context.Context field [fast: true, auto-fix: false]
|
||||
- contextcheck # check the function whether use a non-inherited context [fast: false, auto-fix: false]
|
||||
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false]
|
||||
- durationcheck # check for two durations multiplied together [fast: false, auto-fix: false]
|
||||
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false]
|
||||
- errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occations, where the check for the returned error can be omitted. [fast: false, auto-fix: false]
|
||||
- exportloopref # checks for pointers to enclosing loop variables [fast: true, auto-fix: false]
|
||||
- forbidigo # Forbids identifiers [fast: true, auto-fix: false]
|
||||
- funlen # Tool for detection of long functions [fast: true, auto-fix: false]
|
||||
- gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false]
|
||||
- goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false]
|
||||
- gocritic # The most opinionated Go source code linter [fast: true, auto-fix: false]
|
||||
- gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false]
|
||||
- godox # Tool for detection of FIXME, TODO and other comment keywords [fast: true, auto-fix: false]
|
||||
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true]
|
||||
- gofumpt # Gofumpt checks whether code was gofumpt-ed. [fast: true, auto-fix: true]
|
||||
- goimports # Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true]
|
||||
- gomnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false]
|
||||
- gosec # Inspects source code for security problems [fast: true, auto-fix: false]
|
||||
- gosimple # Linter for Go source code that specializes in simplifying a code [fast: true, auto-fix: false]
|
||||
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: true, auto-fix: false]
|
||||
- ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
|
||||
- misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true]
|
||||
- nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false]
|
||||
- nilerr # Finds the code that returns nil even if it checks that the error is not nil. [fast: false, auto-fix: false]
|
||||
- nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. [fast: false, auto-fix: false]
|
||||
- noctx # noctx finds sending http request without context.Context [fast: true, auto-fix: false]
|
||||
- nolintlint # Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: false]
|
||||
- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. [fast: false, auto-fix: false]
|
||||
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: true, auto-fix: false]
|
||||
- stylecheck # Stylecheck is a replacement for golint [fast: true, auto-fix: false]
|
||||
- tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 [fast: false, auto-fix: false]
|
||||
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: true, auto-fix: false]
|
||||
- unconvert # Remove unnecessary type conversions [fast: true, auto-fix: false]
|
||||
- unused # Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false]
|
||||
- wastedassign # wastedassign finds wasted assignment statements. [fast: false, auto-fix: false]
|
||||
- wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false]
|
||||
|
||||
linters-settings:
|
||||
funlen:
|
||||
lines: 100
|
||||
statements: 60
|
||||
|
||||
gocyclo:
|
||||
# minimal code complexity to report, 30 by default (but we recommend 10-20)
|
||||
min-complexity: 15
|
||||
|
||||
gomnd:
|
||||
settings:
|
||||
mnd:
|
||||
ignored-functions: 'strconv.(?:Format|Parse)\B+'
|
||||
|
||||
revive:
|
||||
rules:
|
||||
#- name: add-constant # Suggests using constant for magic numbers and string literals
|
||||
# Opinion: Makes sense for strings, not for numbers but checks numbers
|
||||
#- name: argument-limit # Specifies the maximum number of arguments a function can receive | Opinion: Don't need this
|
||||
- name: atomic # Check for common mistaken usages of the `sync/atomic` package
|
||||
- name: banned-characters # Checks banned characters in identifiers
|
||||
arguments:
|
||||
- ';' # Greek question mark
|
||||
- name: bare-return # Warns on bare returns
|
||||
- name: blank-imports # Disallows blank imports
|
||||
- name: bool-literal-in-expr # Suggests removing Boolean literals from logic expressions
|
||||
- name: call-to-gc # Warns on explicit call to the garbage collector
|
||||
#- name: cognitive-complexity # Sets restriction for maximum Cognitive complexity.
|
||||
# There is a dedicated linter for this
|
||||
- name: confusing-naming # Warns on methods with names that differ only by capitalization
|
||||
- name: confusing-results # Suggests to name potentially confusing function results
|
||||
- name: constant-logical-expr # Warns on constant logical expressions
|
||||
- name: context-as-argument # `context.Context` should be the first argument of a function.
|
||||
- name: context-keys-type # Disallows the usage of basic types in `context.WithValue`.
|
||||
#- name: cyclomatic # Sets restriction for maximum Cyclomatic complexity.
|
||||
# There is a dedicated linter for this
|
||||
#- name: datarace # Spots potential dataraces
|
||||
# Is not (yet) available?
|
||||
- name: deep-exit # Looks for program exits in funcs other than `main()` or `init()`
|
||||
- name: defer # Warns on some [defer gotchas](https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-iii-36a1ab3d6ef1)
|
||||
- name: dot-imports # Forbids `.` imports.
|
||||
- name: duplicated-imports # Looks for packages that are imported two or more times
|
||||
- name: early-return # Spots if-then-else statements that can be refactored to simplify code reading
|
||||
- name: empty-block # Warns on empty code blocks
|
||||
- name: empty-lines # Warns when there are heading or trailing newlines in a block
|
||||
- name: errorf # Should replace `errors.New(fmt.Sprintf())` with `fmt.Errorf()`
|
||||
- name: error-naming # Naming of error variables.
|
||||
- name: error-return # The error return parameter should be last.
|
||||
- name: error-strings # Conventions around error strings.
|
||||
- name: exported # Naming and commenting conventions on exported symbols.
|
||||
arguments: ['sayRepetitiveInsteadOfStutters']
|
||||
#- name: file-header # Header which each file should have.
|
||||
# Useless without config, have no config for it
|
||||
- name: flag-parameter # Warns on boolean parameters that create a control coupling
|
||||
#- name: function-length # Warns on functions exceeding the statements or lines max
|
||||
# There is a dedicated linter for this
|
||||
#- name: function-result-limit # Specifies the maximum number of results a function can return
|
||||
# Opinion: Don't need this
|
||||
- name: get-return # Warns on getters that do not yield any result
|
||||
- name: identical-branches # Spots if-then-else statements with identical `then` and `else` branches
|
||||
- name: if-return # Redundant if when returning an error.
|
||||
#- name: imports-blacklist # Disallows importing the specified packages
|
||||
# Useless without config, have no config for it
|
||||
- name: import-shadowing # Spots identifiers that shadow an import
|
||||
- name: increment-decrement # Use `i++` and `i--` instead of `i += 1` and `i -= 1`.
|
||||
- name: indent-error-flow # Prevents redundant else statements.
|
||||
#- name: line-length-limit # Specifies the maximum number of characters in a lined
|
||||
# There is a dedicated linter for this
|
||||
#- name: max-public-structs # The maximum number of public structs in a file.
|
||||
# Opinion: Don't need this
|
||||
- name: modifies-parameter # Warns on assignments to function parameters
|
||||
- name: modifies-value-receiver # Warns on assignments to value-passed method receivers
|
||||
#- name: nested-structs # Warns on structs within structs
|
||||
# Opinion: Don't need this
|
||||
- name: optimize-operands-order # Checks inefficient conditional expressions
|
||||
#- name: package-comments # Package commenting conventions.
|
||||
# Opinion: Don't need this
|
||||
- name: range # Prevents redundant variables when iterating over a collection.
|
||||
- name: range-val-address # Warns if address of range value is used dangerously
|
||||
- name: range-val-in-closure # Warns if range value is used in a closure dispatched as goroutine
|
||||
- name: receiver-naming # Conventions around the naming of receivers.
|
||||
- name: redefines-builtin-id # Warns on redefinitions of builtin identifiers
|
||||
#- name: string-format # Warns on specific string literals that fail one or more user-configured regular expressions
|
||||
# Useless without config, have no config for it
|
||||
- name: string-of-int # Warns on suspicious casts from int to string
|
||||
- name: struct-tag # Checks common struct tags like `json`,`xml`,`yaml`
|
||||
- name: superfluous-else # Prevents redundant else statements (extends indent-error-flow)
|
||||
- name: time-equal # Suggests to use `time.Time.Equal` instead of `==` and `!=` for equality check time.
|
||||
- name: time-naming # Conventions around the naming of time variables.
|
||||
- name: unconditional-recursion # Warns on function calls that will lead to (direct) infinite recursion
|
||||
- name: unexported-naming # Warns on wrongly named un-exported symbols
|
||||
- name: unexported-return # Warns when a public return is from unexported type.
|
||||
- name: unhandled-error # Warns on unhandled errors returned by funcion calls
|
||||
arguments:
|
||||
- "fmt.(Fp|P)rint(f|ln|)"
|
||||
- name: unnecessary-stmt # Suggests removing or simplifying unnecessary statements
|
||||
- name: unreachable-code # Warns on unreachable code
|
||||
- name: unused-parameter # Suggests to rename or remove unused function parameters
|
||||
- name: unused-receiver # Suggests to rename or remove unused method receivers
|
||||
#- name: use-any # Proposes to replace `interface{}` with its alias `any`
|
||||
# Is not (yet) available?
|
||||
- name: useless-break # Warns on useless `break` statements in case clauses
|
||||
- name: var-declaration # Reduces redundancies around variable declaration.
|
||||
- name: var-naming # Naming rules.
|
||||
- name: waitgroup-by-value # Warns on functions taking sync.WaitGroup as a by-value parameter
|
||||
|
||||
...
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
|
17
Dockerfile
17
Dockerfile
|
@ -1,4 +1,4 @@
|
|||
FROM luzifer/archlinux as builder
|
||||
FROM golang:1.25-alpine@sha256:f18a072054848d87a8077455f0ac8a25886f2397f88bfdd222d6fafbb5bba440 AS builder
|
||||
|
||||
COPY . /go/src/twitch-bot
|
||||
WORKDIR /go/src/twitch-bot
|
||||
|
@ -7,13 +7,11 @@ ENV CGO_ENABLED=0 \
|
|||
GOPATH=/go
|
||||
|
||||
RUN set -ex \
|
||||
&& pacman -Syy --noconfirm \
|
||||
&& apk --no-cache add \
|
||||
curl \
|
||||
git \
|
||||
git-lfs \
|
||||
go \
|
||||
make \
|
||||
nodejs-lts-hydrogen \
|
||||
nodejs \
|
||||
npm \
|
||||
&& git config --global --add safe.directory /go/src/twitch-bot \
|
||||
&& make node_modules frontend_prod \
|
||||
|
@ -24,9 +22,14 @@ RUN set -ex \
|
|||
-ldflags "-X main.version=$(git describe --tags --always || echo dev)"
|
||||
|
||||
|
||||
FROM alpine:latest
|
||||
FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1
|
||||
|
||||
LABEL maintainer "Knut Ahlers <knut@ahlers.me>"
|
||||
LABEL org.opencontainers.image.authors="Knut Ahlers <knut@ahlers.me>" \
|
||||
org.opencontainers.image.url="https://github.com/users/Luzifer/packages/container/package/twitch-bot" \
|
||||
org.opencontainers.image.documentation="https://luzifer.github.io/twitch-bot/" \
|
||||
org.opencontainers.image.source="https://github.com/Luzifer/twitch-bot" \
|
||||
org.opencontainers.image.licenses="Apache-2.0" \
|
||||
org.opencontainers.image.title="Self-hosted alternative to one of the big Twitch bots managed by big companies"
|
||||
|
||||
ENV CONFIG=/data/config.yaml \
|
||||
STORAGE_CONN_STRING=/data/store.db
|
||||
|
|
150
History.md
150
History.md
|
@ -1,3 +1,153 @@
|
|||
# 3.36.1 / 2025-08-16
|
||||
|
||||
* Bugfixes
|
||||
* fix(deps): update dependency axios to v1.11.0
|
||||
* fix(deps): update module github.com/getsentry/sentry-go to v0.35.1
|
||||
* fix(deps): update module golang.org/x/crypto to v0.41.0
|
||||
* fix(deps): update module golang.org/x/net to v0.43.0
|
||||
* fix(deps): update module gorm.io/gorm to v1.30.1
|
||||
* chore(deps): update dependency @babel/eslint-parser to v7.28.0
|
||||
* chore(deps): update dependency esbuild to v0.25.9
|
||||
* chore(deps): update dependency go to v1.25.0
|
||||
* chore(deps): update mariadb docker tag to v11.8.3
|
||||
* chore(deps): update mysql docker tag to v9.4.0
|
||||
* chore(deps): update postgres docker tag to v17.6
|
||||
|
||||
# 3.36.0 / 2025-06-29
|
||||
|
||||
* Improvements
|
||||
* Use new `channel.hype_train.*` v2 events
|
||||
* Drop support for CockroachDB
|
||||
|
||||
* Bugfixes
|
||||
* Update alpine docker tag to v3.22
|
||||
* Update dependency go to v1.24.4
|
||||
* Update mariadb docker tag to v11.8.2
|
||||
* Update mysql Docker tag to v9.3.0
|
||||
* Update postgres Docker tag to v17.5
|
||||
* Update dependency axios to v1.10.0
|
||||
* Update dependency @babel/eslint-parser to v7.27.5
|
||||
* Update dependency esbuild to v0.25.5
|
||||
* Update module github.com/getsentry/sentry-go to v0.34.0
|
||||
* Update module github.com/go-git/go-git/v5 to v5.16.2
|
||||
* Update module github.com/go-sql-driver/mysql to v1.9.3
|
||||
* Update module github.com/Luzifer/rconfig/v2 to v2.6.0
|
||||
* Update module golang.org/x/net to v0.41.0
|
||||
* Update module golang.org/x/oauth2 to v0.30.0
|
||||
* Update module gorm.io/driver/mysql to v1.6.0
|
||||
* Update module gorm.io/driver/postgres to v1.6.0
|
||||
* Update module gorm.io/gorm to v1.30.0
|
||||
|
||||
# 3.35.4 / 2025-04-12
|
||||
|
||||
* Bugfixes
|
||||
* [docs] Fix: Typo in URL
|
||||
* CI: Drop "stable" branch
|
||||
* Update luzifer/gh-arch-env Docker digest to fd19117
|
||||
* Update mariadb:11 Docker digest to 81e8930
|
||||
* Update module github.com/getsentry/sentry-go to v0.32.0
|
||||
* Update module github.com/go-git/go-git/v5 to v5.15.0
|
||||
* Update module github.com/go-sql-driver/mysql to v1.9.2
|
||||
* Update module golang.org/x/net to v0.39.0
|
||||
* Update postgres:15 Docker digest to fe45ed1
|
||||
|
||||
# 3.35.3 / 2025-04-06
|
||||
|
||||
* Bugfixes
|
||||
* Update Font Awesome to v6.7.2
|
||||
* Update dependency go to v1.24.2
|
||||
* Update module golang.org/x/crypto to v0.37.0
|
||||
* Update dependency eslint-plugin-vue to v9.33.0
|
||||
* Update dependency eslint to v8.57.1
|
||||
* Update dependency @babel/eslint-parser to v7.27.0
|
||||
* Update dependency esbuild to ^0.25.0 [SECURITY]
|
||||
* CI: Switch to alpine based build image, add image labels
|
||||
|
||||
# 3.35.2 / 2025-04-06
|
||||
|
||||
* Bugfixes
|
||||
* Update Go & Node dependencies
|
||||
* Fix: Replace nodejs LTS version
|
||||
* Lint: Migrate linter config, use local linter, fix issues
|
||||
|
||||
# 3.35.1 / 2024-12-12
|
||||
|
||||
* Bugfixes
|
||||
* [core] Fix: Reduce token requirements for category search
|
||||
* Update node dependencies
|
||||
* Update Go dependencies
|
||||
|
||||
# 3.35.0 / 2024-12-02
|
||||
|
||||
* New Features
|
||||
* [template] Add functions `parseDuration`, `parseDurationToSeconds`
|
||||
|
||||
* Bugfixes
|
||||
* [raffle] Fix: Raffle channel did not allow underscore in channel name
|
||||
|
||||
# 3.34.0 / 2024-09-16
|
||||
|
||||
* New Features
|
||||
* [marker] Implement actor to create stream markers
|
||||
* [templating] Add `currentVOD` function
|
||||
|
||||
* Bugfixes
|
||||
* [linkcheck] Fix: Replace static (deprecated) user-agent list
|
||||
|
||||
# 3.33.2 / 2024-08-27
|
||||
|
||||
* Bugfixes
|
||||
* [overlays] Fix KoFi donation currency in eventfeed
|
||||
* [raffle] Lint: Ignore linter false-positive
|
||||
* [CI] Lint: Replace deprecated linter
|
||||
|
||||
# 3.33.1 / 2024-08-14
|
||||
|
||||
* Bugfixes
|
||||
* [core] Fix: Do not execute action after permission check
|
||||
* [editor] Update dependencies
|
||||
* [raffle] Fix: Send ID as string
|
||||
|
||||
# 3.33.0 / 2024-07-27
|
||||
|
||||
* New Features
|
||||
* [overlays] Add eventfeed as default-overlay
|
||||
|
||||
* Improvements
|
||||
* [linkcheck] Add support for meta-redirects
|
||||
|
||||
* Bugfixes
|
||||
* [kofi] Fix: Use message as string
|
||||
* [overlays] Fix: Transmit event-id as string
|
||||
|
||||
# 3.32.0 / 2024-06-09
|
||||
|
||||
* New Features
|
||||
* [templating] Add `streamIsLive` function
|
||||
|
||||
* Bugfixes
|
||||
* [core] Fix: Accept proper token declaration in Authorization header
|
||||
* [core] Fix: Include username and channel in ban errors
|
||||
|
||||
# 3.31.0 / 2024-05-13
|
||||
|
||||
* Improvements
|
||||
* [core] Add locking to prevent concurrent rule executions
|
||||
|
||||
* Bugfixes
|
||||
* [spotify] Fix: Refresh-Token gets revoked when using two functions
|
||||
|
||||
# 3.30.0 / 2024-04-26
|
||||
|
||||
* New Features
|
||||
* [templating] Add `userExists` function
|
||||
|
||||
* Improvements
|
||||
* [eventsub] Suspicious user topics were moved from beta to v1
|
||||
|
||||
* Bugfixes
|
||||
* Update dependencies
|
||||
|
||||
# 3.29.2 / 2024-04-13
|
||||
|
||||
> [!IMPORTANT]
|
||||
|
|
84
Makefile
84
Makefile
|
@ -1,55 +1,77 @@
|
|||
DOCS_BASE_URL:=/
|
||||
HUGO_VERSION:=0.117.0
|
||||
|
||||
default: lint frontend_lint test
|
||||
## Tool Binaries
|
||||
GO_RUN := go run -modfile ./tools/go.mod
|
||||
GO_TEST = $(GO_RUN) gotest.tools/gotestsum --format pkgname
|
||||
GOLANCI_LINT = golangci-lint
|
||||
|
||||
build_prod: frontend_prod
|
||||
##@ General
|
||||
|
||||
# The help target prints out all targets with their descriptions organized
|
||||
# beneath their categories. The categories are represented by '##@' and the
|
||||
# target descriptions by '##'. The awk commands is responsible for reading the
|
||||
# entire set of makefiles included in this invocation, looking for lines of the
|
||||
# file as xyz: ## something, and then pretty-format the target and help. Then,
|
||||
# if there's a line with ##@ something, that gets pretty-printed as a category.
|
||||
# More info on the usage of ANSI control characters for terminal formatting:
|
||||
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
|
||||
# More info on the awk command:
|
||||
# http://linuxcommand.org/lc3_adv_awk.php
|
||||
|
||||
.PHONY: help
|
||||
help: ## Display this help.
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
|
||||
##@ Building
|
||||
|
||||
build_prod: frontend_prod ## Build release binary locally
|
||||
go build \
|
||||
-trimpath \
|
||||
-mod=readonly \
|
||||
-ldflags "-X main.version=$(shell git describe --tags --always || echo dev)"
|
||||
|
||||
lint:
|
||||
golangci-lint run
|
||||
|
||||
publish: frontend_prod
|
||||
publish: frontend_prod ## Run build tooling to produce all binaries
|
||||
bash ./ci/build.sh
|
||||
|
||||
short_test:
|
||||
go test -cover -test.short -v ./...
|
||||
##@ Development
|
||||
|
||||
test:
|
||||
go test -cover -v ./...
|
||||
lint: ## Run Linter against code
|
||||
$(GOLANCI_LINT) run ./...
|
||||
|
||||
# --- Editor frontend
|
||||
short_test: ## Run tests not depending on network
|
||||
$(GO_TEST) --hide-summary skipped -- ./... -cover -short
|
||||
|
||||
test: ## Run all tests
|
||||
$(GO_TEST) --hide-summary skipped -- ./... -cover
|
||||
|
||||
##@ Editor frontend
|
||||
|
||||
frontend_prod: export NODE_ENV=production
|
||||
frontend_prod: frontend
|
||||
frontend_prod: frontend ## Build frontend in production mode
|
||||
|
||||
frontend: node_modules
|
||||
frontend: node_modules ## Build frontend
|
||||
node ci/build.mjs
|
||||
|
||||
frontend_lint: node_modules
|
||||
frontend_lint: node_modules ## Lint frontend files
|
||||
./node_modules/.bin/eslint \
|
||||
--ext .js,.vue \
|
||||
--fix \
|
||||
src
|
||||
|
||||
node_modules:
|
||||
node_modules: ## Install node modules
|
||||
npm ci --include dev
|
||||
|
||||
# --- Tools
|
||||
##@ Tooling
|
||||
|
||||
update_ua_list:
|
||||
# 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
|
||||
update-chrome-major: ## Patch latest Chrome major version into linkcheck
|
||||
sed -i -E \
|
||||
's/chromeMajor = [0-9]+/chromeMajor = $(shell curl -sSf https://lv.luzifer.io/v1/catalog/google-chrome/stable/version | cut -d '.' -f 1)/' \
|
||||
internal/linkcheck/useragent.go
|
||||
|
||||
gh-workflow:
|
||||
bash ci/create-workflow.sh
|
||||
##@ Vulnerability scanning
|
||||
|
||||
# -- Vulnerability scanning --
|
||||
|
||||
trivy:
|
||||
trivy: ## Run Trivy against the code
|
||||
trivy fs . \
|
||||
--dependency-tree \
|
||||
--exit-code 1 \
|
||||
|
@ -58,23 +80,23 @@ trivy:
|
|||
--quiet \
|
||||
--scanners misconfig,license,secret,vuln \
|
||||
--severity HIGH,CRITICAL \
|
||||
--skip-dirs docs
|
||||
--skip-dirs docs,tools
|
||||
|
||||
# -- Documentation Site --
|
||||
##@ Documentation
|
||||
|
||||
docs: actor_docs eventclient_docs template_docs
|
||||
docs: actor_docs eventclient_docs template_docs ## Generate all documentation
|
||||
|
||||
actor_docs:
|
||||
actor_docs: ## Generate actor documentation
|
||||
go run . --storage-conn-string $(shell mktemp).db actor-docs >docs/content/configuration/actors.md
|
||||
|
||||
template_docs:
|
||||
template_docs: ## Generate template function documentation
|
||||
go run . --storage-conn-string $(shell mktemp).db tpl-docs >docs/content/configuration/templating.md
|
||||
|
||||
eventclient_docs:
|
||||
eventclient_docs: ## Generate eventclient documentation
|
||||
echo -e "---\ntitle: EventClient\nweight: 10000\n---\n" >docs/content/overlays/eventclient.md
|
||||
docker run --rm -i -v $(CURDIR):$(CURDIR) -w $(CURDIR) node:18-alpine sh -ec 'npx --yes jsdoc-to-markdown --files ./internal/apimodules/overlays/default/eventclient.js' >>docs/content/overlays/eventclient.md
|
||||
|
||||
render_docs: hugo_$(HUGO_VERSION)
|
||||
render_docs: hugo_$(HUGO_VERSION) ## Render documentation site
|
||||
./hugo_$(HUGO_VERSION) \
|
||||
--baseURL "$(DOCS_BASE_URL)" \
|
||||
--cleanDestinationDir \
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
@ -8,6 +9,7 @@ import (
|
|||
"gopkg.in/irc.v4"
|
||||
|
||||
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||
"github.com/Luzifer/twitch-bot/v3/internal/locker"
|
||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||
)
|
||||
|
||||
|
@ -79,6 +81,9 @@ func handleMessage(c *irc.Client, m *irc.Message, event *string, eventData *fiel
|
|||
}
|
||||
|
||||
func handleMessageRuleExecution(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection) {
|
||||
locker.LockByKey(path.Join("rule-execution", r.MatcherID()))
|
||||
defer locker.UnlockByKey(path.Join("rule-execution", r.MatcherID()))
|
||||
|
||||
var (
|
||||
ruleEventData = fieldcollection.NewFieldCollection()
|
||||
preventCooldown bool
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gofrs/uuid/v3"
|
||||
"github.com/pkg/errors"
|
||||
|
@ -51,7 +52,25 @@ func writeAuthMiddleware(h http.Handler, module string) http.Handler {
|
|||
token = pass
|
||||
|
||||
case r.Header.Get("Authorization") != "":
|
||||
token = r.Header.Get("Authorization")
|
||||
var (
|
||||
tokenType string
|
||||
hadPrefix bool
|
||||
)
|
||||
|
||||
tokenType, token, hadPrefix = strings.Cut(r.Header.Get("Authorization"), " ")
|
||||
switch {
|
||||
case !hadPrefix:
|
||||
// Legacy: Accept `Authorization: tokenhere`
|
||||
token = tokenType
|
||||
|
||||
case strings.EqualFold(tokenType, "token"):
|
||||
// This is perfect: `Authorization: Token tokenhere`
|
||||
|
||||
default:
|
||||
// That was unexpected: `Authorization: Bearer tokenhere` or similar
|
||||
http.Error(w, "invalid token type", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
http.Error(w, "auth not successful", http.StatusForbidden)
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
name: CI Workflow
|
||||
on: push
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs: {}
|
|
@ -1,44 +0,0 @@
|
|||
---
|
||||
|
||||
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
|
||||
|
||||
...
|
|
@ -1,30 +0,0 @@
|
|||
---
|
||||
|
||||
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
|
||||
|
||||
...
|
|
@ -1,54 +0,0 @@
|
|||
---
|
||||
|
||||
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
|
||||
|
||||
...
|
|
@ -1,57 +0,0 @@
|
|||
---
|
||||
|
||||
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
|
||||
|
||||
...
|
|
@ -1,57 +0,0 @@
|
|||
---
|
||||
|
||||
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
|
||||
|
||||
...
|
|
@ -1,43 +0,0 @@
|
|||
---
|
||||
|
||||
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
|
||||
|
||||
...
|
|
@ -1,49 +0,0 @@
|
|||
---
|
||||
|
||||
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
|
||||
|
||||
...
|
|
@ -1,35 +0,0 @@
|
|||
---
|
||||
|
||||
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
|
||||
|
||||
...
|
82
cli.go
82
cli.go
|
@ -1,85 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/Luzifer/go_helpers/v2/cli"
|
||||
)
|
||||
|
||||
type (
|
||||
cliRegistry struct {
|
||||
cmds map[string]cliRegistryEntry
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
cliRegistryEntry struct {
|
||||
Description string
|
||||
Name string
|
||||
Params []string
|
||||
Run func([]string) error
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
cli = newCLIRegistry()
|
||||
errHelpCalled = errors.New("help called")
|
||||
)
|
||||
|
||||
func newCLIRegistry() *cliRegistry {
|
||||
return &cliRegistry{
|
||||
cmds: make(map[string]cliRegistryEntry),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cliRegistry) Add(e cliRegistryEntry) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
c.cmds[e.Name] = e
|
||||
}
|
||||
|
||||
func (c *cliRegistry) Call(args []string) error {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
cmdEntry := c.cmds[args[0]]
|
||||
if cmdEntry.Name != args[0] {
|
||||
c.help()
|
||||
return errHelpCalled
|
||||
}
|
||||
|
||||
return cmdEntry.Run(args)
|
||||
}
|
||||
|
||||
func (c *cliRegistry) help() {
|
||||
// Called from Call, does not need lock
|
||||
|
||||
var (
|
||||
maxCmdLen int
|
||||
cmds []cliRegistryEntry
|
||||
)
|
||||
|
||||
for name := range c.cmds {
|
||||
entry := c.cmds[name]
|
||||
if l := len(entry.CommandDisplay()); l > maxCmdLen {
|
||||
maxCmdLen = l
|
||||
}
|
||||
cmds = append(cmds, entry)
|
||||
}
|
||||
|
||||
sort.Slice(cmds, func(i, j int) bool { return cmds[i].Name < cmds[j].Name })
|
||||
|
||||
tpl := fmt.Sprintf(" %%-%ds %%s\n", maxCmdLen)
|
||||
fmt.Fprintln(os.Stdout, "Supported sub-commands are:")
|
||||
for _, cmd := range cmds {
|
||||
fmt.Fprintf(os.Stdout, tpl, cmd.CommandDisplay(), cmd.Description)
|
||||
}
|
||||
}
|
||||
|
||||
func (c cliRegistryEntry) CommandDisplay() string {
|
||||
return strings.Join(append([]string{c.Name}, c.Params...), " ")
|
||||
}
|
||||
var cliTool = cli.New()
|
||||
|
|
|
@ -4,11 +4,12 @@ import (
|
|||
"bytes"
|
||||
"os"
|
||||
|
||||
"github.com/Luzifer/go_helpers/v2/cli"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cli.Add(cliRegistryEntry{
|
||||
cliTool.Add(cli.RegistryEntry{
|
||||
Name: "actor-docs",
|
||||
Description: "Generate markdown documentation for available actors",
|
||||
Run: func([]string) error {
|
||||
|
|
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"os"
|
||||
|
||||
"github.com/Luzifer/go_helpers/v2/cli"
|
||||
"github.com/gofrs/uuid/v3"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
@ -10,12 +11,12 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
cli.Add(cliRegistryEntry{
|
||||
cliTool.Add(cli.RegistryEntry{
|
||||
Name: "api-token",
|
||||
Description: "Generate an api-token to be entered into the config",
|
||||
Params: []string{"<token-name>", "<scope>", "[...scope]"},
|
||||
Run: func(args []string) error {
|
||||
if len(args) < 3 { //nolint:gomnd // Just a count of parameters
|
||||
if len(args) < 3 { //nolint:mnd // Just a count of parameters
|
||||
return errors.New("Usage: twitch-bot api-token <token name> <scope> [...scope]")
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/Luzifer/go_helpers/v2/cli"
|
||||
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||
"github.com/pkg/errors"
|
||||
|
@ -16,12 +17,12 @@ var (
|
|||
)
|
||||
|
||||
func init() {
|
||||
cli.Add(cliRegistryEntry{
|
||||
cliTool.Add(cli.RegistryEntry{
|
||||
Name: "copy-database",
|
||||
Description: "Copies database contents to a new storage DSN i.e. for migrating to a new DBMS",
|
||||
Params: []string{"<target storage-type>", "<target DSN>"},
|
||||
Run: func(args []string) error {
|
||||
if len(args) < 3 { //nolint:gomnd // Just a count of parameters
|
||||
if len(args) < 3 { //nolint:mnd // Just a count of parameters
|
||||
return errors.New("Usage: twitch-bot copy-database <target storage-type> <target DSN>")
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/Luzifer/go_helpers/v2/cli"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cli.Add(cliRegistryEntry{
|
||||
cliTool.Add(cli.RegistryEntry{
|
||||
Name: "reset-secrets",
|
||||
Description: "Remove encrypted data to reset encryption passphrase",
|
||||
Run: func([]string) error {
|
||||
|
|
|
@ -4,11 +4,12 @@ import (
|
|||
"bytes"
|
||||
"os"
|
||||
|
||||
"github.com/Luzifer/go_helpers/v2/cli"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cli.Add(cliRegistryEntry{
|
||||
cliTool.Add(cli.RegistryEntry{
|
||||
Name: "tpl-docs",
|
||||
Description: "Generate markdown documentation for available template functions",
|
||||
Run: func([]string) error {
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
package main
|
||||
|
||||
import "github.com/pkg/errors"
|
||||
import (
|
||||
"github.com/Luzifer/go_helpers/v2/cli"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cli.Add(cliRegistryEntry{
|
||||
cliTool.Add(cli.RegistryEntry{
|
||||
Name: "validate-config",
|
||||
Description: "Try to load configuration file and report errors if any",
|
||||
Run: func([]string) error {
|
||||
|
|
|
@ -211,10 +211,12 @@ func writeConfigToYAML(filename, authorName, authorEmail, summary string, obj *c
|
|||
}
|
||||
tmpFileName := tmpFile.Name()
|
||||
|
||||
fmt.Fprintf(tmpFile, "# Automatically updated by %s using Config-Editor frontend, last update: %s\n", authorName, time.Now().Format(time.RFC3339))
|
||||
if _, err = fmt.Fprintf(tmpFile, "# Automatically updated by %s using Config-Editor frontend, last update: %s\n", authorName, time.Now().Format(time.RFC3339)); err != nil {
|
||||
return fmt.Errorf("writing file header: %w", err)
|
||||
}
|
||||
|
||||
if err = yaml.NewEncoder(tmpFile).Encode(obj); err != nil {
|
||||
tmpFile.Close() //nolint:errcheck,gosec,revive
|
||||
tmpFile.Close() //nolint:errcheck,gosec
|
||||
return errors.Wrap(err, "encoding config")
|
||||
}
|
||||
|
||||
|
|
|
@ -81,6 +81,7 @@ func configEditorHandleAutoMessageAdd(w http.ResponseWriter, r *http.Request) {
|
|||
user, _, err := getAuthorizationFromRequest(r)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
msg := &autoMessage{}
|
||||
|
@ -106,6 +107,7 @@ func configEditorHandleAutoMessageDelete(w http.ResponseWriter, r *http.Request)
|
|||
user, _, err := getAuthorizationFromRequest(r)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := patchConfig(cfg.Config, user, "", "Delete auto-message", func(c *configFile) error {
|
||||
|
@ -142,6 +144,7 @@ func configEditorHandleAutoMessageUpdate(w http.ResponseWriter, r *http.Request)
|
|||
user, _, err := getAuthorizationFromRequest(r)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
msg := &autoMessage{}
|
||||
|
|
|
@ -172,6 +172,7 @@ func configEditorHandleGeneralDeleteAuthToken(w http.ResponseWriter, r *http.Req
|
|||
user, _, err := getAuthorizationFromRequest(r)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := patchConfig(cfg.Config, user, "", "Delete auth-token", func(cfg *configFile) error {
|
||||
|
@ -234,6 +235,7 @@ func configEditorHandleGeneralUpdate(w http.ResponseWriter, r *http.Request) {
|
|||
user, _, err := getAuthorizationFromRequest(r)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var payload configEditorGeneralConfig
|
||||
|
|
|
@ -81,6 +81,7 @@ func configEditorRulesAdd(w http.ResponseWriter, r *http.Request) {
|
|||
user, _, err := getAuthorizationFromRequest(r)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
msg := &plugins.Rule{}
|
||||
|
@ -119,6 +120,7 @@ func configEditorRulesDelete(w http.ResponseWriter, r *http.Request) {
|
|||
user, _, err := getAuthorizationFromRequest(r)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := patchConfig(cfg.Config, user, "", "Delete rule", func(c *configFile) error {
|
||||
|
@ -155,6 +157,7 @@ func configEditorRulesUpdate(w http.ResponseWriter, r *http.Request) {
|
|||
user, _, err := getAuthorizationFromRequest(r)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
msg := &plugins.Rule{}
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
)
|
||||
|
||||
func updateConfigCron() string {
|
||||
minute := rand.Intn(60) //nolint:gomnd,gosec // Only used to distribute load
|
||||
minute := rand.Intn(60) //nolint:mnd,gosec // Only used to distribute load
|
||||
return fmt.Sprintf("0 %d * * * *", minute)
|
||||
}
|
||||
|
||||
|
|
|
@ -84,6 +84,23 @@ Triggers the creation of a Clip from the given channel owned by the creator (sub
|
|||
add_delay: false
|
||||
```
|
||||
|
||||
## Create Marker
|
||||
|
||||
Creates a marker on the currently running stream of the given channel. The marker will be created on behalf of the channel owner and requires matching scope. (Subsequent actions can use variable `marker` to access marker details.)
|
||||
|
||||
```yaml
|
||||
- type: marker
|
||||
attributes:
|
||||
# Channel to create the marker in, defaults to the channel of the event / message
|
||||
# Optional: true
|
||||
# Type: string (Supports Templating)
|
||||
channel: ""
|
||||
# Description of the marker to create (up to 140 chars)
|
||||
# Optional: true
|
||||
# Type: string (Supports Templating)
|
||||
description: ""
|
||||
```
|
||||
|
||||
## Custom Event
|
||||
|
||||
Create a custom event
|
||||
|
@ -523,7 +540,7 @@ Send raw IRC message
|
|||
|
||||
## Send Whisper
|
||||
|
||||
Send a whisper (requires a verified bot!)
|
||||
Send a whisper
|
||||
|
||||
```yaml
|
||||
- type: whisper
|
||||
|
|
|
@ -165,6 +165,19 @@ Example:
|
|||
* 1 6
|
||||
```
|
||||
|
||||
### `currentVOD`
|
||||
|
||||
Returns the VOD of the currently running stream in the given channel (causes an error if no current stream / VOD is found)
|
||||
|
||||
Syntax: `currentVOD <username>`
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
# {{ currentVOD .channel }}
|
||||
* https://www.twitch.tv/videos/123456789
|
||||
```
|
||||
|
||||
### `displayName`
|
||||
|
||||
Returns the display name the specified user set for themselves
|
||||
|
@ -379,6 +392,32 @@ Example:
|
|||
< @user @user @user
|
||||
```
|
||||
|
||||
### `parseDuration`
|
||||
|
||||
Parses a duration (i.e. 1h25m10s) into a time.Duration
|
||||
|
||||
Syntax: `parseDuration <duration>`
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
# {{ parseDuration "1h30s" }}
|
||||
< 1h0m30s
|
||||
```
|
||||
|
||||
### `parseDurationToSeconds`
|
||||
|
||||
Parses a duration (i.e. 1h25m10s) into a number of seconds
|
||||
|
||||
Syntax: `parseDurationToSeconds <duration>`
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
# {{ parseDurationToSeconds "1h25m10s" }}
|
||||
< 5110
|
||||
```
|
||||
|
||||
### `pow`
|
||||
|
||||
Returns float from calculation: `float1 ** float2`
|
||||
|
@ -467,12 +506,12 @@ Example:
|
|||
|
||||
```
|
||||
# Your int this hour: {{ printf "%.0f" (mulf (seededRandom (list "int" .username (now | date "2006-01-02 15") | join ":")) 100) }}%
|
||||
< Your int this hour: 88%
|
||||
< Your int this hour: 24%
|
||||
```
|
||||
|
||||
### `spotifyCurrentPlaying`
|
||||
|
||||
Retrieves the current playing track for the given channel
|
||||
Retrieves the current playing track for the given channel (returns an empty string when nothing is playing)
|
||||
|
||||
Syntax: `spotifyCurrentPlaying <channel>`
|
||||
|
||||
|
@ -487,7 +526,7 @@ Example:
|
|||
|
||||
### `spotifyLink`
|
||||
|
||||
Retrieves the link for the playing track for the given channel
|
||||
Retrieves the link for the playing track for the given channel (returns an empty string when nothing is playing)
|
||||
|
||||
Syntax: `spotifyLink <channel>`
|
||||
|
||||
|
@ -500,6 +539,19 @@ Example:
|
|||
* https://open.spotify.com/track/3HCzXf0lNpekSqsGBcGrCd
|
||||
```
|
||||
|
||||
### `streamIsLive`
|
||||
|
||||
Check whether a given channel is currently live
|
||||
|
||||
Syntax: `streamIsLive <username>`
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
# {{ streamIsLive "luziferus" }}
|
||||
* true
|
||||
```
|
||||
|
||||
### `streamUptime`
|
||||
|
||||
Returns the duration the stream is online (causes an error if no current stream is found)
|
||||
|
@ -567,6 +619,19 @@ Example:
|
|||
* Weather for Hamburg, DE: Few clouds with a temperature of 22 C (71.6 F). [...]
|
||||
```
|
||||
|
||||
### `userExists`
|
||||
|
||||
Checks whether the given user exists
|
||||
|
||||
Syntax: `userExists <username>`
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
# {{ userExists "luziferus" }}
|
||||
* true
|
||||
```
|
||||
|
||||
### `usernameForID`
|
||||
|
||||
Returns the current login name of an user-id
|
||||
|
|
|
@ -116,7 +116,7 @@ SocketMessage received for every event and passed to the new `(eventObj) => { ..
|
|||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [event_id] | <code>Number</code> | UID of the event used to re-trigger an event |
|
||||
| [event_id] | <code>String</code> | UID of the event used to re-trigger an event |
|
||||
| [is_live] | <code>Boolean</code> | Whether the event was sent through a replay (false) or occurred live (true) |
|
||||
| [reason] | <code>String</code> | Reason of this message (one of `bulk-replay`, `live-event`, `single-replay`) |
|
||||
| [time] | <code>String</code> | RFC3339 timestamp of the event |
|
||||
|
|
|
@ -15,7 +15,7 @@ To use it
|
|||
|
||||
- generate an API token with the `overlays` permission and note it in a secure place
|
||||
- add a Browser-Source to your OBS scenes with at least 1×1 px in size
|
||||
- set the URL to `https://your-bot.example.com/overlays/sounds.html?token=[your-token]&channel=[your-channel]`
|
||||
- set the URL to `https://your-bot.example.com/overlays/sounds.html#token=[your-token]&channel=[your-channel]`
|
||||
|
||||
After you've done this you're already done with the setup inside your OBS.
|
||||
|
||||
|
|
BIN
docs/static/raffle-entrants-closed.png
vendored
BIN
docs/static/raffle-entrants-closed.png
vendored
Binary file not shown.
Before ![]() (image error) Size: 130 B After ![]() (image error) Size: 14 KiB ![]() ![]() |
BIN
docs/static/raffle-entrants.png
vendored
BIN
docs/static/raffle-entrants.png
vendored
Binary file not shown.
Before ![]() (image error) Size: 130 B After ![]() (image error) Size: 13 KiB ![]() ![]() |
BIN
docs/static/raffle-general-config.png
vendored
BIN
docs/static/raffle-general-config.png
vendored
Binary file not shown.
Before ![]() (image error) Size: 130 B After ![]() (image error) Size: 77 KiB ![]() ![]() |
BIN
docs/static/raffle-overview.png
vendored
BIN
docs/static/raffle-overview.png
vendored
Binary file not shown.
Before ![]() (image error) Size: 130 B After ![]() (image error) Size: 16 KiB ![]() ![]() |
BIN
docs/static/raffle-texts.png
vendored
BIN
docs/static/raffle-texts.png
vendored
Binary file not shown.
Before ![]() (image error) Size: 130 B After ![]() (image error) Size: 67 KiB ![]() ![]() |
BIN
docs/static/screen-twitch-console-register-app.png
vendored
BIN
docs/static/screen-twitch-console-register-app.png
vendored
Binary file not shown.
Before ![]() (image error) Size: 130 B After ![]() (image error) Size: 61 KiB ![]() ![]() |
21
functions.go
21
functions.go
|
@ -7,21 +7,15 @@ import (
|
|||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/irc.v4"
|
||||
|
||||
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||
"github.com/Luzifer/go_helpers/v2/str"
|
||||
korvike "github.com/Luzifer/korvike/functions"
|
||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||
)
|
||||
|
||||
var (
|
||||
korvikeBlacklist = []string{"now"}
|
||||
sprigBlacklist = []string{"env"}
|
||||
tplFuncs = newTemplateFuncProvider()
|
||||
)
|
||||
var tplFuncs = newTemplateFuncProvider()
|
||||
|
||||
type templateFuncProvider struct {
|
||||
docs []plugins.TemplateFuncDocumentation
|
||||
|
@ -44,16 +38,6 @@ func (t *templateFuncProvider) GetFuncMap(m *irc.Message, r *plugins.Rule, field
|
|||
|
||||
out := make(template.FuncMap)
|
||||
|
||||
for n, fn := range sprig.TxtFuncMap() {
|
||||
if str.StringInSlice(n, sprigBlacklist) {
|
||||
continue
|
||||
}
|
||||
if out[n] != nil {
|
||||
panic(fmt.Sprintf("duplicate function: %s (add in sprig)", n))
|
||||
}
|
||||
out[n] = fn
|
||||
}
|
||||
|
||||
for n, fg := range t.funcs {
|
||||
if out[n] != nil {
|
||||
panic(fmt.Sprintf("duplicate function: %s (add in registration)", n))
|
||||
|
@ -93,9 +77,6 @@ func (t *templateFuncProvider) Register(name string, fg plugins.TemplateFuncGett
|
|||
func init() {
|
||||
// Register Korvike functions
|
||||
for n, f := range korvike.GetFunctionMap() {
|
||||
if str.StringInSlice(n, korvikeBlacklist) {
|
||||
continue
|
||||
}
|
||||
tplFuncs.Register(n, plugins.GenericTemplateFunctionGetter(f))
|
||||
}
|
||||
|
||||
|
|
116
go.mod
116
go.mod
|
@ -1,77 +1,76 @@
|
|||
module github.com/Luzifer/twitch-bot/v3
|
||||
|
||||
go 1.21
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.25.0
|
||||
|
||||
require (
|
||||
github.com/Luzifer/go-openssl/v4 v4.2.2
|
||||
github.com/Luzifer/go_helpers/v2 v2.24.0
|
||||
github.com/Luzifer/korvike/functions v0.11.0
|
||||
github.com/Luzifer/rconfig/v2 v2.5.0
|
||||
github.com/Masterminds/sprig/v3 v3.2.3
|
||||
github.com/getsentry/sentry-go v0.27.0
|
||||
github.com/Luzifer/go-openssl/v4 v4.2.4
|
||||
github.com/Luzifer/go_helpers/v2 v2.25.0
|
||||
github.com/Luzifer/korvike/functions v1.0.2
|
||||
github.com/Luzifer/rconfig/v2 v2.6.0
|
||||
github.com/getsentry/sentry-go v0.35.1
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-git/go-git/v5 v5.12.0
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/go-git/go-git/v5 v5.16.2
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/gofrs/uuid v4.4.0+incompatible
|
||||
github.com/gofrs/uuid/v3 v3.1.2
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/itchyny/gojq v0.12.15
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/itchyny/gojq v0.12.17
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
github.com/orandin/sentrus v1.0.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/stretchr/testify v1.11.0
|
||||
github.com/wzshiming/openapi v0.0.0-20200703171632-c7220b3c9cfb
|
||||
golang.org/x/crypto v0.21.0
|
||||
golang.org/x/net v0.22.0
|
||||
golang.org/x/oauth2 v0.18.0
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
gopkg.in/irc.v4 v4.0.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/mysql v1.5.6
|
||||
gorm.io/driver/postgres v1.5.7
|
||||
gorm.io/gorm v1.25.9
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.30.1
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-hclog v1.4.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.6 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/vault/api v1.12.2 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/itchyny/timefmt-go v0.1.5 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
|
||||
github.com/hashicorp/vault/api v1.16.0 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/itchyny/timefmt-go v0.1.6 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
|
||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.4 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
|
@ -82,29 +81,26 @@ require (
|
|||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/skeema/knownhosts v1.2.2 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.19.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
gopkg.in/validator.v2 v2.0.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
modernc.org/libc v1.49.0 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/sqlite v1.29.5 // indirect
|
||||
modernc.org/libc v1.62.1 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.9.1 // indirect
|
||||
modernc.org/sqlite v1.37.0 // indirect
|
||||
)
|
||||
|
|
447
go.sum
447
go.sum
|
@ -1,178 +1,127 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
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/go.mod h1:+kAwI4NpyYXoWil85gKSCEJNoCQlMeFikEMn2f+5ffc=
|
||||
github.com/Luzifer/go_helpers/v2 v2.24.0 h1:abACOhsn6a6c6X22jq42mZM1wuOM0Ihfa6yzssrjrOg=
|
||||
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/go.mod h1:osumwH64mWgbwZIfE7rE0BB7Y5HXxrzyO4JfO7fhduU=
|
||||
github.com/Luzifer/rconfig/v2 v2.5.0 h1:zx5lfQbNX3za4VegID97IeY+M+BmfgHxWJTYA94sxok=
|
||||
github.com/Luzifer/rconfig/v2 v2.5.0/go.mod h1:eGWUPQeCPv/Pr/p0hjmwFgI20uqvwi/Szen69hUzGzU=
|
||||
github.com/Luzifer/go-openssl/v4 v4.2.4 h1:3Eu3gSeZpr8Ha+IofVnSWttCL1xejRr/lda4l4TZRWk=
|
||||
github.com/Luzifer/go-openssl/v4 v4.2.4/go.mod h1:ykquxaR0R1Vor83/FAtGBJZZO5zswuSQTVx1FQc1bJY=
|
||||
github.com/Luzifer/go_helpers/v2 v2.25.0 h1:k1J4gd1+BfuokTDoWgcgib9P5mdadjzKEgbtKSVe46k=
|
||||
github.com/Luzifer/go_helpers/v2 v2.25.0/go.mod h1:KSVUdAJAav5cWGyB5oKGxmC27HrKULVTOxwPS/Kr+pc=
|
||||
github.com/Luzifer/korvike/functions v1.0.2 h1:Higwd9CVI03MioXYL8mttAsNBWUfAheKJx1ECaGIcAY=
|
||||
github.com/Luzifer/korvike/functions v1.0.2/go.mod h1:4cNc+vPr4Ag83P5ASdk5QDnPOJLGxhbcBqj5UMVeVVs=
|
||||
github.com/Luzifer/rconfig/v2 v2.6.0 h1:ZKgsO2Wt/XZXawuAZCDkW7xszxZ8hQDTV1Wm63Jvnqk=
|
||||
github.com/Luzifer/rconfig/v2 v2.6.0/go.mod h1:CxISRwCV2WjO5gnUnaRGDq17u1M3TvmFgzJLr87ejtc=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
|
||||
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
|
||||
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
|
||||
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=
|
||||
github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
|
||||
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
|
||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
|
||||
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||
github.com/getsentry/sentry-go v0.35.1 h1:iopow6UVLE2aXu46xKVIs8Z9D/YZkJrHkgozrxa+tOQ=
|
||||
github.com/getsentry/sentry-go v0.35.1/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
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/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
|
||||
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
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-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
|
||||
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
|
||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
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/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
|
||||
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/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-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
|
||||
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
|
||||
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
|
||||
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid/v3 v3.1.2 h1:V3IBv1oU82x6YIr5txe3azVHgmOKYdyKQTowm9moBlY=
|
||||
github.com/gofrs/uuid/v3 v3.1.2/go.mod h1:xPwMqoocQ1L5G6pXX5BcE7N5jlzn2o19oqAKxwZW/kI=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
|
||||
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-hclog v1.4.0 h1:ctuWFGrhFha8BnnzxqeRGidlEcQkDyL5u8J8t5eA11I=
|
||||
github.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
|
||||
github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 h1:iBt4Ew4XEGLfh6/bPk4rSYmuZJGizr6/x/AEizP0CQc=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8/go.mod h1:aiJI+PIApBRQG7FZTEBx5GiiX+HbOHilUdNxUZi4eV0=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
|
||||
github.com/hashicorp/go-sockaddr v1.0.6 h1:RSG8rKU28VTUTvEKghe5gIhIQpv8evvNpnDEyqO4u9I=
|
||||
github.com/hashicorp/go-sockaddr v1.0.6/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q=
|
||||
github.com/hashicorp/vault/api v1.12.2 h1:7YkCTE5Ni90TcmYHDBExdt4WGJxhpzaHqR6uGbQb/rE=
|
||||
github.com/hashicorp/vault/api v1.12.2/go.mod h1:LSGf1NGT1BnvFFnKVtnvcaLBM2Lz+gJdpL6HUYed8KE=
|
||||
github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M=
|
||||
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
|
||||
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
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/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/itchyny/gojq v0.12.15 h1:WC1Nxbx4Ifw5U2oQWACYz32JK8G9qxNtHzrvW4KEcqI=
|
||||
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/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
|
||||
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
|
||||
github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4=
|
||||
github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg=
|
||||
github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY=
|
||||
github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q=
|
||||
github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
|
||||
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
|
@ -188,224 +137,115 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
||||
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/orandin/sentrus v1.0.0 h1:rMZKTUdwuhIaC7C6VbvhQPQeO9hBpliODrj7o/NmipM=
|
||||
github.com/orandin/sentrus v1.0.0/go.mod h1:Mqa1Dcat0IcuD/XPMXUolzuZ74NWptnnX8eRq3gLaSU=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
||||
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
||||
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
||||
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/russross/blackfriday/v2 v2.1.0-pre.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
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/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
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.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
|
||||
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/wzshiming/openapi v0.0.0-20200703171632-c7220b3c9cfb h1:G0Rrif8QdbAz7Xy53H4Xumy6TuyKHom8pu8z/jdLwwM=
|
||||
github.com/wzshiming/openapi v0.0.0-20200703171632-c7220b3c9cfb/go.mod h1:398xiAftMV/w8frjipnUzjr/WQ+E2fnGRv9yXobxyyk=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
|
||||
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
|
||||
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/irc.v4 v4.0.0 h1:5jsLkU2Tg+R2nGNqmkGCrciasyi4kNkDXhyZD+C31yY=
|
||||
gopkg.in/irc.v4 v4.0.0/go.mod h1:BfjDz9MmuWW6OZY7iq4naOhudO8+QQCdO4Ko18jcsRE=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY=
|
||||
gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
|
@ -417,36 +257,33 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
|
||||
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
|
||||
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.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8=
|
||||
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-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
modernc.org/cc/v4 v4.19.5 h1:QlsZyQ1zf78DGeqnQ9ILi9hXyMdoC5e1qoGNUyBjHQw=
|
||||
modernc.org/cc/v4 v4.19.5/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||
modernc.org/ccgo/v4 v4.13.1 h1:qBttaSxEHNze36VBivw1/vkHuyjMDN3RY5wQX+p1Oxg=
|
||||
modernc.org/ccgo/v4 v4.13.1/go.mod h1:Td6RI9W9G2ZpKHaJ7UeGEiB2aIpoDqLBnm4wtkbJTbQ=
|
||||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
|
||||
gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic=
|
||||
modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU=
|
||||
modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
||||
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/libc v1.49.0 h1:/kkNBuCXvlTbOGwrQdgR67eK1Y9+kR+fhdBd89C64VM=
|
||||
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/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||
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/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||
modernc.org/sqlite v1.29.5 h1:8l/SQKAjDtZFo9lkJLdk8g9JEOeYRG4/ghStDCCTiTE=
|
||||
modernc.org/sqlite v1.29.5/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s=
|
||||
modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g=
|
||||
modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
|
||||
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
|
|
|
@ -336,7 +336,7 @@ func routeActorCounterGetValue(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text-plain")
|
||||
fmt.Fprintf(w, template, cv)
|
||||
http.Error(w, fmt.Sprintf(template, cv), http.StatusOK)
|
||||
}
|
||||
|
||||
func routeActorCounterSetValue(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
114
internal/actors/marker/actor.go
Normal file
114
internal/actors/marker/actor.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
// Package marker contains an actor to create markers on the current
|
||||
// running stream
|
||||
package marker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"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/plugins"
|
||||
"gopkg.in/irc.v4"
|
||||
)
|
||||
|
||||
const actorName = "marker"
|
||||
|
||||
var (
|
||||
formatMessage plugins.MsgFormatter
|
||||
hasPerm plugins.ChannelPermissionCheckFunc
|
||||
tcGetter func(string) (*twitch.Client, error)
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
formatMessage = args.FormatMessage
|
||||
hasPerm = args.HasPermissionForChannel
|
||||
tcGetter = args.GetTwitchClientForChannel
|
||||
|
||||
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
||||
|
||||
args.RegisterActorDocumentation(plugins.ActionDocumentation{
|
||||
Description: "Creates a marker on the currently running stream of the given channel. The marker will be created on behalf of the channel owner and requires matching scope. (Subsequent actions can use variable `marker` to access marker details.)",
|
||||
Name: "Create Marker",
|
||||
Type: actorName,
|
||||
Fields: []plugins.ActionDocumentationField{
|
||||
{
|
||||
Description: "Channel to create the marker in, defaults to the channel of the event / message",
|
||||
Key: "channel",
|
||||
Name: "Channel",
|
||||
Optional: true,
|
||||
SupportTemplate: true,
|
||||
Type: plugins.ActionDocumentationFieldTypeString,
|
||||
},
|
||||
{
|
||||
Description: "Description of the marker to create (up to 140 chars)",
|
||||
Key: "description",
|
||||
Name: "Description",
|
||||
Optional: true,
|
||||
SupportTemplate: true,
|
||||
Type: plugins.ActionDocumentationFieldTypeString,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type actor struct{}
|
||||
|
||||
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)
|
||||
if channel, err = formatMessage(attrs.MustString("channel", &channel), m, r, eventData); err != nil {
|
||||
return false, fmt.Errorf("parsing channel: %w", err)
|
||||
}
|
||||
|
||||
var description string
|
||||
if description, err = formatMessage(attrs.MustString("description", &description), m, r, eventData); err != nil {
|
||||
return false, fmt.Errorf("parsing description: %w", err)
|
||||
}
|
||||
|
||||
channel = strings.TrimLeft(channel, "#")
|
||||
|
||||
canCreate, err := hasPerm(channel, twitch.ScopeChannelManageBroadcast)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("checking for required permission: %w", err)
|
||||
}
|
||||
|
||||
if !canCreate {
|
||||
return false, fmt.Errorf("creator has not given %s permission", twitch.ScopeChannelManageBroadcast)
|
||||
}
|
||||
|
||||
tc, err := tcGetter(channel)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("getting Twitch client for %q: %w", channel, err)
|
||||
}
|
||||
|
||||
var marker twitch.StreamMarkerInfo
|
||||
if marker, err = tc.CreateStreamMarker(context.TODO(), description); err != nil {
|
||||
return false, fmt.Errorf("creating marker: %w", err)
|
||||
}
|
||||
|
||||
eventData.Set("marker", marker)
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (actor) IsAsync() bool { return false }
|
||||
|
||||
func (actor) Name() string { return actorName }
|
||||
|
||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
|
||||
if err = attrs.ValidateSchema(
|
||||
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "channel", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "description", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
|
||||
fieldcollection.MustHaveNoUnknowFields,
|
||||
helpers.SchemaValidateTemplateField(tplValidator, "channel", "description"),
|
||||
); err != nil {
|
||||
return fmt.Errorf("validating attributes: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -85,11 +85,11 @@ func getQuote(db database.Connector, channel string, quote int) (int, string, er
|
|||
|
||||
func getQuoteRaw(db database.Connector, channel string, quoteIdx int) (int, int64, string, error) {
|
||||
if quoteIdx == 0 {
|
||||
max, err := getMaxQuoteIdx(db, channel)
|
||||
maxQuoteIdx, err := getMaxQuoteIdx(db, channel)
|
||||
if err != nil {
|
||||
return 0, 0, "", errors.Wrap(err, "getting max quote idx")
|
||||
}
|
||||
quoteIdx = rand.Intn(max) + 1 // #nosec G404 // no need for cryptographic safety
|
||||
quoteIdx = rand.Intn(maxQuoteIdx) + 1 // #nosec G404 // no need for cryptographic safety
|
||||
}
|
||||
|
||||
var q quote
|
||||
|
|
|
@ -181,7 +181,7 @@ func handleDeleteQuote(w http.ResponseWriter, r *http.Request) {
|
|||
func handleListQuotes(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.Header.Get("Accept"), "text/html") {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write(listFrontend) //nolint:errcheck,gosec,revive
|
||||
w.Write(listFrontend) //nolint:errcheck,gosec
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -218,7 +218,7 @@ func handleReplaceQuotes(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
func handleScript(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/javascript")
|
||||
w.Write(listScript) //nolint:errcheck,gosec,revive
|
||||
w.Write(listScript) //nolint:errcheck,gosec
|
||||
}
|
||||
|
||||
func handleUpdateQuote(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
101
internal/actors/spotify/auth.go
Normal file
101
internal/actors/spotify/auth.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
package spotify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Luzifer/twitch-bot/v3/internal/locker"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const expiryGrace = 10 * time.Second
|
||||
|
||||
func getAuthorizedClient(channel, redirectURL string) (client *http.Client, err error) {
|
||||
// In templating functions are called multiple times at once which
|
||||
// with Spotify replacing the refresh-token on each renew would kill
|
||||
// the stored token when multiple spotify functions are called at
|
||||
// once. Therefore we do have this method locking itself until it
|
||||
// has successfully made one request to the users profile and therefore
|
||||
// renewed the token. The next request then will use the token the
|
||||
// previous request renewed.
|
||||
locker.LockByKey(strings.Join([]string{"spotify", "api-access", channel}, ":"))
|
||||
defer locker.UnlockByKey(strings.Join([]string{"spotify", "api-access", channel}, ":"))
|
||||
|
||||
conf, err := oauthConfig(channel, redirectURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting oauth config: %w", err)
|
||||
}
|
||||
|
||||
var token *oauth2.Token
|
||||
if err = db.ReadEncryptedCoreMeta(strings.Join([]string{"spotify-auth", channel}, ":"), &token); err != nil {
|
||||
return nil, fmt.Errorf("loading oauth token: %w", err)
|
||||
}
|
||||
|
||||
ts := conf.TokenSource(context.Background(), token)
|
||||
|
||||
if token.Expiry.After(time.Now().Add(expiryGrace)) {
|
||||
// Token is still valid long enough, we spare the resources to do
|
||||
// the profile fetch and directly return the client with the token
|
||||
// as the scenario described here does not apply.
|
||||
return oauth2.NewClient(context.Background(), ts), nil
|
||||
}
|
||||
|
||||
logrus.WithField("channel", channel).Debug("refreshing spotify token")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), spotifyRequestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// We do a request to /me once to refresh the token if needed
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.spotify.com/v1/me", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating currently-playing request: %w", err)
|
||||
}
|
||||
|
||||
oauthClient := oauth2.NewClient(context.Background(), ts)
|
||||
|
||||
resp, err := oauthClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("executing request: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
logrus.WithError(err).Error("closing Spotify response body (leaked fd)")
|
||||
}
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("requesting user profile: %w", err)
|
||||
}
|
||||
|
||||
updToken, err := ts.Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting updated token: %w", err)
|
||||
}
|
||||
|
||||
if err := db.StoreEncryptedCoreMeta(strings.Join([]string{"spotify-auth", channel}, ":"), updToken); err != nil {
|
||||
logrus.WithError(err).Error("storing back Spotify auth token")
|
||||
}
|
||||
|
||||
return oauthClient, nil
|
||||
}
|
||||
|
||||
func oauthConfig(channel, redirectURL string) (conf *oauth2.Config, err error) {
|
||||
clientID, err := getModuleConfig(actorName, channel).String("clientId")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting clientId for channel: %w", err)
|
||||
}
|
||||
|
||||
return &oauth2.Config{
|
||||
ClientID: clientID,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://accounts.spotify.com/authorize",
|
||||
TokenURL: "https://accounts.spotify.com/api/token",
|
||||
},
|
||||
RedirectURL: redirectURL,
|
||||
Scopes: []string{"user-read-currently-playing"},
|
||||
}, nil
|
||||
}
|
|
@ -3,34 +3,25 @@ package spotify
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
var errNotPlaying = errors.New("nothing playing")
|
||||
|
||||
func getCurrentTrackForChannel(channel string) (track currentPlayingTrackResponse, err error) {
|
||||
channel = strings.TrimLeft(channel, "#")
|
||||
|
||||
conf, err := oauthConfig(channel, "")
|
||||
client, err := getAuthorizedClient(channel, "")
|
||||
if err != nil {
|
||||
return track, fmt.Errorf("getting oauth config: %w", err)
|
||||
return track, fmt.Errorf("retrieving authorized Spotify client: %w", err)
|
||||
}
|
||||
|
||||
var token *oauth2.Token
|
||||
if err = db.ReadEncryptedCoreMeta(strings.Join([]string{"spotify-auth", channel}, ":"), &token); err != nil {
|
||||
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)
|
||||
defer cancel()
|
||||
|
||||
|
@ -39,7 +30,7 @@ func getCurrentTrackForChannel(channel string) (track currentPlayingTrackRespons
|
|||
return track, fmt.Errorf("creating currently-playing request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := conf.Client(context.Background(), token).Do(req)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return track, fmt.Errorf("executing request: %w", err)
|
||||
}
|
||||
|
@ -58,6 +49,10 @@ func getCurrentTrackForChannel(channel string) (track currentPlayingTrackRespons
|
|||
case http.StatusOK:
|
||||
// This is perfect, continue below
|
||||
|
||||
case http.StatusNoContent:
|
||||
// User is not playing anything
|
||||
return track, errNotPlaying
|
||||
|
||||
case http.StatusUnauthorized:
|
||||
// The token is FUBAR
|
||||
return track, fmt.Errorf("token expired (HTTP 401 - unauthorized)")
|
||||
|
@ -85,6 +80,10 @@ func getCurrentTrackForChannel(channel string) (track currentPlayingTrackRespons
|
|||
func getCurrentArtistTitleForChannel(channel string) (artistTitle string, err error) {
|
||||
track, err := getCurrentTrackForChannel(channel)
|
||||
if err != nil {
|
||||
if errors.Is(err, errNotPlaying) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("getting track info: %w", err)
|
||||
}
|
||||
|
||||
|
@ -102,6 +101,10 @@ func getCurrentArtistTitleForChannel(channel string) (artistTitle string, err er
|
|||
func getCurrentLinkForChannel(channel string) (link string, err error) {
|
||||
track, err := getCurrentTrackForChannel(channel)
|
||||
if err != nil {
|
||||
if errors.Is(err, errNotPlaying) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("getting track info: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -68,22 +68,5 @@ func handleStartAuth(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, "Spotify is now authorized for this channel, you can close this page")
|
||||
}
|
||||
|
||||
func oauthConfig(channel, redirectURL string) (conf *oauth2.Config, err error) {
|
||||
clientID, err := getModuleConfig(actorName, channel).String("clientId")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting clientId for channel: %w", err)
|
||||
}
|
||||
|
||||
return &oauth2.Config{
|
||||
ClientID: clientID,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://accounts.spotify.com/authorize",
|
||||
TokenURL: "https://accounts.spotify.com/api/token",
|
||||
},
|
||||
RedirectURL: redirectURL,
|
||||
Scopes: []string{"user-read-currently-playing"},
|
||||
}, nil
|
||||
http.Error(w, "Spotify is now authorized for this channel, you can close this page", http.StatusOK)
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ func Register(args plugins.RegistrationArguments) (err error) {
|
|||
plugins.GenericTemplateFunctionGetter(getCurrentArtistTitleForChannel),
|
||||
plugins.TemplateFuncDocumentation{
|
||||
Name: "spotifyCurrentPlaying",
|
||||
Description: "Retrieves the current playing track for the given channel",
|
||||
Description: "Retrieves the current playing track for the given channel (returns an empty string when nothing is playing)",
|
||||
Syntax: "spotifyCurrentPlaying <channel>",
|
||||
Example: &plugins.TemplateFuncDocumentationExample{
|
||||
MatchMessage: "^!spotify",
|
||||
|
@ -51,7 +51,7 @@ func Register(args plugins.RegistrationArguments) (err error) {
|
|||
plugins.GenericTemplateFunctionGetter(getCurrentLinkForChannel),
|
||||
plugins.TemplateFuncDocumentation{
|
||||
Name: "spotifyLink",
|
||||
Description: "Retrieves the link for the playing track for the given channel",
|
||||
Description: "Retrieves the link for the playing track for the given channel (returns an empty string when nothing is playing)",
|
||||
Syntax: "spotifyLink <channel>",
|
||||
Example: &plugins.TemplateFuncDocumentationExample{
|
||||
MatchMessage: "^!spotifylink",
|
||||
|
|
|
@ -195,7 +195,7 @@ func routeActorSetVarGetValue(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text-plain")
|
||||
fmt.Fprint(w, vc)
|
||||
http.Error(w, vc, http.StatusOK)
|
||||
}
|
||||
|
||||
func routeActorSetVarSetValue(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -188,7 +188,7 @@ func handleModVIP(m *irc.Message, modFn func(tc *twitch.Client, channel, user st
|
|||
channel := strings.TrimLeft(plugins.DeriveChannel(m, nil), "#")
|
||||
|
||||
parts := strings.Split(m.Trailing(), " ")
|
||||
if len(parts) != 2 { //nolint:gomnd // Just a count, makes no sense as a constant
|
||||
if len(parts) != 2 { //nolint:mnd // Just a count, makes no sense as a constant
|
||||
return errors.Errorf("wrong command usage, must consist of 2 words")
|
||||
}
|
||||
|
||||
|
|
|
@ -100,8 +100,8 @@ func handleKoFiPost(w http.ResponseWriter, r *http.Request) {
|
|||
fields.Set("isSubscription", payload.IsSubscriptionPayment)
|
||||
fields.Set("isFirstSubPayment", payload.IsFirstSubscriptionPayment)
|
||||
|
||||
if payload.IsPublic {
|
||||
fields.Set("message", payload.Message)
|
||||
if payload.IsPublic && payload.Message != nil {
|
||||
fields.Set("message", *payload.Message)
|
||||
}
|
||||
|
||||
if payload.IsSubscriptionPayment && payload.TierName != nil {
|
||||
|
|
|
@ -54,5 +54,5 @@ func handleFormattedMessage(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
fmt.Fprint(w, msg)
|
||||
http.Error(w, msg, http.StatusOK)
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
/**
|
||||
* SocketMessage received for every event and passed to the new `(eventObj) => { ... }` handlers
|
||||
* @typedef {Object} SocketMessage
|
||||
* @prop {Number} [event_id] - UID of the event used to re-trigger an event
|
||||
* @prop {String} [event_id] - UID of the event used to re-trigger an event
|
||||
* @prop {Boolean} [is_live] - Whether the event was sent through a replay (false) or occurred live (true)
|
||||
* @prop {String} [reason] - Reason of this message (one of `bulk-replay`, `live-event`, `single-replay`)
|
||||
* @prop {String} [time] - RFC3339 timestamp of the event
|
||||
|
|
19
internal/apimodules/overlays/default/eventfeed.custom.js
Normal file
19
internal/apimodules/overlays/default/eventfeed.custom.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* Allows to add filters for custom events created through the customHandler
|
||||
*
|
||||
* @returns {Object} Custom filter definitions as `filterKey: {name: "Name", visible: true}`
|
||||
*/
|
||||
const customFilters = () => ({})
|
||||
|
||||
/**
|
||||
* Handles custom events and creates feed items from them
|
||||
*
|
||||
* @param {*} param0 Event-Object as returned by the websocket
|
||||
* @returns {Object} Event to add to the event list of the feed
|
||||
*/
|
||||
const customHandler = eventObj => {
|
||||
console.log('custom event unhandled:', eventObj)
|
||||
return null
|
||||
}
|
||||
|
||||
export { customFilters, customHandler }
|
156
internal/apimodules/overlays/default/eventfeed.html
Normal file
156
internal/apimodules/overlays/default/eventfeed.html
Normal file
|
@ -0,0 +1,156 @@
|
|||
<html data-bs-theme="dark">
|
||||
<head>
|
||||
<title>Event-Feed</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/combine/npm/bootstrap@5.3/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.5/css/all.min.css">
|
||||
|
||||
<style>
|
||||
[v-cloak] { display: none; }
|
||||
.border-event {
|
||||
border-left-width: 5px !important;
|
||||
border-left-style: solid !important;
|
||||
border-left-color: #9147ff;
|
||||
}
|
||||
.border-event.event-bits { border-left-color: #5cffbe !important; }
|
||||
.border-event.event-channelpoint { border-left-color: #ffd37a !important; }
|
||||
.border-event.event-follow { border-left-color: #ff38db !important; }
|
||||
.border-event.event-raid { border-left-color: #ebeb00 !important; }
|
||||
.border-event.event-streamOffline { border-left-color: rgb(var(--bs-danger-rgb)) !important; }
|
||||
.border-event.event-subs { border-left-color: #1f69ff !important; }
|
||||
.m50 {
|
||||
max-height: 40vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.premono {
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app" v-cloak>
|
||||
<div class="container-fluid py-3">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
|
||||
<!-- Stream-Summary -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<span
|
||||
v-for="item in sortedStats"
|
||||
class="me-2 d-inline-flex align-items-center"
|
||||
:key="item.key"
|
||||
>
|
||||
<i :class="`fa-fw ${item.icon}`"></i>
|
||||
<span class="badge rounded-pill text-bg-primary ms-1">
|
||||
{{ item.value }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event-List -->
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
Recent events
|
||||
<div class="btn-group btn-group-sm">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary dropdown-toggle"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<i class="fas fa-filter fa-fw me-1"></i>
|
||||
Filters ({{ filterCount }})
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li
|
||||
v-for="(filter, filterKey) in filters"
|
||||
:key="filterKey"
|
||||
>
|
||||
<a
|
||||
:class="{'dropdown-item': true, 'active': filter.visible}" href="#"
|
||||
@click.prevent="toggleFilterVisibility(filterKey)"
|
||||
>
|
||||
{{ filter.name }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
@click="markRead"
|
||||
>
|
||||
<i class="fas fa-eye fa-fw me-1"></i>
|
||||
Mark read
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-group list-group-flush">
|
||||
|
||||
<!-- Active Hypetrain pin -->
|
||||
<div class="list-group-item" v-if="hypetrain.active">
|
||||
<div class="d-flex w-100 align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i :class="`fas fa-train fa-fw me-2`"></i>
|
||||
Hypetrain in progress towards Level {{ hypetrain.level }}…
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="progress my-3">
|
||||
<div class="progress-bar progress-bar-striped"
|
||||
:style="`width: ${(hypetrain.progress * 100).toFixed(2)}%`"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event-Item -->
|
||||
<div
|
||||
:class="eventClass(event)"
|
||||
v-for="event in recentEvents"
|
||||
:key="event.time.getTime()"
|
||||
>
|
||||
<div class="d-flex w-100 align-items-center">
|
||||
<h5 class="mb-0 me-auto"><i :class="`${event.icon} fa-fw me-2`"></i> {{ event.title }}</h5>
|
||||
<button
|
||||
class="btn btn-sm me-1"
|
||||
v-if="event.hasReplay"
|
||||
@click="repeatEvent(event.eventId)"
|
||||
title="Re-Play Event"
|
||||
>
|
||||
<i class="fas fa-share fa-fw"></i>
|
||||
</button>
|
||||
<small :title="timeDisplay(event.time)">
|
||||
{{ timeSince(event.time) }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="d-flex my-1 w-100 justify-content-between align-items-start premono" v-if="event.text">
|
||||
{{ event.text }}
|
||||
</div>
|
||||
<p class="mb-1" v-if="resolveSubtext(event.subtext)">
|
||||
<small>
|
||||
<span class="premono">{{ resolveSubtext(event.subtext) }}</span>
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="eventfeed.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
585
internal/apimodules/overlays/default/eventfeed.js
Normal file
585
internal/apimodules/overlays/default/eventfeed.js
Normal file
|
@ -0,0 +1,585 @@
|
|||
/**
|
||||
* @typedef {Object} Event
|
||||
* @property {number} eventId ID of the event as returned by the server
|
||||
* @property {Object|undefined} extraData Any additional data specific to this event type
|
||||
* @property {string} filterKey Event-Type key
|
||||
* @property {string|undefined} originId ID from the Twitch server for de-duplication
|
||||
* @property {string|function|undefined} subtext Additional text, usually user-message
|
||||
* @property {string|undefined} text Descriptive text of the event
|
||||
* @property {Date} time The moment the event occurred
|
||||
* @property {string} title The title of the event
|
||||
* @property {boolean} hasReplay Whether the replay button should be shown
|
||||
* @property {boolean} isMeta Whether not to display event in frontend
|
||||
*/
|
||||
|
||||
import { customFilters, customHandler } from './eventfeed.custom.js'
|
||||
import { createApp } from 'https://cdn.jsdelivr.net/npm/vue@3.4/dist/vue.esm-browser.prod.js'
|
||||
import dayjs from 'https://cdn.jsdelivr.net/npm/dayjs@1.11/+esm'
|
||||
import dayjsLocalizedFormat from 'https://cdn.jsdelivr.net/npm/dayjs@1.11/plugin/localizedFormat.js/+esm'
|
||||
import dayjsRelativeTime from 'https://cdn.jsdelivr.net/npm/dayjs@1.11/plugin/relativeTime.js/+esm'
|
||||
import EventClient from './eventclient.mjs'
|
||||
|
||||
const STORAGE_KEY = 'io.luzifer.eventfeed'
|
||||
|
||||
const defaultFilters = {
|
||||
adbreak: { name: 'Adbreaks', visible: true },
|
||||
ban: { name: 'Bans / Timeouts', visible: true },
|
||||
bits: { name: 'Bits', visible: true },
|
||||
channelpoint: { name: 'Channel-Points', visible: true },
|
||||
donation: { name: 'Donations', visible: true },
|
||||
follow: { name: 'Follows', visible: true },
|
||||
hypetrain: { name: 'Hypetrains', visible: true },
|
||||
pollEnd: { name: 'Poll-Summary', visible: true },
|
||||
raid: { name: 'Raids', visible: true },
|
||||
shoutout: { name: 'Shoutouts', visible: true },
|
||||
streamOffline: { name: 'Stream-Offline', visible: true },
|
||||
streamUpdate: { name: 'Stream-Update', visible: true },
|
||||
subs: { name: 'Subs', visible: true },
|
||||
watchStreak: { name: 'Watchstreaks', visible: true },
|
||||
}
|
||||
|
||||
const userAnonSubgifter = 'ananonymousgifter'
|
||||
const userAnonCheerer = 'ananonymouscheerer'
|
||||
|
||||
const app = createApp({
|
||||
computed: {
|
||||
filterCount() {
|
||||
const filters = Object.values(this.filters)
|
||||
return `${filters.filter(f => f.visible).length} / ${filters.length}`
|
||||
},
|
||||
|
||||
filters() {
|
||||
return Object.fromEntries(Object.entries({
|
||||
...defaultFilters,
|
||||
...customFilters(),
|
||||
...this.storedData.filters || {},
|
||||
})
|
||||
.filter(e => Object.keys(defaultFilters).includes(e[0]) || Object.keys(customFilters()).includes(e[0]))
|
||||
.sort((a, b) => a[1].name.localeCompare(b[1].name)))
|
||||
},
|
||||
|
||||
hypetrain() {
|
||||
const evts = [...this.events]
|
||||
.filter(evt => evt.filterKey === 'hypetrain')
|
||||
.sort((b, a) => a.time.getTime() - b.time.getTime())
|
||||
|
||||
if (evts.length < 1) {
|
||||
return {
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
|
||||
return evts[0].extraData
|
||||
},
|
||||
|
||||
recentEvents() {
|
||||
return [...this.events]
|
||||
.filter(evt => !evt.isMeta)
|
||||
.filter(evt => this.filters[evt.filterKey]?.visible !== false)
|
||||
.filter(evt => !this.knownMultiGiftIDs.includes(evt.originId))
|
||||
.sort((b, a) => a.time.getTime() - b.time.getTime())
|
||||
},
|
||||
|
||||
sortedStats() {
|
||||
const evts = [...this.events]
|
||||
.filter(evt => evt.time.getTime() > this.streamOfflineTime.getTime())
|
||||
|
||||
|
||||
return [
|
||||
{
|
||||
icon: 'fas fa-gem',
|
||||
key: 'bits',
|
||||
value: evts
|
||||
.filter(evt => evt.filterKey === 'bits')
|
||||
.reduce((sum, evt) => sum + evt.extraData.bits, 0),
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-circle-dollar-to-slot',
|
||||
key: 'donation',
|
||||
value: evts
|
||||
.filter(evt => evt.filterKey === 'donation')
|
||||
.reduce((sum, evt) => sum + evt.extraData.amount, 0)
|
||||
.toFixed(2),
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-heart',
|
||||
key: 'follow',
|
||||
value: evts
|
||||
.filter(evt => evt.filterKey === 'follow')
|
||||
.length,
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-parachute-box',
|
||||
key: 'raid',
|
||||
value: evts
|
||||
.filter(evt => evt.filterKey === 'raid')
|
||||
.length,
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-star',
|
||||
key: 'sub',
|
||||
value: evts
|
||||
.filter(evt => evt.filterKey === 'subs')
|
||||
.filter(evt => !this.knownMultiGiftIDs.includes(evt.originId))
|
||||
.reduce((sum, evt) => sum + evt.extraData.count, 0),
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
window.setInterval(() => {
|
||||
this.now = new Date()
|
||||
}, 60000)
|
||||
|
||||
this.eventClient = new EventClient({
|
||||
handlers: {
|
||||
adbreak_begin: ({ event_id, fields, time }) => this.handleAdBreak(event_id, fields, time),
|
||||
ban: ({ event_id, fields, time }) => this.handleBan(event_id, fields, time),
|
||||
bits: ({ event_id, fields, time }) => this.handleBits(event_id, fields, time),
|
||||
category_update: ({ event_id, fields, time }) => this.handleCategoryUpdate(event_id, fields, time),
|
||||
channelpoint_redeem: ({ event_id, fields, time }) => this.handleChannelPoints(event_id, fields, time),
|
||||
custom: eventobj => this.handleCustom(eventobj),
|
||||
follow: ({ event_id, fields, time }) => this.handleFollow(event_id, fields, time),
|
||||
hypetrain_begin: ({ event_id, fields, time }) => this.handleHypetrain(event_id, fields, time, 'start'),
|
||||
hypetrain_end: ({ event_id, fields, time }) => this.handleHypetrain(event_id, fields, time, 'end'),
|
||||
hypetrain_progress: ({ event_id, fields, time }) => this.handleHypetrain(event_id, fields, time, 'progress'),
|
||||
kofi_donation: ({ event_id, fields, time }) => this.handleKoFiDonation(event_id, fields, time),
|
||||
poll_end: ({ event_id, fields, time }) => this.handlePollEnd(event_id, fields, time),
|
||||
raid: ({ event_id, fields, time }) => this.handleRaid(event_id, fields, time),
|
||||
resub: ({ event_id, fields, reason, time, type }) => this.handleSub(type, event_id, fields, time, reason),
|
||||
shoutout_created: ({ event_id, fields, time }) => this.handleShoutoutCreated(event_id, fields, time),
|
||||
shoutout_received: ({ event_id, fields, time }) => this.handleShoutoutReceived(event_id, fields, time),
|
||||
stream_offline: ({ event_id, time }) => this.handleStreamOffline(event_id, time),
|
||||
sub: ({ event_id, fields, time, type }) => this.handleSub(type, event_id, fields, time),
|
||||
subgift: ({ event_id, fields, time, type }) => this.handleSubgift(type, event_id, fields, time),
|
||||
submysterygift: ({ event_id, fields, time, type }) => this.handleSubgift(type, event_id, fields, time),
|
||||
timeout: ({ event_id, fields, time }) => this.handleTimeout(event_id, fields, time),
|
||||
title_update: ({ event_id, fields, time }) => this.handleTitleUpdate(event_id, fields, time),
|
||||
watch_streak: ({ event_id, fields, time }) => this.handleWatchStreak(event_id, fields, time),
|
||||
},
|
||||
|
||||
maxReplayAge: 168,
|
||||
replay: true,
|
||||
})
|
||||
|
||||
this.storageLoad()
|
||||
window.addEventListener('storage', ev => {
|
||||
if (ev.key !== this.storageKey()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Our key has been changed, reload stored data
|
||||
this.storageLoad()
|
||||
})
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
eventClient: null,
|
||||
events: [],
|
||||
now: new Date(),
|
||||
storedData: {},
|
||||
|
||||
// Workaround for Twitch not sending hypetrain progress in end-event
|
||||
// eslint-disable-next-line sort-keys
|
||||
hypetrainProgress: 0,
|
||||
knownMultiGiftIDs: [],
|
||||
streamOfflineTime: new Date(0),
|
||||
subgiftRecipients: {},
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* @param {Event} event
|
||||
*/
|
||||
addEvent(event) {
|
||||
if (!event.eventId || !event.filterKey || !event.time || !event.title) {
|
||||
throw new Error(`Event missing fields: ${event}`)
|
||||
}
|
||||
|
||||
this.events = [
|
||||
...this.events.filter(evt => evt.eventId !== event.eventId),
|
||||
event,
|
||||
]
|
||||
},
|
||||
|
||||
eventClass(event) {
|
||||
const classes = ['border-event', 'list-group-item']
|
||||
|
||||
if (this.storedData.readDate && this.storedData.readDate > event.time.getTime()) {
|
||||
classes.push('disabled')
|
||||
}
|
||||
|
||||
if (event.filterKey) {
|
||||
classes.push(`event-${event.filterKey}`)
|
||||
}
|
||||
|
||||
return classes.join(' ')
|
||||
},
|
||||
|
||||
handleAdBreak(eventId, data, time) {
|
||||
this.addEvent({
|
||||
eventId,
|
||||
filterKey: 'adbreak',
|
||||
icon: 'fas fa-rectangle-ad text-warning',
|
||||
text: `${data.duration}s ad-break is now running`,
|
||||
time: time ? new Date(time) : null,
|
||||
title: 'Ad-Break started',
|
||||
})
|
||||
},
|
||||
|
||||
handleBan(eventId, data, time) {
|
||||
this.addEvent({
|
||||
eventId,
|
||||
filterKey: 'ban',
|
||||
icon: 'fas fa-ban',
|
||||
time: new Date(time),
|
||||
title: `${data.target_name} has been banned`,
|
||||
})
|
||||
},
|
||||
|
||||
handleBits(eventId, data, time) {
|
||||
const from = data.user === userAnonCheerer ? 'Someone' : data.user
|
||||
|
||||
this.addEvent({
|
||||
eventId,
|
||||
extraData: { bits: data.bits },
|
||||
filterKey: 'bits',
|
||||
hasReplay: true,
|
||||
icon: 'fas fa-gem',
|
||||
subtext: data.message,
|
||||
text: `${from} just spent ${data.bits} Bits`,
|
||||
time: time ? new Date(time) : null,
|
||||
title: 'Bits donated',
|
||||
})
|
||||
},
|
||||
|
||||
handleCategoryUpdate(eventId, data, time) {
|
||||
this.addEvent({
|
||||
eventId,
|
||||
filterKey: 'streamUpdate',
|
||||
icon: 'fas fa-gamepad',
|
||||
text: data.category,
|
||||
time: new Date(time),
|
||||
title: 'Category updated',
|
||||
})
|
||||
},
|
||||
|
||||
handleChannelPoints(eventId, data, time) {
|
||||
this.addEvent({
|
||||
eventId,
|
||||
filterKey: 'channelpoint',
|
||||
hasReplay: true,
|
||||
icon: 'fas fa-diamond',
|
||||
subtext: data.user_input,
|
||||
text: `${data.user} redeemed "${data.reward_title}"`,
|
||||
time: new Date(time),
|
||||
title: 'Reward Redeemed',
|
||||
})
|
||||
},
|
||||
|
||||
handleCustom(eventObj) {
|
||||
const evt = customHandler(eventObj)
|
||||
if (evt !== null) {
|
||||
this.addEvent(evt)
|
||||
}
|
||||
},
|
||||
|
||||
handleFollow(eventId, data, time) {
|
||||
this.addEvent({
|
||||
eventId,
|
||||
filterKey: 'follow',
|
||||
hasReplay: true,
|
||||
icon: 'fas fa-user',
|
||||
text: `${data.user} just followed`,
|
||||
time: new Date(time),
|
||||
title: 'New Follower',
|
||||
})
|
||||
},
|
||||
|
||||
handleHypetrain(eventId, data, time, phase) {
|
||||
const evt = {
|
||||
eventId,
|
||||
extraData: {
|
||||
active: phase !== 'end',
|
||||
level: data.level,
|
||||
progress: data.levelProgress || this.hypetrainProgress,
|
||||
},
|
||||
|
||||
filterKey: 'hypetrain',
|
||||
icon: 'fas fa-train',
|
||||
time: new Date(time),
|
||||
}
|
||||
|
||||
this.hypetrainProgress = evt.extraData.progress
|
||||
|
||||
switch (phase) {
|
||||
case 'start':
|
||||
this.addEvent({
|
||||
...evt,
|
||||
text: `A hypetrain started on ${(data.levelProgress * 100).toFixed(0)}% towards level ${data.level}`,
|
||||
title: 'Hypetrain started',
|
||||
})
|
||||
break
|
||||
|
||||
case 'progress':
|
||||
this.addEvent({
|
||||
...evt,
|
||||
isMeta: true,
|
||||
title: 'Hypetrain progressed',
|
||||
})
|
||||
break
|
||||
|
||||
case 'end':
|
||||
this.addEvent({
|
||||
...evt,
|
||||
text: `A hypetrain ended on ${(this.hypetrainProgress * 100).toFixed(0)}% towards level ${data.level}`,
|
||||
title: 'Hypetrain ended',
|
||||
})
|
||||
break
|
||||
}
|
||||
},
|
||||
|
||||
handleKoFiDonation(eventId, data, time) {
|
||||
let text
|
||||
if (data.isSubscription && data.isFirstSubPayment) {
|
||||
text = `${data.from} just started a monthly subscription of ${Number(data.amount).toFixed(2)} ${data.currency}`
|
||||
} else if (data.isSubscription && !data.isFirstSubPayment) {
|
||||
text = `${data.from} continued their monthly subscription of ${Number(data.amount).toFixed(2)} ${data.currency}`
|
||||
} else {
|
||||
text = `${data.from} just donated ${Number(data.amount).toFixed(2)} ${data.currency}`
|
||||
}
|
||||
|
||||
this.addEvent({
|
||||
eventId,
|
||||
extraData: { amount: Number(data.amount) },
|
||||
filterKey: 'donation',
|
||||
icon: 'fas fa-circle-dollar-to-slot',
|
||||
subtext: data.message ? data.message : undefined,
|
||||
text,
|
||||
time: new Date(time),
|
||||
title: 'Ko-fi Donation received',
|
||||
})
|
||||
},
|
||||
|
||||
handlePollEnd(eventId, data, time) {
|
||||
if (data.poll.status === 'archived') {
|
||||
return
|
||||
}
|
||||
|
||||
this.addEvent({
|
||||
eventId,
|
||||
filterKey: 'pollEnd',
|
||||
icon: 'fas fa-square-poll-vertical',
|
||||
subtext: data.poll.choices.map(choice => `${choice.title} (${choice.votes})`).join(' | '),
|
||||
text: data.poll.title,
|
||||
time: new Date(time),
|
||||
title: `Poll Ended (${data.poll.status})`,
|
||||
})
|
||||
},
|
||||
|
||||
handleRaid(eventId, data, time) {
|
||||
this.addEvent({
|
||||
eventId,
|
||||
filterKey: 'raid',
|
||||
hasReplay: true,
|
||||
icon: 'fas fa-parachute-box',
|
||||
soundUrl: '/public/fanfare.webm',
|
||||
text: `${data.from} just raided with ${data.viewercount} raiders`,
|
||||
time: new Date(time),
|
||||
title: 'Incoming raid',
|
||||
})
|
||||
},
|
||||
|
||||
handleShoutoutCreated(eventId, data, time) {
|
||||
this.addEvent({
|
||||
eventId,
|
||||
filterKey: 'shoutout',
|
||||
icon: 'fas fa-bullhorn',
|
||||
text: `We gave a shoutout for ${data.to} to ${data.viewers} viewers`,
|
||||
time: new Date(time),
|
||||
title: 'Shoutout created',
|
||||
})
|
||||
},
|
||||
|
||||
handleShoutoutReceived(eventId, data, time) {
|
||||
this.addEvent({
|
||||
eventId,
|
||||
filterKey: 'shoutout',
|
||||
icon: 'fas fa-bullhorn',
|
||||
text: `${data.from} just gave us a shoutout to ${data.viewers} viewers`,
|
||||
time: new Date(time),
|
||||
title: 'Shoutout received',
|
||||
})
|
||||
},
|
||||
|
||||
handleStreamOffline(eventId, time) {
|
||||
this.addEvent({
|
||||
eventId,
|
||||
filterKey: 'streamOffline',
|
||||
icon: 'fas fa-clapperboard text-danger',
|
||||
time: new Date(time),
|
||||
title: 'Stream Offline',
|
||||
})
|
||||
|
||||
this.streamOfflineTime = new Date(time)
|
||||
},
|
||||
|
||||
handleSub(evt, eventId, data, time) {
|
||||
const text = evt === 'resub' ? `resubscribed for the ${data.subscribed_months}. time` : 'subscribed'
|
||||
const tier = data.plan === 'Prime' ? 'P' : `T${Number(data.plan) / 1000}`
|
||||
const title = evt === 'resub' ? `Resub (${tier})` : `New Sub (${tier})`
|
||||
this.addEvent({
|
||||
eventId,
|
||||
extraData: { count: 1 },
|
||||
filterKey: 'subs',
|
||||
hasReplay: true,
|
||||
icon: 'fas fa-star',
|
||||
subtext: data.message,
|
||||
text: `${data.user} just ${text} (${tier})`,
|
||||
time: new Date(time),
|
||||
title,
|
||||
})
|
||||
},
|
||||
|
||||
handleSubgift(evt, eventId, data, time) {
|
||||
const from = data.user === userAnonSubgifter ? 'ANON' : data.from
|
||||
|
||||
const tier = data.plan === 'Prime' ? 'Prime' : `Tier ${Number(data.plan) / 1000}`
|
||||
|
||||
if (evt === 'submysterygift') {
|
||||
this.addEvent({
|
||||
eventId,
|
||||
extraData: { count: data.number },
|
||||
filterKey: 'subs',
|
||||
hasReplay: true,
|
||||
icon: 'fas fa-gift',
|
||||
subtext: () => this.subgiftRecipients[data.origin_id] ? `To: ${this.subgiftRecipients[data.origin_id].join(', ')}` : undefined,
|
||||
text: `${from} just gifted ${data.number} subs`,
|
||||
time: time ? new Date(time) : null,
|
||||
title: `Subs gifted (${tier})`,
|
||||
variant: 'warning',
|
||||
})
|
||||
|
||||
this.knownMultiGiftIDs.push(data.origin_id)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.origin_id) {
|
||||
this.subgiftRecipients[data.origin_id] = [
|
||||
...this.subgiftRecipients[data.origin_id] || [],
|
||||
data.to,
|
||||
].sort((a, b) => a.localeCompare(b))
|
||||
}
|
||||
|
||||
this.addEvent({
|
||||
eventId,
|
||||
extraData: { count: 1 },
|
||||
filterKey: 'subs',
|
||||
hasReplay: true,
|
||||
icon: 'fas fa-gift',
|
||||
originId: data.origin_id,
|
||||
text: `${from} just gifted ${data.to} a sub`,
|
||||
time: time ? new Date(time) : null,
|
||||
title: `Sub gifted (${tier})`,
|
||||
variant: 'warning',
|
||||
})
|
||||
},
|
||||
|
||||
handleTimeout(eventId, data, time) {
|
||||
this.addEvent({
|
||||
eventId,
|
||||
filterKey: 'ban',
|
||||
icon: 'fas fa-ban',
|
||||
time: new Date(time),
|
||||
title: `${data.target_name} has been timed out for ${data.seconds}s`,
|
||||
})
|
||||
},
|
||||
|
||||
handleTitleUpdate(eventId, data, time) {
|
||||
this.addEvent({
|
||||
eventId,
|
||||
filterKey: 'streamUpdate',
|
||||
icon: 'fas fa-heading',
|
||||
text: data.title,
|
||||
time: new Date(time),
|
||||
title: 'Title updated',
|
||||
})
|
||||
},
|
||||
|
||||
handleWatchStreak(eventId, data, time) {
|
||||
this.addEvent({
|
||||
eventId,
|
||||
filterKey: 'watchStreak',
|
||||
icon: 'fas fa-circle-info',
|
||||
subtext: data.message,
|
||||
text: `${data.user} watched ${data.streak} consecutive streams`,
|
||||
time: new Date(time),
|
||||
title: 'Watch-Streak shared',
|
||||
})
|
||||
},
|
||||
|
||||
markRead() {
|
||||
this.storedData.readDate = new Date().getTime()
|
||||
this.storageSave()
|
||||
},
|
||||
|
||||
repeatEvent(eventId) {
|
||||
return this.eventClient.replayEvent(eventId)
|
||||
},
|
||||
|
||||
resolveSubtext(subtext) {
|
||||
if (typeof subtext === 'function') {
|
||||
return subtext()
|
||||
}
|
||||
|
||||
return subtext
|
||||
},
|
||||
|
||||
storageKey() {
|
||||
const channel = this.eventClient.paramOptionFallback('channel').replace(/^#*/, '')
|
||||
return [STORAGE_KEY, channel].join('.')
|
||||
},
|
||||
|
||||
storageLoad() {
|
||||
this.storedData = {
|
||||
// Default values
|
||||
filters: {},
|
||||
readDate: 0,
|
||||
|
||||
// Stored data
|
||||
...JSON.parse(window.localStorage.getItem(this.storageKey()) || '{}'),
|
||||
}
|
||||
},
|
||||
|
||||
storageSave() {
|
||||
window.localStorage.setItem(this.storageKey(), JSON.stringify(this.storedData))
|
||||
},
|
||||
|
||||
timeDisplay(time) {
|
||||
return dayjs(time).format('llll')
|
||||
},
|
||||
|
||||
timeSince(time) {
|
||||
return dayjs(time).from(this.now)
|
||||
},
|
||||
|
||||
toggleFilterVisibility(filter) {
|
||||
if (!this.storedData.filters[filter]) {
|
||||
this.storedData.filters[filter] = this.filters[filter]
|
||||
}
|
||||
|
||||
this.storedData.filters[filter].visible = !this.storedData.filters[filter].visible
|
||||
this.storageSave()
|
||||
},
|
||||
},
|
||||
|
||||
name: 'EventFeed',
|
||||
})
|
||||
|
||||
dayjs.extend(dayjsLocalizedFormat)
|
||||
dayjs.extend(dayjsRelativeTime)
|
||||
|
||||
app.mount('#app')
|
|
@ -42,7 +42,7 @@ type (
|
|||
|
||||
// socketMessage represents the message overlay sockets will receive
|
||||
socketMessage struct {
|
||||
EventID uint64 `json:"event_id"`
|
||||
EventID uint64 `json:"event_id,string"`
|
||||
IsLive bool `json:"is_live"`
|
||||
Reason sendReason `json:"reason"`
|
||||
Time time.Time `json:"time"`
|
||||
|
|
|
@ -28,7 +28,7 @@ type (
|
|||
}
|
||||
|
||||
raffle struct {
|
||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||
ID uint64 `gorm:"primaryKey" json:"id,string"`
|
||||
|
||||
Channel string `json:"channel"`
|
||||
Keyword string `json:"keyword"`
|
||||
|
@ -67,7 +67,7 @@ type (
|
|||
}
|
||||
|
||||
raffleEntry struct {
|
||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||
ID uint64 `gorm:"primaryKey" json:"id,string"`
|
||||
RaffleID uint64 `gorm:"uniqueIndex:user_per_raffle" json:"-"`
|
||||
|
||||
UserID string `gorm:"size:128;uniqueIndex:user_per_raffle" json:"userID"`
|
||||
|
|
|
@ -57,7 +57,7 @@ func (cryptRandSrc) Int63() int64 {
|
|||
return -1
|
||||
}
|
||||
// mask off sign bit to ensure positive number
|
||||
return int64(binary.LittleEndian.Uint64(b[:]) & (1<<63 - 1))
|
||||
return int64(binary.LittleEndian.Uint64(b[:]) & (1<<63 - 1)) //#nosec:G115 - Masking ensures conversion is fine
|
||||
}
|
||||
|
||||
// We're using a non-seedable source
|
||||
|
|
|
@ -18,23 +18,23 @@ func testGenerateRaffe() raffle {
|
|||
}
|
||||
|
||||
// Now lets generate 132 non-followers taking part
|
||||
for i := 0; i < 132; i++ {
|
||||
r.Entries = append(r.Entries, raffleEntry{ID: uint64(i), Multiplier: 1})
|
||||
for i := uint64(0); i < 132; i++ {
|
||||
r.Entries = append(r.Entries, raffleEntry{ID: i, Multiplier: 1})
|
||||
}
|
||||
|
||||
// Now lets generate 500 followers taking part
|
||||
for i := 0; i < 500; i++ {
|
||||
r.Entries = append(r.Entries, raffleEntry{ID: 10000 + uint64(i), Multiplier: r.MultiFollower})
|
||||
for i := uint64(0); i < 500; i++ {
|
||||
r.Entries = append(r.Entries, raffleEntry{ID: 10000 + i, Multiplier: r.MultiFollower})
|
||||
}
|
||||
|
||||
// Now lets generate 200 subscribers taking part
|
||||
for i := 0; i < 200; i++ {
|
||||
r.Entries = append(r.Entries, raffleEntry{ID: 20000 + uint64(i), Multiplier: r.MultiSubscriber})
|
||||
for i := uint64(0); i < 200; i++ {
|
||||
r.Entries = append(r.Entries, raffleEntry{ID: 20000 + i, Multiplier: r.MultiSubscriber})
|
||||
}
|
||||
|
||||
// Now lets generate 5 VIPs taking part
|
||||
for i := 0; i < 5; i++ {
|
||||
r.Entries = append(r.Entries, raffleEntry{ID: 30000 + uint64(i), Multiplier: r.MultiVIP})
|
||||
for i := uint64(0); i < 5; i++ {
|
||||
r.Entries = append(r.Entries, raffleEntry{ID: 30000 + i, Multiplier: r.MultiVIP})
|
||||
}
|
||||
|
||||
// They didn't join in order so lets shuffle them
|
||||
|
|
|
@ -57,13 +57,12 @@ func TestScanForLinks(t *testing.T) {
|
|||
t.SkipNow()
|
||||
}
|
||||
|
||||
c := New()
|
||||
|
||||
for _, testCase := range []struct {
|
||||
Heuristic bool
|
||||
Message string
|
||||
ExpectedLinks []string
|
||||
ExpectedContains bool
|
||||
TraceStack bool
|
||||
}{
|
||||
// Case: full URL is present in the message
|
||||
{
|
||||
|
@ -162,7 +161,7 @@ func TestScanForLinks(t *testing.T) {
|
|||
{
|
||||
Heuristic: true,
|
||||
Message: "Hey btw. es kann sein, dass",
|
||||
ExpectedLinks: []string{"https://trusted.evo-media.eu/btw.es"},
|
||||
ExpectedLinks: []string{"https://trusted.domainseller.site/btw.es"},
|
||||
},
|
||||
// Case: Multiple spaces in the link
|
||||
{
|
||||
|
@ -183,6 +182,13 @@ func TestScanForLinks(t *testing.T) {
|
|||
{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) {
|
||||
var c *Checker
|
||||
if testCase.TraceStack {
|
||||
c = New(withResolver(newResolver(resolverPoolSize, withTesting(t))))
|
||||
} else {
|
||||
c = New()
|
||||
}
|
||||
|
||||
var linksFound []string
|
||||
if testCase.Heuristic {
|
||||
linksFound = c.HeuristicScanForLinks(testCase.Message)
|
||||
|
@ -209,23 +215,3 @@ func TestScanForLinks(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserAgentListNotEmpty(t *testing.T) {
|
||||
if len(defaultUserAgents) == 0 {
|
||||
t.Fatal("found empty user-agent list")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserAgentRandomizer(t *testing.T) {
|
||||
uas := map[string]int{}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
uas[defaultResolver.userAgent()]++
|
||||
}
|
||||
|
||||
for _, c := range uas {
|
||||
assert.Less(t, c, 10)
|
||||
}
|
||||
|
||||
assert.Equal(t, 0, uas[""]) // there should be no empty UA
|
||||
}
|
||||
|
|
66
internal/linkcheck/meta.go
Normal file
66
internal/linkcheck/meta.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package linkcheck
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
var (
|
||||
errNoMetaRedir = fmt.Errorf("no meta-redir found")
|
||||
metaRedirContent = regexp.MustCompile(`^[0-9]+;\s*url=(.*)$`)
|
||||
)
|
||||
|
||||
//nolint:gocognit // Makes no sense to split
|
||||
func resolveMetaRedirect(body []byte) (redir string, err error) {
|
||||
tok := html.NewTokenizer(bytes.NewReader(body))
|
||||
|
||||
tokenLoop:
|
||||
for {
|
||||
token := tok.Next()
|
||||
switch token {
|
||||
case html.ErrorToken:
|
||||
if errors.Is(tok.Err(), io.EOF) {
|
||||
break tokenLoop
|
||||
}
|
||||
return "", fmt.Errorf("scanning tokens: %w", tok.Err())
|
||||
|
||||
case html.StartTagToken:
|
||||
t := tok.Token()
|
||||
if t.Data == "meta" {
|
||||
var (
|
||||
content string
|
||||
isRedirect bool
|
||||
)
|
||||
|
||||
for _, attr := range t.Attr {
|
||||
isRedirect = isRedirect || attr.Key == "http-equiv" && attr.Val == "refresh"
|
||||
|
||||
if attr.Key == "content" {
|
||||
content = attr.Val
|
||||
}
|
||||
}
|
||||
|
||||
if !isRedirect {
|
||||
continue tokenLoop
|
||||
}
|
||||
|
||||
// It is a redirect, get the content and parse it
|
||||
if matches := metaRedirContent.FindStringSubmatch(content); len(matches) > 1 {
|
||||
redir = matches[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if redir == "" {
|
||||
// We did not find anything
|
||||
return "", errNoMetaRedir
|
||||
}
|
||||
|
||||
return redir, nil
|
||||
}
|
41
internal/linkcheck/meta_test.go
Normal file
41
internal/linkcheck/meta_test.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package linkcheck
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResolveMetaRedir(t *testing.T) {
|
||||
testDoc := []byte(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta property="twitter:image" content="">
|
||||
<meta http-equiv='refresh' content='0; url=https://github.com/Luzifer/twitch-bot'>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>`)
|
||||
|
||||
redir, err := resolveMetaRedirect(testDoc)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/Luzifer/twitch-bot", redir)
|
||||
|
||||
testDoc = []byte(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta property="twitter:image" content="">
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>`)
|
||||
|
||||
redir, err = resolveMetaRedirect(testDoc)
|
||||
require.ErrorIs(t, err, errNoMetaRedir)
|
||||
assert.Equal(t, "", redir)
|
||||
}
|
|
@ -2,18 +2,16 @@ package linkcheck
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
_ "embed"
|
||||
"math/big"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Luzifer/go_helpers/v2/str"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
@ -30,6 +28,8 @@ type (
|
|||
resolver struct {
|
||||
resolverC chan resolverQueueEntry
|
||||
skipValidation bool
|
||||
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
resolverQueueEntry struct {
|
||||
|
@ -40,20 +40,12 @@ type (
|
|||
)
|
||||
|
||||
var (
|
||||
defaultUserAgents = []string{}
|
||||
linkTest = regexp.MustCompile(`(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]`)
|
||||
numericHost = regexp.MustCompile(`^(?:[0-9]+\.)*[0-9]+(?::[0-9]+)?$`)
|
||||
|
||||
//go:embed user-agents.txt
|
||||
uaList string
|
||||
linkTest = regexp.MustCompile(`(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]`)
|
||||
numericHost = regexp.MustCompile(`^(?:[0-9]+\.)*[0-9]+(?::[0-9]+)?$`)
|
||||
|
||||
defaultResolver = newResolver(resolverPoolSize)
|
||||
)
|
||||
|
||||
func init() {
|
||||
defaultUserAgents = strings.Split(strings.TrimSpace(uaList), "\n")
|
||||
}
|
||||
|
||||
func newResolver(poolSize int, opts ...func(*resolver)) *resolver {
|
||||
r := &resolver{
|
||||
resolverC: make(chan resolverQueueEntry),
|
||||
|
@ -74,6 +66,10 @@ func withSkipVerify() func(*resolver) {
|
|||
return func(r *resolver) { r.skipValidation = true }
|
||||
}
|
||||
|
||||
func withTesting(t *testing.T) func(*resolver) {
|
||||
return func(r *resolver) { r.t = t }
|
||||
}
|
||||
|
||||
func (r resolver) Resolve(qe resolverQueueEntry) {
|
||||
qe.WaitGroup.Add(1)
|
||||
r.resolverC <- qe
|
||||
|
@ -88,12 +84,12 @@ func (resolver) getJar() *cookiejar.Jar {
|
|||
// that link after all redirects were followed
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (r resolver) resolveFinal(link string, cookieJar *cookiejar.Jar, callStack []string, userAgent string) string {
|
||||
func (r resolver) resolveFinal(link string, cookieJar *cookiejar.Jar, callStack *stack) string {
|
||||
if !linkTest.MatchString(link) && !r.skipValidation {
|
||||
return ""
|
||||
}
|
||||
|
||||
if str.StringInSlice(link, callStack) || len(callStack) == maxRedirects {
|
||||
if callStack.Count(link) > 2 || callStack.Height() == maxRedirects {
|
||||
// We got ourselves a loop: Yay!
|
||||
return link
|
||||
}
|
||||
|
@ -131,12 +127,19 @@ func (r resolver) resolveFinal(link string, cookieJar *cookiejar.Jar, callStack
|
|||
// Sanitize host: Trailing dots are valid but not required
|
||||
u.Host = strings.TrimRight(u.Host, ".")
|
||||
|
||||
if r.t != nil {
|
||||
r.t.Logf("resolving link: link=%q jar_c=%#v stack_c=%d stack_h=%d",
|
||||
link, len(cookieJar.Cookies(u)), callStack.Count(link), callStack.Height())
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
for k, v := range generateUserAgentHeaders() {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
@ -155,10 +158,35 @@ func (r resolver) resolveFinal(link string, cookieJar *cookiejar.Jar, callStack
|
|||
return ""
|
||||
}
|
||||
target := r.resolveReference(u, tu)
|
||||
return r.resolveFinal(target, cookieJar, append(callStack, link), userAgent)
|
||||
callStack.Visit(link)
|
||||
return r.resolveFinal(target, cookieJar, callStack)
|
||||
}
|
||||
|
||||
// We got a response, it's no redirect, we count this as a success
|
||||
// We got a response, it's no redirect, lets check for in-document stuff
|
||||
docBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if metaRedir, err := resolveMetaRedirect(docBody); err == nil {
|
||||
// Meta-Redirect found
|
||||
tu, err := url.Parse(metaRedir)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
target := r.resolveReference(u, tu)
|
||||
callStack.Visit(link)
|
||||
return r.resolveFinal(target, cookieJar, callStack)
|
||||
}
|
||||
|
||||
if resp.Header.Get("Set-Cookie") != "" {
|
||||
// A new cookie was set, lets refresh the page once to see if stuff
|
||||
// changes with that new cookie
|
||||
callStack.Visit(link)
|
||||
return r.resolveFinal(u.String(), cookieJar, callStack)
|
||||
}
|
||||
|
||||
// We had no in-document redirects: we count this as a success
|
||||
return u.String()
|
||||
}
|
||||
|
||||
|
@ -201,14 +229,9 @@ func (resolver) resolveReference(origin *url.URL, loc *url.URL) string {
|
|||
|
||||
func (r resolver) runResolver() {
|
||||
for qe := range r.resolverC {
|
||||
if link := r.resolveFinal(qe.Link, r.getJar(), nil, r.userAgent()); link != "" {
|
||||
if link := r.resolveFinal(qe.Link, r.getJar(), &stack{}); link != "" {
|
||||
qe.Callback(link)
|
||||
}
|
||||
qe.WaitGroup.Done()
|
||||
}
|
||||
}
|
||||
|
||||
func (resolver) userAgent() string {
|
||||
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(defaultUserAgents))))
|
||||
return defaultUserAgents[n.Int64()]
|
||||
}
|
||||
|
|
27
internal/linkcheck/stack.go
Normal file
27
internal/linkcheck/stack.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package linkcheck
|
||||
|
||||
import "strings"
|
||||
|
||||
type (
|
||||
stack struct {
|
||||
visits []string
|
||||
}
|
||||
)
|
||||
|
||||
func (s stack) Count(url string) (n int) {
|
||||
for _, v := range s.visits {
|
||||
if strings.EqualFold(v, url) {
|
||||
n++
|
||||
}
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func (s stack) Height() int {
|
||||
return len(s.visits)
|
||||
}
|
||||
|
||||
func (s *stack) Visit(url string) {
|
||||
s.visits = append(s.visits, url)
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63
|
||||
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.57
|
||||
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
|
||||
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 OPR/95.0.0.0
|
||||
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36
|
||||
Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.41
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.56
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.53 Safari/537.36 Edg/103.0.1264.37
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36 Edg/90.0.818.46
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Whale/3.19.166.16 Safari/537.36
|
||||
Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.76
|
||||
Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.46
|
||||
Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0
|
||||
Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0
|
||||
Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36 Core/1.94.192.400 QQBrowser/11.5.5250.400
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.78
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36
|
||||
Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 OPR/95.0.0.0
|
||||
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0
|
||||
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36
|
||||
Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763
|
||||
Mozilla/5.0 (X11; Linux x86_64; rv:108.0) Gecko/20100101 Firefox/108.0
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36
|
||||
Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.54
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.61
|
||||
Mozilla/5.0 (Windows NT 10.0; rv:109.0) Gecko/20100101 Firefox/110.0
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.70
|
38
internal/linkcheck/useragent.go
Normal file
38
internal/linkcheck/useragent.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package linkcheck
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
chromeMajor = 128
|
||||
webkitMajor = 537
|
||||
webkitMinor = 36
|
||||
)
|
||||
|
||||
// generateUserAgent resembles the Chrome user agent generation as
|
||||
// closely as possible in order to blend into the crowd of browsers
|
||||
//
|
||||
// https://github.com/chromium/chromium/blob/58e23d958ee8d2bb4b085c843a18eb28b9da17da/content/common/user_agent.cc
|
||||
func generateUserAgentHeaders() map[string]string {
|
||||
return map[string]string{
|
||||
// New UA hints method
|
||||
"Sec-CH-UA": fmt.Sprintf(
|
||||
`"Chromium";v="%[1]d", "Not;A=Brand";v="24", "Google Chrome";v="%[1]d"`,
|
||||
chromeMajor,
|
||||
),
|
||||
|
||||
// Not a mobile browser
|
||||
"Sec-CH-UA-Mobile": "?0",
|
||||
|
||||
// We're always Windows
|
||||
"Sec-CH-UA-Platform": "Windows",
|
||||
|
||||
// "old" user-agent
|
||||
"User-Agent": fmt.Sprintf(
|
||||
"Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) %s Safari/537.36",
|
||||
"Windows NT 10.0; Win64; x64", // We're always Windows 10 / 11 on x64
|
||||
fmt.Sprintf("Chrome/%d.0.0.0", chromeMajor), // UA-Reduction enabled
|
||||
),
|
||||
}
|
||||
}
|
50
internal/locker/locker.go
Normal file
50
internal/locker/locker.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
// Package locker contains a way to interact with arbitrary locks
|
||||
package locker
|
||||
|
||||
import "sync"
|
||||
|
||||
var (
|
||||
locks = map[string]*sync.RWMutex{}
|
||||
locksOLocks sync.RWMutex
|
||||
)
|
||||
|
||||
// LockByKey takes a key to lock and locks the corresponding RWMutex
|
||||
func LockByKey(key string) { getLockByKey(key).Lock() }
|
||||
|
||||
// RLockByKey takes a key to lock and read-locks the corresponding RWMutex
|
||||
func RLockByKey(key string) { getLockByKey(key).RLock() }
|
||||
|
||||
// RUnlockByKey takes a key to lock and read-unlocks the corresponding RWMutex
|
||||
func RUnlockByKey(key string) { getLockByKey(key).RUnlock() }
|
||||
|
||||
// UnlockByKey takes a key to lock and unlocks the corresponding RWMutex
|
||||
func UnlockByKey(key string) { getLockByKey(key).Unlock() }
|
||||
|
||||
// WithLock takes a key to lock and a function to execute during the
|
||||
// lock of this key
|
||||
func WithLock(key string, fn func()) {
|
||||
LockByKey(key)
|
||||
defer UnlockByKey(key)
|
||||
|
||||
fn()
|
||||
}
|
||||
|
||||
// WithRLock takes a key to lock and a function to execute during the
|
||||
// read-lock of this key
|
||||
func WithRLock(key string, fn func()) {
|
||||
RLockByKey(key)
|
||||
defer RUnlockByKey(key)
|
||||
|
||||
fn()
|
||||
}
|
||||
|
||||
func getLockByKey(key string) *sync.RWMutex {
|
||||
locksOLocks.Lock()
|
||||
defer locksOLocks.Unlock()
|
||||
|
||||
if locks[key] == nil {
|
||||
locks[key] = new(sync.RWMutex)
|
||||
}
|
||||
|
||||
return locks[key]
|
||||
}
|
|
@ -72,9 +72,7 @@ func (s Service) InCooldown(tt plugins.TimerType, limiter, ruleID string) (bool,
|
|||
}
|
||||
|
||||
func (Service) getCooldownTimerKey(tt plugins.TimerType, limiter, ruleID string) string {
|
||||
h := sha256.New()
|
||||
fmt.Fprintf(h, "%d:%s:%s", tt, limiter, ruleID)
|
||||
return fmt.Sprintf("sha256:%x", h.Sum(nil))
|
||||
return fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(fmt.Sprintf("%d:%s:%s", tt, limiter, ruleID))))
|
||||
}
|
||||
|
||||
// Permit timer
|
||||
|
@ -90,9 +88,10 @@ func (s Service) HasPermit(channel, username string) (bool, error) {
|
|||
}
|
||||
|
||||
func (Service) getPermitTimerKey(channel, username string) string {
|
||||
h := sha256.New()
|
||||
fmt.Fprintf(h, "%d:%s:%s", plugins.TimerTypePermit, channel, strings.ToLower(strings.TrimLeft(username, "@")))
|
||||
return fmt.Sprintf("sha256:%x", h.Sum(nil))
|
||||
return fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(fmt.Sprintf(
|
||||
"%d:%s:%s",
|
||||
plugins.TimerTypePermit, channel, strings.ToLower(strings.TrimLeft(username, "@")),
|
||||
))))
|
||||
}
|
||||
|
||||
// Generic timer
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
package date
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||
)
|
||||
|
||||
|
@ -27,5 +30,37 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
},
|
||||
})
|
||||
|
||||
args.RegisterTemplateFunction("parseDuration", plugins.GenericTemplateFunctionGetter(func(duration string) (time.Duration, error) {
|
||||
d, err := time.ParseDuration(duration)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parsing duration: %w", err)
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}), plugins.TemplateFuncDocumentation{
|
||||
Description: `Parses a duration (i.e. 1h25m10s) into a time.Duration`,
|
||||
Syntax: "parseDuration <duration>",
|
||||
Example: &plugins.TemplateFuncDocumentationExample{
|
||||
Template: `{{ parseDuration "1h30s" }}`,
|
||||
ExpectedOutput: "1h0m30s",
|
||||
},
|
||||
})
|
||||
|
||||
args.RegisterTemplateFunction("parseDurationToSeconds", plugins.GenericTemplateFunctionGetter(func(duration string) (int64, error) {
|
||||
d, err := time.ParseDuration(duration)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parsing duration: %w", err)
|
||||
}
|
||||
|
||||
return int64(d / time.Second), nil
|
||||
}), plugins.TemplateFuncDocumentation{
|
||||
Description: `Parses a duration (i.e. 1h25m10s) into a number of seconds`,
|
||||
Syntax: "parseDurationToSeconds <duration>",
|
||||
Example: &plugins.TemplateFuncDocumentationExample{
|
||||
Template: `{{ parseDurationToSeconds "1h25m10s" }}`,
|
||||
ExpectedOutput: "5110",
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -40,15 +40,15 @@ func NewInterval(a, b time.Time) (i Interval) {
|
|||
i.Seconds = u.Second() - l.Second()
|
||||
|
||||
if i.Seconds < 0 {
|
||||
i.Minutes, i.Seconds = i.Minutes-1, i.Seconds+60 //nolint:gomnd
|
||||
i.Minutes, i.Seconds = i.Minutes-1, i.Seconds+60 //nolint:mnd
|
||||
}
|
||||
|
||||
if i.Minutes < 0 {
|
||||
i.Hours, i.Minutes = i.Hours-1, i.Minutes+60 //nolint:gomnd
|
||||
i.Hours, i.Minutes = i.Hours-1, i.Minutes+60 //nolint:mnd
|
||||
}
|
||||
|
||||
if i.Hours < 0 {
|
||||
i.Days, i.Hours = i.Days-1, i.Hours+24 //nolint:gomnd
|
||||
i.Days, i.Hours = i.Days-1, i.Hours+24 //nolint:mnd
|
||||
}
|
||||
|
||||
if i.Days < 0 {
|
||||
|
@ -57,7 +57,7 @@ func NewInterval(a, b time.Time) (i Interval) {
|
|||
}
|
||||
|
||||
if i.Months < 0 {
|
||||
i.Years, i.Months = i.Years-1, i.Months+12 //nolint:gomnd
|
||||
i.Years, i.Months = i.Years-1, i.Months+12 //nolint:mnd
|
||||
}
|
||||
|
||||
return i
|
||||
|
|
|
@ -13,7 +13,6 @@ import (
|
|||
//go:embed tzdata
|
||||
var tzDataEuropeBerlin []byte
|
||||
|
||||
//nolint:funlen // This is just a collection of test cases
|
||||
func TestNewInterval(t *testing.T) {
|
||||
tz, err := time.LoadLocationFromTZData("Europe/Berlin", tzDataEuropeBerlin)
|
||||
require.NoError(t, err)
|
||||
|
|
|
@ -63,7 +63,7 @@ func stringToSeed(s string) (int64, error) {
|
|||
)
|
||||
|
||||
for i := 0; i < len(hashSum); i++ {
|
||||
sum += int64(float64(hashSum[len(hashSum)-1-i]%10) * math.Pow(10, float64(i))) //nolint:gomnd // No need to put the 10 of 10**i into a constant named "ten"
|
||||
sum += int64(float64(hashSum[len(hashSum)-1-i]%10) * math.Pow(10, float64(i))) //nolint:mnd // No need to put the 10 of 10**i into a constant named "ten"
|
||||
}
|
||||
|
||||
return sum, nil
|
||||
|
|
|
@ -15,6 +15,7 @@ func init() {
|
|||
regFn,
|
||||
tplTwitchRecentGame,
|
||||
tplTwitchRecentTitle,
|
||||
tplTwitchStreamIsLive,
|
||||
tplTwitchStreamUptime,
|
||||
)
|
||||
}
|
||||
|
@ -55,6 +56,20 @@ func tplTwitchRecentTitle(args plugins.RegistrationArguments) {
|
|||
})
|
||||
}
|
||||
|
||||
func tplTwitchStreamIsLive(args plugins.RegistrationArguments) {
|
||||
args.RegisterTemplateFunction("streamIsLive", plugins.GenericTemplateFunctionGetter(func(username string) bool {
|
||||
_, err := args.GetTwitchClient().GetCurrentStreamInfo(context.Background(), strings.TrimLeft(username, "#"))
|
||||
return err == nil
|
||||
}), plugins.TemplateFuncDocumentation{
|
||||
Description: "Check whether a given channel is currently live",
|
||||
Syntax: "streamIsLive <username>",
|
||||
Example: &plugins.TemplateFuncDocumentationExample{
|
||||
Template: `{{ streamIsLive "luziferus" }}`,
|
||||
FakedOutput: "true",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func tplTwitchStreamUptime(args plugins.RegistrationArguments) {
|
||||
args.RegisterTemplateFunction("streamUptime", plugins.GenericTemplateFunctionGetter(func(username string) (time.Duration, error) {
|
||||
si, err := args.GetTwitchClient().GetCurrentStreamInfo(context.Background(), strings.TrimLeft(username, "#"))
|
||||
|
|
|
@ -14,6 +14,7 @@ func init() {
|
|||
tplTwitchDisplayName,
|
||||
tplTwitchIDForUsername,
|
||||
tplTwitchProfileImage,
|
||||
tplTwitchUserExists,
|
||||
tplTwitchUsernameForID,
|
||||
)
|
||||
}
|
||||
|
@ -68,6 +69,25 @@ func tplTwitchProfileImage(args plugins.RegistrationArguments) {
|
|||
})
|
||||
}
|
||||
|
||||
func tplTwitchUserExists(args plugins.RegistrationArguments) {
|
||||
args.RegisterTemplateFunction("userExists", plugins.GenericTemplateFunctionGetter(func(username string) bool {
|
||||
user, err := args.GetTwitchClient().GetUserInformation(context.Background(), strings.TrimLeft(username, "#@"))
|
||||
if err != nil {
|
||||
// Well, they probably don't exist
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.EqualFold(username, user.Login)
|
||||
}), plugins.TemplateFuncDocumentation{
|
||||
Description: "Checks whether the given user exists",
|
||||
Syntax: "userExists <username>",
|
||||
Example: &plugins.TemplateFuncDocumentationExample{
|
||||
Template: `{{ userExists "luziferus" }}`,
|
||||
FakedOutput: "true",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func tplTwitchUsernameForID(args plugins.RegistrationArguments) {
|
||||
args.RegisterTemplateFunction("usernameForID", plugins.GenericTemplateFunctionGetter(func(id string) (string, error) {
|
||||
username, err := args.GetTwitchClient().GetUsernameForID(context.Background(), id)
|
||||
|
|
50
internal/template/twitch/videos.go
Normal file
50
internal/template/twitch/videos.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package twitch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||
)
|
||||
|
||||
func init() {
|
||||
regFn = append(
|
||||
regFn,
|
||||
tplTwitchCurrentVOD,
|
||||
)
|
||||
}
|
||||
|
||||
func tplTwitchCurrentVOD(args plugins.RegistrationArguments) {
|
||||
args.RegisterTemplateFunction("currentVOD", plugins.GenericTemplateFunctionGetter(func(username string) (string, error) {
|
||||
si, err := args.GetTwitchClient().GetCurrentStreamInfo(context.Background(), strings.TrimLeft(username, "#"))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting stream info: %w", err)
|
||||
}
|
||||
|
||||
vids, err := args.GetTwitchClient().GetVideos(context.TODO(), twitch.GetVideoOpts{
|
||||
UserID: si.UserID,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting videos: %w", err)
|
||||
}
|
||||
|
||||
for _, v := range vids {
|
||||
if v.StreamID == nil || *v.StreamID != si.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
return v.URL, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no matching VOD found")
|
||||
}), plugins.TemplateFuncDocumentation{
|
||||
Description: "Returns the VOD of the currently running stream in the given channel (causes an error if no current stream / VOD is found)",
|
||||
Syntax: "currentVOD <username>",
|
||||
Example: &plugins.TemplateFuncDocumentationExample{
|
||||
Template: `{{ currentVOD .channel }}`,
|
||||
FakedOutput: "https://www.twitch.tv/videos/123456789",
|
||||
},
|
||||
})
|
||||
}
|
4
irc.go
4
irc.go
|
@ -63,7 +63,7 @@ func newIRCHandler() (*ircHandler, error) {
|
|||
|
||||
h.ctx, h.ctxCancelFn = context.WithCancel(context.Background())
|
||||
|
||||
conn, err := tls.Dial("tcp", "irc.chat.twitch.tv:6697", nil)
|
||||
conn, err := tls.Dial("tcp", "irc.chat.twitch.tv:6697", nil) //nolint:noctx // Would use background context
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "connect to IRC server")
|
||||
}
|
||||
|
@ -294,7 +294,7 @@ func (i ircHandler) handlePermit(m *irc.Message) {
|
|||
}
|
||||
|
||||
msgParts := strings.Split(m.Trailing(), " ")
|
||||
if len(msgParts) != 2 { //nolint:gomnd // This is not a magic number but just an expected count
|
||||
if len(msgParts) != 2 { //nolint:mnd // This is not a magic number but just an expected count
|
||||
return
|
||||
}
|
||||
|
||||
|
|
4
main.go
4
main.go
|
@ -193,7 +193,7 @@ func main() {
|
|||
}
|
||||
|
||||
if len(rconfig.Args()) > 1 {
|
||||
if err = cli.Call(rconfig.Args()[1:]); err != nil {
|
||||
if err = cliTool.Call(rconfig.Args()[1:]); err != nil {
|
||||
log.Fatalf("error in command: %s", err)
|
||||
}
|
||||
return
|
||||
|
@ -265,7 +265,7 @@ func main() {
|
|||
|
||||
if config.HTTPListen != "" {
|
||||
// If listen address is configured start HTTP server
|
||||
listener, err := net.Listen("tcp", config.HTTPListen)
|
||||
listener, err := net.Listen("tcp", config.HTTPListen) //nolint:noctx // Would use background context
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("Unable to open http_listen port")
|
||||
}
|
||||
|
|
1782
package-lock.json
generated
1782
package-lock.json
generated
File diff suppressed because it is too large
Load diff
36
package.json
36
package.json
|
@ -1,24 +1,24 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.21.3",
|
||||
"esbuild": "^0.17.13",
|
||||
"esbuild-vue": "^1.2.2",
|
||||
"eslint": "^8.36.0",
|
||||
"eslint-plugin-vue": "^9.10.0",
|
||||
"vue-template-compiler": "^2.7.14"
|
||||
"@babel/eslint-parser": "7.28.0",
|
||||
"esbuild": "0.25.9",
|
||||
"esbuild-vue": "1.2.2",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-plugin-vue": "9.33.0",
|
||||
"vue-template-compiler": "2.7.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.10",
|
||||
"axios": "^1.3.4",
|
||||
"bootstrap": "^4.6.2",
|
||||
"bootstrap-vue": "^2.23.1",
|
||||
"bootswatch": "^4.6.2",
|
||||
"codejar": "^3.7.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"vue": "^2.7.14",
|
||||
"vue-router": "^3.6.5"
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
||||
"@fortawesome/vue-fontawesome": "2.0.10",
|
||||
"axios": "1.11.0",
|
||||
"bootstrap": "4.6.2",
|
||||
"bootstrap-vue": "2.23.1",
|
||||
"bootswatch": "4.6.2",
|
||||
"codejar": "3.7.0",
|
||||
"prismjs": "1.30.0",
|
||||
"vue": "2.7.16",
|
||||
"vue-router": "3.6.5"
|
||||
}
|
||||
}
|
|
@ -78,12 +78,9 @@ func (c connector) StoreEncryptedCoreMeta(key string, value any) error {
|
|||
}
|
||||
|
||||
func (c connector) ValidateEncryption() error {
|
||||
validationHasher := sha512.New()
|
||||
fmt.Fprint(validationHasher, c.encryptionSecret)
|
||||
|
||||
var (
|
||||
storedHash string
|
||||
validationHash = fmt.Sprintf("%x", validationHasher.Sum(nil))
|
||||
validationHash = fmt.Sprintf("%x", sha512.Sum512([]byte(c.encryptionSecret)))
|
||||
)
|
||||
|
||||
err := backoff.NewBackoff().
|
||||
|
|
|
@ -21,10 +21,10 @@ func NewLogrusLogWriterWithLevel(logger *logrus.Logger, level logrus.Level, dbDr
|
|||
|
||||
// Print implements the gorm.Logger interface
|
||||
func (l LogWriter) Print(a ...any) {
|
||||
fmt.Fprint(l.Writer, a...)
|
||||
fmt.Fprint(l.Writer, a...) //nolint:errcheck // Interface ignores this error
|
||||
}
|
||||
|
||||
// Printf implements the gorm.Logger interface
|
||||
func (l LogWriter) Printf(format string, a ...any) {
|
||||
fmt.Fprintf(l.Writer, format, a...)
|
||||
fmt.Fprintf(l.Writer, format, a...) //nolint:errcheck // Interface ignores this error
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ func ParseBadgeLevels(m *irc.Message) BadgeCollection {
|
|||
badges := strings.Split(badgeString, ",")
|
||||
for _, b := range badges {
|
||||
badgeParts := strings.Split(b, "/")
|
||||
if len(badgeParts) != 2 { //nolint:gomnd // This is not a magic number but just an expected count
|
||||
if len(badgeParts) != 2 { //nolint:mnd // This is not a magic number but just an expected count
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
@ -127,7 +127,6 @@ type (
|
|||
BroadcasterUserID string `json:"broadcaster_user_id"`
|
||||
BroadcasterUserLogin string `json:"broadcaster_user_login"`
|
||||
BroadcasterUserName string `json:"broadcaster_user_name"`
|
||||
Level int64 `json:"level"`
|
||||
Total int64 `json:"total"`
|
||||
Progress int64 `json:"progress"` // Only Beginn, Progress
|
||||
Goal int64 `json:"goal"` // Only Beginn, Progress
|
||||
|
@ -138,17 +137,22 @@ type (
|
|||
Type string `json:"type"`
|
||||
Total int64 `json:"total"`
|
||||
} `json:"top_contributions"`
|
||||
LastContribution *struct { // Only Begin, Progress
|
||||
UserID string `json:"user_id"`
|
||||
UserLogin string `json:"user_login"`
|
||||
UserName string `json:"user_name"`
|
||||
Type string `json:"type"`
|
||||
Total int64 `json:"total"`
|
||||
} `json:"last_contribution,omitempty"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"` // Only Begin, Progress
|
||||
EndedAt *time.Time `json:"ended_at,omitempty"` // Only End
|
||||
CooldownEndsAt *time.Time `json:"cooldown_ends_at,omitempty"` // Only End
|
||||
Level int64 `json:"level"`
|
||||
AllTimeHighLevel int64 `json:"all_time_high_level"` // Only Begin
|
||||
AllTimeHighTotal int64 `json:"all_time_high_total"` // Only Begin
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
CooldownEndsAt *time.Time `json:"cooldown_ends_at,omitempty"` // Only End
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"` // Only Begin, Progress
|
||||
EndedAt *time.Time `json:"ended_at,omitempty"` // Only End
|
||||
Type string `json:"type"` // treasure, golden_kappa, regular
|
||||
|
||||
// Feature: Shared Trains, all events
|
||||
IsSharedTrain bool `json:"is_shared_train"`
|
||||
SharedTrainParticipants []struct {
|
||||
BroadcasterUserID string `json:"broadcaster_user_id"`
|
||||
BroadcasterUserLogin string `json:"broadcaster_user_login"`
|
||||
BroadcasterUserName string `json:"broadcaster_user_name"`
|
||||
} `json:"shared_train_participants"`
|
||||
}
|
||||
|
||||
// EventSubEventPoll contains the payload for a poll change event
|
||||
|
|
|
@ -58,7 +58,7 @@ func (c *Client) BanUser(ctx context.Context, channel, username string, duration
|
|||
return errors.Wrap(err, "encoding payload")
|
||||
}
|
||||
|
||||
return errors.Wrap(
|
||||
return errors.Wrapf(
|
||||
c.Request(ctx, ClientRequestOpts{
|
||||
AuthType: AuthTypeBearerToken,
|
||||
Method: http.MethodPost,
|
||||
|
@ -89,7 +89,7 @@ func (c *Client) BanUser(ctx context.Context, channel, username string, duration
|
|||
return ValidateStatus(opts, resp)
|
||||
},
|
||||
}),
|
||||
"executing ban request",
|
||||
"executing ban request for %q in %q", username, channel,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ func (c *Client) SearchCategories(ctx context.Context, name string) ([]Category,
|
|||
|
||||
for {
|
||||
if err := c.Request(ctx, ClientRequestOpts{
|
||||
AuthType: AuthTypeBearerToken,
|
||||
AuthType: AuthTypeAppAccessToken,
|
||||
Method: http.MethodGet,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &resp,
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package twitch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
@ -27,12 +29,59 @@ type (
|
|||
TagIds []string `json:"tag_ids"` //revive:disable-line:var-naming // Disabled to prevent breaking change
|
||||
IsMature bool `json:"is_mature"`
|
||||
}
|
||||
|
||||
// StreamMarkerInfo contains information about a marker on a stream
|
||||
StreamMarkerInfo struct {
|
||||
ID string `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Description string `json:"description"`
|
||||
PositionSeconds int64 `json:"position_seconds"`
|
||||
}
|
||||
)
|
||||
|
||||
// ErrNoStreamsFound allows to differntiate between an HTTP error and
|
||||
// the fact there just is no stream found
|
||||
var ErrNoStreamsFound = errors.New("no streams found")
|
||||
|
||||
// CreateStreamMarker creates a marker for the currently running stream.
|
||||
// The stream must be live, no VoD, no upload and no re-run.
|
||||
// The description may be up to 140 chars and can be omitted.
|
||||
func (c *Client) CreateStreamMarker(ctx context.Context, description string) (marker StreamMarkerInfo, err error) {
|
||||
body := new(bytes.Buffer)
|
||||
|
||||
userID, _, err := c.GetAuthorizedUser(ctx)
|
||||
if err != nil {
|
||||
return marker, fmt.Errorf("getting ID for current user: %w", err)
|
||||
}
|
||||
|
||||
if err = json.NewEncoder(body).Encode(struct {
|
||||
UserID string `json:"user_id"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}{
|
||||
UserID: userID,
|
||||
Description: description,
|
||||
}); err != nil {
|
||||
return marker, fmt.Errorf("encoding payload: %w", err)
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Data []StreamMarkerInfo `json:"data"`
|
||||
}
|
||||
|
||||
if err := c.Request(ctx, ClientRequestOpts{
|
||||
AuthType: AuthTypeBearerToken,
|
||||
Body: body,
|
||||
Method: http.MethodPost,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &payload,
|
||||
URL: "https://api.twitch.tv/helix/streams/markers",
|
||||
}); err != nil {
|
||||
return marker, fmt.Errorf("creating marker: %w", err)
|
||||
}
|
||||
|
||||
return payload.Data[0], nil
|
||||
}
|
||||
|
||||
// GetCurrentStreamInfo returns the StreamInfo of the currently running
|
||||
// stream of the given username
|
||||
func (c *Client) GetCurrentStreamInfo(ctx context.Context, username string) (*StreamInfo, error) {
|
||||
|
|
150
pkg/twitch/videos.go
Normal file
150
pkg/twitch/videos.go
Normal file
|
@ -0,0 +1,150 @@
|
|||
package twitch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/hashstructure/v2"
|
||||
)
|
||||
|
||||
type (
|
||||
// GetVideoOpts contain the query parameter for the GetVideos query
|
||||
//
|
||||
// See https://dev.twitch.tv/docs/api/reference/#get-videos for details
|
||||
GetVideoOpts struct {
|
||||
ID string // Required: Exactly one of ID, UserID, GameID
|
||||
UserID string // Required: Exactly one of ID, UserID, GameID
|
||||
GameID string // Required: Exactly one of ID, UserID, GameID
|
||||
Language string // Optional: Use only with GameID
|
||||
Period GetVideoOptsPeriod // Optional: Use only with GameID or UserID
|
||||
Sort GetVideoOptsSort // Optional: Use only with GameID or UserID
|
||||
Type GetVideoOptsType // Optional: Use only with GameID or UserID
|
||||
First int64 // Optional: Use only with GameID or UserID
|
||||
After string // Optional: Use only with UserID
|
||||
Before string // Optional: Use only with UserID
|
||||
}
|
||||
|
||||
// GetVideoOptsPeriod represents a filter used to filter the list of
|
||||
// videos by when they were published
|
||||
GetVideoOptsPeriod string
|
||||
// GetVideoOptsSort represents the order to sort the returned videos in
|
||||
GetVideoOptsSort string
|
||||
// GetVideoOptsType represents a filter used to filter the list of
|
||||
// videos by the video's type
|
||||
GetVideoOptsType string
|
||||
|
||||
// Video contains information about a published video
|
||||
Video struct {
|
||||
ID string `json:"id"`
|
||||
StreamID *string `json:"stream_id"`
|
||||
UserID string `json:"user_id"`
|
||||
UserLogin string `json:"user_login"`
|
||||
UserName string `json:"user_name"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
PublishedAt time.Time `json:"published_at"`
|
||||
URL string `json:"url"`
|
||||
ThumbnailURL string `json:"thumbnail_url"`
|
||||
Viewable string `json:"viewable"`
|
||||
ViewCount int64 `json:"view_count"`
|
||||
Language string `json:"language"`
|
||||
Type string `json:"type"`
|
||||
Duration string `json:"duration"`
|
||||
MutedSegments []struct {
|
||||
Duration int64 `json:"duration"`
|
||||
Offset int64 `json:"offset"`
|
||||
} `json:"muted_segments"`
|
||||
}
|
||||
)
|
||||
|
||||
// List of filters for GetVideoOpts.Period
|
||||
const (
|
||||
GetVideoOptsPeriodAll GetVideoOptsPeriod = "all"
|
||||
GetVideoOptsPeriodDay GetVideoOptsPeriod = "day"
|
||||
GetVideoOptsPeriodMonth GetVideoOptsPeriod = "month"
|
||||
GetVideoOptsPeriodWeek GetVideoOptsPeriod = "week"
|
||||
)
|
||||
|
||||
// List of sort options for GetVideoOpts.Sort
|
||||
const (
|
||||
GetVideoOptsSortTime GetVideoOptsSort = "time"
|
||||
GetVideoOptsSortTrending GetVideoOptsSort = "trending"
|
||||
GetVideoOptsSortViews GetVideoOptsSort = "views"
|
||||
)
|
||||
|
||||
// List of types for GetVideoOpts.Type
|
||||
const (
|
||||
GetVideoOptsTypeAll GetVideoOptsType = "all"
|
||||
GetVideoOptsTypeArchive GetVideoOptsType = "archive"
|
||||
GetVideoOptsTypeHighlight GetVideoOptsType = "highlight"
|
||||
GetVideoOptsTypeUpload GetVideoOptsType = "upload"
|
||||
)
|
||||
|
||||
// GetVideos fetches information about one or more published videos
|
||||
func (c *Client) GetVideos(ctx context.Context, opts GetVideoOpts) (videos []Video, err error) {
|
||||
optsCacheKey, err := opts.cacheKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting opts cache key: %w", err)
|
||||
}
|
||||
|
||||
cacheKey := []string{"currentVideos", optsCacheKey}
|
||||
if vids := c.apiCache.Get(cacheKey); vids != nil {
|
||||
return vids.([]Video), nil
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Data []Video `json:"data"`
|
||||
}
|
||||
|
||||
if err := c.Request(ctx, ClientRequestOpts{
|
||||
AuthType: AuthTypeAppAccessToken,
|
||||
Method: http.MethodGet,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &payload,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/videos?%s", opts.queryParams()),
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("requesting videos: %w", err)
|
||||
}
|
||||
|
||||
// Videos can be changed at any moment, cache for a short period of time
|
||||
c.apiCache.Set(cacheKey, twitchMinCacheTime, payload.Data)
|
||||
|
||||
return payload.Data, nil
|
||||
}
|
||||
|
||||
func (g GetVideoOpts) cacheKey() (string, error) {
|
||||
h, err := hashstructure.Hash(g, hashstructure.FormatV2, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("hashing opts: %w", err)
|
||||
}
|
||||
|
||||
return strconv.FormatUint(h, 10), nil
|
||||
}
|
||||
|
||||
func (g GetVideoOpts) queryParams() string {
|
||||
params := url.Values{}
|
||||
|
||||
for k, v := range map[string]string{
|
||||
"id": g.ID,
|
||||
"user_id": g.UserID,
|
||||
"game_id": g.GameID,
|
||||
"language": g.Language,
|
||||
"period": string(g.Period),
|
||||
"sort": string(g.Sort),
|
||||
"type": string(g.Type),
|
||||
"first": strconv.FormatInt(g.First, 10),
|
||||
"after": g.After,
|
||||
"before": g.Before,
|
||||
} {
|
||||
if v != "" && v != "0" {
|
||||
params.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return params.Encode()
|
||||
}
|
|
@ -52,9 +52,7 @@ func (t *testTimerStore) InCooldown(tt TimerType, limiter, ruleID string) (bool,
|
|||
}
|
||||
|
||||
func (testTimerStore) getCooldownTimerKey(tt TimerType, limiter, ruleID string) string {
|
||||
h := sha256.New()
|
||||
fmt.Fprintf(h, "%d:%s:%s", tt, limiter, ruleID)
|
||||
return fmt.Sprintf("sha256:%x", h.Sum(nil))
|
||||
return fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(fmt.Sprintf("%d:%s:%s", tt, limiter, ruleID))))
|
||||
}
|
||||
|
||||
// Permit timer
|
||||
|
@ -69,7 +67,5 @@ func (t *testTimerStore) HasPermit(channel, username string) (bool, error) {
|
|||
}
|
||||
|
||||
func (testTimerStore) getPermitTimerKey(channel, username string) string {
|
||||
h := sha256.New()
|
||||
fmt.Fprintf(h, "%d:%s:%s", TimerTypePermit, channel, strings.ToLower(strings.TrimLeft(username, "@")))
|
||||
return fmt.Sprintf("sha256:%x", h.Sum(nil))
|
||||
return fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(fmt.Sprintf("%d:%s:%s", TimerTypePermit, channel, strings.ToLower(strings.TrimLeft(username, "@"))))))
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"github.com/Luzifer/twitch-bot/v3/internal/actors/linkdetector"
|
||||
"github.com/Luzifer/twitch-bot/v3/internal/actors/linkprotect"
|
||||
logActor "github.com/Luzifer/twitch-bot/v3/internal/actors/log"
|
||||
"github.com/Luzifer/twitch-bot/v3/internal/actors/marker"
|
||||
"github.com/Luzifer/twitch-bot/v3/internal/actors/messagehook"
|
||||
"github.com/Luzifer/twitch-bot/v3/internal/actors/modchannel"
|
||||
"github.com/Luzifer/twitch-bot/v3/internal/actors/nuke"
|
||||
|
@ -78,6 +79,7 @@ var (
|
|||
linkdetector.Register,
|
||||
linkprotect.Register,
|
||||
logActor.Register,
|
||||
marker.Register,
|
||||
messagehook.Register,
|
||||
modchannel.Register,
|
||||
nuke.Register,
|
||||
|
|
16
renovate.json
Normal file
16
renovate.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"https://git.luzifer.io/luzifer/renovate-config/raw/branch/master/default.json"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchFileNames": [
|
||||
"(^|/).github/workflows/.*"
|
||||
],
|
||||
"schedule": [
|
||||
"* 21 * * 0"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -5,7 +5,7 @@ import "github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
|||
var (
|
||||
channelExtendedScopes = map[string]string{
|
||||
twitch.ScopeChannelEditCommercial: "run commercial",
|
||||
twitch.ScopeChannelManageBroadcast: "modify category / title",
|
||||
twitch.ScopeChannelManageBroadcast: "modify category / title, create markers",
|
||||
twitch.ScopeChannelManagePolls: "manage polls",
|
||||
twitch.ScopeChannelManagePredictions: "manage predictions",
|
||||
twitch.ScopeChannelManageRaids: "start raids",
|
||||
|
|
|
@ -1127,7 +1127,7 @@ export default {
|
|||
},
|
||||
|
||||
validateRaffleChannel() {
|
||||
if (!/^[a-zA-Z0-9]{4,25}$/.test(this.models.raffle.channel)) {
|
||||
if (!constants.REGEXP_USER.test(this.models.raffle.channel)) {
|
||||
return false
|
||||
}
|
||||
return null
|
||||
|
|
22
tools/go.mod
Normal file
22
tools/go.mod
Normal file
|
@ -0,0 +1,22 @@
|
|||
module tools
|
||||
|
||||
go 1.23.1
|
||||
|
||||
require gotest.tools/gotestsum v1.12.3
|
||||
|
||||
require (
|
||||
github.com/bitfield/gotestdox v0.2.2 // indirect
|
||||
github.com/dnephin/pflag v1.0.7 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
)
|
35
tools/go.sum
Normal file
35
tools/go.sum
Normal file
|
@ -0,0 +1,35 @@
|
|||
github.com/bitfield/gotestdox v0.2.2 h1:x6RcPAbBbErKLnapz1QeAlf3ospg8efBsedU93CDsnE=
|
||||
github.com/bitfield/gotestdox v0.2.2/go.mod h1:D+gwtS0urjBrzguAkTM2wodsTQYFHdpx8eqRJ3N+9pY=
|
||||
github.com/dnephin/pflag v1.0.7 h1:oxONGlWxhmUct0YzKTgrpQv9AUA1wtPBn7zuSjJqptk=
|
||||
github.com/dnephin/pflag v1.0.7/go.mod h1:uxE91IoWURlOiTUIA8Mq5ZZkAv3dPUfZNaT80Zm7OQE=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
gotest.tools/gotestsum v1.12.3 h1:jFwenGJ0RnPkuKh2VzAYl1mDOJgbhobBDeL2W1iEycs=
|
||||
gotest.tools/gotestsum v1.12.3/go.mod h1:Y1+e0Iig4xIRtdmYbEV7K7H6spnjc1fX4BOuUhWw2Wk=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
7
tools/tools.go
Normal file
7
tools/tools.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
//go:build tools
|
||||
|
||||
package tools
|
||||
|
||||
import (
|
||||
_ "gotest.tools/gotestsum"
|
||||
)
|
|
@ -133,7 +133,7 @@ func (t *twitchWatcher) getTopicRegistrations(userID string) []topicRegistration
|
|||
},
|
||||
{
|
||||
Topic: twitch.EventSubEventTypeChannelHypetrainBegin,
|
||||
Version: twitch.EventSubTopicVersion1,
|
||||
Version: twitch.EventSubTopicVersion2,
|
||||
Condition: twitch.EventSubCondition{BroadcasterUserID: userID},
|
||||
RequiredScopes: []string{twitch.ScopeChannelReadHypetrain},
|
||||
Hook: t.handleEventSubHypetrainEvent(eventTypeHypetrainBegin),
|
||||
|
@ -141,7 +141,7 @@ func (t *twitchWatcher) getTopicRegistrations(userID string) []topicRegistration
|
|||
},
|
||||
{
|
||||
Topic: twitch.EventSubEventTypeChannelHypetrainEnd,
|
||||
Version: twitch.EventSubTopicVersion1,
|
||||
Version: twitch.EventSubTopicVersion2,
|
||||
Condition: twitch.EventSubCondition{BroadcasterUserID: userID},
|
||||
RequiredScopes: []string{twitch.ScopeChannelReadHypetrain},
|
||||
Hook: t.handleEventSubHypetrainEvent(eventTypeHypetrainEnd),
|
||||
|
@ -149,7 +149,7 @@ func (t *twitchWatcher) getTopicRegistrations(userID string) []topicRegistration
|
|||
},
|
||||
{
|
||||
Topic: twitch.EventSubEventTypeChannelHypetrainProgress,
|
||||
Version: twitch.EventSubTopicVersion1,
|
||||
Version: twitch.EventSubTopicVersion2,
|
||||
Condition: twitch.EventSubCondition{BroadcasterUserID: userID},
|
||||
RequiredScopes: []string{twitch.ScopeChannelReadHypetrain},
|
||||
Hook: t.handleEventSubHypetrainEvent(eventTypeHypetrainProgress),
|
||||
|
@ -234,7 +234,6 @@ func (t *twitchWatcher) getTopicRegistrations(userID string) []topicRegistration
|
|||
},
|
||||
{
|
||||
Topic: twitch.EventSubEventTypeChannelSuspiciousUserMessage,
|
||||
Version: twitch.EventSubTopicVersionBeta,
|
||||
Condition: twitch.EventSubCondition{BroadcasterUserID: userID, ModeratorUserID: userID},
|
||||
RequiredScopes: []string{twitch.ScopeModeratorReadSuspiciousUsers},
|
||||
Hook: t.handleEventSubSusUserMessage,
|
||||
|
@ -242,7 +241,6 @@ func (t *twitchWatcher) getTopicRegistrations(userID string) []topicRegistration
|
|||
},
|
||||
{
|
||||
Topic: twitch.EventSubEventTypeChannelSuspiciousUserUpdate,
|
||||
Version: twitch.EventSubTopicVersionBeta,
|
||||
Condition: twitch.EventSubCondition{BroadcasterUserID: userID, ModeratorUserID: userID},
|
||||
RequiredScopes: []string{twitch.ScopeModeratorReadSuspiciousUsers},
|
||||
Hook: t.handleEventSubSusUserUpdate,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue