diff --git a/frontend/src/lib/react-utils.ts b/frontend/src/lib/react-utils.ts
index 029d2f7..0ddc0e7 100644
--- a/frontend/src/lib/react-utils.ts
+++ b/frontend/src/lib/react-utils.ts
@@ -62,30 +62,7 @@ export function useUserPoints(): LoyaltyStorage {
return data;
}
-export function useInterval(
- initialValue: number,
-): [
- number,
- number,
- number,
- (newNum: number) => void,
- (newMult: number) => void,
-] {
- const [value, setValue] = useState(initialValue);
-
- const [numInitialValue, multInitialValue] = getInterval(value);
- const [num, setNum] = useState(numInitialValue);
- const [mult, setMult] = useState(multInitialValue);
-
- useEffect(() => {
- setValue(num * mult);
- }, [num, mult]);
-
- return [value, num, mult, setNum, setMult];
-}
-
export default {
useModule,
useUserPoints,
- useInterval,
};
diff --git a/frontend/src/locale/en/translation.json b/frontend/src/locale/en/translation.json
index 27c0ab8..12b8b51 100644
--- a/frontend/src/locale/en/translation.json
+++ b/frontend/src/locale/en/translation.json
@@ -6,6 +6,7 @@
"/twitch/settings": "Module configuration",
"/twitch/bot/settings": "Bot configuration",
"/twitch/bot/commands": "Bot commands",
+ "/twitch/bot/timers": "Bot timers",
"/twitch/bot/modules": "Bot modules",
"/loyalty": "Loyalty points",
"/loyalty/settings": "Configuration",
@@ -204,6 +205,20 @@
"new-command": "New command",
"search": "Search by name",
"modify-command": "Modify command"
+ },
+ "timers": {
+ "header": "Bot timers",
+ "new-timer": "New timer",
+ "modify-timer": "Modify timer",
+ "search": "Search by timer name",
+ "messages": "Messages",
+ "name-hint": "Timer name",
+ "name": "Name",
+ "message-help": "What to write in chat",
+ "minimum-delay": "Interval",
+ "minimum-delay-help": "How many time must pass between each repeat.",
+ "minimum-activity": "Minimum chat activity",
+ "minimum-activity-post": "messages in the last 5 minutes"
}
}
}
diff --git a/frontend/src/store/api/reducer.ts b/frontend/src/store/api/reducer.ts
index 40cf4dd..ed6129c 100644
--- a/frontend/src/store/api/reducer.ts
+++ b/frontend/src/store/api/reducer.ts
@@ -147,6 +147,13 @@ export const modules = {
state.moduleConfigs.twitchBotConfig = payload;
},
),
+ twitchBotModulesConfig: makeModule(
+ 'twitch/bot-modules/config',
+ (state) => state.twitchBot?.modules,
+ (state, { payload }) => {
+ state.twitchBot.modules = payload;
+ },
+ ),
twitchBotCommands: makeModule(
'twitch/bot-custom-commands',
(state) => state.twitchBot?.commands,
@@ -154,6 +161,13 @@ export const modules = {
state.twitchBot.commands = payload;
},
),
+ twitchBotTimers: makeModule(
+ 'twitch/bot-modules/timers/config',
+ (state) => state.twitchBot?.timers,
+ (state, { payload }) => {
+ state.twitchBot.timers = payload;
+ },
+ ),
stulbeConfig: makeModule(
'stulbe/config',
(state) => state.moduleConfigs?.stulbeConfig,
@@ -229,6 +243,8 @@ const initialState: APIState = {
},
twitchBot: {
commands: null,
+ modules: null,
+ timers: null,
},
moduleConfigs: {
moduleConfig: null,
diff --git a/frontend/src/store/api/types.ts b/frontend/src/store/api/types.ts
index 161a724..2e4bc76 100644
--- a/frontend/src/store/api/types.ts
+++ b/frontend/src/store/api/types.ts
@@ -30,6 +30,10 @@ interface TwitchBotConfig {
chat_history: number;
}
+interface TwitchModulesConfig {
+ enable_timers: boolean;
+}
+
type AccessLevelType = 'everyone' | 'vip' | 'moderators' | 'streamer';
export interface TwitchBotCustomCommand {
@@ -57,6 +61,18 @@ interface LoyaltyConfig {
banlist: string[];
}
+export interface TwitchBotTimer {
+ enabled: boolean;
+ name: string;
+ minimum_chat_activity: number;
+ minimum_delay: number;
+ messages: string[];
+}
+
+interface TwitchBotTimersConfig {
+ timers: Record;
+}
+
export interface LoyaltyPointsEntry {
points: number;
}
@@ -105,6 +121,8 @@ export interface APIState {
};
twitchBot: {
commands: TwitchBotCustomCommands;
+ modules: TwitchModulesConfig;
+ timers: TwitchBotTimersConfig;
};
moduleConfigs: {
moduleConfig: ModuleConfig;
diff --git a/frontend/src/ui/App.tsx b/frontend/src/ui/App.tsx
index 86b6940..678da15 100644
--- a/frontend/src/ui/App.tsx
+++ b/frontend/src/ui/App.tsx
@@ -22,6 +22,7 @@ import TwitchBotCommandsPage from './pages/twitch/Commands';
import TwitchBotModulesPage from './pages/twitch/Modules';
import StulbeConfigPage from './pages/stulbe/Config';
import StulbeWebhooksPage from './pages/stulbe/Webhook';
+import TwitchBotTimersPage from './pages/twitch/Timers';
interface RouteItem {
name?: string;
@@ -38,6 +39,7 @@ const menu: RouteItem[] = [
{ route: '/twitch/settings' },
{ route: '/twitch/bot/settings' },
{ route: '/twitch/bot/commands' },
+ { route: '/twitch/bot/timers' },
{ route: '/twitch/bot/modules' },
],
},
@@ -127,6 +129,7 @@ export default function App(): React.ReactElement {
+
diff --git a/frontend/src/ui/components/Interval.tsx b/frontend/src/ui/components/Interval.tsx
index d4bc92d..6f034a9 100644
--- a/frontend/src/ui/components/Interval.tsx
+++ b/frontend/src/ui/components/Interval.tsx
@@ -1,19 +1,29 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { useInterval } from '../../lib/react-utils';
+import { getInterval } from '../../lib/time-utils';
export interface IntervalProps {
active: boolean;
value: number;
+ min?: number;
onChange?: (value: number) => void;
}
-function Interval({ active, value, onChange }: IntervalProps) {
+function Interval({ active, value, min, onChange }: IntervalProps) {
const { t } = useTranslation();
- const [valueNum, num, mult, setNum, setMult] = useInterval(value);
+
+ const [numInitialValue, multInitialValue] = getInterval(value);
+ const [num, setNum] = useState(numInitialValue);
+ const [mult, setMult] = useState(multInitialValue);
+
useEffect(() => {
- onChange(valueNum);
- }, [valueNum]);
+ const seconds = num * mult;
+ if (min && seconds < min) {
+ setNum(5);
+ setMult(1);
+ }
+ onChange(Math.max(min ?? 0, seconds));
+ }, [num, mult]);
return (
<>
@@ -24,6 +34,7 @@ function Interval({ active, value, onChange }: IntervalProps) {
type="number"
placeholder="#"
value={num ?? ''}
+ style={{ width: '6em' }}
onChange={(ev) => {
const intNum = parseInt(ev.target.value, 10);
if (Number.isNaN(intNum)) {
diff --git a/frontend/src/ui/pages/loyalty/Rewards.tsx b/frontend/src/ui/pages/loyalty/Rewards.tsx
index 2469066..ad12ee2 100644
--- a/frontend/src/ui/pages/loyalty/Rewards.tsx
+++ b/frontend/src/ui/pages/loyalty/Rewards.tsx
@@ -3,7 +3,7 @@ import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import prettyTime from 'pretty-ms';
import { useTranslation } from 'react-i18next';
-import { useInterval, useModule } from '../../../lib/react-utils';
+import { useModule } from '../../../lib/react-utils';
import { RootState } from '../../../store';
import { createRedeem, modules } from '../../../store/api/reducer';
import Modal from '../../components/Modal';
diff --git a/frontend/src/ui/pages/loyalty/Settings.tsx b/frontend/src/ui/pages/loyalty/Settings.tsx
index f55fa36..cb2ac57 100644
--- a/frontend/src/ui/pages/loyalty/Settings.tsx
+++ b/frontend/src/ui/pages/loyalty/Settings.tsx
@@ -122,7 +122,12 @@ export default function LoyaltySettingPage(
{t('loyalty.config.points-every')}
-
+
diff --git a/frontend/src/ui/pages/twitch/Timers.tsx b/frontend/src/ui/pages/twitch/Timers.tsx
new file mode 100644
index 0000000..68065f3
--- /dev/null
+++ b/frontend/src/ui/pages/twitch/Timers.tsx
@@ -0,0 +1,319 @@
+import { RouteComponentProps } from '@reach/router';
+import React, { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useDispatch } from 'react-redux';
+import { useModule } from '../../../lib/react-utils';
+import { modules } from '../../../store/api/reducer';
+import Modal from '../../components/Modal';
+import { TwitchBotTimer } from '../../../store/api/types';
+import Field from '../../components/Field';
+import Interval from '../../components/Interval';
+
+interface TimerItemProps {
+ item: TwitchBotTimer;
+ onToggleState: () => void;
+ onEdit: () => void;
+ onDelete: () => void;
+}
+function TimerItem({ item, onToggleState, onEdit, onDelete }: TimerItemProps) {
+ const { t } = useTranslation();
+ const [expanded, setExpanded] = useState(false);
+
+ return (
+
+
+ {expanded ? (
+
+ {t('twitch.timers.messages')}:{' '}
+ {item.messages.map((message) => (
+
{message}
+ ))}
+
+
+ ) : null}
+
+ );
+}
+
+interface TimerModalProps {
+ active: boolean;
+ onConfirm: (newName: string, r: TwitchBotTimer) => void;
+ onClose: () => void;
+ initialData?: TwitchBotTimer;
+ initialName?: string;
+ title: string;
+ confirmText: string;
+}
+
+function TimerModal({
+ active,
+ onConfirm,
+ onClose,
+ initialName,
+ initialData,
+ title,
+ confirmText,
+}: TimerModalProps) {
+ const [name, setName] = useState(initialName ?? '');
+ const [messages, setMessages] = useState(initialData?.messages ?? ['']);
+ const [minDelay, setMinDelay] = useState(initialData?.minimum_delay ?? 300);
+ const [minActivity, setMinActivity] = useState(
+ initialData?.minimum_chat_activity ?? 5,
+ );
+
+ const { t } = useTranslation();
+ const validForm = name !== '' && messages.length > 0 && messages[0] !== '';
+
+ const confirm = () => {
+ if (onConfirm) {
+ onConfirm(name, {
+ name,
+ messages,
+ minimum_chat_activity: 0,
+ minimum_delay: 0,
+ enabled: initialData?.enabled ?? false,
+ });
+ }
+ };
+
+ const setMessageIndex = (value: string, index: number) => {
+ const newMessages = [...messages];
+ newMessages[index] = value;
+ setMessages(newMessages);
+ };
+
+ return (
+ confirm()}
+ onClose={() => onClose()}
+ >
+
+
+
+
+ setName(ev.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ {messages.map((message, index) => (
+
+ ))}
+
+
+
+ );
+}
+
+export default function TwitchBotTimersPage(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ props: RouteComponentProps,
+): React.ReactElement {
+ const [timers, setTimers] = useModule(modules.twitchBotTimers);
+ const dispatch = useDispatch();
+ const { t } = useTranslation();
+
+ const [createModal, setCreateModal] = useState(false);
+ const [showModifyTimer, setShowModifyTimer] = useState(null);
+ const [timerFilter, setTimerFilter] = useState('');
+ const timerFilterLC = timerFilter.toLowerCase();
+
+ const createTimer = (name: string, data: TwitchBotTimer): void => {
+ dispatch(
+ setTimers({
+ ...timers,
+ [name]: data,
+ }),
+ );
+ setCreateModal(false);
+ };
+
+ const modifyTimer = (
+ oldName: string,
+ newName: string,
+ data: TwitchBotTimer,
+ ): void => {
+ dispatch(
+ setTimers({
+ ...timers,
+ [oldName]: undefined,
+ [newName]: {
+ ...timers[oldName],
+ ...data,
+ },
+ }),
+ );
+ setShowModifyTimer(null);
+ };
+
+ const deleteTimer = (cmd: string): void => {
+ dispatch(
+ setTimers({
+ ...timers,
+ [cmd]: undefined,
+ }),
+ );
+ };
+
+ const toggleTimer = (cmd: string): void => {
+ dispatch(
+ setTimers({
+ ...timers,
+ [cmd]: {
+ ...timers[cmd],
+ enabled: !timers[cmd].enabled,
+ },
+ }),
+ );
+ };
+
+ return (
+ <>
+ {t('twitch.timers.header')}
+
+
+
+
+
+
+ setTimerFilter(ev.target.value)}
+ />
+
+
+
+ setCreateModal(false)}
+ />
+ {showModifyTimer ? (
+
+ modifyTimer(showModifyTimer, newName, cmdData)
+ }
+ initialName={showModifyTimer}
+ initialData={showModifyTimer ? timers[showModifyTimer] : null}
+ onClose={() => setShowModifyTimer(null)}
+ />
+ ) : null}
+
+ {Object.keys(timers ?? {})
+ ?.filter((cmd) => cmd.toLowerCase().includes(timerFilterLC))
+ .map((timer) => (
+ deleteTimer(timer)}
+ onEdit={() => setShowModifyTimer(timer)}
+ onToggleState={() => toggleTimer(timer)}
+ />
+ ))}
+
+ >
+ );
+}
diff --git a/modules/twitch/modules.timer.go b/modules/twitch/modules.timer.go
index e3e01fe..879726f 100644
--- a/modules/twitch/modules.timer.go
+++ b/modules/twitch/modules.timer.go
@@ -91,7 +91,11 @@ func (m *BotTimerModule) runTimers() {
if !ok {
continue
}
- if now.Sub(lastTriggeredTime) < time.Duration(timer.MinimumDelay)*time.Second {
+ minDelay := timer.MinimumDelay
+ if minDelay < 5 {
+ minDelay = 5
+ }
+ if now.Sub(lastTriggeredTime) < time.Duration(minDelay)*time.Second {
continue
}
// Make sure chat activity is high enough