Switch to properly tested database interface
and with that support all databases the bot does support Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
f14be1e73f
commit
f1d35ce7c0
13 changed files with 540 additions and 231 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
twitch-bot-streak
|
||||
twitch-bot-streak_linux_amd64.tgz
|
||||
|
|
1
Makefile
1
Makefile
|
@ -7,3 +7,4 @@ build:
|
|||
-mod=readonly \
|
||||
-modcacherw \
|
||||
-trimpath
|
||||
tar -czf twitch-bot-streak_linux_amd64.tgz twitch-bot-streak
|
||||
|
|
|
@ -5,9 +5,9 @@ Addon for my [twitch-bot](https://github.com/Luzifer/twitch-bot) executed throug
|
|||
## Features
|
||||
|
||||
- By default uses same database connection as the bot through the same environment variable
|
||||
- Needs two tables (see `schema.sql`) to store its data
|
||||
- Needs two extra tables to store its data (does not interfere with the bot)
|
||||
- Reward is only counted once per stream (in case Twitch messes up stuff)
|
||||
- If a stream goes offline for a few minutes a grace period (30m) is taken which will not reset the stream streak
|
||||
- If a stream goes offline for a few minutes a grace period is taken which will not reset the stream streak
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
33
go.mod
33
go.mod
|
@ -4,15 +4,42 @@ go 1.22.1
|
|||
|
||||
require (
|
||||
github.com/Luzifer/rconfig/v2 v2.5.0
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/jmoiron/sqlx v1.3.5
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-sql-driver/mysql v1.8.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.8.4
|
||||
gorm.io/driver/mysql v1.5.6
|
||||
gorm.io/driver/postgres v1.5.7
|
||||
gorm.io/gorm v1.25.8
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // 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/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
golang.org/x/crypto v0.21.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
|
||||
gopkg.in/validator.v2 v2.0.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.47.0 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/sqlite v1.29.5 // indirect
|
||||
)
|
||||
|
|
93
go.sum
93
go.sum
|
@ -1,34 +1,74 @@
|
|||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
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/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/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
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/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/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4=
|
||||
github.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
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.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
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=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||
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/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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=
|
||||
|
@ -37,3 +77,34 @@ gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPu
|
|||
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.8 h1:WAGEZ/aEcznN4D03laj8DKnehe1e9gYQAjW8xyPRdeo=
|
||||
gorm.io/gorm v1.25.8/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
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.0 h1:99E8QHRoPrXN8VpS0zgAgJ5nSjpXrPKpsJIMvGL/2Oc=
|
||||
modernc.org/ccgo/v4 v4.13.0/go.mod h1:Td6RI9W9G2ZpKHaJ7UeGEiB2aIpoDqLBnm4wtkbJTbQ=
|
||||
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.47.0 h1:BXrzId9fOOkBtS+uFQ5aZyVGmt7WcSEPrXF5Kwsho90=
|
||||
modernc.org/libc v1.47.0/go.mod h1:gzCncw0a74aCiVqHeWAYHHaW//fkSHHS/3S/gfhLlCI=
|
||||
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.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
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/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
|
|
35
main.go
35
main.go
|
@ -7,24 +7,35 @@ import (
|
|||
"os"
|
||||
"regexp"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"git.luzifer.io/luzifer/twitch-bot-streak/pkg/database"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/Luzifer/rconfig/v2"
|
||||
)
|
||||
|
||||
type (
|
||||
botResponse struct {
|
||||
Type string `json:"type"`
|
||||
Attributes map[string]any `json:"attributes"`
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
cfg = struct {
|
||||
Action string `flag:"action,a" description:"Which action to perform"`
|
||||
TwitchID uint64 `flag:"twitch-id" description:"ID of the user to access the streak of"`
|
||||
TwitchUsername string `flag:"twitch-username" description:"Username of the user to update in streaks table"`
|
||||
Action string `flag:"action,a" description:"Which action to perform"`
|
||||
TwitchID uint64 `flag:"twitch-id" description:"ID of the user to access the streak of"`
|
||||
TwitchUsername string `flag:"twitch-username" description:"Username of the user to update in streaks table"`
|
||||
StreamOfflineGrace time.Duration `flag:"stream-offline-grace" default:"30m" description:"How long to not start a new stream after previous went offline"`
|
||||
|
||||
StorageConnString string `flag:"storage-conn-string" description:"How to connect to the database"`
|
||||
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
|
||||
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
||||
StorageConnString string `flag:"storage-conn-string" default:"./storage.db" description:"Connection string for the database"`
|
||||
StorageConnType string `flag:"storage-conn-type" default:"sqlite" description:"One of: mysql, postgres, sqlite"`
|
||||
|
||||
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
|
||||
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
||||
}{}
|
||||
|
||||
version = "dev"
|
||||
|
@ -64,28 +75,28 @@ func main() {
|
|||
logrus.WithError(err).Fatal("parsing message template")
|
||||
}
|
||||
|
||||
db, err := sqlx.Open("mysql", cfg.StorageConnString)
|
||||
db, err := database.New(cfg.StorageConnType, cfg.StorageConnString)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("connecting to database")
|
||||
}
|
||||
|
||||
switch cfg.Action {
|
||||
case "stream_start":
|
||||
if err = startStream(db); err != nil {
|
||||
if err = db.StartStream(cfg.StreamOfflineGrace); err != nil {
|
||||
logrus.WithError(err).Fatal("starting stream")
|
||||
}
|
||||
|
||||
case "stream_offline":
|
||||
if err = setStreamOffline(db); err != nil {
|
||||
if err = db.SetStreamOffline(); err != nil {
|
||||
logrus.WithError(err).Fatal("stopping stream")
|
||||
}
|
||||
|
||||
case "count_stream":
|
||||
if err = startStream(db); err != nil {
|
||||
if err = db.StartStream(cfg.StreamOfflineGrace); err != nil {
|
||||
logrus.WithError(err).Fatal("starting stream")
|
||||
}
|
||||
|
||||
user, err := countStreak(db, cfg.TwitchID, cfg.TwitchUsername)
|
||||
user, err := db.CountStreak(cfg.TwitchID, cfg.TwitchUsername)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("counting streak")
|
||||
}
|
||||
|
|
70
pkg/database/db.go
Normal file
70
pkg/database/db.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type (
|
||||
DB struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
)
|
||||
|
||||
// New creates a new DB with the given driver and database
|
||||
func New(driverName, connString string) (d *DB, err error) {
|
||||
var innerDB gorm.Dialector
|
||||
|
||||
switch driverName {
|
||||
case "mysql":
|
||||
innerDB = mysql.Open(connString)
|
||||
|
||||
case "postgres":
|
||||
innerDB = postgres.Open(connString)
|
||||
|
||||
case "sqlite":
|
||||
innerDB = sqlite.Open(connString)
|
||||
|
||||
default:
|
||||
return nil, errors.Errorf("unknown database driver %s", driverName)
|
||||
}
|
||||
|
||||
db, err := gorm.Open(innerDB, &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "connecting database")
|
||||
}
|
||||
|
||||
d = &DB{
|
||||
db: db,
|
||||
}
|
||||
|
||||
if err = db.AutoMigrate(
|
||||
&StreakMeta{},
|
||||
&StreakUser{},
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("applying schema: %w", err)
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Close closes the underlying database
|
||||
func (d DB) Close() error {
|
||||
sdb, err := d.db.DB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting underlying database: %w", err)
|
||||
}
|
||||
|
||||
if err = sdb.Close(); err != nil {
|
||||
return fmt.Errorf("closing db: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
138
pkg/database/query.go
Normal file
138
pkg/database/query.go
Normal file
|
@ -0,0 +1,138 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var defaultMetaTime = time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
func (d DB) CountStreak(twitchID uint64, username string) (user StreakUser, err error) {
|
||||
if err = d.db.Transaction(func(tx *gorm.DB) (err error) {
|
||||
if err = tx.First(&user, "twitch_id = ?", twitchID).Error; err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fmt.Errorf("getting user: %w", err)
|
||||
}
|
||||
|
||||
// User was not yet inserted
|
||||
user = StreakUser{
|
||||
TwitchID: twitchID,
|
||||
Username: username,
|
||||
StreamsCount: 0,
|
||||
CurrentStreak: 0,
|
||||
MaxStreak: 0,
|
||||
StreakStatus: StatusBroken,
|
||||
}
|
||||
}
|
||||
|
||||
switch user.StreakStatus {
|
||||
case StatusActive:
|
||||
// User has an active streak, do nothing
|
||||
return nil
|
||||
|
||||
case StatusBroken:
|
||||
// User needs a new streak
|
||||
user.CurrentStreak = 1
|
||||
|
||||
case StatusPending:
|
||||
// User can prolong their streak
|
||||
user.CurrentStreak += 1
|
||||
}
|
||||
|
||||
// In any case set the streak active and count the current stream
|
||||
user.StreamsCount++
|
||||
user.StreakStatus = StatusActive
|
||||
if user.CurrentStreak > user.MaxStreak {
|
||||
user.MaxStreak = user.CurrentStreak
|
||||
}
|
||||
|
||||
if err = tx.Save(&user).Error; err != nil {
|
||||
return fmt.Errorf("saving user: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return user, fmt.Errorf("counting streak for user: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (d DB) SetStreamOffline() (err error) {
|
||||
return d.storeTimeToMeta(d.db, "stream_offline", time.Now())
|
||||
}
|
||||
|
||||
func (d DB) StartStream(streamOfflineGrace time.Duration) (err error) {
|
||||
if err = d.db.Transaction(func(tx *gorm.DB) (err error) {
|
||||
lastOffline, err := d.getTimeFromMeta(tx, "stream_offline")
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting offline time: %w", err)
|
||||
}
|
||||
|
||||
lastOnline, err := d.getTimeFromMeta(tx, "stream_online")
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting online time: %w", err)
|
||||
}
|
||||
|
||||
if err = d.storeTimeToMeta(tx, "stream_online", time.Now()); err != nil {
|
||||
return fmt.Errorf("storing stream start: %w", err)
|
||||
}
|
||||
|
||||
if time.Since(lastOffline) < streamOfflineGrace || lastOnline.After(lastOffline) {
|
||||
// We only had a short break or the stream was already started
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = tx.Model(&StreakUser{}).
|
||||
Where("streak_status = ?", StatusPending).
|
||||
Update("streak_status", StatusBroken).
|
||||
Error; err != nil {
|
||||
return fmt.Errorf("breaking streaks for pending users: %w", err)
|
||||
}
|
||||
|
||||
if err = tx.Model(&StreakUser{}).
|
||||
Where("streak_status = ?", StatusActive).
|
||||
Update("streak_status", StatusPending).
|
||||
Error; err != nil {
|
||||
return fmt.Errorf("breaking streaks for pending users: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return fmt.Errorf("starting stream: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d DB) getTimeFromMeta(db *gorm.DB, key string) (t time.Time, err error) {
|
||||
var meta StreakMeta
|
||||
if err = db.First(&meta, "name = ?", key).Error; err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return t, fmt.Errorf("getting last %s time: %w", key, err)
|
||||
}
|
||||
|
||||
meta.Value = defaultMetaTime.Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
t, err = time.Parse(time.RFC3339Nano, meta.Value)
|
||||
if err != nil {
|
||||
return t, fmt.Errorf("parsing %s time: %w", key, err)
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (d DB) storeTimeToMeta(db *gorm.DB, key string, t time.Time) (err error) {
|
||||
if err = db.Save(&StreakMeta{
|
||||
Name: key,
|
||||
Value: t.Format(time.RFC3339Nano),
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("updating stream meta: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
168
pkg/database/query_test.go
Normal file
168
pkg/database/query_test.go
Normal file
|
@ -0,0 +1,168 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testStreamOfflineGrace = 30 * time.Second
|
||||
|
||||
func TestCountStreak(t *testing.T) {
|
||||
db, err := New("sqlite", "file::memory:?cache=shared")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { assert.NoError(t, db.Close()) })
|
||||
|
||||
err = db.StartStream(testStreamOfflineGrace)
|
||||
require.NoError(t, err)
|
||||
|
||||
// First time the user registers
|
||||
user, err := db.CountStreak(1, "test")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint64(1), user.TwitchID)
|
||||
assert.Equal(t, "test", user.Username)
|
||||
assert.Equal(t, uint64(1), user.StreamsCount)
|
||||
assert.Equal(t, uint64(1), user.MaxStreak)
|
||||
assert.Equal(t, uint64(1), user.CurrentStreak)
|
||||
assert.Equal(t, StatusActive, user.StreakStatus)
|
||||
|
||||
// Register on the same stream should not change anything
|
||||
user, err = db.CountStreak(1, "test")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, uint64(1), user.StreamsCount)
|
||||
assert.Equal(t, uint64(1), user.MaxStreak)
|
||||
assert.Equal(t, uint64(1), user.CurrentStreak)
|
||||
assert.Equal(t, StatusActive, user.StreakStatus)
|
||||
|
||||
// Interrupt the stream for less than the grace
|
||||
err = db.SetStreamOffline()
|
||||
require.NoError(t, err)
|
||||
err = db.StartStream(testStreamOfflineGrace)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Streak should still be active
|
||||
err = db.db.First(&user).Error
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, StatusActive, user.StreakStatus)
|
||||
|
||||
// Register on the same stream should not change anything
|
||||
user, err = db.CountStreak(1, "test")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, uint64(1), user.StreamsCount)
|
||||
assert.Equal(t, uint64(1), user.MaxStreak)
|
||||
assert.Equal(t, uint64(1), user.CurrentStreak)
|
||||
assert.Equal(t, StatusActive, user.StreakStatus)
|
||||
|
||||
// Interrupt the stream and start a new one
|
||||
err = db.SetStreamOffline()
|
||||
require.NoError(t, err)
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
err = db.StartStream(10 * time.Millisecond)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Streak should now be pending
|
||||
err = db.db.First(&user).Error
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, StatusPending, user.StreakStatus)
|
||||
|
||||
// Register on the next stream should not break the streak
|
||||
user, err = db.CountStreak(1, "test")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, uint64(2), user.StreamsCount)
|
||||
assert.Equal(t, uint64(2), user.MaxStreak)
|
||||
assert.Equal(t, uint64(2), user.CurrentStreak)
|
||||
assert.Equal(t, StatusActive, user.StreakStatus)
|
||||
|
||||
// Interrupt the stream and start a new one
|
||||
err = db.SetStreamOffline()
|
||||
require.NoError(t, err)
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
err = db.StartStream(10 * time.Millisecond)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Streak should now be pending
|
||||
err = db.db.First(&user).Error
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, StatusPending, user.StreakStatus)
|
||||
|
||||
// Interrupt the stream and start a new one (twice)
|
||||
err = db.SetStreamOffline()
|
||||
require.NoError(t, err)
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
err = db.StartStream(10 * time.Millisecond)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Streak should now be broken
|
||||
err = db.db.First(&user).Error
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, StatusBroken, user.StreakStatus)
|
||||
|
||||
// Register with one stream left out should break the streak
|
||||
user, err = db.CountStreak(1, "test")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, uint64(3), user.StreamsCount)
|
||||
assert.Equal(t, uint64(2), user.MaxStreak)
|
||||
assert.Equal(t, uint64(1), user.CurrentStreak)
|
||||
assert.Equal(t, StatusActive, user.StreakStatus)
|
||||
}
|
||||
|
||||
func TestMetaTimestore(t *testing.T) {
|
||||
db, err := New("sqlite", "file::memory:?cache=shared")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { assert.NoError(t, db.Close()) })
|
||||
|
||||
parsed, err := db.getTimeFromMeta(db.db, "test")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, defaultMetaTime.Equal(parsed))
|
||||
|
||||
now := time.Now()
|
||||
|
||||
err = db.storeTimeToMeta(db.db, "test", now)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var meta StreakMeta
|
||||
err = db.db.First(&meta, "name = ?", "test").Error
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test", meta.Name)
|
||||
assert.Equal(t, now.Format(time.RFC3339Nano), meta.Value)
|
||||
|
||||
parsed, err = db.getTimeFromMeta(db.db, "test")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, now.Equal(parsed))
|
||||
}
|
||||
|
||||
func TestStreamOffline(t *testing.T) {
|
||||
db, err := New("sqlite", "file::memory:?cache=shared")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { assert.NoError(t, db.Close()) })
|
||||
|
||||
parsed, err := db.getTimeFromMeta(db.db, "stream_offline")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, defaultMetaTime.Equal(parsed))
|
||||
|
||||
err = db.SetStreamOffline()
|
||||
require.NoError(t, err)
|
||||
|
||||
parsed, err = db.getTimeFromMeta(db.db, "stream_offline")
|
||||
assert.NoError(t, err)
|
||||
assert.Less(t, time.Since(parsed), testStreamOfflineGrace)
|
||||
}
|
||||
|
||||
func TestStreamOnline(t *testing.T) {
|
||||
db, err := New("sqlite", "file::memory:?cache=shared")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { assert.NoError(t, db.Close()) })
|
||||
|
||||
parsed, err := db.getTimeFromMeta(db.db, "stream_online")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, defaultMetaTime.Equal(parsed))
|
||||
|
||||
err = db.StartStream(testStreamOfflineGrace)
|
||||
require.NoError(t, err)
|
||||
|
||||
parsed, err = db.getTimeFromMeta(db.db, "stream_online")
|
||||
assert.NoError(t, err)
|
||||
assert.Less(t, time.Since(parsed), testStreamOfflineGrace)
|
||||
}
|
25
pkg/database/struct.go
Normal file
25
pkg/database/struct.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package database
|
||||
|
||||
type (
|
||||
StreakMeta struct {
|
||||
Name string `gorm:"primaryKey"`
|
||||
Value string
|
||||
}
|
||||
|
||||
StreakUser struct {
|
||||
TwitchID uint64 `gorm:"primaryKey"`
|
||||
Username string
|
||||
StreamsCount uint64
|
||||
CurrentStreak uint64
|
||||
MaxStreak uint64
|
||||
StreakStatus Status
|
||||
}
|
||||
|
||||
Status string
|
||||
)
|
||||
|
||||
const (
|
||||
StatusBroken Status = "broken" // Streak is broken and must be started anew
|
||||
StatusPending Status = "pending" // Streak is pending to be broken and can be continued
|
||||
StatusActive Status = "active" // Streak is active (renewed in current stream)
|
||||
)
|
163
query.go
163
query.go
|
@ -1,163 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
const streamOfflineGrace = 30 * time.Minute
|
||||
|
||||
func countStreak(db *sqlx.DB, twitchID uint64, username string) (user streakUser, err error) {
|
||||
if err = withTx(db, func(tx *sqlx.Tx) (err error) {
|
||||
if err = tx.Get(&user, "SELECT * FROM streak_users WHERE twitch_id = ?", twitchID); err != nil {
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return fmt.Errorf("getting user: %w", err)
|
||||
}
|
||||
|
||||
// User was not yet inserted
|
||||
user = streakUser{
|
||||
TwitchID: twitchID,
|
||||
Username: username,
|
||||
StreamsCount: 0,
|
||||
CurrentStreak: 0,
|
||||
MaxStreak: 0,
|
||||
StreakStatus: statusBroken,
|
||||
}
|
||||
}
|
||||
|
||||
switch user.StreakStatus {
|
||||
case statusActive:
|
||||
// User has an active streak, do nothing
|
||||
return nil
|
||||
|
||||
case statusBroken:
|
||||
// User needs a new streak
|
||||
user.CurrentStreak = 1
|
||||
|
||||
case statusPending:
|
||||
// User can prolong their streak
|
||||
user.CurrentStreak += 1
|
||||
}
|
||||
|
||||
// In any case set the streak active and count the current stream
|
||||
user.StreamsCount++
|
||||
user.StreakStatus = statusActive
|
||||
if user.CurrentStreak > user.MaxStreak {
|
||||
user.MaxStreak = user.CurrentStreak
|
||||
}
|
||||
|
||||
if _, err = db.NamedExec(
|
||||
`INSERT INTO streak_users VALUES (:twitch_id, :username, :streams_count, :current_streak, :max_streak, :streak_status)
|
||||
ON DUPLICATE KEY UPDATE username=:username, streams_count=:streams_count, current_streak=:current_streak, max_streak=:max_streak, streak_status=:streak_status`,
|
||||
user,
|
||||
); err != nil {
|
||||
return fmt.Errorf("updating user streak status: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return user, fmt.Errorf("counting streak for user: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func getTimeFromMeta(tx *sqlx.Tx, key string) (t time.Time, err error) {
|
||||
var lastOfflineStr string
|
||||
if err = tx.Get(&lastOfflineStr, "SELECT value FROM streak_meta WHERE `key` = ?", key); err != nil {
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return t, fmt.Errorf("getting last %s time: %w", key, err)
|
||||
}
|
||||
|
||||
lastOfflineStr = time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC).Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
t, err = time.Parse(time.RFC3339Nano, lastOfflineStr)
|
||||
if err != nil {
|
||||
return t, fmt.Errorf("parsing offline time: %w", err)
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func setStreamOffline(db *sqlx.DB) (err error) {
|
||||
return withTx(db, func(tx *sqlx.Tx) error {
|
||||
return storeTimeToMeta(tx, "stream_offline", time.Now())
|
||||
})
|
||||
}
|
||||
|
||||
func startStream(db *sqlx.DB) (err error) {
|
||||
if err = withTx(db, func(tx *sqlx.Tx) (err error) {
|
||||
lastOffline, err := getTimeFromMeta(tx, "stream_offline")
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting offline time: %w", err)
|
||||
}
|
||||
|
||||
lastOnline, err := getTimeFromMeta(tx, "stream_online")
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting online time: %w", err)
|
||||
}
|
||||
|
||||
if err = storeTimeToMeta(tx, "stream_online", time.Now()); err != nil {
|
||||
return fmt.Errorf("storing stream start: %w", err)
|
||||
}
|
||||
|
||||
if time.Since(lastOffline) < streamOfflineGrace || lastOnline.After(lastOffline) {
|
||||
// We only had a short break or the stream was already started
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err = tx.Exec("UPDATE streak_users SET streak_status = ?, current_streak = 0 WHERE streak_status = ?", statusBroken, statusPending); err != nil {
|
||||
return fmt.Errorf("breaking streaks for pending users: %w", err)
|
||||
}
|
||||
|
||||
if _, err = tx.Exec("UPDATE streak_users SET streak_status = ? WHERE streak_status = ?", statusPending, statusActive); err != nil {
|
||||
return fmt.Errorf("breaking streaks for pending users: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return fmt.Errorf("starting stream: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func storeTimeToMeta(tx *sqlx.Tx, key string, t time.Time) (err error) {
|
||||
if _, err = tx.NamedExec(
|
||||
`INSERT INTO streak_meta VALUES (:key, :value)
|
||||
ON DUPLICATE KEY UPDATE value = :value`,
|
||||
map[string]any{
|
||||
"key": key,
|
||||
"value": t.Format(time.RFC3339Nano),
|
||||
},
|
||||
); err != nil {
|
||||
return fmt.Errorf("updating stream meta: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func withTx(db *sqlx.DB, fn func(*sqlx.Tx) error) error {
|
||||
tx, err := db.Beginx()
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting transaction: %w", err)
|
||||
}
|
||||
|
||||
if err = fn(tx); err != nil {
|
||||
if rerr := tx.Rollback(); rerr != nil {
|
||||
return fmt.Errorf("rolling back after error: %w", rerr)
|
||||
}
|
||||
return fmt.Errorf("executing transaction (rolled back): %w", err)
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return fmt.Errorf("committing transaction: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
15
schema.sql
15
schema.sql
|
@ -1,15 +0,0 @@
|
|||
CREATE TABLE `streak_meta` (
|
||||
`key` varchar(255) NOT NULL,
|
||||
`value` varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY (`key`)
|
||||
) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `streak_users` (
|
||||
`twitch_id` bigint(20) unsigned NOT NULL,
|
||||
`username` varchar(255) NOT NULL,
|
||||
`streams_count` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`current_streak` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`max_streak` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`streak_status` enum('broken','pending','active') NOT NULL DEFAULT 'broken',
|
||||
PRIMARY KEY (`twitch_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
25
struct.go
25
struct.go
|
@ -1,25 +0,0 @@
|
|||
package main
|
||||
|
||||
type (
|
||||
botResponse struct {
|
||||
Type string `json:"type"`
|
||||
Attributes map[string]any `json:"attributes"`
|
||||
}
|
||||
|
||||
streakUser struct {
|
||||
TwitchID uint64 `db:"twitch_id"`
|
||||
Username string `db:"username"`
|
||||
StreamsCount uint64 `db:"streams_count"`
|
||||
CurrentStreak uint64 `db:"current_streak"`
|
||||
MaxStreak uint64 `db:"max_streak"`
|
||||
StreakStatus status `db:"streak_status"`
|
||||
}
|
||||
|
||||
status string
|
||||
)
|
||||
|
||||
const (
|
||||
statusBroken status = "broken" // Streak is broken and must be started anew
|
||||
statusPending status = "pending" // Streak is pending to be broken and can be continued
|
||||
statusActive status = "active" // Streak is active (renewed in current stream)
|
||||
)
|
Loading…
Reference in a new issue