Compare commits

..

280 commits

Author SHA1 Message Date
4caa8d114e
chore(deps): update ghcr.io/luzifer-docker/action-env:master docker digest to 33e747a 2025-08-24 19:18:26 +00:00
313e42a6cb
fix(deps): update module github.com/stretchr/testify to v1.11.0 2025-08-24 18:34:50 +00:00
428735b3fe
ci(deps): update actions/checkout action to v5 2025-08-24 16:20:51 +00:00
4b220a0553
fix: link target changed
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-08-24 18:20:09 +02:00
da1112a4f5
chore(deps): update mysql:9.4.0 docker digest to 439bfb4 2025-08-22 00:18:02 +00:00
6dec23718b
chore(deps): update mysql:9.4.0 docker digest to 4ff1791 2025-08-21 21:18:01 +00:00
038fef397e
chore(deps): update ghcr.io/luzifer-docker/action-env:master docker digest to 371d25a 2025-08-20 08:48:01 +00:00
c629e37dad
chore(deps): update golang:1.25-alpine docker digest to f18a072 2025-08-17 11:48:10 +00:00
9879782dfe
chore(deps): update postgres:17.6 docker digest to 29e0bb0 2025-08-16 14:48:12 +00:00
24e4378b7a
Release: Twitch-Bot v3.36.1 2025-08-16 12:15:51 +02:00
253ced8a68
fix(ci): replace tool installation for new actions env
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-08-16 12:06:49 +02:00
fe902b908e
chore: "fix" linter errors
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-08-16 11:59:44 +02:00
0b2a0812d0
chore: replace actions runner image
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-08-16 11:53:12 +02:00
16ef36687a
chore(deps): update dependency esbuild to v0.25.9 2025-08-15 23:48:23 +00:00
cd4ee2538b
chore(deps): update postgres docker tag to v17.6 2025-08-14 20:48:10 +00:00
01fdd6235e
chore(deps): update dependency go to v1.25.0 2025-08-14 11:30:17 +00:00
21e990f94d
chore(deps): update postgres:17.5 docker digest to aadf2c0 2025-08-14 11:29:43 +00:00
f743479af2
fix(deps): update module github.com/getsentry/sentry-go to v0.35.1 2025-08-14 05:49:05 +00:00
6b2d42a7ee
chore(deps): update golang:1.25-alpine docker digest to 77dd832 2025-08-14 05:48:25 +00:00
2591d31045
chore(deps): update golang:1.25-alpine docker digest to 6032270 2025-08-14 00:18:23 +00:00
0cca5353dc
chore(deps): update golang docker tag to v1.25 2025-08-13 21:18:16 +00:00
25b22b70e7
chore(deps): update postgres:17.5 docker digest to 0f9d52b 2025-08-13 14:18:08 +00:00
e6aac58fbf
chore(deps): update mariadb docker tag to v11.8.3 2025-08-13 08:18:27 +00:00
fc77212892
chore(deps): update postgres:17.5 docker digest to aef2e62 2025-08-13 08:18:08 +00:00
718e1fa5f0
chore(deps): update postgres:17.5 docker digest to 7a554f4 2025-08-13 05:18:06 +00:00
590a5a53a7
chore(deps): update postgres:17.5 docker digest to 0d5b8e3 2025-08-13 02:18:06 +00:00
6b284d364c
chore(deps): update postgres:17.5 docker digest to fa6e571 2025-08-12 23:18:07 +00:00
ff713ea159
ci(deps): replace actions/checkout action with actions/checkout v4.3.0 2025-08-11 22:48:35 +00:00
f4a4d3d51c
ci(deps): update actions/checkout action to v5 2025-08-11 16:48:24 +00:00
51a1918522
ci(deps): update actions/checkout action to v4.3.0 2025-08-11 10:48:07 +00:00
4931b3ea1a
chore(deps): update luzifer/gh-arch-env docker digest to c4c8584 2025-08-11 00:18:07 +00:00
906f9b374c
fix(deps): update module golang.org/x/net to v0.43.0 2025-08-08 08:07:37 +00:00
1bccf39bab
fix(deps): update module golang.org/x/crypto to v0.41.0 2025-08-08 03:48:35 +00:00
4aec4e8168
chore(deps): update mysql:9.4.0 docker digest to a776e89 2025-08-08 03:47:56 +00:00
4376fe6499
chore(deps): update golang:1.24-alpine docker digest to c8c5f95 2025-08-06 22:48:15 +00:00
0e83695c05
chore(deps): update dependency go to v1.24.6 2025-08-06 18:48:15 +00:00
cbb4bc713f
chore(deps): update mysql:9.4.0 docker digest to 40e657e 2025-08-04 21:48:15 +00:00
e71a04fc6c
ci(deps): update docker/login-action action to v3.5.0 2025-08-04 14:17:57 +00:00
2863f6d07b
chore(deps): update luzifer/gh-arch-env docker digest to 4e4139b 2025-08-04 00:17:59 +00:00
fa2fa655c3
fix(deps): update module github.com/getsentry/sentry-go to v0.35.0 2025-07-31 22:18:30 +00:00
0c3cfc7f26
chore(deps): update mysql:9.4.0 docker digest to 082063d 2025-07-29 21:18:19 +00:00
62866e3595
chore(deps): update luzifer/gh-arch-env docker digest to ba7f618 2025-07-28 00:18:16 +00:00
21a0b3c453
fix(deps): update dependency axios to v1.11.0 2025-07-26 06:18:27 +00:00
085c04642a
fix(deps): update module gorm.io/gorm to v1.30.1 2025-07-24 03:18:31 +00:00
1064a28086
chore(deps): update mysql:9.4.0 docker digest to 2426e02 2025-07-24 00:48:24 +00:00
b1958b9135
chore(deps): update dependency esbuild to v0.25.8 2025-07-23 08:23:14 +00:00
6cbf195d4b
chore(deps): update mysql docker tag to v9.4.0 2025-07-22 21:48:25 +00:00
e50227ff92
chore(deps): update postgres:17.5 docker digest to 4d89c90 2025-07-22 14:48:26 +00:00
98d0f7561a
chore(deps): update postgres:17.5 docker digest to 378ef4a 2025-07-22 08:48:23 +00:00
2a081c9a9d
chore(deps): update dependency esbuild to v0.25.7 2025-07-22 05:48:51 +00:00
9dcde1ede4
chore(deps): update postgres:17.5 docker digest to 4a04f62 2025-07-22 05:48:22 +00:00
45f261feb6
chore(deps): update luzifer/gh-arch-env docker digest to ee481a0 2025-07-21 03:18:24 +00:00
a1e393a22b
chore(deps): update luzifer/gh-arch-env docker digest to c5eccd4 2025-07-21 00:18:24 +00:00
86ece55ccf
chore(deps): update golang:1.24-alpine docker digest to daae04e 2025-07-17 18:18:03 +00:00
d1a9dfda21
chore(deps): update mysql:9.3.0 docker digest to b9d8b7e 2025-07-17 06:17:56 +00:00
a338471d89
chore(deps): update mysql:9.3.0 docker digest to 1b3da21 2025-07-16 21:47:55 +00:00
b0f463643d
chore(deps): update mariadb:11.8.2 docker digest to 2bcbaec 2025-07-16 15:47:54 +00:00
85adc8c9a0
chore(deps): update mariadb:11.8.2 docker digest to 4d2b2f2 2025-07-16 12:47:55 +00:00
2c17456e53
chore(deps): update mariadb:11.8.2 docker digest to acf55e2 2025-07-16 10:17:55 +00:00
37276ee335
chore(deps): update golang:1.24-alpine docker digest to 48ee313 2025-07-16 02:48:09 +00:00
18b1587267
chore(deps): update mariadb:11.8.2 docker digest to ee8fadc 2025-07-16 00:48:10 +00:00
cacf08158e
chore(deps): update golang:1.24-alpine docker digest to 9c4b616 2025-07-15 23:48:10 +00:00
30eec0bcd9
chore(deps): update alpine:3.22 docker digest to 4bcff63 2025-07-15 22:18:10 +00:00
ceaa444b8a
chore(deps): update luzifer/gh-arch-env docker digest to 94f9a24 2025-07-14 00:18:23 +00:00
a6110ea605
fix(deps): update module golang.org/x/net to v0.42.0 2025-07-11 11:18:18 +00:00
3cfa35ba5e
chore(deps): update dependency esbuild to v0.25.6 2025-07-11 03:18:51 +00:00
c9fbcfcd87
chore(deps): update mysql:9.3.0 docker digest to 297f5ea 2025-07-11 03:18:22 +00:00
263137e947
chore(deps): update golang:1.24-alpine docker digest to ddf5200 2025-07-09 18:48:17 +00:00
50195a0f33
chore(deps): update dependency go to v1.24.5 2025-07-08 17:18:24 +00:00
1d17cfff31
fix(deps): update module github.com/getsentry/sentry-go to v0.34.1 2025-07-07 16:48:13 +00:00
98ee09ad87
chore(deps): update luzifer/gh-arch-env docker digest to e5f806a 2025-07-07 03:17:59 +00:00
ed2657f981
chore(deps): update luzifer/gh-arch-env docker digest to 3136663 2025-07-07 00:17:57 +00:00
d3ad8e2dab
chore(deps): update dependency @babel/eslint-parser to v7.28.0 2025-07-05 08:48:30 +00:00
8f135dc79d
chore(deps): update mysql:9.3.0 docker digest to f1049ce 2025-07-02 09:18:23 +00:00
da5ba74aaa
chore(deps): update mariadb:11.8.2 docker digest to 1e4ec03 2025-07-02 07:18:22 +00:00
aa17043566
chore(deps): update mysql:9.3.0 docker digest to f246779 2025-07-02 00:18:28 +00:00
bb8d62cca2
chore(deps): update postgres:17.5 docker digest to 3962158 2025-07-01 14:48:22 +00:00
6e6ef5227c
chore(deps): update postgres:17.5 docker digest to fc50da4 2025-07-01 08:48:21 +00:00
0d866c702f
chore(deps): update postgres:17.5 docker digest to 016154b 2025-07-01 05:48:21 +00:00
8d6f74d8eb
chore(deps): update luzifer/gh-arch-env docker digest to 9ce1419 2025-06-30 00:18:21 +00:00
ebef4df8c2
ci(deps): update ncipollo/release-action digest to bcfe547 2025-06-29 21:48:21 +00:00
8ff90c1bf3
Release: Twitch-Bot v3.36.0 2025-06-29 11:54:50 +02:00
0c3c066e25
ci(deps): update ncipollo/release-action digest to 9128f23 2025-06-29 02:18:10 +00:00
bf7385fab4
chore(deps): update mysql:9.3.0 docker digest to 9a084cc 2025-06-26 06:18:18 +00:00
efbd1f2e14
chore(deps): update mysql:9.3.0 docker digest to 5c9a1db 2025-06-26 03:18:18 +00:00
01fa504216
fix(deps): update module github.com/getsentry/sentry-go to v0.34.0 2025-06-23 14:48:37 +00:00
61a4a20395
chore(deps): update luzifer/gh-arch-env docker digest to 8800227 2025-06-23 03:48:14 +00:00
c261dba13a
chore(deps): update luzifer/gh-arch-env docker digest to 2bd09ea 2025-06-23 00:18:16 +00:00
a719e79962
fix(deps): update module gotest.tools/gotestsum to v1.12.3 2025-06-19 03:48:37 +00:00
0c6bc3a844
fix(deps): update dependency axios to v1.10.0 2025-06-17 12:24:16 +00:00
f5532cd86c
chore(deps): update luzifer/gh-arch-env docker digest to 58c1c17 2025-06-16 03:35:17 +00:00
d0068c6b84
chore(deps): update luzifer/gh-arch-env docker digest to 1eda280 2025-06-16 00:05:28 +00:00
a86e13960d
feat: migrate to new channel.hype_train.* v2 events
as the v1 versions of the events are now deprecated

