1
0
Fork 0
mirror of https://git.sr.ht/~ashkeel/strimertul synced 2024-09-20 02:00:49 +00:00
This commit is contained in:
Ash Keel 2021-11-05 22:54:42 +01:00
parent 08d3b68f2e
commit 71c0b96091
No known key found for this signature in database
GPG key ID: BAD8D93E7314ED3E
8 changed files with 192 additions and 72 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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>
</>
);

View file

@ -20,6 +20,7 @@ func (b *Bot) LoadModules() error {
}
}
if cfg.EnableTimers {
b.logger.Debug("starting timer module")
b.Timers = SetupTimers(b)
}
return nil

View file

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