mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-20 02:00:49 +00:00
Timers!
This commit is contained in:
parent
08d3b68f2e
commit
71c0b96091
8 changed files with 192 additions and 72 deletions
|
@ -1,5 +1,5 @@
|
|||
import { ActionCreatorWithOptionalPayload, AsyncThunk } from '@reduxjs/toolkit';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
KilovoltMessage,
|
||||
|
@ -8,7 +8,6 @@ import {
|
|||
import { RootState } from '../store';
|
||||
import apiReducer, { getUserPoints } from '../store/api/reducer';
|
||||
import { APIState, LoyaltyStorage } from '../store/api/types';
|
||||
import { getInterval } from './time-utils';
|
||||
|
||||
export function useModule<T>({
|
||||
key,
|
||||
|
|
|
@ -218,7 +218,10 @@
|
|||
"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"
|
||||
"minimum-activity-post": "messages in the last 5 minutes",
|
||||
"condition-text": "every {{time}}, at least {{messages}} messages in the last 5 minutes",
|
||||
"err-twitchbot-disabled": "Twitch bot must be enabled in order to use timers!",
|
||||
"enable": "Enable timers"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,27 +2,40 @@ import React, { useEffect, useState } from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { getInterval } from '../../lib/time-utils';
|
||||
|
||||
export interface TimeUnit {
|
||||
multiplier: number;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export const seconds = { multiplier: 1, unit: 'form-common.time.seconds' };
|
||||
export const minutes = { multiplier: 60, unit: 'form-common.time.minutes' };
|
||||
export const hours = { multiplier: 3600, unit: 'form-common.time.hours' };
|
||||
|
||||
export interface IntervalProps {
|
||||
active: boolean;
|
||||
value: number;
|
||||
min?: number;
|
||||
units?: TimeUnit[];
|
||||
onChange?: (value: number) => void;
|
||||
}
|
||||
|
||||
function Interval({ active, value, min, onChange }: IntervalProps) {
|
||||
function Interval({ active, value, min, units, onChange }: IntervalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const timeUnits = units ?? [seconds, minutes, hours];
|
||||
|
||||
const [numInitialValue, multInitialValue] = getInterval(value);
|
||||
const [num, setNum] = useState(numInitialValue);
|
||||
const [mult, setMult] = useState(multInitialValue);
|
||||
|
||||
useEffect(() => {
|
||||
const seconds = num * mult;
|
||||
if (min && seconds < min) {
|
||||
setNum(5);
|
||||
setMult(1);
|
||||
const total = num * mult;
|
||||
if (min && total < min) {
|
||||
const [minNum, minMult] = getInterval(min);
|
||||
setNum(minNum);
|
||||
setMult(minMult);
|
||||
}
|
||||
onChange(Math.max(min ?? 0, seconds));
|
||||
onChange(Math.max(min ?? 0, total));
|
||||
}, [num, mult]);
|
||||
|
||||
return (
|
||||
|
@ -57,9 +70,11 @@ function Interval({ active, value, min, onChange }: IntervalProps) {
|
|||
setMult(intMult);
|
||||
}}
|
||||
>
|
||||
<option value="1">{t('form-common.time.seconds')}</option>
|
||||
<option value="60">{t('form-common.time.minutes')}</option>
|
||||
<option value="3600">{t('form-common.time.hours')}</option>
|
||||
{timeUnits.map((unit) => (
|
||||
<option key={unit.unit} value={unit.multiplier.toString()}>
|
||||
{t(unit.unit)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
</p>
|
||||
|
|
|
@ -90,7 +90,7 @@ function RewardItem({
|
|||
{t('actions.test')}
|
||||
</a>{' '}
|
||||
<a className="button is-small" onClick={onToggleState}>
|
||||
{t(item.enabled ? 'actions.disable' : 'actions.enable')}
|
||||
{item.enabled ? t('actions.disable') : t('actions.enable')}
|
||||
</a>{' '}
|
||||
<a className="button is-small" onClick={onEdit}>
|
||||
{t('actions.edit')}
|
||||
|
|
|
@ -54,7 +54,7 @@ function CommandItem({
|
|||
<blockquote>{item.response}</blockquote>
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<a className="button is-small" onClick={onToggleState}>
|
||||
{item.enabled ? 'Disable' : 'Enable'}
|
||||
{item.enabled ? t('actions.disable') : t('actions.enable')}
|
||||
</a>{' '}
|
||||
<a className="button is-small" onClick={onEdit}>
|
||||
{t('actions.edit')}
|
||||
|
|
|
@ -2,12 +2,13 @@ import { RouteComponentProps } from '@reach/router';
|
|||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import prettyTime from 'pretty-ms';
|
||||
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';
|
||||
import Interval, { hours, minutes } from '../../components/Interval';
|
||||
|
||||
interface TimerItemProps {
|
||||
item: TwitchBotTimer;
|
||||
|
@ -24,7 +25,14 @@ function TimerItem({ item, onToggleState, onEdit, onDelete }: TimerItemProps) {
|
|||
<header className="card-header">
|
||||
<div className="card-header-title">
|
||||
{item.enabled ? (
|
||||
<code>{item.name}</code>
|
||||
<>
|
||||
<code>{item.name}</code> (
|
||||
{t('twitch.timers.condition-text', {
|
||||
time: prettyTime(item.minimum_delay * 1000),
|
||||
messages: item.minimum_chat_activity,
|
||||
})}
|
||||
)
|
||||
</>
|
||||
) : (
|
||||
<span className="reward-disabled">
|
||||
<code>{item.name}</code>
|
||||
|
@ -44,12 +52,12 @@ function TimerItem({ item, onToggleState, onEdit, onDelete }: TimerItemProps) {
|
|||
{expanded ? (
|
||||
<div className="content">
|
||||
{t('twitch.timers.messages')}:{' '}
|
||||
{item.messages.map((message) => (
|
||||
<blockquote>{message}</blockquote>
|
||||
{item.messages.map((message, index) => (
|
||||
<blockquote key={index}>{message}</blockquote>
|
||||
))}
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<a className="button is-small" onClick={onToggleState}>
|
||||
{item.enabled ? 'Disable' : 'Enable'}
|
||||
{item.enabled ? t('actions.disable') : t('actions.enable')}
|
||||
</a>{' '}
|
||||
<a className="button is-small" onClick={onEdit}>
|
||||
{t('actions.edit')}
|
||||
|
@ -91,15 +99,16 @@ function TimerModal({
|
|||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const validForm = name !== '' && messages.length > 0 && messages[0] !== '';
|
||||
const validForm =
|
||||
name !== '' && messages.length > 0 && messages.every((msg) => msg !== '');
|
||||
|
||||
const confirm = () => {
|
||||
if (onConfirm) {
|
||||
onConfirm(name, {
|
||||
name,
|
||||
messages,
|
||||
minimum_chat_activity: 0,
|
||||
minimum_delay: 0,
|
||||
minimum_chat_activity: minActivity,
|
||||
minimum_delay: minDelay,
|
||||
enabled: initialData?.enabled ?? false,
|
||||
});
|
||||
}
|
||||
|
@ -126,7 +135,7 @@ function TimerModal({
|
|||
<Field name={t('twitch.timers.name')} horizontal>
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
<p className="control">
|
||||
<div className="control">
|
||||
<input
|
||||
className={name !== '' ? 'input' : 'input is-danger'}
|
||||
type="text"
|
||||
|
@ -134,7 +143,7 @@ function TimerModal({
|
|||
value={name}
|
||||
onChange={(ev) => setName(ev.target.value)}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
|
@ -145,7 +154,8 @@ function TimerModal({
|
|||
value={minDelay}
|
||||
onChange={setMinDelay}
|
||||
active={active}
|
||||
min={5}
|
||||
min={60}
|
||||
units={[minutes, hours]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -153,7 +163,7 @@ function TimerModal({
|
|||
<Field name={t('twitch.timers.minimum-activity')} horizontal>
|
||||
<div className="field-body">
|
||||
<div className="field has-addons" style={{ marginBottom: 0 }}>
|
||||
<p className="control">
|
||||
<div className="control">
|
||||
<input
|
||||
disabled={!active}
|
||||
className="input"
|
||||
|
@ -169,7 +179,7 @@ function TimerModal({
|
|||
setMinActivity(amount);
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<p className="control">
|
||||
<a className="button is-static">
|
||||
{t('twitch.timers.minimum-activity-post')}
|
||||
|
@ -180,18 +190,49 @@ function TimerModal({
|
|||
</Field>
|
||||
<Field name={t('twitch.timers.messages')} horizontal>
|
||||
<div className="field-body">
|
||||
<div className="control">
|
||||
{messages.map((message, index) => (
|
||||
<div className="field">
|
||||
<div
|
||||
className="field has-addons"
|
||||
key={index}
|
||||
style={{ marginTop: index > 0 ? '0.5rem' : '' }}
|
||||
>
|
||||
<p className="control">
|
||||
<textarea
|
||||
className={message !== '' ? 'textarea' : 'textarea is-danger'}
|
||||
<input
|
||||
placeholder={t('twitch.timers.message-help')}
|
||||
onChange={(ev) => setMessageIndex(ev.target.value, index)}
|
||||
value={message}
|
||||
className={message !== '' ? 'input' : 'input is-danger'}
|
||||
style={{ width: '28rem' }}
|
||||
/>
|
||||
</p>
|
||||
<p className="control">
|
||||
<button
|
||||
className="button is-danger"
|
||||
onClick={() => {
|
||||
const newMessages = [...messages];
|
||||
newMessages.splice(index, 1);
|
||||
setMessages(newMessages.length > 0 ? newMessages : ['']);
|
||||
}}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
<div className="field" style={{ marginTop: '0.5rem' }}>
|
||||
<p className="control">
|
||||
<button
|
||||
className="button is-primary"
|
||||
onClick={() => {
|
||||
setMessages([...messages, '']);
|
||||
}}
|
||||
>
|
||||
Add new
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
</Modal>
|
||||
|
@ -202,7 +243,11 @@ export default function TwitchBotTimersPage(
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
props: RouteComponentProps<unknown>,
|
||||
): React.ReactElement {
|
||||
const [timers, setTimers] = useModule(modules.twitchBotTimers);
|
||||
const [twitchConfig] = useModule(modules.twitchConfig);
|
||||
const [moduleConfig, setModuleConfig] = useModule(
|
||||
modules.twitchBotModulesConfig,
|
||||
);
|
||||
const [timerConfig, setTimerConfig] = useModule(modules.twitchBotTimers);
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
@ -211,11 +256,18 @@ export default function TwitchBotTimersPage(
|
|||
const [timerFilter, setTimerFilter] = useState('');
|
||||
const timerFilterLC = timerFilter.toLowerCase();
|
||||
|
||||
const botActive = twitchConfig?.enable_bot ?? false;
|
||||
const timersActive = moduleConfig?.enable_timers ?? false;
|
||||
const active = botActive && timersActive;
|
||||
|
||||
const createTimer = (name: string, data: TwitchBotTimer): void => {
|
||||
dispatch(
|
||||
setTimers({
|
||||
...timers,
|
||||
setTimerConfig({
|
||||
...timerConfig,
|
||||
timers: {
|
||||
...timerConfig.timers,
|
||||
[name]: data,
|
||||
},
|
||||
}),
|
||||
);
|
||||
setCreateModal(false);
|
||||
|
@ -227,13 +279,16 @@ export default function TwitchBotTimersPage(
|
|||
data: TwitchBotTimer,
|
||||
): void => {
|
||||
dispatch(
|
||||
setTimers({
|
||||
...timers,
|
||||
setTimerConfig({
|
||||
...timerConfig,
|
||||
timers: {
|
||||
...timerConfig.timers,
|
||||
[oldName]: undefined,
|
||||
[newName]: {
|
||||
...timers[oldName],
|
||||
...timerConfig.timers[oldName],
|
||||
...data,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
setShowModifyTimer(null);
|
||||
|
@ -241,31 +296,68 @@ export default function TwitchBotTimersPage(
|
|||
|
||||
const deleteTimer = (cmd: string): void => {
|
||||
dispatch(
|
||||
setTimers({
|
||||
...timers,
|
||||
setTimerConfig({
|
||||
...timerConfig,
|
||||
timers: {
|
||||
...timerConfig.timers,
|
||||
[cmd]: undefined,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const toggleTimer = (cmd: string): void => {
|
||||
dispatch(
|
||||
setTimers({
|
||||
...timers,
|
||||
setTimerConfig({
|
||||
...timerConfig,
|
||||
timers: {
|
||||
...timerConfig.timers,
|
||||
[cmd]: {
|
||||
...timers[cmd],
|
||||
enabled: !timers[cmd].enabled,
|
||||
...timerConfig.timers[cmd],
|
||||
enabled: !timerConfig.timers[cmd].enabled,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
if (!botActive) {
|
||||
return (
|
||||
<>
|
||||
<h1 className="title is-4">{t('twitch.timers.header')}</h1>
|
||||
<p>{t('twitch.timers.err-twitchbot-disabled')}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="title is-4">{t('twitch.timers.header')}</h1>
|
||||
<Field>
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={!botActive}
|
||||
checked={active}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
setModuleConfig({
|
||||
...moduleConfig,
|
||||
enable_timers: ev.target.checked,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
{` ${t('twitch.timers.enable')} `}
|
||||
</label>
|
||||
</Field>
|
||||
<div className="field is-grouped">
|
||||
<p className="control">
|
||||
<button className="button" onClick={() => setCreateModal(true)}>
|
||||
<button
|
||||
className="button"
|
||||
disabled={!timersActive}
|
||||
onClick={() => setCreateModal(true)}
|
||||
>
|
||||
{t('twitch.timers.new-timer')}
|
||||
</button>
|
||||
</p>
|
||||
|
@ -274,6 +366,7 @@ export default function TwitchBotTimersPage(
|
|||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
disabled={!timersActive}
|
||||
placeholder={t('twitch.timers.search')}
|
||||
value={timerFilter}
|
||||
onChange={(ev) => setTimerFilter(ev.target.value)}
|
||||
|
@ -297,22 +390,26 @@ export default function TwitchBotTimersPage(
|
|||
modifyTimer(showModifyTimer, newName, cmdData)
|
||||
}
|
||||
initialName={showModifyTimer}
|
||||
initialData={showModifyTimer ? timers[showModifyTimer] : null}
|
||||
initialData={
|
||||
showModifyTimer ? timerConfig.timers[showModifyTimer] : null
|
||||
}
|
||||
onClose={() => setShowModifyTimer(null)}
|
||||
/>
|
||||
) : null}
|
||||
<div className="reward-list" style={{ marginTop: '1rem' }}>
|
||||
{Object.keys(timers ?? {})
|
||||
{timersActive
|
||||
? Object.keys(timerConfig?.timers ?? {})
|
||||
?.filter((cmd) => cmd.toLowerCase().includes(timerFilterLC))
|
||||
.map((timer) => (
|
||||
<TimerItem
|
||||
key={timer}
|
||||
item={timer[timer]}
|
||||
item={timerConfig.timers[timer]}
|
||||
onDelete={() => deleteTimer(timer)}
|
||||
onEdit={() => setShowModifyTimer(timer)}
|
||||
onToggleState={() => toggleTimer(timer)}
|
||||
/>
|
||||
))}
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -20,6 +20,7 @@ func (b *Bot) LoadModules() error {
|
|||
}
|
||||
}
|
||||
if cfg.EnableTimers {
|
||||
b.logger.Debug("starting timer module")
|
||||
b.Timers = SetupTimers(b)
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -39,16 +39,20 @@ func SetupTimers(bot *Bot) *BotTimerModule {
|
|||
mod := &BotTimerModule{
|
||||
bot: bot,
|
||||
startTime: time.Now().Round(time.Minute),
|
||||
lastTrigger: make(map[string]time.Time),
|
||||
}
|
||||
|
||||
// Load config from database
|
||||
err := bot.api.db.GetJSON(BotTimersKey, &mod.Config)
|
||||
if err != nil {
|
||||
bot.logger.WithError(err).Debug("config load error")
|
||||
mod.Config = BotTimersConfig{
|
||||
Timers: make(map[string]BotTimer),
|
||||
}
|
||||
}
|
||||
|
||||
bot.logger.WithField("timers", len(mod.Config.Timers)).Debug("loaded timers")
|
||||
|
||||
// Start goroutine for clearing message counters and running timers
|
||||
go mod.runTimers()
|
||||
|
||||
|
@ -89,11 +93,12 @@ func (m *BotTimerModule) runTimers() {
|
|||
// Check if enough time has passed
|
||||
lastTriggeredTime, ok := m.lastTrigger[name]
|
||||
if !ok {
|
||||
continue
|
||||
// If it's the first time we're checking it, start the cooldown
|
||||
lastTriggeredTime = time.Now()
|
||||
}
|
||||
minDelay := timer.MinimumDelay
|
||||
if minDelay < 5 {
|
||||
minDelay = 5
|
||||
if minDelay < 60 {
|
||||
minDelay = 60
|
||||
}
|
||||
if now.Sub(lastTriggeredTime) < time.Duration(minDelay)*time.Second {
|
||||
continue
|
||||
|
|
Loading…
Reference in a new issue