fixes 

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-06-15 13:09:09 +02:00
ebe66edc3c
fix(deps): update module github.com/go-sql-driver/mysql to v1.9.3 2025-06-13 07:05:31 +00:00
f3a5fa3be0
chore(deps): update mysql:9.3.0 docker digest to 072f96c 2025-06-12 06:35:21 +00:00
fa529a5936
chore(deps): update mysql:9.3.0 docker digest to b3b4f16 2025-06-11 18:05:13 +00:00
b7013c1fbb
chore(deps): update mariadb docker tag to v11.8.2 2025-06-11 12:05:38 +00:00
f7e1509716
chore(deps): update postgres:17.5 docker digest to 6cf6142 2025-06-11 12:05:12 +00:00
24335e9451
chore(deps): update postgres:17.5 docker digest to cb51e9f 2025-06-11 06:04:52 +00:00
fd966855b4
chore(deps): update postgres:17.5 docker digest to 62d6869 2025-06-11 02:35:25 +00:00
8a28f58b06
chore(deps): update postgres:17.5 docker digest to 30a7233 2025-06-09 21:04:54 +00:00
74cd388928
fix(deps): update module github.com/go-git/go-git/v5 to v5.16.2 2025-06-09 09:52:06 +00:00
d0ed052a5a
ci: remove CRDB integration test
as for the license changes in CRDB it is no longer a supported database
for the bot.

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-06-09 11:50:31 +02:00
df9c548e2d
chore(deps): update luzifer/gh-arch-env docker digest to 9810b8a 2025-06-09 03:04:36 +00:00
7b837db5f6
chore(deps): update luzifer/gh-arch-env docker digest to 8f47e58 2025-06-09 00:04:47 +00:00
4e2241c7f4
chore(deps): update dependency @babel/eslint-parser to v7.27.5 2025-06-06 11:35:14 +00:00
432d06535f
chore(deps): update dependency go to v1.24.4 2025-06-05 22:37:04 +00:00
99775cb815
fix(deps): update module golang.org/x/net to v0.41.0 2025-06-05 21:06:26 +00:00
9fcb6fca77
chore(deps): update golang:1.24-alpine docker digest to 68932fa 2025-06-05 20:35:21 +00:00
a6c8f0aa15
fix(deps): update module github.com/go-git/go-git/v5 to v5.16.1 2025-06-04 10:15:36 +00:00
7fbc7b565b
fix(deps): update module gorm.io/driver/mysql to v1.6.0 2025-06-04 08:05:20 +00:00
0a45c0d92e
chore(deps): update mariadb:11.7.2 docker digest to fcc7fcd 2025-06-03 07:05:25 +00:00
438297fc75
chore(deps): update luzifer/gh-arch-env docker digest to b39254f 2025-06-02 03:34:43 +00:00
74e647de27
chore(deps): update luzifer/gh-arch-env docker digest to 53642f2 2025-06-02 00:05:12 +00:00
37cfc399e8
chore(deps): update golang:1.24-alpine docker digest to b4f875e 2025-05-31 08:34:28 +00:00
51328629b1
chore(deps): update golang:1.24-alpine docker digest to 2853d62 2025-05-31 06:05:07 +00:00
2e7a631e1c
chore(deps): update golang:1.24-alpine docker digest to b158e66 2025-05-31 02:33:53 +00:00
0d42d5336a
chore(deps): update mysql:9.3.0 docker digest to 04768cb 2025-05-30 21:34:16 +00:00
07eebd47b7
chore(deps): update alpine docker tag to v3.22 2025-05-30 19:04:13 +00:00
e3aa795604
chore(deps): update dependency esbuild to v0.25.5 2025-05-30 03:34:53 +00:00
de782bae4e
Update ghcr.io/luzifer-docker/crdb-gh-service Docker tag to v25.2.0 2025-05-27 16:04:21 +00:00
91a41f53c6
Update module gorm.io/driver/postgres to v1.6.0 2025-05-27 13:05:27 +00:00
7a1eb5b9d0
Update luzifer/gh-arch-env Docker digest to 1113ddb 2025-05-26 03:34:10 +00:00
e337b47938
Update luzifer/gh-arch-env Docker digest to cf87665 2025-05-26 00:04:54 +00:00
5bc8f1fa23
Update module gorm.io/gorm to v1.30.0 2025-05-25 08:07:53 +00:00
50190346d5
Update postgres:17.5 Docker digest to 6efd0df 2025-05-23 08:37:47 +00:00
beae543e0b
Update postgres:17.5 Docker digest to ea51edb 2025-05-23 02:37:32 +00:00
ebce29eb6a
Update postgres:17.5 Docker digest to bf0f80f 2025-05-22 21:06:24 +00:00
59862ccb8d
Update postgres:17.5 Docker digest to 20b542a 2025-05-22 18:07:45 +00:00
eb1f57d253
Update postgres:17.5 Docker digest to bbdcc04 2025-05-22 05:37:45 +00:00
ed20672011
Update postgres:17.5 Docker digest to 2718f68 2025-05-22 02:36:02 +00:00
183f5233e2
Update luzifer/gh-arch-env Docker digest to d0d04e6 2025-05-16 04:09:32 +00:00
5c86323bf5
Update luzifer/gh-arch-env Docker digest to 664cf41 2025-05-16 00:10:45 +00:00
bfa50a7d1c
Update module github.com/getsentry/sentry-go to v0.33.0 2025-05-15 13:07:50 +00:00
93ad13ce70
Update ghcr.io/luzifer-docker/crdb-gh-service Docker tag to v25.1.6 2025-05-14 08:08:21 +00:00
59546e19b2
Update postgres Docker tag to v17.5 2025-05-09 03:07:35 +00:00
d22132caac
Update luzifer/gh-arch-env Docker digest to 2a1435e 2025-05-09 03:07:11 +00:00
c368fea4de
Update luzifer/gh-arch-env Docker digest to 1fc386f 2025-05-09 00:08:46 +00:00
733bbbb6bf
Update module gorm.io/gorm to v1.26.1 2025-05-07 08:18:18 +00:00
702ef21be1
Update dependency go to v1.24.3 2025-05-06 23:07:40 +00:00
60d71ce66b
Update golang:1.24-alpine Docker digest to ef18ee7 2025-05-06 21:07:19 +00:00
5ec75f7a98
Update module gotest.tools/gotestsum to v1.12.2 2025-05-06 07:49:12 +00:00
fdcb02f7a9
Update dependency esbuild to v0.25.4 2025-05-06 00:36:53 +00:00
db873c9706
Update module golang.org/x/net to v0.40.0 2025-05-05 20:35:56 +00:00
5c6bf33914
Update mariadb:11.7.2 Docker digest to 11706a6 2025-05-05 19:05:58 +00:00
62d24e8fe1
Update module golang.org/x/oauth2 to v0.30.0 2025-05-05 13:37:57 +00:00
276d952be3
Update luzifer/gh-arch-env Docker digest to 90b9c6e 2025-05-02 04:35:46 +00:00
b4a9a7fee0
Update luzifer/gh-arch-env Docker digest to b6512b1 2025-05-01 04:06:11 +00:00
c29e82e3ab
Update dependency @babel/eslint-parser to v7.27.1 2025-04-30 15:36:17 +00:00
a4d505f505
Update mysql:9.3.0 Docker digest to 2247f6d 2025-04-30 09:34:18 +00:00
6d175a801c
Update luzifer/gh-arch-env Docker digest to 1f2ab7e 2025-04-30 03:34:41 +00:00
e9bf7b84a6
Update luzifer/gh-arch-env Docker digest to 1f2ab7e 2025-04-30 03:34:14 +00:00
992b7fe4af
Update mysql:9.3.0 Docker digest to 5b67216 2025-04-29 21:35:32 +00:00
e8f96f19d1
Pin dependencies 2025-04-29 13:07:38 +00:00
790576ddb5
Update postgres:17.4 Docker digest to 304ab81 2025-04-29 08:34:53 +00:00
6ec96f9daa
Update postgres:17.4 Docker digest to 4aed4b0 2025-04-29 06:06:14 +00:00
678a003307
Update luzifer/gh-arch-env Docker digest to 6af5cae 2025-04-29 03:35:38 +00:00
1fcc35524e
Update postgres:17.4 Docker digest to f57cfa8 2025-04-29 02:34:44 +00:00
865e38125a
Update postgres:17.4 Docker digest to cdb3f38 2025-04-29 00:04:22 +00:00
68a899cd79
Update ghcr.io/luzifer-docker/crdb-gh-service Docker tag to v25.1.5 2025-04-28 15:34:35 +00:00
34abf9d564
Update luzifer/gh-arch-env Docker digest to c8454dc 2025-04-28 04:05:00 +00:00
8ce0b5b294
Update luzifer/gh-arch-env Docker digest to 2c35624 2025-04-27 04:34:42 +00:00
574ca67c69
Update luzifer/gh-arch-env Docker digest to eb4fcf8 2025-04-27 03:36:31 +00:00
05d6c8bc50
Update luzifer/gh-arch-env Docker digest to b25582c 2025-04-26 04:04:11 +00:00
e4f0ec004d
Update dependency axios to v1.9.0 2025-04-25 09:19:37 +00:00
3db82d9c7b
Update module gorm.io/gorm to v1.26.0 2025-04-25 08:34:45 +00:00
53c6aa07fa
Update luzifer/gh-arch-env Docker digest to a5a2097 2025-04-25 04:05:41 +00:00
8355e6306a
Update luzifer/gh-arch-env Docker digest to 0ce058d 2025-04-24 03:05:25 +00:00
e0edb68d44
Update dependency esbuild to v0.25.3 2025-04-23 08:07:46 +00:00
5519e4de68
Update luzifer/gh-arch-env Docker digest to da08b5c 2025-04-23 04:05:53 +00:00
e76358fc4f
Update luzifer/gh-arch-env Docker digest to 1c9dcc4 2025-04-22 03:34:45 +00:00
04cc7762c2
Update luzifer/gh-arch-env Docker digest to 34982d6 2025-04-21 04:04:24 +00:00
acfc5cbe12
Update luzifer/gh-arch-env Docker digest to 975506f 2025-04-21 00:34:10 +00:00
09cf117501
Update luzifer/gh-arch-env Docker digest to c0b7839 2025-04-20 23:05:07 +00:00
facbdbe477
Update luzifer/gh-arch-env Docker digest to 35360d7 2025-04-20 04:32:56 +00:00
aebe017b27
Update luzifer/gh-arch-env Docker digest to f175b6a 2025-04-20 03:33:06 +00:00
fb628c24fc
Update luzifer/gh-arch-env Docker digest to dff7b93 2025-04-19 03:02:42 +00:00
68d24ccefc
Update luzifer/gh-arch-env Docker digest to 3e8be63 2025-04-18 03:32:35 +00:00
05012ecc96
Update luzifer/gh-arch-env Docker digest to 3ee6c15 2025-04-17 04:02:29 +00:00
92321e4054
Update luzifer/gh-arch-env Docker digest to 68f318a 2025-04-16 03:32:32 +00:00
8119140c46
Update module github.com/go-git/go-git/v5 to v5.16.0 2025-04-15 20:03:26 +00:00
ddded3decb
Update mysql Docker tag to v9.3.0 2025-04-15 04:02:58 +00:00
848c222233
Update luzifer/gh-arch-env Docker digest to 0088a8b 2025-04-15 04:02:36 +00:00
a7c238d67f
Tools: Remove dependency to gofumpt
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-04-14 10:23:47 +02:00
2ea0c3e746
Update luzifer/gh-arch-env Docker digest to f18a6a4 2025-04-14 04:02:24 +00:00
e46e7bd863
Update module github.com/Luzifer/rconfig/v2 to v2.6.0 2025-04-13 12:03:05 +00:00
870efae0b9
Update luzifer/gh-arch-env Docker digest to 3574a2f 2025-04-13 11:32:18 +00:00
2a01049812
Update luzifer/gh-arch-env Docker digest to bef18dd 2025-04-13 05:02:27 +00:00
2104690965
Update luzifer/gh-arch-env Docker digest to 889b3ac 2025-04-13 04:02:58 +00:00
5e9929f7be
CI: Lock down updates to workflow to once a week
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-04-12 15:42:46 +02:00
9c1718e090
Pin dependencies 2025-04-12 12:39:30 +00:00
bab21223c1
CI: Update to pinned CRDB service image
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-04-12 14:38:35 +02:00
4843f97231
CI: Update integration test to latest database versions
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-04-12 14:27:01 +02:00
d62e978c4e
Release: Twitch-Bot v3.35.4 2025-04-12 14:10:50 +02:00
1059f70468
[docs] Fix: Typo in URL
params are read from hash, not query

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-04-12 12:33:53 +02:00
365e1c9fcd
Update luzifer/gh-arch-env Docker digest to fd19117 2025-04-12 03:32:21 +00:00
1bd9cd578f
Update module github.com/go-git/go-git/v5 to v5.15.0 2025-04-11 09:02:31 +00:00
2719af4960
Update luzifer/gh-arch-env Docker digest to 6bfca0d 2025-04-11 04:02:35 +00:00
cf7811591c
Update module github.com/getsentry/sentry-go to v0.32.0 2025-04-10 10:03:06 +00:00
e86e1f9f91
Update luzifer/gh-arch-env Docker digest to d4a565d 2025-04-10 04:01:56 +00:00
a97dc76e1a
Update mariadb:11 Docker digest to 81e8930 2025-04-09 10:02:23 +00:00
14e8a5e2b7
Update mariadb:11 Docker digest to 2010ede 2025-04-09 07:02:54 +00:00
5da4196ba1
Update luzifer/gh-arch-env Docker digest to 23109db 2025-04-09 03:02:39 +00:00
b06b9d7645
Update postgres:15 Docker digest to fe45ed1 2025-04-08 18:02:45 +00:00
0be1a89019
Update postgres:15 Docker digest to 16c7497 2025-04-08 14:32:28 +00:00
2748dfe82b
Update postgres:15 Docker digest to 71807dd 2025-04-08 08:33:35 +00:00
5271b50ae6
Update module golang.org/x/net to v0.39.0 2025-04-08 06:02:54 +00:00
0bb1948a82
Update postgres:15 Docker digest to 2d3fd09 2025-04-08 06:02:11 +00:00
48b253d213
Update luzifer/gh-arch-env Docker digest to 9b07ef8 2025-04-08 03:02:25 +00:00
5b977d3f31
Update module github.com/go-sql-driver/mysql to v1.9.2 2025-04-07 12:02:53 +00:00
f7308345e0
Revert "CI: Disable Vue v3 / Bootstrap v5 updates"
Doesn't work, Renovate ignores this for security fixes

