From 5ade0e30664b745fdcc1fbae2fa777919a3924f8 Mon Sep 17 00:00:00 2001 From: Ash Keel Date: Fri, 14 May 2021 13:15:38 +0200 Subject: [PATCH] Integrate Twitch APIs internally rather than through stulbe --- frontend/src/overrides.css | 20 ++ frontend/src/store/api/reducer.ts | 24 ++- frontend/src/ui/App.tsx | 36 ++-- frontend/src/ui/pages/loyalty/Goals.tsx | 4 +- frontend/src/ui/pages/loyalty/Rewards.tsx | 4 +- frontend/src/ui/pages/loyalty/Settings.tsx | 40 +--- frontend/src/ui/pages/twitch/APISettings.tsx | 108 +++++++++++ .../Settings.tsx => twitch/BotSettings.tsx} | 19 +- .../pages/{twitchbot => twitch}/Commands.tsx | 0 .../ui/pages/{twitchbot => twitch}/Main.tsx | 0 .../pages/{twitchbot => twitch}/Modules.tsx | 0 go.mod | 4 +- go.sum | 6 +- main.go | 112 +++-------- modules/loyalty/data.go | 5 +- modules/modules.go | 18 +- modules/stulbe/client.go | 4 +- modules/twitch/bot.go | 181 ++++++++++++++++++ modules/twitch/client.go | 40 ++++ {twitchbot => modules/twitch}/commands.go | 12 +- modules/twitch/module.go | 17 ++ twitchbot/bot.go | 110 ----------- 22 files changed, 468 insertions(+), 296 deletions(-) create mode 100644 frontend/src/ui/pages/twitch/APISettings.tsx rename frontend/src/ui/pages/{twitchbot/Settings.tsx => twitch/BotSettings.tsx} (83%) rename frontend/src/ui/pages/{twitchbot => twitch}/Commands.tsx (100%) rename frontend/src/ui/pages/{twitchbot => twitch}/Main.tsx (100%) rename frontend/src/ui/pages/{twitchbot => twitch}/Modules.tsx (100%) create mode 100644 modules/twitch/bot.go create mode 100644 modules/twitch/client.go rename {twitchbot => modules/twitch}/commands.go (94%) create mode 100644 modules/twitch/module.go delete mode 100644 twitchbot/bot.go diff --git a/frontend/src/overrides.css b/frontend/src/overrides.css index 2f25c94..ad699e5 100644 --- a/frontend/src/overrides.css +++ b/frontend/src/overrides.css @@ -33,6 +33,10 @@ body .button.is-success { padding: 1rem 1.5rem; } +.copyblock { + padding-bottom: 1rem; +} + /* Custom reward/goal classes */ .reward-disabled, .goal-disabled { opacity: 0.5; @@ -175,3 +179,19 @@ span.sortable { position: fixed; bottom: 0; right: 0; } + +/* Inline definition lists */ +.inline-dl { + display: grid; + grid-template-columns: min-content 1fr; + padding: 0.5rem; +} +.inline-dl dt { + white-space: nowrap; + font-weight: bold; + text-align: right; + padding-right: 0.5rem; +} +.inline-dl dt:after { + content: ":"; +} diff --git a/frontend/src/store/api/reducer.ts b/frontend/src/store/api/reducer.ts index aaab73e..98cbffa 100644 --- a/frontend/src/store/api/reducer.ts +++ b/frontend/src/store/api/reducer.ts @@ -13,7 +13,8 @@ import KilovoltWS from '@strimertul/kilovolt-client'; // Storage const moduleConfigKey = 'stul-meta/modules'; const httpConfigKey = 'http/config'; -const twitchBotConfigKey = 'twitchbot/config'; +const twitchConfigKey = 'twitch/config'; +const twitchBotConfigKey = 'twitch/bot-config'; const stulbeConfigKey = 'stulbe/config'; const loyaltyConfigKey = 'loyalty/config'; const loyaltyStorageKey = 'loyalty/users'; @@ -29,7 +30,7 @@ interface ModuleConfig { configured: boolean; kv: boolean; static: boolean; - twitchbot: boolean; + twitch: boolean; stulbe: boolean; loyalty: boolean; } @@ -39,6 +40,12 @@ interface HTTPConfig { path: string; } +interface TwitchConfig { + enable_bot: boolean; + api_client_id: string; + api_client_secret: string; +} + interface TwitchBotConfig { username: string; oauth: string; @@ -53,7 +60,6 @@ interface StulbeConfig { interface LoyaltyConfig { currency: string; - enable_live_check: boolean; points: { interval: number; amount: number; @@ -105,6 +111,7 @@ export interface APIState { moduleConfigs: { moduleConfig: ModuleConfig; httpConfig: HTTPConfig; + twitchConfig: TwitchConfig; twitchBotConfig: TwitchBotConfig; stulbeConfig: StulbeConfig; loyaltyConfig: LoyaltyConfig; @@ -124,6 +131,7 @@ const initialState: APIState = { moduleConfigs: { moduleConfig: null, httpConfig: null, + twitchConfig: null, twitchBotConfig: null, stulbeConfig: null, loyaltyConfig: null, @@ -206,6 +214,13 @@ export const modules = { state.moduleConfigs.httpConfig = payload; }, ), + twitchConfig: makeModule( + twitchConfigKey, + (state) => state.moduleConfigs?.twitchConfig, + (state, { payload }) => { + state.moduleConfigs.twitchConfig = payload; + }, + ), twitchBotConfig: makeModule( twitchBotConfigKey, (state) => state.moduleConfigs?.twitchBotConfig, @@ -312,6 +327,9 @@ const apiReducer = createSlice({ httpConfigChanged(state, { payload }: PayloadAction) { state.moduleConfigs.httpConfig = payload; }, + twitchConfigChanged(state, { payload }: PayloadAction) { + state.moduleConfigs.twitchConfig = payload; + }, twitchBotConfigChanged(state, { payload }: PayloadAction) { state.moduleConfigs.twitchBotConfig = payload; }, diff --git a/frontend/src/ui/App.tsx b/frontend/src/ui/App.tsx index cb4de02..8d65966 100644 --- a/frontend/src/ui/App.tsx +++ b/frontend/src/ui/App.tsx @@ -5,8 +5,7 @@ import { RootState } from '../store'; import { createWSClient } from '../store/api/reducer'; import Home from './pages/Home'; import HTTPPage from './pages/HTTP'; -import TwitchBotPage from './pages/twitchbot/Main'; -import TwitchBotSettingsPage from './pages/twitchbot/Settings'; +import TwitchPage from './pages/twitch/Main'; import StulbePage from './pages/Stulbe'; import LoyaltyPage from './pages/loyalty/Main'; import DebugPage from './pages/Debug'; @@ -14,9 +13,9 @@ import LoyaltySettingPage from './pages/loyalty/Settings'; import LoyaltyRewardsPage from './pages/loyalty/Rewards'; import LoyaltyUserListPage from './pages/loyalty/UserList'; import LoyaltyGoalsPage from './pages/loyalty/Goals'; -import TwitchBotCommandsPage from './pages/twitchbot/Commands'; -import TwitchBotModulesPage from './pages/twitchbot/Modules'; import LoyaltyRedeemQueuePage from './pages/loyalty/Queue'; +import TwitchSettingsPage from './pages/twitch/APISettings'; +import TwitchBotSettingsPage from './pages/twitch/BotSettings'; interface RouteItem { name: string; @@ -34,20 +33,16 @@ const menu: RouteItem[] = [ route: '/http', }, { - name: 'Twitch Bot', - route: '/twitchbot/', + name: 'Twitch integration', + route: '/twitch/', subroutes: [ { - name: 'Configuration', - route: '/twitchbot/settings', + name: 'Module Configuration', + route: '/twitch/settings', }, { - name: 'Modules', - route: '/twitchbot/modules', - }, - { - name: 'Custom commands', - route: '/twitchbot/commands', + name: 'Bot Configuration', + route: '/twitch/bot/settings', }, ], }, @@ -77,10 +72,6 @@ const menu: RouteItem[] = [ }, ], }, - { - name: 'Stulbe integration', - route: '/stulbe', - }, ]; export default function App(): React.ReactElement { @@ -147,12 +138,11 @@ export default function App(): React.ReactElement { - + - - - - + + + diff --git a/frontend/src/ui/pages/loyalty/Goals.tsx b/frontend/src/ui/pages/loyalty/Goals.tsx index b222ff4..e062d06 100644 --- a/frontend/src/ui/pages/loyalty/Goals.tsx +++ b/frontend/src/ui/pages/loyalty/Goals.tsx @@ -291,9 +291,9 @@ export default function LoyaltyGoalsPage( const dispatch = useDispatch(); - const twitchBotActive = moduleConfig?.twitchbot ?? false; + const twitchActive = moduleConfig?.twitch ?? false; const loyaltyEnabled = moduleConfig?.loyalty ?? false; - const active = twitchBotActive && loyaltyEnabled; + const active = twitchActive && loyaltyEnabled; const [goalFilter, setGoalFilter] = useState(''); const goalFilterLC = goalFilter.toLowerCase(); diff --git a/frontend/src/ui/pages/loyalty/Rewards.tsx b/frontend/src/ui/pages/loyalty/Rewards.tsx index 3be84a1..b3e64c3 100644 --- a/frontend/src/ui/pages/loyalty/Rewards.tsx +++ b/frontend/src/ui/pages/loyalty/Rewards.tsx @@ -324,9 +324,9 @@ export default function LoyaltyRewardsPage( const dispatch = useDispatch(); - const twitchBotActive = moduleConfig?.twitchbot ?? false; + const twitchActive = moduleConfig?.twitch ?? false; const loyaltyEnabled = moduleConfig?.loyalty ?? false; - const active = twitchBotActive && loyaltyEnabled; + const active = twitchActive && loyaltyEnabled; const [rewardFilter, setRewardFilter] = useState(''); const rewardFilterLC = rewardFilter.toLowerCase(); diff --git a/frontend/src/ui/pages/loyalty/Settings.tsx b/frontend/src/ui/pages/loyalty/Settings.tsx index 49158f0..4666015 100644 --- a/frontend/src/ui/pages/loyalty/Settings.tsx +++ b/frontend/src/ui/pages/loyalty/Settings.tsx @@ -20,12 +20,14 @@ export default function LoyaltySettingPage( ): React.ReactElement { const [loyaltyConfig, setLoyaltyConfig] = useModule(modules.loyaltyConfig); const [moduleConfig, setModuleConfig] = useModule(modules.moduleConfig); + const [twitchConfig] = useModule(modules.twitchConfig); const dispatch = useDispatch(); - const twitchBotActive = moduleConfig?.twitchbot ?? false; + const twitchActive = moduleConfig?.twitch ?? false; + const twitchBotActive = twitchConfig?.enable_bot ?? false; const loyaltyEnabled = moduleConfig?.loyalty ?? false; - const active = twitchBotActive && loyaltyEnabled; + const active = twitchActive && twitchBotActive && loyaltyEnabled; const [tempIntervalNum, setTempIntervalNum] = useState(null); const [tempIntervalMult, setTempIntervalMult] = useState(null); @@ -34,9 +36,6 @@ export default function LoyaltySettingPage( loyaltyConfig?.points?.interval ?? 0, ); - const stulbeEnabled = moduleConfig?.stulbe ?? false; - const liveCheck = loyaltyConfig?.enable_live_check ?? false; - useEffect(() => { if (loyaltyConfig?.points) { if (tempIntervalNum === null) { @@ -67,7 +66,7 @@ export default function LoyaltySettingPage(
@@ -199,31 +200,6 @@ export default function LoyaltySettingPage( />

-
- -
+ + ); +} diff --git a/frontend/src/ui/pages/twitchbot/Settings.tsx b/frontend/src/ui/pages/twitch/BotSettings.tsx similarity index 83% rename from frontend/src/ui/pages/twitchbot/Settings.tsx rename to frontend/src/ui/pages/twitch/BotSettings.tsx index 8115085..47b1b37 100644 --- a/frontend/src/ui/pages/twitchbot/Settings.tsx +++ b/frontend/src/ui/pages/twitch/BotSettings.tsx @@ -9,33 +9,37 @@ export default function TwitchBotSettingsPage( params: RouteComponentProps, ): React.ReactElement { const [moduleConfig, setModuleConfig] = useModule(modules.moduleConfig); + const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig); const [twitchBotConfig, setTwitchBotConfig] = useModule( modules.twitchBotConfig, ); const dispatch = useDispatch(); const busy = moduleConfig === null; - const active = moduleConfig?.twitchbot ?? false; + const twitchActive = moduleConfig?.twitch ?? false; + const botActive = twitchConfig?.enable_bot ?? false; + const active = twitchActive && botActive; return ( <> -

Twitch bot configuration

+

Twitch module configuration

@@ -108,6 +112,7 @@ export default function TwitchBotSettingsPage( className="button" onClick={() => { dispatch(setModuleConfig(moduleConfig)); + dispatch(setTwitchConfig(twitchConfig)); dispatch(setTwitchBotConfig(twitchBotConfig)); }} > diff --git a/frontend/src/ui/pages/twitchbot/Commands.tsx b/frontend/src/ui/pages/twitch/Commands.tsx similarity index 100% rename from frontend/src/ui/pages/twitchbot/Commands.tsx rename to frontend/src/ui/pages/twitch/Commands.tsx diff --git a/frontend/src/ui/pages/twitchbot/Main.tsx b/frontend/src/ui/pages/twitch/Main.tsx similarity index 100% rename from frontend/src/ui/pages/twitchbot/Main.tsx rename to frontend/src/ui/pages/twitch/Main.tsx diff --git a/frontend/src/ui/pages/twitchbot/Modules.tsx b/frontend/src/ui/pages/twitch/Modules.tsx similarity index 100% rename from frontend/src/ui/pages/twitchbot/Modules.tsx rename to frontend/src/ui/pages/twitch/Modules.tsx diff --git a/go.mod b/go.mod index d9bcbec..4920827 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,9 @@ require ( github.com/gempir/go-twitch-irc/v2 v2.5.0 github.com/json-iterator/go v1.1.11 github.com/mattn/go-colorable v0.1.8 - github.com/nicklaw5/helix v1.14.0 // indirect + github.com/nicklaw5/helix v1.15.0 github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 github.com/sirupsen/logrus v1.8.1 github.com/strimertul/kilovolt/v3 v3.0.3 - github.com/strimertul/stulbe v0.2.5 + github.com/strimertul/stulbe-client-go v0.1.0 ) diff --git a/go.sum b/go.sum index 7926402..e29cf8d 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/nicklaw5/helix v1.13.1/go.mod h1:XeeXY7oY5W+MVMu6wF4qGm8uvjZ1/Nss0FqprVkXKrg= -github.com/nicklaw5/helix v1.14.0 h1:yJI+dUDxFzmlSelNygWs/lhirvuzCqgIXIZy05JdHVk= -github.com/nicklaw5/helix v1.14.0/go.mod h1:XeeXY7oY5W+MVMu6wF4qGm8uvjZ1/Nss0FqprVkXKrg= +github.com/nicklaw5/helix v1.15.0 h1:CiWWPk7sBOnqS8ZrsJogx8wzDRD4d+CnrtVHMMyWJSY= +github.com/nicklaw5/helix v1.15.0/go.mod h1:XeeXY7oY5W+MVMu6wF4qGm8uvjZ1/Nss0FqprVkXKrg= github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc h1:Ak86L+yDSOzKFa7WM5bf5itSOo1e3Xh8bm5YCMUXIjQ= github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= @@ -110,6 +110,8 @@ github.com/strimertul/kilovolt/v3 v3.0.3 h1:1+WmI8bi3Uwylr2l7+zkzr3kFHN73fm1Oala github.com/strimertul/kilovolt/v3 v3.0.3/go.mod h1:eweKrkaRD061PYcLS06L0FirEZ+uuQCWMcew7aZhXfk= github.com/strimertul/stulbe v0.2.5 h1:qrJFwttrWfwSfHsvTgI3moZjhBwbYN8Xe8gicCeISpc= github.com/strimertul/stulbe v0.2.5/go.mod h1:0AsY4OVf1dNCwOn9s7KySuAxJ85w88pXeostu1n9E7w= +github.com/strimertul/stulbe-client-go v0.1.0 h1:3lMPqrELDd4j5IBP0KDgdnuN2cEdr40Wore8DXioxFk= +github.com/strimertul/stulbe-client-go v0.1.0/go.mod h1:KtfuDhxCHZ9DCFHnrBOHqb2Pu9zoj+EqA8ZRIUqLD/w= github.com/twitchyliquid64/golang-asm v0.15.0/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= diff --git a/main.go b/main.go index ea4f1e1..96f15fe 100644 --- a/main.go +++ b/main.go @@ -14,8 +14,7 @@ import ( "github.com/strimertul/strimertul/database" "github.com/strimertul/strimertul/modules" "github.com/strimertul/strimertul/modules/loyalty" - "github.com/strimertul/strimertul/modules/stulbe" - "github.com/strimertul/strimertul/twitchbot" + "github.com/strimertul/strimertul/modules/twitch" "github.com/dgraph-io/badger/v3" @@ -103,7 +102,7 @@ func main() { failOnError(db.PutJSON(modules.ModuleConfigKey, modules.ModuleConfig{ EnableKV: true, EnableStaticServer: false, - EnableTwitchbot: false, + EnableTwitch: false, EnableStulbe: false, CompletedOnboarding: true, }), "could not save onboarding config") @@ -119,6 +118,7 @@ func main() { failOnError(db.GetJSON(modules.HTTPServerConfigKey, &httpConfig), "Could not retrieve HTTP server config") // Get Stulbe config, if enabled + /* Kinda deprecated, This will probably be removed/replaced in the future! var stulbeManager *stulbe.Manager = nil if moduleConfig.EnableStulbe { stulbeManager, err = stulbe.Initialize(db, wrapLogger("stulbe")) @@ -128,105 +128,47 @@ func main() { } defer stulbeManager.Close() } + */ var loyaltyManager *loyalty.Manager loyaltyLogger := wrapLogger("loyalty") if moduleConfig.EnableLoyalty { loyaltyManager, err = loyalty.NewManager(db, hub, loyaltyLogger) if err != nil { - log.WithError(err).Error("Loyalty initialization failed! Module was temporarely disabled") + log.WithError(err).Error("Loyalty initialization failed! Module was temporarily disabled") moduleConfig.EnableLoyalty = false } - - if stulbeManager != nil { - go stulbeManager.ReplicateKey(loyalty.ConfigKey) - go stulbeManager.ReplicateKey(loyalty.GoalsKey) - go stulbeManager.ReplicateKey(loyalty.RewardsKey) - go stulbeManager.ReplicateKey(loyalty.PointsKey) - } } //TODO Refactor this to something sane - if moduleConfig.EnableTwitchbot { + if moduleConfig.EnableTwitch { // Create logger - twitchLogger := wrapLogger("twitchbot") + twitchLogger := wrapLogger("twitch") - // Get Twitchbot config - var twitchConfig modules.TwitchBotConfig - failOnError(db.GetJSON(modules.TwitchBotConfigKey, &twitchConfig), "Could not retrieve twitch bot config") + // Get Twitch config + var twitchConfig twitch.Config + failOnError(db.GetJSON(twitch.ConfigKey, &twitchConfig), "Could not retrieve twitch config") - bot := twitchbot.NewBot(twitchConfig.Username, twitchConfig.Token, twitchLogger) - bot.Client.Join(twitchConfig.Channel) + // Create Twitch client + twitchClient, err := twitch.NewClient(twitchConfig, twitchLogger) + if err == nil { - if moduleConfig.EnableLoyalty { - config := loyaltyManager.Config() - bot.Loyalty = loyaltyManager - bot.SetBanList(config.BanList) - bot.Client.OnConnect(func() { - if config.Points.Interval > 0 { - go func() { - twitchLogger.Info("loyalty poll started") - for { - // Wait for next poll - time.Sleep(time.Duration(config.Points.Interval) * time.Second) + // Get Twitchbot config + var twitchBotConfig twitch.BotConfig + failOnError(db.GetJSON(twitch.BotConfigKey, &twitchBotConfig), "Could not retrieve twitch bot config") - // Check if streamer is online, if possible - streamOnline := true - if config.LiveCheck && stulbeManager != nil { - status, err := stulbeManager.Client.StreamStatus(twitchConfig.Channel) - if err != nil { - twitchLogger.WithError(err).Error("Error checking stream status") - } else { - streamOnline = status != nil - } - } - - // If stream is confirmed offline, don't give points away! - if !streamOnline { - twitchLogger.Info("loyalty poll active but stream is offline!") - continue - } - - // Get user list - users, err := bot.Client.Userlist(twitchConfig.Channel) - if err != nil { - twitchLogger.WithError(err).Error("error listing users") - continue - } - - // Iterate for each user in the list - pointsToGive := make(map[string]int64) - for _, user := range users { - // Check if user is blocked - if bot.IsBanned(user) { - continue - } - - // Check if user was active (chatting) for the bonus dingus - award := config.Points.Amount - if bot.IsActive(user) { - award += config.Points.ActivityBonus - } - - // Add to point pool if already on it, otherwise initialize - pointsToGive[user] = award - } - - bot.ResetActivity() - - // If changes were made, save the pool! - if len(users) > 0 { - loyaltyManager.GivePoints(pointsToGive) - } - } - }() - } - }) + // Create and run IRC bot + bot := twitch.NewBot(twitchClient, twitchBotConfig) + if moduleConfig.EnableLoyalty { + bot.SetupLoyalty(loyaltyManager) + } + go func() { + failOnError(bot.Connect(), "connection failed") + }() + } else { + log.WithError(err).Error("Twitch initialization failed! Module was temporarily disabled") + moduleConfig.EnableTwitch = false } - - go func() { - failOnError(bot.Connect(), "connection failed") - }() } // Create logger and endpoints diff --git a/modules/loyalty/data.go b/modules/loyalty/data.go index 6c84266..b26d076 100644 --- a/modules/loyalty/data.go +++ b/modules/loyalty/data.go @@ -5,9 +5,8 @@ import "time" const ConfigKey = "loyalty/config" type Config struct { - Currency string `json:"currency"` - LiveCheck bool `json:"enable_live_check"` - Points struct { + Currency string `json:"currency"` + Points struct { Interval int64 `json:"interval"` // in seconds! Amount int64 `json:"amount"` ActivityBonus int64 `json:"activity_bonus"` diff --git a/modules/modules.go b/modules/modules.go index f628e4e..ab91c8d 100644 --- a/modules/modules.go +++ b/modules/modules.go @@ -6,7 +6,7 @@ type ModuleConfig struct { CompletedOnboarding bool `json:"configured"` EnableKV bool `json:"kv"` EnableStaticServer bool `json:"static"` - EnableTwitchbot bool `json:"twitchbot"` + EnableTwitch bool `json:"twitch"` EnableStulbe bool `json:"stulbe"` EnableLoyalty bool `json:"loyalty"` } @@ -17,19 +17,3 @@ type HTTPServerConfig struct { Bind string `json:"bind"` Path string `json:"path"` } - -const TwitchBotConfigKey = "twitchbot/config" - -type TwitchBotConfig struct { - Username string `json:"username"` - Token string `json:"oauth"` - Channel string `json:"channel"` -} - -const StulbeConfigKey = "stulbe/config" - -type StulbeConfig struct { - Endpoint string `json:"endpoint"` - Token string `json:"token"` - EnableLoyalty bool `json:"enable_loyalty"` -} diff --git a/modules/stulbe/client.go b/modules/stulbe/client.go index 83a4386..da3a57d 100644 --- a/modules/stulbe/client.go +++ b/modules/stulbe/client.go @@ -5,9 +5,9 @@ import ( "github.com/sirupsen/logrus" - "github.com/strimertul/strimertul/database" + "github.com/strimertul/stulbe-client-go" - stulbe "github.com/strimertul/stulbe/client" + "github.com/strimertul/strimertul/database" ) type Manager struct { diff --git a/modules/twitch/bot.go b/modules/twitch/bot.go new file mode 100644 index 0000000..36ef85e --- /dev/null +++ b/modules/twitch/bot.go @@ -0,0 +1,181 @@ +package twitch + +import ( + "strings" + "time" + + irc "github.com/gempir/go-twitch-irc/v2" + "github.com/nicklaw5/helix" + "github.com/sirupsen/logrus" + "github.com/strimertul/strimertul/modules/loyalty" +) + +type Bot struct { + Client *irc.Client + + api *Client + username string + config BotConfig + logger logrus.FieldLogger + lastMessage time.Time + activeUsers map[string]bool + banlist map[string]bool + + // Module specific vars + Loyalty *loyalty.Manager +} + +func NewBot(api *Client, config BotConfig) *Bot { + // Create client + client := irc.NewClient(config.Username, config.Token) + + bot := &Bot{ + Client: client, + username: strings.ToLower(config.Username), // Normalize username + config: config, + logger: api.logger, + api: api, + lastMessage: time.Now(), + activeUsers: make(map[string]bool), + banlist: make(map[string]bool), + } + + client.OnPrivateMessage(func(message irc.PrivateMessage) { + bot.logger.Debugf("MSG: <%s> %s", message.User.Name, message.Message) + // Ignore messages for a while or twitch will get mad! + if message.Time.Before(bot.lastMessage.Add(time.Second * 2)) { + bot.logger.Debug("message received too soon, ignoring") + return + } + bot.activeUsers[message.User.Name] = true + + // Check if it's a command + if strings.HasPrefix(message.Message, "!") { + // Run through supported commands + for cmd, data := range commands { + if strings.HasPrefix(message.Message, cmd) { + data.Handler(bot, message) + bot.lastMessage = time.Now() + } + } + } + }) + + client.OnUserJoinMessage(func(message irc.UserJoinMessage) { + if strings.ToLower(message.User) == bot.username { + bot.logger.WithField("channel", message.Channel).Info("joined channel") + } else { + bot.logger.WithFields(logrus.Fields{ + "username": message.User, + "channel": message.Channel, + }).Debug("user joined channel") + } + }) + client.OnUserPartMessage(func(message irc.UserPartMessage) { + if strings.ToLower(message.User) == bot.username { + bot.logger.WithField("channel", message.Channel).Info("left channel") + } else { + bot.logger.WithFields(logrus.Fields{ + "username": message.User, + "channel": message.Channel, + }).Debug("user left channel") + } + }) + + bot.Client.Join(config.Channel) + + return bot +} + +func (b *Bot) SetupLoyalty(loyalty *loyalty.Manager) { + config := loyalty.Config() + b.Loyalty = loyalty + b.SetBanList(config.BanList) + b.Client.OnConnect(func() { + if config.Points.Interval > 0 { + go func() { + b.logger.Info("loyalty poll started") + for { + // Wait for next poll + time.Sleep(time.Duration(config.Points.Interval) * time.Second) + + // Check if streamer is online, if possible + streamOnline := true + status, err := b.api.API.GetStreams(&helix.StreamsParams{ + UserLogins: []string{b.config.Channel}, + }) + if err != nil { + b.logger.WithError(err).Error("Error checking stream status") + } else { + streamOnline = len(status.Data.Streams) > 0 + } + + // If stream is confirmed offline, don't give points away! + if !streamOnline { + b.logger.Debug("loyalty poll active but stream is offline!") + continue + } else { + b.logger.Debug("awarding points") + } + + // Get user list + users, err := b.Client.Userlist(b.config.Channel) + if err != nil { + b.logger.WithError(err).Error("error listing users") + continue + } + + // Iterate for each user in the list + pointsToGive := make(map[string]int64) + for _, user := range users { + // Check if user is blocked + if b.IsBanned(user) { + continue + } + + // Check if user was active (chatting) for the bonus dingus + award := config.Points.Amount + if b.IsActive(user) { + award += config.Points.ActivityBonus + } + + // Add to point pool if already on it, otherwise initialize + pointsToGive[user] = award + } + + b.ResetActivity() + + // If changes were made, save the pool! + if len(users) > 0 { + b.Loyalty.GivePoints(pointsToGive) + } + } + }() + } + }) +} + +func (b *Bot) SetBanList(banned []string) { + b.banlist = make(map[string]bool) + for _, usr := range banned { + b.banlist[usr] = true + } +} + +func (b *Bot) IsBanned(user string) bool { + banned, ok := b.banlist[user] + return ok && banned +} + +func (b *Bot) IsActive(user string) bool { + active, ok := b.activeUsers[user] + return ok && active +} + +func (b *Bot) ResetActivity() { + b.activeUsers = make(map[string]bool) +} + +func (b *Bot) Connect() error { + return b.Client.Connect() +} diff --git a/modules/twitch/client.go b/modules/twitch/client.go new file mode 100644 index 0000000..063c738 --- /dev/null +++ b/modules/twitch/client.go @@ -0,0 +1,40 @@ +package twitch + +import ( + "github.com/nicklaw5/helix" + "github.com/sirupsen/logrus" +) + +type Client struct { + API *helix.Client + logger logrus.FieldLogger +} + +func NewClient(config Config, log logrus.FieldLogger) (*Client, error) { + if log == nil { + log = logrus.New() + } + + // Create Twitch client + api, err := helix.NewClient(&helix.Options{ + ClientID: config.APIClientID, + ClientSecret: config.APIClientSecret, + }) + if err != nil { + return nil, err + } + + // Get access token + resp, err := api.RequestAppAccessToken([]string{"user:read:email"}) + if err != nil { + return nil, err + } + // Set the access token on the client + api.SetAppAccessToken(resp.Data.AccessToken) + log.Info("obtained API access token") + + return &Client{ + API: api, + logger: log, + }, nil +} diff --git a/twitchbot/commands.go b/modules/twitch/commands.go similarity index 94% rename from twitchbot/commands.go rename to modules/twitch/commands.go index 72edfb3..d04f274 100644 --- a/twitchbot/commands.go +++ b/modules/twitch/commands.go @@ -1,4 +1,4 @@ -package twitchbot +package twitch import ( "fmt" @@ -19,7 +19,7 @@ const ( ALTStreamer AccessLevelType = "streamer" ) -type BotCommandHandler func(bot *TwitchBot, message irc.PrivateMessage) +type BotCommandHandler func(bot *Bot, message irc.PrivateMessage) type BotCommand struct { Description string @@ -55,13 +55,13 @@ var commands = map[string]BotCommand{ }, } -func cmdBalance(bot *TwitchBot, message irc.PrivateMessage) { +func cmdBalance(bot *Bot, message irc.PrivateMessage) { // Get user balance balance := bot.Loyalty.GetPoints(message.User.Name) bot.Client.Say(message.Channel, fmt.Sprintf("%s: You have %d %s!", message.User.DisplayName, balance, bot.Loyalty.Config().Currency)) } -func cmdRedeemReward(bot *TwitchBot, message irc.PrivateMessage) { +func cmdRedeemReward(bot *Bot, message irc.PrivateMessage) { parts := strings.Fields(message.Message) if len(parts) < 2 { return @@ -112,7 +112,7 @@ func cmdRedeemReward(bot *TwitchBot, message irc.PrivateMessage) { } } -func cmdGoalList(bot *TwitchBot, message irc.PrivateMessage) { +func cmdGoalList(bot *Bot, message irc.PrivateMessage) { goals := bot.Loyalty.Goals() if len(goals) < 1 { bot.Client.Say(message.Channel, fmt.Sprintf("%s: There are no active community goals right now :(!", message.User.DisplayName)) @@ -129,7 +129,7 @@ func cmdGoalList(bot *TwitchBot, message irc.PrivateMessage) { bot.Client.Say(message.Channel, msg) } -func cmdContributeGoal(bot *TwitchBot, message irc.PrivateMessage) { +func cmdContributeGoal(bot *Bot, message irc.PrivateMessage) { goals := bot.Loyalty.Goals() // Set defaults if user doesn't provide them diff --git a/modules/twitch/module.go b/modules/twitch/module.go new file mode 100644 index 0000000..175bc46 --- /dev/null +++ b/modules/twitch/module.go @@ -0,0 +1,17 @@ +package twitch + +const ConfigKey = "twitch/config" + +type Config struct { + EnableBot bool `json:"enable_bot"` + APIClientID string `json:"api_client_id"` + APIClientSecret string `json:"api_client_secret"` +} + +const BotConfigKey = "twitch/bot-config" + +type BotConfig struct { + Username string `json:"username"` + Token string `json:"oauth"` + Channel string `json:"channel"` +} diff --git a/twitchbot/bot.go b/twitchbot/bot.go deleted file mode 100644 index 204d8d8..0000000 --- a/twitchbot/bot.go +++ /dev/null @@ -1,110 +0,0 @@ -package twitchbot - -import ( - "strings" - "time" - - irc "github.com/gempir/go-twitch-irc/v2" - "github.com/sirupsen/logrus" - "github.com/strimertul/strimertul/modules/loyalty" -) - -type TwitchBot struct { - Client *irc.Client - - username string - logger logrus.FieldLogger - lastMessage time.Time - activeUsers map[string]bool - banlist map[string]bool - - // Module specific vars - Loyalty *loyalty.Manager -} - -func NewBot(username string, token string, log logrus.FieldLogger) *TwitchBot { - if log == nil { - log = logrus.New() - } - - // Create client - client := irc.NewClient(username, token) - - bot := &TwitchBot{ - Client: client, - username: strings.ToLower(username), // Normalize username - logger: log, - lastMessage: time.Now(), - activeUsers: make(map[string]bool), - banlist: make(map[string]bool), - } - - client.OnPrivateMessage(func(message irc.PrivateMessage) { - bot.logger.Debugf("MSG: <%s> %s", message.User.Name, message.Message) - // Ignore messages for a while or twitch will get mad! - if message.Time.Before(bot.lastMessage.Add(time.Second * 2)) { - bot.logger.Debug("message received too soon, ignoring") - return - } - bot.activeUsers[message.User.Name] = true - - // Check if it's a command - if strings.HasPrefix(message.Message, "!") { - // Run through supported commands - for cmd, data := range commands { - if strings.HasPrefix(message.Message, cmd) { - data.Handler(bot, message) - bot.lastMessage = time.Now() - } - } - } - }) - - client.OnUserJoinMessage(func(message irc.UserJoinMessage) { - if strings.ToLower(message.User) == bot.username { - bot.logger.WithField("channel", message.Channel).Info("joined channel") - } else { - bot.logger.WithFields(logrus.Fields{ - "username": message.User, - "channel": message.Channel, - }).Debug("user joined channel") - } - }) - client.OnUserPartMessage(func(message irc.UserPartMessage) { - if strings.ToLower(message.User) == bot.username { - bot.logger.WithField("channel", message.Channel).Info("left channel") - } else { - bot.logger.WithFields(logrus.Fields{ - "username": message.User, - "channel": message.Channel, - }).Debug("user left channel") - } - }) - - return bot -} - -func (b *TwitchBot) SetBanList(banned []string) { - b.banlist = make(map[string]bool) - for _, usr := range banned { - b.banlist[usr] = true - } -} - -func (b *TwitchBot) IsBanned(user string) bool { - banned, ok := b.banlist[user] - return ok && banned -} - -func (b *TwitchBot) IsActive(user string) bool { - active, ok := b.activeUsers[user] - return ok && active -} - -func (b *TwitchBot) ResetActivity() { - b.activeUsers = make(map[string]bool) -} - -func (b *TwitchBot) Connect() error { - return b.Client.Connect() -}