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:
parent
019e48355a
commit
5ade0e3066
22 changed files with 468 additions and 296 deletions
|
@ -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: ":";
|
||||
}
|
||||
|
|
|
@ -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<TwitchConfig>(
|
||||
twitchConfigKey,
|
||||
(state) => state.moduleConfigs?.twitchConfig,
|
||||
(state, { payload }) => {
|
||||
state.moduleConfigs.twitchConfig = payload;
|
||||
},
|
||||
),
|
||||
twitchBotConfig: makeModule<TwitchBotConfig>(
|
||||
twitchBotConfigKey,
|
||||
(state) => state.moduleConfigs?.twitchBotConfig,
|
||||
|
@ -312,6 +327,9 @@ const apiReducer = createSlice({
|
|||
httpConfigChanged(state, { payload }: PayloadAction<HTTPConfig>) {
|
||||
state.moduleConfigs.httpConfig = payload;
|
||||
},
|
||||
twitchConfigChanged(state, { payload }: PayloadAction<TwitchConfig>) {
|
||||
state.moduleConfigs.twitchConfig = payload;
|
||||
},
|
||||
twitchBotConfigChanged(state, { payload }: PayloadAction<TwitchBotConfig>) {
|
||||
state.moduleConfigs.twitchBotConfig = payload;
|
||||
},
|
||||
|
|
|
@ -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 {
|
|||
<Router basepath={basepath}>
|
||||
<Home path="/" />
|
||||
<HTTPPage path="http" />
|
||||
<TwitchBotPage path="twitchbot">
|
||||
<TwitchPage path="twitch">
|
||||
<Redirect from="/" to="settings" noThrow />
|
||||
<TwitchBotSettingsPage path="settings" />
|
||||
<TwitchBotModulesPage path="modules" />
|
||||
<TwitchBotCommandsPage path="commands" />
|
||||
</TwitchBotPage>
|
||||
<TwitchSettingsPage path="settings" />
|
||||
<TwitchBotSettingsPage path="bot/settings" />
|
||||
</TwitchPage>
|
||||
<LoyaltyPage path="loyalty">
|
||||
<Redirect from="/" to="settings" noThrow />
|
||||
<LoyaltySettingPage path="settings" />
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(
|
|||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={!twitchBotActive}
|
||||
disabled={!twitchActive || !twitchBotActive}
|
||||
checked={active}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
|
@ -79,7 +78,9 @@ export default function LoyaltySettingPage(
|
|||
}
|
||||
/>{' '}
|
||||
Enable loyalty points{' '}
|
||||
{twitchBotActive ? '' : '(TwitchBot must be enabled for this!)'}
|
||||
{twitchActive && twitchBotActive
|
||||
? ''
|
||||
: '(Twitch bot must be enabled for this!)'}
|
||||
</label>
|
||||
</div>
|
||||
<div className="field">
|
||||
|
@ -199,31 +200,6 @@ export default function LoyaltySettingPage(
|
|||
/>
|
||||
</p>
|
||||
</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
|
||||
className="button"
|
||||
onClick={() => {
|
||||
|
|
108
frontend/src/ui/pages/twitch/APISettings.tsx
Normal file
108
frontend/src/ui/pages/twitch/APISettings.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -9,33 +9,37 @@ export default function TwitchBotSettingsPage(
|
|||
params: RouteComponentProps<unknown>,
|
||||
): 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 (
|
||||
<>
|
||||
<h1 className="title is-4">Twitch bot configuration</h1>
|
||||
<h1 className="title is-4">Twitch module configuration</h1>
|
||||
<div className="field">
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={active}
|
||||
disabled={busy}
|
||||
checked={botActive}
|
||||
disabled={!twitchActive || busy}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.moduleConfigChanged({
|
||||
...moduleConfig,
|
||||
twitchbot: ev.target.checked,
|
||||
apiReducer.actions.twitchConfigChanged({
|
||||
...twitchConfig,
|
||||
enable_bot: ev.target.checked,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>{' '}
|
||||
Enable twitch bot
|
||||
{twitchActive ? '' : '(Twitch integration must be enabled for this!)'}
|
||||
</label>
|
||||
</div>
|
||||
<div className="field">
|
||||
|
@ -108,6 +112,7 @@ export default function TwitchBotSettingsPage(
|
|||
className="button"
|
||||
onClick={() => {
|
||||
dispatch(setModuleConfig(moduleConfig));
|
||||
dispatch(setTwitchConfig(twitchConfig));
|
||||
dispatch(setTwitchBotConfig(twitchBotConfig));
|
||||
}}
|
||||
>
|
4
go.mod
4
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
|
||||
)
|
||||
|
|
6
go.sum
6
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=
|
||||
|
|
106
main.go
106
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 Twitch config
|
||||
var twitchConfig twitch.Config
|
||||
failOnError(db.GetJSON(twitch.ConfigKey, &twitchConfig), "Could not retrieve twitch config")
|
||||
|
||||
// Create Twitch client
|
||||
twitchClient, err := twitch.NewClient(twitchConfig, twitchLogger)
|
||||
if err == nil {
|
||||
|
||||
// Get Twitchbot config
|
||||
var twitchConfig modules.TwitchBotConfig
|
||||
failOnError(db.GetJSON(modules.TwitchBotConfigKey, &twitchConfig), "Could not retrieve twitch bot config")
|
||||
|
||||
bot := twitchbot.NewBot(twitchConfig.Username, twitchConfig.Token, twitchLogger)
|
||||
bot.Client.Join(twitchConfig.Channel)
|
||||
var twitchBotConfig twitch.BotConfig
|
||||
failOnError(db.GetJSON(twitch.BotConfigKey, &twitchBotConfig), "Could not retrieve twitch bot config")
|
||||
|
||||
// Create and run IRC bot
|
||||
bot := twitch.NewBot(twitchClient, twitchBotConfig)
|
||||
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)
|
||||
|
||||
// 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
|
||||
bot.SetupLoyalty(loyaltyManager)
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
}()
|
||||
} else {
|
||||
log.WithError(err).Error("Twitch initialization failed! Module was temporarily disabled")
|
||||
moduleConfig.EnableTwitch = false
|
||||
}
|
||||
}
|
||||
|
||||
// Create logger and endpoints
|
||||
|
|
|
@ -6,7 +6,6 @@ const ConfigKey = "loyalty/config"
|
|||
|
||||
type Config struct {
|
||||
Currency string `json:"currency"`
|
||||
LiveCheck bool `json:"enable_live_check"`
|
||||
Points struct {
|
||||
Interval int64 `json:"interval"` // in seconds!
|
||||
Amount int64 `json:"amount"`
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
181
modules/twitch/bot.go
Normal file
181
modules/twitch/bot.go
Normal 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
40
modules/twitch/client.go
Normal 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
|
||||
}
|
|
@ -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
|
17
modules/twitch/module.go
Normal file
17
modules/twitch/module.go
Normal 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"`
|
||||
}
|
110
twitchbot/bot.go
110
twitchbot/bot.go
|
@ -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()
|
||||
}
|
Loading…
Reference in a new issue