This reverts commit 207ebd3e52.
2025-04-07 10:16:01 +02:00
207ebd3e52
CI: Disable Vue v3 / Bootstrap v5 updates
as they need a full rewrite of the frontend which is in progress but
will take way more time.

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-04-07 10:11:56 +02:00
12de1efef2
Update luzifer/gh-arch-env Docker digest to 4c27bbc 2025-04-07 03:02:22 +00:00
0df88165c7
CI: Drop "stable" branch
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-04-07 00:08:31 +02:00
6421c87ed0
Update luzifer/gh-arch-env Docker digest to 60bef93 2025-04-06 21:46:50 +00:00
0eb8523939
Release: Twitch-Bot v3.35.3 2025-04-06 23:44:24 +02:00
de973e7fdf
Pin dependencies 2025-04-06 21:25:22 +00:00
2959c79e7e
Update Font Awesome to v6.7.2 2025-04-06 21:23:54 +00:00
7bfa1e6166
Update dependency go to v1.24.2 2025-04-06 21:22:10 +00:00
ed5768f80e
Update module golang.org/x/crypto to v0.37.0 2025-04-06 21:20:03 +00:00
5a842c8160
Update dependency eslint-plugin-vue to v9.33.0 2025-04-06 21:17:03 +00:00
52e54017e4
Update dependency eslint to v8.57.1 2025-04-06 21:14:18 +00:00
d3ba7c965a
Update dependency @babel/eslint-parser to v7.27.0 2025-04-06 21:11:18 +00:00
8c0fed012c
Pin dependencies 2025-04-06 21:04:37 +00:00
7f045fa928
Update dependency esbuild to ^0.25.0 [SECURITY] 2025-04-06 20:57:31 +00:00
bbe454ed4f
CI: Switch to alpine based build image, add image labels
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-04-06 22:56:11 +02:00
2543622abf
CI: Fix: Options must not end with newline
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-04-06 22:47:21 +02:00
22325da84f
Pin dependencies 2025-04-06 20:39:51 +00:00
1bde118f9d
CI: Pin Alpine repo to latest version
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-04-06 22:38:01 +02:00
019d46817e
Fix: Workflows do not understand references
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-04-06 16:29:35 +02:00
edc1d9da5b
CI: Re-join workflow, enable renovate
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-04-06 16:01:42 +02:00
a57aa101db
CI: Remove dependency on git-lfs
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-04-06 13:46:25 +02:00
c3788e19ab
Release: Twitch-Bot v3.35.2 2025-04-06 13:34:59 +02:00
5998a07892
Replace nodejs LTS version
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-04-06 13:24:59 +02:00
2c4fccd56d
Update Go & Node dependencies
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-04-06 13:20:29 +02:00
2393b2c4ab
Lint: Migrate linter config, use local linter, fix issues
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-04-06 13:14:57 +02:00
a6b30aa6e7
Replace LFS files
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-04-06 12:37:21 +02:00
99eecd1631
Release: Twitch-Bot v3.35.1 2024-12-12 11:18:57 +01:00
00320ba09c
Update node dependencies
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-12-12 11:11:58 +01:00
db2d80642a
Update Go dependencies
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-12-12 11:11:58 +01:00
3cfee5ccc9
[core] Fix: Reduce token requirements for category search
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-12-12 00:59:42 +01:00
9cac1686b8
CI: Configure git-changerelease
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-12-02 13:25:25 +01:00
f26ce9b0da
prepare release v3.35.0 2024-12-02 13:24:22 +01:00
dd80433cb0
[raffle] Fix: Raffle channel did not allow underscore in channel name
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-12-02 13:08:18 +01:00
0d76c58ede
[template] Add functions parseDuration, parseDurationToSeconds
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-11-25 00:40:53 +01:00
096657bcee
Improve CI and document Makefile
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-09-26 13:18:37 +02:00
ff475f286b
prepare release v3.34.0 2024-09-16 11:10:16 +02:00
06d7fcb019
Lint: Fix (theoretical) overflow issues
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-09-16 11:09:03 +02:00
710783aaf7
[core] Fix: StreamMarker contained wrong ID format
as of broken documentation

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-09-13 00:08:41 +02:00
19038dbc6e
[templating] Add currentVOD function
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-09-11 20:30:55 +02:00
740a71a173
[marker] Add marker info to actor result
in order to enable rules to access i.e. the position of the marker

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-09-11 14:23:42 +02:00
e0a8ce3684
[linkcheck] Fix: Replace static (deprecated) user-agent list
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-09-05 12:50:58 +02:00
5a8459cedc
[marker] Implement actor to create stream markers
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-09-04 00:06:55 +02:00
8819b4031a
prepare release v3.33.2 2024-08-27 17:08:56 +02:00
41535bc4df
Lint: Replace deprecated linter
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-08-27 14:52:36 +02:00
150daf8a80
[raffle] Lint: Ignore linter false-positive
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-08-27 14:49:21 +02:00
1d192ad796
[overlays] Fix KoFi donation currency in eventfeed
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-08-27 00:23:01 +02:00
b1ceb29bfb
prepare release v3.33.1 2024-08-14 16:23:51 +02:00
26a57c379d
[editor] Update dependencies
fixes CVE-2024-39338

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-08-14 16:13:47 +02:00
13bc753b7d
[core] Fix: Do not execute action after permission check
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-08-14 15:56:06 +02:00
e8d60e2733
[raffle] Fix: Send ID as string
in order to be able to transport big uint64 through JSON

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-08-11 00:59:46 +02:00
4964ed25cf
prepare release v3.33.0 2024-07-27 23:39:48 +02:00
014df155ae
[overlays] Fix: Transmit event-id as string
in order to compensate for i.e. CRDB very large IDs being truncated in
JSON transmit

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-07-26 14:43:05 +02:00
c4be936c63
[overlays] Add eventfeed as default-overlay
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-07-26 14:42:57 +02:00
b38ecc9d0b
[kofi] Fix: Use message as string
with pointer of string comparisons do not work properly and make
templating hard

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-07-03 12:27:54 +02:00
621d266391
[linkcheck] Add support for meta-redirects
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-06-10 14:17:49 +02:00
f1d4c1a283
prepare release v3.32.0 2024-06-09 13:52:51 +02:00
0355713f7c
CI: Disable SSL on mysql test container
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-06-09 13:25:06 +02:00
c63793be2d
Lint: Update linter config
and fix some newly appearing linter errors

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-06-09 13:01:32 +02:00
2a64caec09
[core] Fix: Include username and channel in ban errors
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-05-31 12:26:24 +02:00
8e8895d32e
[templating] Add streamIsLive function
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-05-26 15:50:10 +02:00
0a37873241
[core] Fix: Accept proper token declaration in Authorization header
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-05-24 13:59:29 +02:00
19a30d342a
prepare release v3.31.0 2024-05-13 18:33:34 +02:00
30305600e7
[spotify] Fix: Refresh-Token gets revoked when using two functions
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-05-13 18:26:38 +02:00
5dd6a5323c
[core] Add locking to prevent concurrent rule executions
refs 

