Compare commits
10 commits
8a439c81a7
...
4d7d14e0b5
Author | SHA1 | Date | |
---|---|---|---|
4d7d14e0b5 | |||
d59b1c2d3b | |||
c09e3a5988 | |||
f0f31a8415 | |||
65fc1ccad4 | |||
8b5673434a | |||
17db60dea1 | |||
601747352a | |||
5352ef8434 | |||
22d0e095e3 |
26 changed files with 1664 additions and 23 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
||||||
tokens/*token
|
tokens/*token
|
||||||
|
tokens/*logins
|
||||||
bin/*
|
bin/*
|
||||||
!./bin/.gitkeep
|
!./bin/.gitkeep
|
||||||
!./bin/*
|
!./bin/*
|
||||||
|
|
52
changelog.md
52
changelog.md
|
@ -1,5 +1,57 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 5.1.0 - 2020-03-18
|
||||||
|
### Added
|
||||||
|
- PetCat twitch chat handler
|
||||||
|
- DailyEmote twitch chat handler
|
||||||
|
|
||||||
|
## 5.0.1 - 2020-03-08
|
||||||
|
### Changed
|
||||||
|
- Updated `go-twitch-irc` lib to v2.2.2
|
||||||
|
|
||||||
|
|
||||||
|
## 5.0.0 - 2020-01-11
|
||||||
|
### Added
|
||||||
|
- Twitch point song request feature with dedicated web server and twitch
|
||||||
|
chat handler
|
||||||
|
- Mentions of `!galched` command in bot messages
|
||||||
|
- Chiki-briki song for discord bot
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Typos in the bot messages
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated `go-twitch-irc` lib to v2.2.1
|
||||||
|
|
||||||
|
## 4.2.0 - 2019-10-20
|
||||||
|
### Added
|
||||||
|
- Universal song handler with polka and sax commands
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated discordgo library up to v0.20.1
|
||||||
|
|
||||||
|
## 4.1.0 - 2019-07-28
|
||||||
|
### Added
|
||||||
|
- Song player handler
|
||||||
|
|
||||||
|
## 4.0.2 - 2019-07-28
|
||||||
|
### Changed
|
||||||
|
- Info about 7th subday
|
||||||
|
|
||||||
|
## 4.0.1 - 2019-07-22
|
||||||
|
### Changed
|
||||||
|
- Twitch chat library version from v1 to v2
|
||||||
|
|
||||||
|
## 4.0.0 - 2019-06-23
|
||||||
|
### Added
|
||||||
|
- Twitch chat module
|
||||||
|
- Twitch chat handler that calculates duplicates in chat
|
||||||
|
|
||||||
|
## 3.0.2 - 2019-06-03
|
||||||
|
### Added
|
||||||
|
- Command !subhistory
|
||||||
|
- Info about 5th and 6th subday
|
||||||
|
|
||||||
## 3.0.1 - 2019-05-10
|
## 3.0.1 - 2019-05-10
|
||||||
### Added
|
### Added
|
||||||
- Readme file
|
- Readme file
|
||||||
|
|
11
go.mod
11
go.mod
|
@ -1,12 +1,19 @@
|
||||||
module galched-bot
|
module galched-bot
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bwmarrin/discordgo v0.19.0
|
github.com/bwmarrin/discordgo v0.20.1
|
||||||
|
github.com/gempir/go-twitch-irc/v2 v2.2.2
|
||||||
|
github.com/gorilla/websocket v1.4.1 // indirect
|
||||||
github.com/pkg/errors v0.8.1
|
github.com/pkg/errors v0.8.1
|
||||||
github.com/stretchr/testify v1.3.0 // indirect
|
github.com/stretchr/testify v1.3.0 // indirect
|
||||||
go.uber.org/atomic v1.3.2 // indirect
|
go.uber.org/atomic v1.3.2
|
||||||
go.uber.org/dig v1.7.0 // indirect
|
go.uber.org/dig v1.7.0 // indirect
|
||||||
go.uber.org/fx v1.9.0
|
go.uber.org/fx v1.9.0
|
||||||
go.uber.org/goleak v0.10.0 // indirect
|
go.uber.org/goleak v0.10.0 // indirect
|
||||||
go.uber.org/multierr v1.1.0 // indirect
|
go.uber.org/multierr v1.1.0 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20191018095205-727590c5006e // indirect
|
||||||
|
google.golang.org/api v0.15.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
97
go.sum
97
go.sum
|
@ -1,9 +1,39 @@
|
||||||
github.com/bwmarrin/discordgo v0.19.0 h1:kMED/DB0NR1QhRcalb85w0Cu3Ep2OrGAqZH1R5awQiY=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
github.com/bwmarrin/discordgo v0.19.0/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q=
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo=
|
||||||
|
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/bwmarrin/discordgo v0.20.1 h1:Ihh3/mVoRwy3otmaoPDUioILBJq4fdWkpsi83oj2Lmk=
|
||||||
|
github.com/bwmarrin/discordgo v0.20.1/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gempir/go-twitch-irc/v2 v2.2.1 h1:jMiEgw6zzrgiz4viG7lgj148J6enLls5aicF+zsi1bk=
|
||||||
|
github.com/gempir/go-twitch-irc/v2 v2.2.1/go.mod h1:0HXoEr9l7gNjwajosptV0w0xGpHeU6gsD7JDlfvjTYI=
|
||||||
|
github.com/gempir/go-twitch-irc/v2 v2.2.2 h1:uzinel2qApXL1UVfr3QcZ3dJsf+YU+PaUp0qJk03qNo=
|
||||||
|
github.com/gempir/go-twitch-irc/v2 v2.2.2/go.mod h1:0HXoEr9l7gNjwajosptV0w0xGpHeU6gsD7JDlfvjTYI=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.2.0/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 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||||
|
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
@ -11,6 +41,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg=
|
||||||
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
|
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
|
||||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/dig v1.7.0 h1:E5/L92iQTNJTjfgJF2KgU+/JpMaiuvK2DHLBj0+kSZk=
|
go.uber.org/dig v1.7.0 h1:E5/L92iQTNJTjfgJF2KgU+/JpMaiuvK2DHLBj0+kSZk=
|
||||||
|
@ -23,3 +55,64 @@ go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
|
||||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
|
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
|
||||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
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-20190108225652-1e06a53dbb7e/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-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw=
|
||||||
|
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
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/sys v0.0.0-20180830151530-49385e6e1522/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-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191018095205-727590c5006e h1:ZtoklVMHQy6BFRHkbG6JzK+S6rX82//Yeok1vMlizfQ=
|
||||||
|
golang.org/x/sys v0.0.0-20191018095205-727590c5006e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
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-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||||
|
google.golang.org/api v0.15.0 h1:yzlyyDW/J0w8yNFJIhiAJy4kq74S+1DOLdawELNxFMA=
|
||||||
|
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
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.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg=
|
||||||
|
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU=
|
||||||
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
|
42
main.go
42
main.go
|
@ -4,12 +4,16 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
|
"go.uber.org/fx"
|
||||||
|
|
||||||
"galched-bot/modules/discord"
|
"galched-bot/modules/discord"
|
||||||
"galched-bot/modules/grace"
|
"galched-bot/modules/grace"
|
||||||
|
"galched-bot/modules/patpet"
|
||||||
"galched-bot/modules/settings"
|
"galched-bot/modules/settings"
|
||||||
"galched-bot/modules/subday"
|
"galched-bot/modules/subday"
|
||||||
|
"galched-bot/modules/twitchat"
|
||||||
"go.uber.org/fx"
|
"galched-bot/modules/web"
|
||||||
|
"galched-bot/modules/youtube"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -21,6 +25,8 @@ type (
|
||||||
Context context.Context
|
Context context.Context
|
||||||
Discord *discord.Discord
|
Discord *discord.Discord
|
||||||
Settings *settings.Settings
|
Settings *settings.Settings
|
||||||
|
Chat *twitchat.TwitchIRC
|
||||||
|
Server *web.WebServer
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -35,14 +41,39 @@ func start(p appParam) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print("discord: cannot start instance", err)
|
log.Print("discord: cannot start instance", err)
|
||||||
return err
|
return err
|
||||||
|
|
||||||
}
|
}
|
||||||
log.Printf("main: discord instance running")
|
log.Printf("main: discord instance running")
|
||||||
log.Printf("main: — — —")
|
|
||||||
|
|
||||||
|
err = p.Chat.Start()
|
||||||
|
if err != nil {
|
||||||
|
log.Print("chat: cannot start instance", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("main: twitch chat instance running")
|
||||||
|
|
||||||
|
err = p.Server.Start()
|
||||||
|
if err != nil {
|
||||||
|
log.Print("web: cannot start instance", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("main: web server instance running")
|
||||||
|
|
||||||
|
log.Printf("main: — — —")
|
||||||
<-p.Context.Done()
|
<-p.Context.Done()
|
||||||
log.Print("main: stopping galched-bot")
|
log.Print("main: stopping galched-bot")
|
||||||
|
|
||||||
|
err = p.Server.Stop(p.Context)
|
||||||
|
if err != nil {
|
||||||
|
log.Print("web: cannot stop instance", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.Chat.Stop()
|
||||||
|
if err != nil {
|
||||||
|
log.Print("chat: cannot stop instance", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err = p.Discord.Stop()
|
err = p.Discord.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print("discord: cannot stop instance", err)
|
log.Print("discord: cannot stop instance", err)
|
||||||
|
@ -57,7 +88,8 @@ func main() {
|
||||||
var err error
|
var err error
|
||||||
app := fx.New(
|
app := fx.New(
|
||||||
fx.Logger(new(silentPrinter)),
|
fx.Logger(new(silentPrinter)),
|
||||||
fx.Provide(settings.New, grace.New, discord.New, subday.New),
|
fx.Provide(settings.New, grace.New, discord.New, subday.New,
|
||||||
|
twitchat.New, web.New, youtube.New, patpet.New),
|
||||||
fx.Invoke(start))
|
fx.Invoke(start))
|
||||||
|
|
||||||
err = app.Start(context.Background())
|
err = app.Start(context.Background())
|
||||||
|
|
|
@ -31,6 +31,10 @@ func New(s *settings.Settings, subday *subday.Subday) (*Discord, error) {
|
||||||
processor.AddHandler(subdayHandler)
|
processor.AddHandler(subdayHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, songHandler := range SongHandlers(s) {
|
||||||
|
processor.AddHandler(songHandler)
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf("discord: added %d message handlers", len(processor.handlers))
|
log.Printf("discord: added %d message handlers", len(processor.handlers))
|
||||||
if len(processor.handlers) > 0 {
|
if len(processor.handlers) > 0 {
|
||||||
for i := range processor.handlers {
|
for i := range processor.handlers {
|
||||||
|
|
|
@ -53,6 +53,7 @@ func (h *HandlerProcessor) Process(s *discordgo.Session, m *discordgo.MessageCre
|
||||||
if strings.HasPrefix(m.Content, "!galched") {
|
if strings.HasPrefix(m.Content, "!galched") {
|
||||||
LogMessage(m)
|
LogMessage(m)
|
||||||
SendMessage(s, m, h.HelpMessage())
|
SendMessage(s, m, h.HelpMessage())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range h.handlers {
|
for i := range h.handlers {
|
||||||
|
|
191
modules/discord/songhandlers.go
Normal file
191
modules/discord/songhandlers.go
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
package discord
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bwmarrin/discordgo"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"go.uber.org/atomic"
|
||||||
|
|
||||||
|
"galched-bot/modules/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
songData [][]byte
|
||||||
|
|
||||||
|
SongHandler struct {
|
||||||
|
globalLock *atomic.Bool
|
||||||
|
songLock *atomic.Bool
|
||||||
|
|
||||||
|
song songData
|
||||||
|
signature string
|
||||||
|
description string
|
||||||
|
voiceChannel string
|
||||||
|
permissions []string
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *SongHandler) Signature() string {
|
||||||
|
return h.signature
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SongHandler) Description() string {
|
||||||
|
return h.description
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SongHandler) IsValid(msg string) bool {
|
||||||
|
return msg == h.signature
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SongHandler) Handle(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||||
|
var permitted bool
|
||||||
|
for i := range h.permissions {
|
||||||
|
if m.Author.Username == h.permissions[i] {
|
||||||
|
permitted = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(h.permissions) > 0 && !permitted {
|
||||||
|
log.Printf("discord: unathorized %s message from %s",
|
||||||
|
h.signature, m.Author.Username)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the channel that the message came from.
|
||||||
|
c, err := s.State.Channel(m.ChannelID)
|
||||||
|
if err != nil {
|
||||||
|
// Could not find channel.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the guild for that channel.
|
||||||
|
g, err := s.State.Guild(c.GuildID)
|
||||||
|
if err != nil {
|
||||||
|
// Could not find guild.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for the message sender in that guild's current voice states.
|
||||||
|
LogMessage(m)
|
||||||
|
if h.globalLock.CAS(false, true) {
|
||||||
|
if h.songLock.CAS(false, true) {
|
||||||
|
|
||||||
|
err = playSound(s, g.ID, h.voiceChannel, h.song)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("discord: error playing sound:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.globalLock.Store(false)
|
||||||
|
time.Sleep(h.timeout)
|
||||||
|
defer h.songLock.Store(false)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.globalLock.Store(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SongHandlers(s *settings.Settings) []MessageHandler {
|
||||||
|
result := make([]MessageHandler, 0, len(s.Songs))
|
||||||
|
g := new(atomic.Bool)
|
||||||
|
|
||||||
|
for i := range s.Songs {
|
||||||
|
song, err := loadSong(s.Songs[i].Path)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("discord: error loading song file", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
handler := &SongHandler{
|
||||||
|
globalLock: g,
|
||||||
|
songLock: new(atomic.Bool),
|
||||||
|
song: song,
|
||||||
|
signature: s.Songs[i].Signature,
|
||||||
|
description: s.Songs[i].Description,
|
||||||
|
voiceChannel: s.DiscordVoiceChannel,
|
||||||
|
permissions: s.Songs[i].Permissions,
|
||||||
|
timeout: s.Songs[i].Timeout,
|
||||||
|
}
|
||||||
|
result = append(result, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// playSound plays the current buffer to the provided channel.
|
||||||
|
func playSound(s *discordgo.Session, guildID, channelID string, song songData) error {
|
||||||
|
|
||||||
|
// Join the provided voice channel.
|
||||||
|
vc, err := s.ChannelVoiceJoin(guildID, channelID, false, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sleep for a specified amount of time before playing the sound
|
||||||
|
time.Sleep(250 * time.Millisecond)
|
||||||
|
|
||||||
|
// Start speaking.
|
||||||
|
vc.Speaking(true)
|
||||||
|
|
||||||
|
// Send the buffer data.
|
||||||
|
for _, buff := range song {
|
||||||
|
vc.OpusSend <- buff
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop speaking
|
||||||
|
vc.Speaking(false)
|
||||||
|
|
||||||
|
// Sleep for a specified amount of time before ending.
|
||||||
|
time.Sleep(250 * time.Millisecond)
|
||||||
|
|
||||||
|
// Disconnect from the provided voice channel.
|
||||||
|
vc.Disconnect()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadSong(path string) (songData, error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error opening dca file")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
opuslen int16
|
||||||
|
buffer = make(songData, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
// Read opus frame length from dca file.
|
||||||
|
err = binary.Read(file, binary.LittleEndian, &opuslen)
|
||||||
|
|
||||||
|
// If this is the end of the file, just return.
|
||||||
|
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
||||||
|
err := file.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buffer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error reading from dca file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read encoded pcm from dca file.
|
||||||
|
InBuf := make([]byte, opuslen)
|
||||||
|
err = binary.Read(file, binary.LittleEndian, &InBuf)
|
||||||
|
|
||||||
|
// Should not be any end of file errors
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error reading from dca file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append encoded pcm data to the buffer.
|
||||||
|
buffer = append(buffer, InBuf)
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,10 +38,7 @@ func (h *SubdayListHandler) Handle(s *discordgo.Session, m *discordgo.MessageCre
|
||||||
log.Print("discord: cannot obtain guild", err)
|
log.Print("discord: cannot obtain guild", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
message := "Игры предыдущих сабдеев:\n**20.10.18**: _DmC_ -> _Fable 1_ -> _Overcooked 2_\n" +
|
message := "Игры предыдущих сабдеев доступны по команде **!subhistory**\n" +
|
||||||
"**17.11.18**: _The Witcher_ -> _Xenus: Белое Золото_ -> _NFS: Underground 2_\n" +
|
|
||||||
"**22.12.18**: _True Crime: Streets of LA_ -> _Serious Sam 3_ -> _Kholat_\n" +
|
|
||||||
"**26.01.19**: _Disney’s Aladdin_ -> _~~Gothic~~_ -> _Scrapland_ -> _Donut County_\n\n" +
|
|
||||||
"Список игр для следующего сабдея:\n"
|
"Список игр для следующего сабдея:\n"
|
||||||
for k, v := range h.subday.Database() {
|
for k, v := range h.subday.Database() {
|
||||||
nickname := " "
|
nickname := " "
|
||||||
|
@ -58,6 +55,7 @@ func (h *SubdayListHandler) Handle(s *discordgo.Session, m *discordgo.MessageCre
|
||||||
message += fmt.Sprintf(" **- %s** от _%s_\n", game, nickname)
|
message += fmt.Sprintf(" **- %s** от _%s_\n", game, nickname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
message += "\nВсе команды бота: !galched\n"
|
||||||
SendMessage(s, m, strings.Trim(message, "\n"))
|
SendMessage(s, m, strings.Trim(message, "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,10 +136,35 @@ loop:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SubdayHistoryHandler struct{}
|
||||||
|
|
||||||
|
func (h *SubdayHistoryHandler) Signature() string {
|
||||||
|
return "!subhistory"
|
||||||
|
}
|
||||||
|
func (h *SubdayHistoryHandler) Description() string {
|
||||||
|
return "история прошлых сабдеев"
|
||||||
|
}
|
||||||
|
func (h *SubdayHistoryHandler) IsValid(msg string) bool {
|
||||||
|
return strings.HasPrefix(msg, "!subhistory")
|
||||||
|
}
|
||||||
|
func (h *SubdayHistoryHandler) Handle(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||||
|
LogMessage(m)
|
||||||
|
message := "Игры предыдущих сабдеев:\n**20.10.18**: _DmC_ -> _Fable 1_ -> _Overcooked 2_\n" +
|
||||||
|
"**17.11.18**: _The Witcher_ -> _Xenus: Белое Золото_ -> _NFS: Underground 2_\n" +
|
||||||
|
"**22.12.18**: _True Crime: Streets of LA_ -> _Serious Sam 3_ -> _Kholat_\n" +
|
||||||
|
"**26.01.19**: _Disney’s Aladdin_ -> _~~Gothic~~_ -> _Scrapland_ -> _Donut County_\n" +
|
||||||
|
"**24.02.19**: _Tetris 99_ -> _~~Bully~~_ -> _~~GTA: Vice City~~_\n" +
|
||||||
|
"**02.06.19**: _Spec Ops: The Line_ -> _Escape from Tarkov_\n" +
|
||||||
|
"**28.07.19**: _Crypt of the Necrodancer_ -> _My Friend Pedro_ -> _Ape Out_\n" +
|
||||||
|
"\nВсе команды бота: !galched\n"
|
||||||
|
SendMessage(s, m, message)
|
||||||
|
}
|
||||||
|
|
||||||
func SubdayHandlers(s *subday.Subday, r []string) []MessageHandler {
|
func SubdayHandlers(s *subday.Subday, r []string) []MessageHandler {
|
||||||
var result []MessageHandler
|
var result []MessageHandler
|
||||||
|
|
||||||
addHandler := &SubdayAddHandler{s, r}
|
addHandler := &SubdayAddHandler{s, r}
|
||||||
listHandler := &SubdayListHandler{s}
|
listHandler := &SubdayListHandler{s}
|
||||||
return append(result, addHandler, listHandler)
|
histHandler := new(SubdayHistoryHandler)
|
||||||
|
return append(result, addHandler, listHandler, histHandler)
|
||||||
}
|
}
|
||||||
|
|
88
modules/patpet/patpet.go
Normal file
88
modules/patpet/patpet.go
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
package patpet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"galched-bot/modules/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Pet struct {
|
||||||
|
sync.RWMutex
|
||||||
|
path string
|
||||||
|
counter int
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(s *settings.Settings) (*Pet, error) {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
counter int
|
||||||
|
)
|
||||||
|
|
||||||
|
petData, err := ioutil.ReadFile(s.PetDataPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Print("pet: cannot read data file", err)
|
||||||
|
log.Print("pet: creating new counter")
|
||||||
|
} else {
|
||||||
|
err = json.Unmarshal(petData, &counter)
|
||||||
|
if err != nil {
|
||||||
|
counter = 0
|
||||||
|
log.Print("pet: cannot unmarshal data file", err)
|
||||||
|
log.Print("pet: creating new counter")
|
||||||
|
} else {
|
||||||
|
log.Print("pet: using previously saved counter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Pet{
|
||||||
|
RWMutex: sync.RWMutex{},
|
||||||
|
path: s.PetDataPath,
|
||||||
|
counter: counter,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pet) Pet() int {
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
|
||||||
|
p.counter++
|
||||||
|
return p.counter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pet) Counter() int {
|
||||||
|
p.RUnlock()
|
||||||
|
defer p.RUnlock()
|
||||||
|
|
||||||
|
return p.counter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pet) Dump() {
|
||||||
|
p.RLock()
|
||||||
|
defer p.RUnlock()
|
||||||
|
|
||||||
|
data, err := json.Marshal(p.counter)
|
||||||
|
if err != nil {
|
||||||
|
log.Print("pet: cannot marshal counter", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file, err := os.Create(p.path)
|
||||||
|
if err != nil {
|
||||||
|
log.Print("pet: cannot open counter file", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = fmt.Fprintf(file, string(data))
|
||||||
|
if err != nil {
|
||||||
|
log.Print("pet: cannot write to counter file")
|
||||||
|
}
|
||||||
|
err = file.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Print("pet: cannot close counter file")
|
||||||
|
}
|
||||||
|
log.Print("pet: counter dumped to file:", p.counter)
|
||||||
|
}
|
|
@ -1,28 +1,56 @@
|
||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
version = "3.0.1"
|
version = "5.1.0"
|
||||||
|
twitchUser = "galchedbot"
|
||||||
|
twitchIRCRoom = "galched"
|
||||||
discordTokenPath = "./tokens/.discordtoken"
|
discordTokenPath = "./tokens/.discordtoken"
|
||||||
|
twitchTokenPath = "./tokens/.twitchtoken"
|
||||||
subdayDataPath = "./backups/subday"
|
subdayDataPath = "./backups/subday"
|
||||||
|
petDataPath = "./backups/pets"
|
||||||
|
youtubeTokenPath = "./tokens/.youtubetoken"
|
||||||
|
webLoginsPath = "./tokens/.weblogins"
|
||||||
|
|
||||||
// Permitted roles in discord for subday
|
// Permitted roles in discord for subday
|
||||||
subRole1 = "433672344737677322"
|
subRole1 = "433672344737677322"
|
||||||
subRole2 = "433680494635515904"
|
subRole2 = "433680494635515904"
|
||||||
galchedRole = "301467455497175041"
|
galchedRole = "301467455497175041"
|
||||||
smorcRole = "301470784491356172"
|
smorcRole = "301470784491356172"
|
||||||
|
|
||||||
|
defaultQueueAddr = ":8888"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
SongInfo struct {
|
||||||
|
Path string
|
||||||
|
Signature string
|
||||||
|
Description string
|
||||||
|
Permissions []string
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
Settings struct {
|
Settings struct {
|
||||||
Version string
|
Version string
|
||||||
DiscordToken string
|
DiscordToken string
|
||||||
SubdayDataPath string
|
TwitchUser string
|
||||||
PermittedRoles []string
|
TwitchIRCRoom string
|
||||||
|
TwitchToken string
|
||||||
|
YoutubeToken string
|
||||||
|
SubdayDataPath string
|
||||||
|
PetDataPath string
|
||||||
|
PermittedRoles []string
|
||||||
|
DiscordVoiceChannel string
|
||||||
|
Songs []SongInfo
|
||||||
|
|
||||||
|
QueueAddress string
|
||||||
|
LoginUsers map[string]string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,11 +59,59 @@ func New() (*Settings, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print("settings: cannot read discord token file", err)
|
log.Print("settings: cannot read discord token file", err)
|
||||||
}
|
}
|
||||||
|
twitchToken, err := ioutil.ReadFile(twitchTokenPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Print("settings: cannot read twitch token file", err)
|
||||||
|
}
|
||||||
|
youtubetoken, err := ioutil.ReadFile(youtubeTokenPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Print("settings: cannot read twitch token file", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
webLogins := make(map[string]string)
|
||||||
|
webLoginsRaw, err := ioutil.ReadFile(webLoginsPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Print("settings: cannot read web login file", err)
|
||||||
|
} else {
|
||||||
|
err = json.Unmarshal(webLoginsRaw, &webLogins)
|
||||||
|
if err != nil {
|
||||||
|
log.Print("settings: cannot parse web login file", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &Settings{
|
return &Settings{
|
||||||
Version: version,
|
Version: version,
|
||||||
DiscordToken: string(discordToken),
|
DiscordToken: string(discordToken),
|
||||||
SubdayDataPath: subdayDataPath,
|
TwitchToken: string(twitchToken),
|
||||||
PermittedRoles: []string{subRole1, subRole2, galchedRole, smorcRole},
|
YoutubeToken: string(youtubetoken),
|
||||||
|
TwitchUser: twitchUser,
|
||||||
|
TwitchIRCRoom: twitchIRCRoom,
|
||||||
|
SubdayDataPath: subdayDataPath,
|
||||||
|
PetDataPath: petDataPath,
|
||||||
|
DiscordVoiceChannel: "301793085522706432",
|
||||||
|
PermittedRoles: []string{subRole1, subRole2, galchedRole, smorcRole},
|
||||||
|
Songs: []SongInfo{
|
||||||
|
{
|
||||||
|
Path: "songs/polka.dca",
|
||||||
|
Signature: "!song",
|
||||||
|
Description: "сыграть гимн галчед (только для избранных)",
|
||||||
|
Permissions: []string{"AlexV", "Rummy_Quamox", "Lidiya_owl"},
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "songs/whisper.dca",
|
||||||
|
Signature: "!sax",
|
||||||
|
Description: "kreygasm",
|
||||||
|
Timeout: 20 * time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "songs/st.dca",
|
||||||
|
Signature: "!chiki",
|
||||||
|
Description: "briki v damki",
|
||||||
|
Timeout: 20 * time.Second,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
QueueAddress: defaultQueueAddr,
|
||||||
|
LoginUsers: webLogins,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
257
modules/twitchat/dailyemote.go
Normal file
257
modules/twitchat/dailyemote.go
Normal file
|
@ -0,0 +1,257 @@
|
||||||
|
package twitchat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gempir/go-twitch-irc/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const emoteMsg = "!emote"
|
||||||
|
|
||||||
|
var (
|
||||||
|
emotes = []string{
|
||||||
|
"4Head",
|
||||||
|
"ANELE",
|
||||||
|
"ArgieB8",
|
||||||
|
"ArsonNoSexy",
|
||||||
|
"AsexualPride",
|
||||||
|
"AsianGlow",
|
||||||
|
"BCWarrior",
|
||||||
|
"BOP",
|
||||||
|
"BabyRage",
|
||||||
|
"BatChest",
|
||||||
|
"BegWan",
|
||||||
|
"BibleThump",
|
||||||
|
"BigBrother",
|
||||||
|
"BigPhish",
|
||||||
|
"BisexualPride",
|
||||||
|
"BlargNaut",
|
||||||
|
"BlessRNG",
|
||||||
|
"BloodTrail",
|
||||||
|
"BrainSlug",
|
||||||
|
"BrokeBack",
|
||||||
|
"BuddhaBar",
|
||||||
|
"CarlSmile",
|
||||||
|
"ChefFrank",
|
||||||
|
"CoolCat",
|
||||||
|
"CoolStoryBob",
|
||||||
|
"CorgiDerp",
|
||||||
|
"CrreamAwk",
|
||||||
|
"CurseLit",
|
||||||
|
"DAESuppy",
|
||||||
|
"DBstyle",
|
||||||
|
"DansGame",
|
||||||
|
"DarkMode",
|
||||||
|
"DatSheffy",
|
||||||
|
"DendiFace",
|
||||||
|
"DogFace",
|
||||||
|
"DoritosChip",
|
||||||
|
"DrinkPurple",
|
||||||
|
"DxCat",
|
||||||
|
"EarthDay",
|
||||||
|
"EleGiggle",
|
||||||
|
"EntropyWins",
|
||||||
|
"FBBlock",
|
||||||
|
"FBCatch",
|
||||||
|
"FBChallenge",
|
||||||
|
"FBPass",
|
||||||
|
"FBPenalty",
|
||||||
|
"FBRun",
|
||||||
|
"FBSpiral",
|
||||||
|
"FBtouchdown",
|
||||||
|
"FUNgineer",
|
||||||
|
"FailFish",
|
||||||
|
"FrankerZ",
|
||||||
|
"FreakinStinkin",
|
||||||
|
"FutureMan",
|
||||||
|
"GayPride",
|
||||||
|
"GenderFluidPride",
|
||||||
|
"GingerPower",
|
||||||
|
"GivePLZ",
|
||||||
|
"GrammarKing",
|
||||||
|
"GreenTeam",
|
||||||
|
"GunRun",
|
||||||
|
"HSCheers",
|
||||||
|
"HSWP",
|
||||||
|
"HassaanChop",
|
||||||
|
"HassanChop",
|
||||||
|
"HeyGuys",
|
||||||
|
"HolidayCookie",
|
||||||
|
"HolidayLog",
|
||||||
|
"HolidayOrnament",
|
||||||
|
"HolidayPresent",
|
||||||
|
"HolidaySanta",
|
||||||
|
"HolidayTree",
|
||||||
|
"HotPokket",
|
||||||
|
"HumbleLife",
|
||||||
|
"IntersexPride",
|
||||||
|
"InuyoFace",
|
||||||
|
"ItsBoshyTime",
|
||||||
|
"JKanStyle",
|
||||||
|
"Jebaited",
|
||||||
|
"JonCarnage",
|
||||||
|
"KAPOW",
|
||||||
|
"Kappa",
|
||||||
|
"KappaClaus",
|
||||||
|
"KappaPride",
|
||||||
|
"KappaRoss",
|
||||||
|
"KappaWealth",
|
||||||
|
"Kappu",
|
||||||
|
"Keepo",
|
||||||
|
"KevinTurtle",
|
||||||
|
"Kippa",
|
||||||
|
"KomodoHype",
|
||||||
|
"KonCha",
|
||||||
|
"Kreygasm",
|
||||||
|
"LUL",
|
||||||
|
"LesbianPride",
|
||||||
|
"MVGame",
|
||||||
|
"Mau5",
|
||||||
|
"MaxLOL",
|
||||||
|
"MercyWing1",
|
||||||
|
"MercyWing2",
|
||||||
|
"MikeHogu",
|
||||||
|
"MingLee",
|
||||||
|
"MorphinTime",
|
||||||
|
"MrDestructoid",
|
||||||
|
"NinjaGrumpy",
|
||||||
|
"NomNom",
|
||||||
|
"NonBinaryPride",
|
||||||
|
"NotATK",
|
||||||
|
"NotLikeThis",
|
||||||
|
"OSFrog",
|
||||||
|
"OhMyDog",
|
||||||
|
"OneHand",
|
||||||
|
"OpieOP",
|
||||||
|
"OptimizePrime",
|
||||||
|
"PJSalt",
|
||||||
|
"PJSugar",
|
||||||
|
"PMSTwin",
|
||||||
|
"PRChase",
|
||||||
|
"PanicVis",
|
||||||
|
"PansexualPride",
|
||||||
|
"PartyHat",
|
||||||
|
"PartyTime",
|
||||||
|
"PeoplesChamp",
|
||||||
|
"PermaSmug",
|
||||||
|
"PicoMause",
|
||||||
|
"PinkMercy",
|
||||||
|
"PipeHype",
|
||||||
|
"PixelBob",
|
||||||
|
"PogChamp",
|
||||||
|
"Poooound",
|
||||||
|
"PopCorn",
|
||||||
|
"PorscheWIN",
|
||||||
|
"PowerUpL",
|
||||||
|
"PowerUpR",
|
||||||
|
"PraiseIt",
|
||||||
|
"PrimeMe",
|
||||||
|
"PunOko",
|
||||||
|
"PunchTrees",
|
||||||
|
"PurpleStar",
|
||||||
|
"RaccAttack",
|
||||||
|
"RalpherZ",
|
||||||
|
"RedCoat",
|
||||||
|
"RedTeam",
|
||||||
|
"ResidentSleeper",
|
||||||
|
"RitzMitz",
|
||||||
|
"RlyTho",
|
||||||
|
"RuleFive",
|
||||||
|
"SMOrc",
|
||||||
|
"SSSsss",
|
||||||
|
"SabaPing",
|
||||||
|
"SeemsGood",
|
||||||
|
"SeriousSloth",
|
||||||
|
"ShadyLulu",
|
||||||
|
"ShazBotstix",
|
||||||
|
"SingsMic",
|
||||||
|
"SingsNote",
|
||||||
|
"SmoocherZ",
|
||||||
|
"SoBayed",
|
||||||
|
"SoonerLater",
|
||||||
|
"Squid1",
|
||||||
|
"Squid2",
|
||||||
|
"Squid3",
|
||||||
|
"Squid4",
|
||||||
|
"StinkyCheese",
|
||||||
|
"StoneLightning",
|
||||||
|
"StrawBeary",
|
||||||
|
"SuperVinlin",
|
||||||
|
"SwiftRage",
|
||||||
|
"TBAngel",
|
||||||
|
"TF2John",
|
||||||
|
"TPFufun",
|
||||||
|
"TPcrunchyroll",
|
||||||
|
"TTours",
|
||||||
|
"TakeNRG",
|
||||||
|
"TearGlove",
|
||||||
|
"TehePelo",
|
||||||
|
"ThankEgg",
|
||||||
|
"TheIlluminati",
|
||||||
|
"TheRinger",
|
||||||
|
"TheTarFu",
|
||||||
|
"TheThing",
|
||||||
|
"ThunBeast",
|
||||||
|
"TinyFace",
|
||||||
|
"TombRaid",
|
||||||
|
"TooSpicy",
|
||||||
|
"TransgenderPride",
|
||||||
|
"TriHard",
|
||||||
|
"TwitchLit",
|
||||||
|
"TwitchRPG",
|
||||||
|
"TwitchSings",
|
||||||
|
"TwitchUnity",
|
||||||
|
"TwitchVotes",
|
||||||
|
"UWot",
|
||||||
|
"UnSane",
|
||||||
|
"UncleNox",
|
||||||
|
"VoHiYo",
|
||||||
|
"VoteNay",
|
||||||
|
"VoteYea",
|
||||||
|
"WTRuck",
|
||||||
|
"WholeWheat",
|
||||||
|
"WutFace",
|
||||||
|
"YouDontSay",
|
||||||
|
"YouWHY",
|
||||||
|
"bleedPurple",
|
||||||
|
"cmonBruh",
|
||||||
|
"copyThis",
|
||||||
|
"duDudu",
|
||||||
|
"imGlitch",
|
||||||
|
"mcaT",
|
||||||
|
"panicBasket",
|
||||||
|
"pastaThat",
|
||||||
|
"riPepperonis",
|
||||||
|
"twitchRaid",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
dailyEmote struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func DailyEmote() *dailyEmote {
|
||||||
|
return new(dailyEmote)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *dailyEmote) IsValid(m *twitch.PrivateMessage) bool {
|
||||||
|
return (m.Tags["msg-id"] == "highlighted-message") && m.Message == emoteMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *dailyEmote) Handle(m *twitch.PrivateMessage, r Responser) {
|
||||||
|
data := time.Now().Format("2006-01-02") + m.User.DisplayName
|
||||||
|
rng := rand.New(rand.NewSource(hashSeed(data)))
|
||||||
|
emote := emotes[rng.Intn(len(emotes))]
|
||||||
|
|
||||||
|
msg := fmt.Sprintf("@%s твой эмоут дня: %s", m.User.DisplayName, emote)
|
||||||
|
r.Say(m.Channel, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashSeed(s string) int64 {
|
||||||
|
h := fnv.New64()
|
||||||
|
h.Write([]byte(s))
|
||||||
|
return int64(h.Sum64())
|
||||||
|
}
|
48
modules/twitchat/duphandler.go
Normal file
48
modules/twitchat/duphandler.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
package twitchat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gempir/go-twitch-irc/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
dupHandler struct {
|
||||||
|
lastMessage string
|
||||||
|
counter int
|
||||||
|
dupMinimal int
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const DupMinimal = 3
|
||||||
|
|
||||||
|
func DupHandler() PrivateMessageHandler {
|
||||||
|
return &dupHandler{
|
||||||
|
lastMessage: "",
|
||||||
|
counter: 0,
|
||||||
|
dupMinimal: DupMinimal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *dupHandler) IsValid(m *twitch.PrivateMessage) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *dupHandler) Handle(m *twitch.PrivateMessage, r Responser) {
|
||||||
|
data := strings.Fields(m.Message)
|
||||||
|
for i := range data {
|
||||||
|
if data[i] == h.lastMessage {
|
||||||
|
h.counter++
|
||||||
|
} else {
|
||||||
|
if h.counter >= h.dupMinimal {
|
||||||
|
msg := fmt.Sprintf("%d %s подряд", h.counter, h.lastMessage)
|
||||||
|
r.Say(m.Channel, msg)
|
||||||
|
log.Print("chat: ", msg)
|
||||||
|
}
|
||||||
|
h.counter = 1
|
||||||
|
h.lastMessage = data[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
modules/twitchat/handlers.go
Normal file
16
modules/twitchat/handlers.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package twitchat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gempir/go-twitch-irc/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Responser interface {
|
||||||
|
Say(channel, message string)
|
||||||
|
}
|
||||||
|
|
||||||
|
PrivateMessageHandler interface {
|
||||||
|
IsValid(m *twitch.PrivateMessage) bool
|
||||||
|
Handle(m *twitch.PrivateMessage, r Responser)
|
||||||
|
}
|
||||||
|
)
|
23
modules/twitchat/logcheck.go
Normal file
23
modules/twitchat/logcheck.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package twitchat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/gempir/go-twitch-irc/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
logCheck struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func LogCheck() PrivateMessageHandler {
|
||||||
|
return new(logCheck)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *logCheck) IsValid(m *twitch.PrivateMessage) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *logCheck) Handle(m *twitch.PrivateMessage, r Responser) {
|
||||||
|
log.Print("chat <", m.User.DisplayName, "> : ", m.Message)
|
||||||
|
}
|
39
modules/twitchat/petcat.go
Normal file
39
modules/twitchat/petcat.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package twitchat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"galched-bot/modules/patpet"
|
||||||
|
"github.com/gempir/go-twitch-irc/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
petMsg1 = "!погладь"
|
||||||
|
petMsg2 = "!гладь"
|
||||||
|
petMsg3 = "!погладить"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
petCat struct {
|
||||||
|
cat *patpet.Pet
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func PetCat(pet *patpet.Pet) *petCat {
|
||||||
|
return &petCat{
|
||||||
|
cat: pet,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *petCat) IsValid(m *twitch.PrivateMessage) bool {
|
||||||
|
return (m.Tags["msg-id"] == "highlighted-message") && (strings.HasPrefix(m.Message, petMsg1) ||
|
||||||
|
strings.HasPrefix(m.Message, petMsg2) ||
|
||||||
|
strings.HasPrefix(m.Message, petMsg3))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *petCat) Handle(m *twitch.PrivateMessage, r Responser) {
|
||||||
|
msg := fmt.Sprintf("Котэ поглажен уже %d раз(а) InuyoFace", h.cat.Pet())
|
||||||
|
r.Say(m.Channel, msg)
|
||||||
|
h.cat.Dump()
|
||||||
|
}
|
61
modules/twitchat/songrequest.go
Normal file
61
modules/twitchat/songrequest.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package twitchat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gempir/go-twitch-irc/v2"
|
||||||
|
|
||||||
|
"galched-bot/modules/youtube"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
songMsg = "!song"
|
||||||
|
reqPrefix = "!req " // space in the end is important
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
songRequest struct {
|
||||||
|
r *youtube.Requester
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func SongRequest(r *youtube.Requester) PrivateMessageHandler {
|
||||||
|
return &songRequest{r: r}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *songRequest) IsValid(m *twitch.PrivateMessage) bool {
|
||||||
|
return (strings.HasPrefix(m.Message, reqPrefix) && m.Tags["msg-id"] == "highlighted-message") ||
|
||||||
|
strings.TrimSpace(m.Message) == songMsg
|
||||||
|
// return strings.HasPrefix(m.Message, reqPrefix) || strings.TrimSpace(m.Message) == songMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *songRequest) Handle(m *twitch.PrivateMessage, r Responser) {
|
||||||
|
if strings.TrimSpace(m.Message) == "!song" {
|
||||||
|
list := h.r.List()
|
||||||
|
if len(list) > 0 {
|
||||||
|
line := fmt.Sprintf("Сейчас играет: <%s>", list[0].Title)
|
||||||
|
r.Say(m.Channel, line)
|
||||||
|
} else {
|
||||||
|
r.Say(m.Channel, "Очередь видео пуста")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := strings.TrimPrefix(m.Message, "!req ")
|
||||||
|
if len(query) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chatMsg, err := h.r.AddVideo(query, m.User.DisplayName)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("yt: cannot add song from msg <%s>, err: %v", m.Message, err)
|
||||||
|
if len(chatMsg) > 0 {
|
||||||
|
r.Say(m.Channel, m.User.DisplayName+" "+chatMsg)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Say(m.Channel, m.User.DisplayName+" добавил "+chatMsg)
|
||||||
|
return
|
||||||
|
}
|
58
modules/twitchat/twitchat.go
Normal file
58
modules/twitchat/twitchat.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package twitchat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"galched-bot/modules/patpet"
|
||||||
|
"galched-bot/modules/settings"
|
||||||
|
"galched-bot/modules/youtube"
|
||||||
|
|
||||||
|
"github.com/gempir/go-twitch-irc/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
TwitchIRC struct {
|
||||||
|
username string
|
||||||
|
chat *twitch.Client
|
||||||
|
handlers []PrivateMessageHandler
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(s *settings.Settings, r *youtube.Requester, pet *patpet.Pet) (*TwitchIRC, error) {
|
||||||
|
var irc = new(TwitchIRC)
|
||||||
|
|
||||||
|
irc.username = s.TwitchUser
|
||||||
|
|
||||||
|
irc.handlers = append(irc.handlers, DupHandler())
|
||||||
|
irc.handlers = append(irc.handlers, DailyEmote())
|
||||||
|
irc.handlers = append(irc.handlers, SongRequest(r))
|
||||||
|
irc.handlers = append(irc.handlers, PetCat(pet))
|
||||||
|
// irc.handlers = append(irc.handlers, LogCheck())
|
||||||
|
|
||||||
|
irc.chat = twitch.NewClient(s.TwitchUser, s.TwitchToken)
|
||||||
|
irc.chat.OnPrivateMessage(irc.PrivateMessageHandler)
|
||||||
|
irc.chat.Join(s.TwitchIRCRoom)
|
||||||
|
|
||||||
|
return irc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TwitchIRC) Start() error {
|
||||||
|
go func() {
|
||||||
|
err := c.chat.Connect()
|
||||||
|
_ = err // no point in error because disconnect will be called anyway
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TwitchIRC) Stop() error {
|
||||||
|
return c.chat.Disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TwitchIRC) PrivateMessageHandler(msg twitch.PrivateMessage) {
|
||||||
|
if msg.User.Name == c.username {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := range c.handlers {
|
||||||
|
if c.handlers[i].IsValid(&msg) {
|
||||||
|
c.handlers[i].Handle(&msg, c.chat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
159
modules/web/server.go
Normal file
159
modules/web/server.go
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"galched-bot/modules/settings"
|
||||||
|
"galched-bot/modules/youtube"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
WebServer struct {
|
||||||
|
server http.Server
|
||||||
|
r *youtube.Requester
|
||||||
|
|
||||||
|
users map[string]string
|
||||||
|
authed map[string]struct{}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(s *settings.Settings, r *youtube.Requester) *WebServer {
|
||||||
|
srv := http.Server{
|
||||||
|
Addr: s.QueueAddress,
|
||||||
|
}
|
||||||
|
|
||||||
|
webServer := &WebServer{
|
||||||
|
server: srv,
|
||||||
|
r: r,
|
||||||
|
|
||||||
|
users: s.LoginUsers,
|
||||||
|
authed: make(map[string]struct{}, 10),
|
||||||
|
}
|
||||||
|
|
||||||
|
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if request.Method != http.MethodGet {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if webServer.IsAuthorized(request) {
|
||||||
|
http.ServeFile(writer, request, "web/index.html")
|
||||||
|
} else {
|
||||||
|
http.Redirect(writer, request, "/login", 301)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/login", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if request.Method == http.MethodGet {
|
||||||
|
http.ServeFile(writer, request, "web/login.html")
|
||||||
|
return
|
||||||
|
} else if request.Method != http.MethodPost {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
login := request.FormValue("login")
|
||||||
|
pwd := request.FormValue("password")
|
||||||
|
|
||||||
|
log.Print("web: trying to log in with user: ", login)
|
||||||
|
|
||||||
|
if webServer.IsRegistered(login, pwd) {
|
||||||
|
webServer.Authorize(writer)
|
||||||
|
} else {
|
||||||
|
log.Print("web: incorrect password attempt")
|
||||||
|
}
|
||||||
|
http.Redirect(writer, request, "/", http.StatusSeeOther)
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/scripts.js", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if request.Method != http.MethodGet {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.ServeFile(writer, request, "web/scripts.js")
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/style.css", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if request.Method != http.MethodGet {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.ServeFile(writer, request, "web/style.css")
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/queue", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if !webServer.IsAuthorized(request) {
|
||||||
|
http.Error(writer, "not authorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch request.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
case http.MethodPost:
|
||||||
|
body, err := ioutil.ReadAll(request.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Print("web: cannot read body msg, %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := string(body)
|
||||||
|
if len(id) != youtube.YoutubeIDLength && len(id) > 0 {
|
||||||
|
log.Printf("web: incorrect data in body, <%s>", id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Remove(id)
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writer.Header().Set("Content-Type", "application/json")
|
||||||
|
writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
json.NewEncoder(writer).Encode(webServer.r.List())
|
||||||
|
})
|
||||||
|
|
||||||
|
return webServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s WebServer) Start() error {
|
||||||
|
go func() {
|
||||||
|
s.server.ListenAndServe()
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s WebServer) Stop(ctx context.Context) error {
|
||||||
|
return s.server.Shutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s WebServer) IsAuthorized(request *http.Request) bool {
|
||||||
|
if cookie, err := request.Cookie("session"); err == nil {
|
||||||
|
if _, ok := s.authed[cookie.Value]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s WebServer) IsRegistered(login, pwd string) bool {
|
||||||
|
return s.users[login] == pwd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s WebServer) Authorize(response http.ResponseWriter) {
|
||||||
|
var byteKey = make([]byte, 16)
|
||||||
|
rand.Read(byteKey[:])
|
||||||
|
stringKey := hex.EncodeToString(byteKey)
|
||||||
|
|
||||||
|
s.authed[stringKey] = struct{}{}
|
||||||
|
log.Print("web: authenticated new user")
|
||||||
|
|
||||||
|
expires := time.Now().AddDate(0, 1, 0)
|
||||||
|
http.SetCookie(response, &http.Cookie{
|
||||||
|
Name: "session",
|
||||||
|
Value: stringKey,
|
||||||
|
Path: "/",
|
||||||
|
Expires: expires,
|
||||||
|
})
|
||||||
|
}
|
195
modules/youtube/requester.go
Normal file
195
modules/youtube/requester.go
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
package youtube
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/api/option"
|
||||||
|
"google.golang.org/api/youtube/v3"
|
||||||
|
|
||||||
|
"galched-bot/modules/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
YoutubeIDLength = 11
|
||||||
|
youtubeRegexpID = `^.*((youtu.be\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?\s]*).*`
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
urlRegex = regexp.MustCompile(youtubeRegexpID)
|
||||||
|
durationRegex = regexp.MustCompile(`P(?P<years>\d+Y)?(?P<months>\d+M)?(?P<days>\d+D)?T?(?P<hours>\d+H)?(?P<minutes>\d+M)?(?P<seconds>\d+S)?`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Video struct {
|
||||||
|
ID string
|
||||||
|
Title string
|
||||||
|
From string
|
||||||
|
Duration string
|
||||||
|
|
||||||
|
Upvotes uint64
|
||||||
|
Downvotes uint64
|
||||||
|
Views uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
Requester struct {
|
||||||
|
mu *sync.RWMutex
|
||||||
|
|
||||||
|
srv *youtube.Service
|
||||||
|
requests []Video
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(ctx context.Context, s *settings.Settings) (*Requester, error) {
|
||||||
|
srv, err := youtube.NewService(ctx, option.WithAPIKey(s.YoutubeToken))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Requester{
|
||||||
|
mu: new(sync.RWMutex),
|
||||||
|
srv: srv,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Requester) AddVideo(query, from string) (string, error) {
|
||||||
|
var (
|
||||||
|
id string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// try parse video id from the query
|
||||||
|
id, err = videoID(query)
|
||||||
|
if err != nil {
|
||||||
|
// if we can't fo that, then search for the query
|
||||||
|
resp, err := r.srv.Search.List("snippet").Type("video").MaxResults(1).Q(query).Do()
|
||||||
|
if err != nil || len(resp.Items) == 0 || resp.Items[0].Id == nil {
|
||||||
|
return "", fmt.Errorf("cannot parse youtube id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
id = resp.Items[0].Id.VideoId
|
||||||
|
}
|
||||||
|
|
||||||
|
// get video info from api
|
||||||
|
resp, err := r.srv.Videos.List("snippet,statistics,contentDetails").Id(id).Do()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("cannot send request to youtube api: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if response have all required fields
|
||||||
|
if len(resp.Items) == 0 {
|
||||||
|
return "", errors.New("youtube api response does not contain items")
|
||||||
|
}
|
||||||
|
if resp.Items[0].Snippet == nil {
|
||||||
|
return "", errors.New("youtube api response does not contain snippet")
|
||||||
|
}
|
||||||
|
if resp.Items[0].Statistics == nil {
|
||||||
|
return "", errors.New("youtube api response does not contain statistics")
|
||||||
|
}
|
||||||
|
if resp.Items[0].ContentDetails == nil {
|
||||||
|
return "", errors.New("youtube api response does not contain content details")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check length of the video not more than 5 minutes
|
||||||
|
if parseDuration(resp.Items[0].ContentDetails.Duration) == 0 {
|
||||||
|
err = errors.New("видео не должно быть трансляцией")
|
||||||
|
return err.Error(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check video is not live
|
||||||
|
if parseDuration(resp.Items[0].ContentDetails.Duration) > time.Minute*5 {
|
||||||
|
err = errors.New("видео должно быть короче 5 минут")
|
||||||
|
return err.Error(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
// check if video already in the queue
|
||||||
|
for i := range r.requests {
|
||||||
|
if r.requests[i].ID == id {
|
||||||
|
err = errors.New("видео уже есть в очереди")
|
||||||
|
return err.Error(), err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.requests = append(r.requests, Video{
|
||||||
|
ID: id,
|
||||||
|
From: from,
|
||||||
|
Duration: strings.ToLower(resp.Items[0].ContentDetails.Duration[2:]),
|
||||||
|
Title: resp.Items[0].Snippet.Title,
|
||||||
|
Upvotes: resp.Items[0].Statistics.LikeCount,
|
||||||
|
Views: resp.Items[0].Statistics.ViewCount,
|
||||||
|
Downvotes: resp.Items[0].Statistics.DislikeCount,
|
||||||
|
})
|
||||||
|
log.Printf("yt: added video < %s > from < %s >\n", resp.Items[0].Snippet.Title, from)
|
||||||
|
|
||||||
|
return resp.Items[0].Snippet.Title, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Requester) List() []Video {
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
|
result := make([]Video, len(r.requests))
|
||||||
|
copy(result, r.requests)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Requester) Remove(id string) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
for i := range r.requests {
|
||||||
|
if r.requests[i].ID == id {
|
||||||
|
r.requests = append(r.requests[:i], r.requests[i+1:]...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func videoID(url string) (string, error) {
|
||||||
|
result := urlRegex.FindStringSubmatch(url)
|
||||||
|
|
||||||
|
ln := len(result)
|
||||||
|
if ln == 0 || len(result[ln-1]) != YoutubeIDLength {
|
||||||
|
return "", fmt.Errorf("id haven't matched in \"%s\"", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result[ln-1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDuration(str string) time.Duration {
|
||||||
|
matches := durationRegex.FindStringSubmatch(str)
|
||||||
|
|
||||||
|
years := parseInt64(matches[1])
|
||||||
|
months := parseInt64(matches[2])
|
||||||
|
days := parseInt64(matches[3])
|
||||||
|
hours := parseInt64(matches[4])
|
||||||
|
minutes := parseInt64(matches[5])
|
||||||
|
seconds := parseInt64(matches[6])
|
||||||
|
|
||||||
|
hour := int64(time.Hour)
|
||||||
|
minute := int64(time.Minute)
|
||||||
|
second := int64(time.Second)
|
||||||
|
return time.Duration(years*24*365*hour + months*30*24*hour + days*24*hour + hours*hour + minutes*minute + seconds*second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseInt64(value string) int64 {
|
||||||
|
if len(value) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
parsed, err := strconv.Atoi(value[:len(value)-1])
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int64(parsed)
|
||||||
|
}
|
67
modules/youtube/requester_test.go
Normal file
67
modules/youtube/requester_test.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package youtube
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPlayground(t *testing.T) {
|
||||||
|
youtubeTokenPath := "../../tokens/.youtubetoken"
|
||||||
|
youtubetoken, err := ioutil.ReadFile(youtubeTokenPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cannot read youtube token: %v", err)
|
||||||
|
}
|
||||||
|
_ = youtubetoken
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchID(t *testing.T) {
|
||||||
|
positiveCases := [][]string{
|
||||||
|
{
|
||||||
|
"https://www.youtube.com/watch?v=_v5IzvVTw7A&feature=feedrec_grec_index",
|
||||||
|
"_v5IzvVTw7A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"https://www.youtube.com/watch?v=_v5IzvVTw7A#t=0m10s",
|
||||||
|
"_v5IzvVTw7A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"https://www.youtube.com/embed/_v5IzvVTw7A?rel=0",
|
||||||
|
"_v5IzvVTw7A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"https://www.youtube.com/watch?v=_v5IzvVTw7A ", // multiple spaces in the end
|
||||||
|
"_v5IzvVTw7A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"https://youtu.be/_v5IzvVTw7A",
|
||||||
|
"_v5IzvVTw7A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"https://www.youtube.com/watch?v=PCp2iXA1uLE&list=PLvx4lPhqncyf10ymYz8Ph8EId0cafzhdZ&index=2&t=0s",
|
||||||
|
"PCp2iXA1uLE",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
negativeCases := []string{
|
||||||
|
"https://youtu.be/_vIzvVTw7A", // short video
|
||||||
|
"https://vimeo.com/_v5IzvVTw7A", // incorrect domain
|
||||||
|
"youtube prime video",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range positiveCases {
|
||||||
|
res, err := videoID(testCase[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expecting error: <nil>, got: <%v>, url: %s\n", err, testCase[0])
|
||||||
|
}
|
||||||
|
if res != testCase[1] {
|
||||||
|
t.Errorf("expecting result: %s, got: %s\n", testCase[0], res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range negativeCases {
|
||||||
|
_, err := videoID(testCase)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expecting error: <non nil>, got: <nil>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
0
songs/.gitkeep
Normal file
0
songs/.gitkeep
Normal file
12
web/index.html
Normal file
12
web/index.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" type="text/css" href="./style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="player"></div>
|
||||||
|
<div><button onclick="nextVideo()">Skip song</button></div>
|
||||||
|
<div id="video_queue"></div>
|
||||||
|
<script src="scripts.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
14
web/login.html
Normal file
14
web/login.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<form action="./login" method="post">
|
||||||
|
<div class="container">
|
||||||
|
<input type="text" placeholder="Username" name="login" required> <br><br>
|
||||||
|
|
||||||
|
<input type="password" placeholder="Password" name="password" required> <br><br>
|
||||||
|
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
120
web/scripts.js
Normal file
120
web/scripts.js
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
// queue is used to play videos
|
||||||
|
var queue
|
||||||
|
// player is a iframe youtube player
|
||||||
|
var player
|
||||||
|
// vid is a current playing video id
|
||||||
|
var vid = ""
|
||||||
|
|
||||||
|
function updateTable() {
|
||||||
|
var myTableDiv = document.getElementById("video_queue")
|
||||||
|
myTableDiv.innerHTML = ""
|
||||||
|
|
||||||
|
var table = document.createElement('TABLE')
|
||||||
|
var tableBody = document.createElement('TBODY')
|
||||||
|
table.border = '1'
|
||||||
|
table.appendChild(tableBody);
|
||||||
|
|
||||||
|
for (i = 0; i < queue.length; i++) {
|
||||||
|
var tr = document.createElement('TR');
|
||||||
|
|
||||||
|
var td = document.createElement('TD');
|
||||||
|
td.appendChild(document.createTextNode(queue[i].Duration));
|
||||||
|
tr.appendChild(td);
|
||||||
|
|
||||||
|
var td = document.createElement('TD');
|
||||||
|
td.appendChild(document.createTextNode(queue[i].Title));
|
||||||
|
tr.appendChild(td)
|
||||||
|
|
||||||
|
var td = document.createElement('TD');
|
||||||
|
td.appendChild(document.createTextNode( queue[i].From));
|
||||||
|
tr.appendChild(td)
|
||||||
|
|
||||||
|
var td = document.createElement('TD');
|
||||||
|
td.appendChild(document.createTextNode( queue[i].Views));
|
||||||
|
tr.appendChild(td)
|
||||||
|
|
||||||
|
var td = document.createElement('TD');
|
||||||
|
td.appendChild(document.createTextNode(((queue[i].Upvotes/(queue[i].Upvotes + queue[i].Downvotes)) * 100).toFixed(2) + '%'));
|
||||||
|
tr.appendChild(td)
|
||||||
|
|
||||||
|
tableBody.appendChild(tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
myTableDiv.appendChild(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
function bgUpdateQueue() {
|
||||||
|
initQueue()
|
||||||
|
updateTable()
|
||||||
|
|
||||||
|
if (vid === "" && queue.length > 0) {
|
||||||
|
vid = queue[0].ID
|
||||||
|
player.loadVideoById(vid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initQueue() {
|
||||||
|
var request = new XMLHttpRequest()
|
||||||
|
request.open('GET', './queue', false)
|
||||||
|
request.onload = function () {
|
||||||
|
queue = JSON.parse(this.response)
|
||||||
|
}
|
||||||
|
request.send()
|
||||||
|
updateTable()
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateQueue() {
|
||||||
|
var request = new XMLHttpRequest()
|
||||||
|
request.open('POST', './queue', false)
|
||||||
|
request.onload = function () {
|
||||||
|
queue = JSON.parse(this.response)
|
||||||
|
}
|
||||||
|
request.send(vid)
|
||||||
|
updateTable()
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextVideo() {
|
||||||
|
updateQueue()
|
||||||
|
if (queue.length > 0) {
|
||||||
|
vid = queue[0].ID
|
||||||
|
} else {
|
||||||
|
vid = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
player.loadVideoById(vid)
|
||||||
|
updateTable()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onYouTubeIframeAPIReady() {
|
||||||
|
if (queue.length != 0) {
|
||||||
|
vid = queue[0].ID
|
||||||
|
}
|
||||||
|
console.log(vid)
|
||||||
|
player = new YT.Player('player', {
|
||||||
|
height: '390',
|
||||||
|
width: '640',
|
||||||
|
videoId: vid,
|
||||||
|
events: {
|
||||||
|
'onReady': onPlayerReady,
|
||||||
|
'onStateChange': onPlayerStateChange
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPlayerReady(event) {
|
||||||
|
event.target.playVideo();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPlayerStateChange(event) {
|
||||||
|
if (event.data === 0) {
|
||||||
|
nextVideo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initQueue()
|
||||||
|
setInterval(bgUpdateQueue, 4000);
|
||||||
|
var tag = document.createElement('script');
|
||||||
|
tag.src = "https://www.youtube.com/iframe_api";
|
||||||
|
var firstScriptTag = document.getElementsByTagName('script')[0];
|
||||||
|
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
4
web/style.css
Normal file
4
web/style.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
body {
|
||||||
|
background-color: #000000;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
Loading…
Reference in a new issue