1
0
Fork 0
mirror of https://git.sr.ht/~ashkeel/strimertul synced 2024-09-18 01:50:50 +00:00

Integrate Twitch APIs internally rather than through stulbe

This commit is contained in:
Ash Keel 2021-05-14 13:15:38 +02:00
parent 019e48355a
commit 5ade0e3066
No known key found for this signature in database
GPG key ID: CF2CC050478BD7E5
22 changed files with 468 additions and 296 deletions

View file

@ -33,6 +33,10 @@ body .button.is-success {
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
} }
.copyblock {
padding-bottom: 1rem;
}
/* Custom reward/goal classes */ /* Custom reward/goal classes */
.reward-disabled, .goal-disabled { .reward-disabled, .goal-disabled {
opacity: 0.5; opacity: 0.5;
@ -175,3 +179,19 @@ span.sortable {
position: fixed; position: fixed;
bottom: 0; right: 0; 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: ":";
}

View file

@ -13,7 +13,8 @@ import KilovoltWS from '@strimertul/kilovolt-client';
// Storage // Storage
const moduleConfigKey = 'stul-meta/modules'; const moduleConfigKey = 'stul-meta/modules';
const httpConfigKey = 'http/config'; const httpConfigKey = 'http/config';
const twitchBotConfigKey = 'twitchbot/config'; const twitchConfigKey = 'twitch/config';
const twitchBotConfigKey = 'twitch/bot-config';
const stulbeConfigKey = 'stulbe/config'; const stulbeConfigKey = 'stulbe/config';
const loyaltyConfigKey = 'loyalty/config'; const loyaltyConfigKey = 'loyalty/config';
const loyaltyStorageKey = 'loyalty/users'; const loyaltyStorageKey = 'loyalty/users';
@ -29,7 +30,7 @@ interface ModuleConfig {
configured: boolean; configured: boolean;
kv: boolean; kv: boolean;
static: boolean; static: boolean;
twitchbot: boolean; twitch: boolean;
stulbe: boolean; stulbe: boolean;
loyalty: boolean; loyalty: boolean;
} }
@ -39,6 +40,12 @@ interface HTTPConfig {
path: string; path: string;
} }
interface TwitchConfig {
enable_bot: boolean;
api_client_id: string;
api_client_secret: string;
}
interface TwitchBotConfig { interface TwitchBotConfig {
username: string; username: string;
oauth: string; oauth: string;
@ -53,7 +60,6 @@ interface StulbeConfig {
interface LoyaltyConfig { interface LoyaltyConfig {
currency: string; currency: string;
enable_live_check: boolean;
points: { points: {
interval: number; interval: number;
amount: number; amount: number;
@ -105,6 +111,7 @@ export interface APIState {
moduleConfigs: { moduleConfigs: {
moduleConfig: ModuleConfig; moduleConfig: ModuleConfig;
httpConfig: HTTPConfig; httpConfig: HTTPConfig;
twitchConfig: TwitchConfig;
twitchBotConfig: TwitchBotConfig; twitchBotConfig: TwitchBotConfig;
stulbeConfig: StulbeConfig; stulbeConfig: StulbeConfig;
loyaltyConfig: LoyaltyConfig; loyaltyConfig: LoyaltyConfig;
@ -124,6 +131,7 @@ const initialState: APIState = {
moduleConfigs: { moduleConfigs: {
moduleConfig: null, moduleConfig: null,
httpConfig: null, httpConfig: null,
twitchConfig: null,
twitchBotConfig: null, twitchBotConfig: null,
stulbeConfig: null, stulbeConfig: null,
loyaltyConfig: null, loyaltyConfig: null,
@ -206,6 +214,13 @@ export const modules = {
state.moduleConfigs.httpConfig = payload; state.moduleConfigs.httpConfig = payload;
}, },
), ),
twitchConfig: makeModule<TwitchConfig>(
twitchConfigKey,
(state) => state.moduleConfigs?.twitchConfig,
(state, { payload }) => {
state.moduleConfigs.twitchConfig = payload;
},
),
twitchBotConfig: makeModule<TwitchBotConfig>( twitchBotConfig: makeModule<TwitchBotConfig>(
twitchBotConfigKey, twitchBotConfigKey,
(state) => state.moduleConfigs?.twitchBotConfig, (state) => state.moduleConfigs?.twitchBotConfig,
@ -312,6 +327,9 @@ const apiReducer = createSlice({
httpConfigChanged(state, { payload }: PayloadAction<HTTPConfig>) { httpConfigChanged(state, { payload }: PayloadAction<HTTPConfig>) {
state.moduleConfigs.httpConfig = payload; state.moduleConfigs.httpConfig = payload;
}, },
twitchConfigChanged(state, { payload }: PayloadAction<TwitchConfig>) {
state.moduleConfigs.twitchConfig = payload;
},
twitchBotConfigChanged(state, { payload }: PayloadAction<TwitchBotConfig>) { twitchBotConfigChanged(state, { payload }: PayloadAction<TwitchBotConfig>) {
state.moduleConfigs.twitchBotConfig = payload; state.moduleConfigs.twitchBotConfig = payload;
}, },

View file

@ -5,8 +5,7 @@ import { RootState } from '../store';
import { createWSClient } from '../store/api/reducer'; import { createWSClient } from '../store/api/reducer';
import Home from './pages/Home'; import Home from './pages/Home';
import HTTPPage from './pages/HTTP'; import HTTPPage from './pages/HTTP';
import TwitchBotPage from './pages/twitchbot/Main'; import TwitchPage from './pages/twitch/Main';
import TwitchBotSettingsPage from './pages/twitchbot/Settings';
import StulbePage from './pages/Stulbe'; import StulbePage from './pages/Stulbe';
import LoyaltyPage from './pages/loyalty/Main'; import LoyaltyPage from './pages/loyalty/Main';
import DebugPage from './pages/Debug'; import DebugPage from './pages/Debug';
@ -14,9 +13,9 @@ import LoyaltySettingPage from './pages/loyalty/Settings';
import LoyaltyRewardsPage from './pages/loyalty/Rewards'; import LoyaltyRewardsPage from './pages/loyalty/Rewards';
import LoyaltyUserListPage from './pages/loyalty/UserList'; import LoyaltyUserListPage from './pages/loyalty/UserList';
import LoyaltyGoalsPage from './pages/loyalty/Goals'; 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 LoyaltyRedeemQueuePage from './pages/loyalty/Queue';
import TwitchSettingsPage from './pages/twitch/APISettings';
import TwitchBotSettingsPage from './pages/twitch/BotSettings';
interface RouteItem { interface RouteItem {
name: string; name: string;
@ -34,20 +33,16 @@ const menu: RouteItem[] = [
route: '/http', route: '/http',
}, },
{ {
name: 'Twitch Bot', name: 'Twitch integration',
route: '/twitchbot/', route: '/twitch/',
subroutes: [ subroutes: [
{ {
name: 'Configuration', name: 'Module Configuration',
route: '/twitchbot/settings', route: '/twitch/settings',
}, },
{ {
name: 'Modules', name: 'Bot Configuration',
route: '/twitchbot/modules', route: '/twitch/bot/settings',
},
{
name: 'Custom commands',
route: '/twitchbot/commands',
}, },
], ],
}, },
@ -77,10 +72,6 @@ const menu: RouteItem[] = [
}, },
], ],
}, },
{
name: 'Stulbe integration',
route: '/stulbe',
},
]; ];
export default function App(): React.ReactElement { export default function App(): React.ReactElement {
@ -147,12 +138,11 @@ export default function App(): React.ReactElement {
<Router basepath={basepath}> <Router basepath={basepath}>
<Home path="/" /> <Home path="/" />
<HTTPPage path="http" /> <HTTPPage path="http" />
<TwitchBotPage path="twitchbot"> <TwitchPage path="twitch">
<Redirect from="/" to="settings" noThrow /> <Redirect from="/" to="settings" noThrow />
<TwitchBotSettingsPage path="settings" /> <TwitchSettingsPage path="settings" />
<TwitchBotModulesPage path="modules" /> <TwitchBotSettingsPage path="bot/settings" />
<TwitchBotCommandsPage path="commands" /> </TwitchPage>
</TwitchBotPage>
<LoyaltyPage path="loyalty"> <LoyaltyPage path="loyalty">
<Redirect from="/" to="settings" noThrow /> <Redirect from="/" to="settings" noThrow />
<LoyaltySettingPage path="settings" /> <LoyaltySettingPage path="settings" />

View file

@ -291,9 +291,9 @@ export default function LoyaltyGoalsPage(
const dispatch = useDispatch(); const dispatch = useDispatch();
const twitchBotActive = moduleConfig?.twitchbot ?? false; const twitchActive = moduleConfig?.twitch ?? false;
const loyaltyEnabled = moduleConfig?.loyalty ?? false; const loyaltyEnabled = moduleConfig?.loyalty ?? false;
const active = twitchBotActive && loyaltyEnabled; const active = twitchActive && loyaltyEnabled;
const [goalFilter, setGoalFilter] = useState(''); const [goalFilter, setGoalFilter] = useState('');
const goalFilterLC = goalFilter.toLowerCase(); const goalFilterLC = goalFilter.toLowerCase();

View file

@ -324,9 +324,9 @@ export default function LoyaltyRewardsPage(
const dispatch = useDispatch(); const dispatch = useDispatch();
const twitchBotActive = moduleConfig?.twitchbot ?? false; const twitchActive = moduleConfig?.twitch ?? false;
const loyaltyEnabled = moduleConfig?.loyalty ?? false; const loyaltyEnabled = moduleConfig?.loyalty ?? false;
const active = twitchBotActive && loyaltyEnabled; const active = twitchActive && loyaltyEnabled;
const [rewardFilter, setRewardFilter] = useState(''); const [rewardFilter, setRewardFilter] = useState('');
const rewardFilterLC = rewardFilter.toLowerCase(); const rewardFilterLC = rewardFilter.toLowerCase();

View file

@ -20,12 +20,14 @@ export default function LoyaltySettingPage(
): React.ReactElement { ): React.ReactElement {
const [loyaltyConfig, setLoyaltyConfig] = useModule(modules.loyaltyConfig); const [loyaltyConfig, setLoyaltyConfig] = useModule(modules.loyaltyConfig);
const [moduleConfig, setModuleConfig] = useModule(modules.moduleConfig); const [moduleConfig, setModuleConfig] = useModule(modules.moduleConfig);
const [twitchConfig] = useModule(modules.twitchConfig);
const dispatch = useDispatch(); 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 loyaltyEnabled = moduleConfig?.loyalty ?? false;
const active = twitchBotActive && loyaltyEnabled; const active = twitchActive && twitchBotActive && loyaltyEnabled;
const [tempIntervalNum, setTempIntervalNum] = useState(null); const [tempIntervalNum, setTempIntervalNum] = useState(null);
const [tempIntervalMult, setTempIntervalMult] = useState(null); const [tempIntervalMult, setTempIntervalMult] = useState(null);
@ -34,9 +36,6 @@ export default function LoyaltySettingPage(
loyaltyConfig?.points?.interval ?? 0, loyaltyConfig?.points?.interval ?? 0,
); );
const stulbeEnabled = moduleConfig?.stulbe ?? false;
const liveCheck = loyaltyConfig?.enable_live_check ?? false;
useEffect(() => { useEffect(() => {
if (loyaltyConfig?.points) { if (loyaltyConfig?.points) {
if (tempIntervalNum === null) { if (tempIntervalNum === null) {
@ -67,7 +66,7 @@ export default function LoyaltySettingPage(
<label className="checkbox"> <label className="checkbox">
<input <input
type="checkbox" type="checkbox"
disabled={!twitchBotActive} disabled={!twitchActive || !twitchBotActive}
checked={active} checked={active}
onChange={(ev) => onChange={(ev) =>
dispatch( dispatch(
@ -79,7 +78,9 @@ export default function LoyaltySettingPage(
} }
/>{' '} />{' '}
Enable loyalty points{' '} Enable loyalty points{' '}
{twitchBotActive ? '' : '(TwitchBot must be enabled for this!)'} {twitchActive && twitchBotActive
? ''
: '(Twitch bot must be enabled for this!)'}
</label> </label>
</div> </div>
<div className="field"> <div className="field">
@ -199,31 +200,6 @@ export default function LoyaltySettingPage(
/> />
</p> </p>
</div> </div>
<div className="field">
<label className="checkbox">
<input
type="checkbox"
disabled={!active || !stulbeEnabled}
checked={liveCheck}
onChange={(ev) =>
dispatch(
setLoyaltyConfig({
...loyaltyConfig,
enable_live_check: ev.target.checked,
}),
)
}
/>{' '}
Enable check to only add points when live{' '}
{stulbeEnabled ? (
'(requires Stulbe)'
) : (
<span className="has-text-danger">
(Stulbe must be enabled for this!)
</span>
)}
</label>
</div>
<button <button
className="button" className="button"
onClick={() => { onClick={() => {

View file

@ -0,0 +1,108 @@
import { RouteComponentProps } from '@reach/router';
import React from 'react';
import { useDispatch } from 'react-redux';
import { useModule } from '../../../lib/react-utils';
import apiReducer, { modules } from '../../../store/api/reducer';
export default function TwitchBotSettingsPage(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
params: RouteComponentProps<unknown>,
): React.ReactElement {
const [moduleConfig, setModuleConfig] = useModule(modules.moduleConfig);
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
const dispatch = useDispatch();
const busy = moduleConfig === null;
const active = moduleConfig?.twitch ?? false;
return (
<>
<h1 className="title is-4">Twitch module configuration</h1>
<div className="field">
<label className="checkbox">
<input
type="checkbox"
checked={active}
disabled={busy}
onChange={(ev) =>
dispatch(
apiReducer.actions.moduleConfigChanged({
...moduleConfig,
twitch: ev.target.checked,
}),
)
}
/>{' '}
Enable twitch integration
</label>
</div>
<div className="copyblock">
<p>You will need to create an application, here's how:</p>
<p>
- Go to{' '}
<a href="https://dev.twitch.tv/console/apps/create">
https://dev.twitch.tv/console/apps/create
</a>
.
</p>
<p>- Use the following data for the required fields:</p>
<dl className="inline-dl">
<dt>OAuth Redirect URLs</dt>
<dd>http://localhost:4337/oauth</dd>
<dt>Category</dt>
<dd>Broadcasting Suite</dd>
</dl>
- Once created, create a <b>New Secret</b>, then copy both fields below!
</div>
<div className="field">
<label className="label">App Client ID</label>
<p className="control">
<input
disabled={!active}
className="input"
type="text"
placeholder="App Client ID"
value={twitchConfig?.api_client_id ?? ''}
onChange={(ev) =>
dispatch(
apiReducer.actions.twitchConfigChanged({
...twitchConfig,
api_client_id: ev.target.value,
}),
)
}
/>
</p>
</div>
<div className="field">
<label className="label">App Client Secret</label>
<p className="control">
<input
disabled={!active}
className="input"
type="password"
placeholder="App Client Secret"
value={twitchConfig?.api_client_secret ?? ''}
onChange={(ev) =>
dispatch(
apiReducer.actions.twitchConfigChanged({
...twitchConfig,
api_client_secret: ev.target.value,
}),
)
}
/>
</p>
</div>
<button
className="button"
onClick={() => {
dispatch(setModuleConfig(moduleConfig));
dispatch(setTwitchConfig(twitchConfig));
}}
>
Save
</button>
</>
);
}

View file

@ -9,33 +9,37 @@ export default function TwitchBotSettingsPage(
params: RouteComponentProps<unknown>, params: RouteComponentProps<unknown>,
): React.ReactElement { ): React.ReactElement {
const [moduleConfig, setModuleConfig] = useModule(modules.moduleConfig); const [moduleConfig, setModuleConfig] = useModule(modules.moduleConfig);
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
const [twitchBotConfig, setTwitchBotConfig] = useModule( const [twitchBotConfig, setTwitchBotConfig] = useModule(
modules.twitchBotConfig, modules.twitchBotConfig,
); );
const dispatch = useDispatch(); const dispatch = useDispatch();
const busy = moduleConfig === null; 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 ( return (
<> <>
<h1 className="title is-4">Twitch bot configuration</h1> <h1 className="title is-4">Twitch module configuration</h1>
<div className="field"> <div className="field">
<label className="checkbox"> <label className="checkbox">
<input <input
type="checkbox" type="checkbox"
checked={active} checked={botActive}
disabled={busy} disabled={!twitchActive || busy}
onChange={(ev) => onChange={(ev) =>
dispatch( dispatch(
apiReducer.actions.moduleConfigChanged({ apiReducer.actions.twitchConfigChanged({
...moduleConfig, ...twitchConfig,
twitchbot: ev.target.checked, enable_bot: ev.target.checked,
}), }),
) )
} }
/>{' '} />{' '}
Enable twitch bot Enable twitch bot
{twitchActive ? '' : '(Twitch integration must be enabled for this!)'}
</label> </label>
</div> </div>
<div className="field"> <div className="field">
@ -108,6 +112,7 @@ export default function TwitchBotSettingsPage(
className="button" className="button"
onClick={() => { onClick={() => {
dispatch(setModuleConfig(moduleConfig)); dispatch(setModuleConfig(moduleConfig));
dispatch(setTwitchConfig(twitchConfig));
dispatch(setTwitchBotConfig(twitchBotConfig)); dispatch(setTwitchBotConfig(twitchBotConfig));
}} }}
> >

4
go.mod
View file

@ -7,9 +7,9 @@ require (
github.com/gempir/go-twitch-irc/v2 v2.5.0 github.com/gempir/go-twitch-irc/v2 v2.5.0
github.com/json-iterator/go v1.1.11 github.com/json-iterator/go v1.1.11
github.com/mattn/go-colorable v0.1.8 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/pkg/browser v0.0.0-20210115035449-ce105d075bb4
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/strimertul/kilovolt/v3 v3.0.3 github.com/strimertul/kilovolt/v3 v3.0.3
github.com/strimertul/stulbe v0.2.5 github.com/strimertul/stulbe-client-go v0.1.0
) )

6
go.sum
View file

@ -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 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 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.13.1/go.mod h1:XeeXY7oY5W+MVMu6wF4qGm8uvjZ1/Nss0FqprVkXKrg=
github.com/nicklaw5/helix v1.14.0 h1:yJI+dUDxFzmlSelNygWs/lhirvuzCqgIXIZy05JdHVk= github.com/nicklaw5/helix v1.15.0 h1:CiWWPk7sBOnqS8ZrsJogx8wzDRD4d+CnrtVHMMyWJSY=
github.com/nicklaw5/helix v1.14.0/go.mod h1:XeeXY7oY5W+MVMu6wF4qGm8uvjZ1/Nss0FqprVkXKrg= 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 h1:Ak86L+yDSOzKFa7WM5bf5itSOo1e3Xh8bm5YCMUXIjQ=
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= 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= 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/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 h1:qrJFwttrWfwSfHsvTgI3moZjhBwbYN8Xe8gicCeISpc=
github.com/strimertul/stulbe v0.2.5/go.mod h1:0AsY4OVf1dNCwOn9s7KySuAxJ85w88pXeostu1n9E7w= 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/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/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= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=

112
main.go
View file

@ -14,8 +14,7 @@ import (
"github.com/strimertul/strimertul/database" "github.com/strimertul/strimertul/database"
"github.com/strimertul/strimertul/modules" "github.com/strimertul/strimertul/modules"
"github.com/strimertul/strimertul/modules/loyalty" "github.com/strimertul/strimertul/modules/loyalty"
"github.com/strimertul/strimertul/modules/stulbe" "github.com/strimertul/strimertul/modules/twitch"
"github.com/strimertul/strimertul/twitchbot"
"github.com/dgraph-io/badger/v3" "github.com/dgraph-io/badger/v3"
@ -103,7 +102,7 @@ func main() {
failOnError(db.PutJSON(modules.ModuleConfigKey, modules.ModuleConfig{ failOnError(db.PutJSON(modules.ModuleConfigKey, modules.ModuleConfig{
EnableKV: true, EnableKV: true,
EnableStaticServer: false, EnableStaticServer: false,
EnableTwitchbot: false, EnableTwitch: false,
EnableStulbe: false, EnableStulbe: false,
CompletedOnboarding: true, CompletedOnboarding: true,
}), "could not save onboarding config") }), "could not save onboarding config")
@ -119,6 +118,7 @@ func main() {
failOnError(db.GetJSON(modules.HTTPServerConfigKey, &httpConfig), "Could not retrieve HTTP server config") failOnError(db.GetJSON(modules.HTTPServerConfigKey, &httpConfig), "Could not retrieve HTTP server config")
// Get Stulbe config, if enabled // Get Stulbe config, if enabled
/* Kinda deprecated, This will probably be removed/replaced in the future!
var stulbeManager *stulbe.Manager = nil var stulbeManager *stulbe.Manager = nil
if moduleConfig.EnableStulbe { if moduleConfig.EnableStulbe {
stulbeManager, err = stulbe.Initialize(db, wrapLogger("stulbe")) stulbeManager, err = stulbe.Initialize(db, wrapLogger("stulbe"))
@ -128,105 +128,47 @@ func main() {
} }
defer stulbeManager.Close() defer stulbeManager.Close()
} }
*/
var loyaltyManager *loyalty.Manager var loyaltyManager *loyalty.Manager
loyaltyLogger := wrapLogger("loyalty") loyaltyLogger := wrapLogger("loyalty")
if moduleConfig.EnableLoyalty { if moduleConfig.EnableLoyalty {
loyaltyManager, err = loyalty.NewManager(db, hub, loyaltyLogger) loyaltyManager, err = loyalty.NewManager(db, hub, loyaltyLogger)
if err != nil { 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 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 //TODO Refactor this to something sane
if moduleConfig.EnableTwitchbot { if moduleConfig.EnableTwitch {
// Create logger // Create logger
twitchLogger := wrapLogger("twitchbot") twitchLogger := wrapLogger("twitch")
// Get Twitchbot config // Get Twitch config
var twitchConfig modules.TwitchBotConfig var twitchConfig twitch.Config
failOnError(db.GetJSON(modules.TwitchBotConfigKey, &twitchConfig), "Could not retrieve twitch bot config") failOnError(db.GetJSON(twitch.ConfigKey, &twitchConfig), "Could not retrieve twitch config")
bot := twitchbot.NewBot(twitchConfig.Username, twitchConfig.Token, twitchLogger) // Create Twitch client
bot.Client.Join(twitchConfig.Channel) twitchClient, err := twitch.NewClient(twitchConfig, twitchLogger)
if err == nil {
if moduleConfig.EnableLoyalty { // Get Twitchbot config
config := loyaltyManager.Config() var twitchBotConfig twitch.BotConfig
bot.Loyalty = loyaltyManager failOnError(db.GetJSON(twitch.BotConfigKey, &twitchBotConfig), "Could not retrieve twitch bot config")
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)
// Check if streamer is online, if possible // Create and run IRC bot
streamOnline := true bot := twitch.NewBot(twitchClient, twitchBotConfig)
if config.LiveCheck && stulbeManager != nil { if moduleConfig.EnableLoyalty {
status, err := stulbeManager.Client.StreamStatus(twitchConfig.Channel) bot.SetupLoyalty(loyaltyManager)
if err != nil { }
twitchLogger.WithError(err).Error("Error checking stream status") go func() {
} else { failOnError(bot.Connect(), "connection failed")
streamOnline = status != nil }()
} } else {
} log.WithError(err).Error("Twitch initialization failed! Module was temporarily disabled")
moduleConfig.EnableTwitch = false
// 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)
}
}
}()
}
})
} }
go func() {
failOnError(bot.Connect(), "connection failed")
}()
} }
// Create logger and endpoints // Create logger and endpoints

View file

@ -5,9 +5,8 @@ import "time"
const ConfigKey = "loyalty/config" const ConfigKey = "loyalty/config"
type Config struct { type Config struct {
Currency string `json:"currency"` Currency string `json:"currency"`
LiveCheck bool `json:"enable_live_check"` Points struct {
Points struct {
Interval int64 `json:"interval"` // in seconds! Interval int64 `json:"interval"` // in seconds!
Amount int64 `json:"amount"` Amount int64 `json:"amount"`
ActivityBonus int64 `json:"activity_bonus"` ActivityBonus int64 `json:"activity_bonus"`

View file

@ -6,7 +6,7 @@ type ModuleConfig struct {
CompletedOnboarding bool `json:"configured"` CompletedOnboarding bool `json:"configured"`
EnableKV bool `json:"kv"` EnableKV bool `json:"kv"`
EnableStaticServer bool `json:"static"` EnableStaticServer bool `json:"static"`
EnableTwitchbot bool `json:"twitchbot"` EnableTwitch bool `json:"twitch"`
EnableStulbe bool `json:"stulbe"` EnableStulbe bool `json:"stulbe"`
EnableLoyalty bool `json:"loyalty"` EnableLoyalty bool `json:"loyalty"`
} }
@ -17,19 +17,3 @@ type HTTPServerConfig struct {
Bind string `json:"bind"` Bind string `json:"bind"`
Path string `json:"path"` 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"`
}

View file

@ -5,9 +5,9 @@ import (
"github.com/sirupsen/logrus" "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 { type Manager struct {

181
modules/twitch/bot.go Normal file
View file

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

40
modules/twitch/client.go Normal file
View file

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

View file

@ -1,4 +1,4 @@
package twitchbot package twitch
import ( import (
"fmt" "fmt"
@ -19,7 +19,7 @@ const (
ALTStreamer AccessLevelType = "streamer" ALTStreamer AccessLevelType = "streamer"
) )
type BotCommandHandler func(bot *TwitchBot, message irc.PrivateMessage) type BotCommandHandler func(bot *Bot, message irc.PrivateMessage)
type BotCommand struct { type BotCommand struct {
Description string 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 // Get user balance
balance := bot.Loyalty.GetPoints(message.User.Name) 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)) 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) parts := strings.Fields(message.Message)
if len(parts) < 2 { if len(parts) < 2 {
return 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() goals := bot.Loyalty.Goals()
if len(goals) < 1 { if len(goals) < 1 {
bot.Client.Say(message.Channel, fmt.Sprintf("%s: There are no active community goals right now :(!", message.User.DisplayName)) 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) bot.Client.Say(message.Channel, msg)
} }
func cmdContributeGoal(bot *TwitchBot, message irc.PrivateMessage) { func cmdContributeGoal(bot *Bot, message irc.PrivateMessage) {
goals := bot.Loyalty.Goals() goals := bot.Loyalty.Goals()
// Set defaults if user doesn't provide them // Set defaults if user doesn't provide them

17
modules/twitch/module.go Normal file
View file

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

View file

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