ensures counter actions are not triggered concurrently by two persons

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-05-01 22:39:18 +02:00
a01ce9aa5f
prepare release v3.30.0 2024-04-26 19:49:45 +02:00
eb37a75da8
Update dependencies
remove addition of Sprig functions as korvike functions now are based on
the Sprig functions

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-04-26 19:34:35 +02:00
3cefd39960
CI: Update linter config
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-04-26 17:48:17 +02:00
ebf734be40
[templating] Add userExists function
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-04-18 00:26:14 +02:00
f56a7a3266
[eventsub] Suspicious user topics were moved from beta to v1
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-04-13 17:29:14 +02:00
100 changed files with 3583 additions and 2158 deletions
.git_changerelease.yaml.gitattributes
.github/workflows
.golangci.ymlDockerfileHistory.mdMakefileactions.goauthMiddleware.go
ci/workflow-parts
cli.gocli_actorDocs.gocli_apiToken.gocli_migrateDatabase.gocli_resetSecrets.gocli_tplDocs.gocli_validateConfig.goconfig.goconfigEditor_automessage.goconfigEditor_general.goconfigEditor_rules.goconfigRemoteUpdate.go
docs
functions.gogo.modgo.sum
internal
irc.gomain.gopackage-lock.jsonpackage.json
pkg
plugins
plugins_core.gorenovate.jsonscopes.go
src
tools
twitchWatcher.go

