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;
|
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: ":";
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
},
|
},
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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={() => {
|
||||||
|
|
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>,
|
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
4
go.mod
|
@ -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
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 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=
|
||||||
|
|
106
main.go
106
main.go
|
@ -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 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
|
// Get Twitchbot config
|
||||||
var twitchConfig modules.TwitchBotConfig
|
var twitchBotConfig twitch.BotConfig
|
||||||
failOnError(db.GetJSON(modules.TwitchBotConfigKey, &twitchConfig), "Could not retrieve twitch bot config")
|
failOnError(db.GetJSON(twitch.BotConfigKey, &twitchBotConfig), "Could not retrieve twitch bot config")
|
||||||
|
|
||||||
bot := twitchbot.NewBot(twitchConfig.Username, twitchConfig.Token, twitchLogger)
|
|
||||||
bot.Client.Join(twitchConfig.Channel)
|
|
||||||
|
|
||||||
|
// Create and run IRC bot
|
||||||
|
bot := twitch.NewBot(twitchClient, twitchBotConfig)
|
||||||
if moduleConfig.EnableLoyalty {
|
if moduleConfig.EnableLoyalty {
|
||||||
config := loyaltyManager.Config()
|
bot.SetupLoyalty(loyaltyManager)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 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() {
|
go func() {
|
||||||
failOnError(bot.Connect(), "connection failed")
|
failOnError(bot.Connect(), "connection failed")
|
||||||
}()
|
}()
|
||||||
|
} else {
|
||||||
|
log.WithError(err).Error("Twitch initialization failed! Module was temporarily disabled")
|
||||||
|
moduleConfig.EnableTwitch = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create logger and endpoints
|
// Create logger and endpoints
|
||||||
|
|
|
@ -6,7 +6,6 @@ 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"`
|
||||||
|
|
|
@ -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"`
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
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 (
|
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
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