19
.git_changerelease.yaml Normal file
View 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
View file

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

View file

@ -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

View file

@ -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$

View file

@ -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

View file

@ -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]

View file

@ -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 \

View file

@ -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

View file

@ -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)

View file

@ -1,7 +0,0 @@
name: CI Workflow
on: push
permissions:
contents: read
jobs: {}

View file

@ -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
...

View file

@ -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
...

View file

@ -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
...

View file

@ -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
...

View file

@ -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
...

View file

@ -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
...

View file

@ -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
...

View file

@ -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
View file

@ -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()

View file

@ -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 {

View file

@ -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]")
}

View file

@ -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>")
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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")
}

View file

@ -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{}

View file

@ -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

View file

@ -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{}

View file

@ -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)
}

View file

@ -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

View file

@ -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

View file

@ -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 |

View file

@ -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.

Binary file not shown.

Before

(image error) Size: 130 B

After

(image error) Size: 14 KiB

Before After
Before After

Binary file not shown.

Before

(image error) Size: 130 B

After

(image error) Size: 13 KiB

Before After
Before After

Binary file not shown.

Before

(image error) Size: 130 B

After

(image error) Size: 77 KiB

Before After
Before After

Binary file not shown.

Before

(image error) Size: 130 B

After

(image error) Size: 16 KiB

Before After
Before After

Binary file not shown.

Before

(image error) Size: 130 B

After

(image error) Size: 67 KiB

Before After
Before After

Binary file not shown.

Before

(image error) Size: 130 B

After

(image error) Size: 61 KiB

Before After
Before After

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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) {

View 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
}

View file

@ -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

View file

@ -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) {

View 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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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",

View file

@ -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) {

View file

@ -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")
}

View file

@ -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 {

View file

@ -54,5 +54,5 @@ func handleFormattedMessage(w http.ResponseWriter, r *http.Request) {
return
}
fmt.Fprint(w, msg)
http.Error(w, msg, http.StatusOK)
}

View file

@ -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

View 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 }

View 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>

View 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')

View file

@ -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"`

View file

@ -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"`

View file

@ -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

View file

@ -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

View file

@ -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
}

View 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
}

View 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)
}

View file

@ -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()]
}

View 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)
}

View file

@ -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

View 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
View 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]
}

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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, "#"))

View file

@ -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)

View 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
View file

@ -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
}

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View file

@ -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().

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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,
)
}

View file

@ -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,

View file

@ -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
View 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()
}

View file

@ -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, "@"))))))
}

View file

@ -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
View 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"
]
}
]
}

View file

@ -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",

View file

@ -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
View 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
View 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
View file

@ -0,0 +1,7 @@
//go:build tools
package tools
import (
_ "gotest.tools/gotestsum"
)

View file

@ -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,