1
0
Fork 0
mirror of https://git.sr.ht/~ashkeel/strimertul synced 2024-09-20 02:00:49 +00:00

Add cooldowns to rewards

This commit is contained in:
Ash Keel 2021-07-11 15:34:39 +02:00
parent 2513c2bc5a
commit 599e6a5540
No known key found for this signature in database
GPG key ID: CF2CC050478BD7E5
9 changed files with 139 additions and 26 deletions

View file

@ -6524,6 +6524,11 @@
"error-ex": "^1.2.0" "error-ex": "^1.2.0"
} }
}, },
"parse-ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz",
"integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA=="
},
"parse5": { "parse5": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz",
@ -8370,6 +8375,14 @@
"fast-diff": "^1.1.2" "fast-diff": "^1.1.2"
} }
}, },
"pretty-ms": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz",
"integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==",
"requires": {
"parse-ms": "^2.1.0"
}
},
"process": { "process": {
"version": "0.11.10", "version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",

View file

@ -12,6 +12,7 @@
"@types/react-dom": "^17.0.4", "@types/react-dom": "^17.0.4",
"bulma": "^0.9.2", "bulma": "^0.9.2",
"parcel": "^2.0.0-beta.2", "parcel": "^2.0.0-beta.2",
"pretty-ms": "^7.0.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-redux": "^7.2.4", "react-redux": "^7.2.4",

View file

@ -0,0 +1,14 @@
export function getInterval(duration: number): [number, number] {
if (duration < 60) {
return [duration, 1];
}
if (duration % 3600 === 0) {
return [duration / 3600, 3600];
}
if (duration % 60 === 0) {
return [duration / 60, 60];
}
return [duration, 1];
}
export default { getInterval };

View file

@ -82,6 +82,7 @@ export interface LoyaltyReward {
image: string; image: string;
price: number; price: number;
required_info?: string; required_info?: string;
cooldown: number;
} }
export interface LoyaltyGoal { export interface LoyaltyGoal {

View file

@ -1,6 +1,7 @@
import { RouteComponentProps } from '@reach/router'; import { RouteComponentProps } from '@reach/router';
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import prettyTime from 'pretty-ms';
import { useModule } from '../../../lib/react-utils'; import { useModule } from '../../../lib/react-utils';
import { RootState } from '../../../store'; import { RootState } from '../../../store';
import { import {
@ -9,6 +10,7 @@ import {
modules, modules,
} from '../../../store/api/reducer'; } from '../../../store/api/reducer';
import Modal from '../../components/Modal'; import Modal from '../../components/Modal';
import { getInterval } from '../../../lib/time-utils';
interface RewardItemProps { interface RewardItemProps {
item: LoyaltyReward; item: LoyaltyReward;
@ -71,6 +73,11 @@ function RewardItem({
{expanded ? ( {expanded ? (
<div className="content"> <div className="content">
{item.description} {item.description}
{item.cooldown > 0 ? (
<div style={{ marginTop: '1rem' }}>
<b>Cooldown:</b> {prettyTime(item.cooldown * 1000)}
</div>
) : null}
{item.required_info ? ( {item.required_info ? (
<div style={{ marginTop: '1rem' }}> <div style={{ marginTop: '1rem' }}>
<b>Required info:</b> {item.required_info} <b>Required info:</b> {item.required_info}
@ -126,12 +133,19 @@ function RewardModal({
initialData?.description ?? '', initialData?.description ?? '',
); );
const [price, setPrice] = useState(initialData?.price ?? 0); const [price, setPrice] = useState(initialData?.price ?? 0);
const [extraRequired, setExtraRequired] = useState(
initialData?.required_info !== null,
);
const [extraDetails, setExtraDetails] = useState( const [extraDetails, setExtraDetails] = useState(
initialData?.required_info ?? '', initialData?.required_info ?? '',
); );
const [extraRequired, setExtraRequired] = useState(extraDetails !== '');
const [cooldown, setCooldown] = useState(initialData?.cooldown ?? 0);
const [cooldownNum, cooldownMultiplier] = getInterval(cooldown);
const [tempCooldownNum, setTempCooldownNum] = useState(cooldownNum);
const [tempCooldownMult, setTempCooldownMult] = useState(cooldownMultiplier);
useEffect(() => {
setCooldown(tempCooldownNum * tempCooldownMult);
}, [tempCooldownNum, tempCooldownMult]);
const setIDex = (newID) => const setIDex = (newID) =>
setID(newID.toLowerCase().replace(/[^a-zA-Z0-9]/gi, '-')); setID(newID.toLowerCase().replace(/[^a-zA-Z0-9]/gi, '-'));
@ -152,6 +166,7 @@ function RewardModal({
enabled: initialData?.enabled ?? false, enabled: initialData?.enabled ?? false,
image, image,
required_info: extraRequired ? extraDetails : undefined, required_info: extraRequired ? extraDetails : undefined,
cooldown,
}); });
} }
}; };
@ -311,6 +326,50 @@ function RewardModal({
</div> </div>
</> </>
) : null} ) : null}
<div className="field is-horizontal">
<div className="field-label is-normal">
<label className="label">Cooldown</label>
</div>
<div className="field-body">
<div className="field has-addons">
<p className="control">
<input
disabled={!active}
className="input"
type="number"
placeholder="#"
value={tempCooldownNum ?? ''}
onChange={(ev) => {
const intNum = parseInt(ev.target.value, 10);
if (Number.isNaN(intNum)) {
return;
}
setTempCooldownNum(intNum);
}}
/>
</p>
<p className="control">
<span className="select">
<select
value={tempCooldownMult.toString() ?? ''}
disabled={!active}
onChange={(ev) => {
const intMult = parseInt(ev.target.value, 10);
if (Number.isNaN(intMult)) {
return;
}
setTempCooldownMult(intMult);
}}
>
<option value="1">seconds</option>
<option value="60">minutes</option>
<option value="3600">hours</option>
</select>
</span>
</p>
</div>
</div>
</div>
</Modal> </Modal>
); );
} }

View file

@ -2,18 +2,9 @@ import { RouteComponentProps } from '@reach/router';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useModule } from '../../../lib/react-utils'; import { useModule } from '../../../lib/react-utils';
import { getInterval } from '../../../lib/time-utils';
import apiReducer, { modules } from '../../../store/api/reducer'; import apiReducer, { modules } from '../../../store/api/reducer';
function getInterval(duration: number): [number, number] {
if (duration % 3600 === 0) {
return [duration / 3600, 3600];
}
if (duration % 60 === 0) {
return [duration / 60, 60];
}
return [duration, 1];
}
export default function LoyaltySettingPage( export default function LoyaltySettingPage(
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
props: RouteComponentProps<unknown>, props: RouteComponentProps<unknown>,

View file

@ -30,6 +30,7 @@ type Reward struct {
Image string `json:"image"` Image string `json:"image"`
Price int64 `json:"price"` Price int64 `json:"price"`
CustomRequest string `json:"required_info,omitempty"` CustomRequest string `json:"required_info,omitempty"`
Cooldown int64 `json:"cooldown"`
} }
type Goal struct { type Goal struct {

View file

@ -16,6 +16,7 @@ import (
) )
var ( var (
ErrRedeemInCooldown = errors.New("redeem is on cooldown")
ErrGoalNotFound = errors.New("goal not found") ErrGoalNotFound = errors.New("goal not found")
ErrGoalAlreadyReached = errors.New("goal already reached") ErrGoalAlreadyReached = errors.New("goal already reached")
) )
@ -29,6 +30,7 @@ type Manager struct {
mu sync.Mutex mu sync.Mutex
db *database.DB db *database.DB
logger logrus.FieldLogger logger logrus.FieldLogger
cooldowns map[string]time.Time
} }
func NewManager(db *database.DB, log logrus.FieldLogger) (*Manager, error) { func NewManager(db *database.DB, log logrus.FieldLogger) (*Manager, error) {
@ -47,6 +49,7 @@ func NewManager(db *database.DB, log logrus.FieldLogger) (*Manager, error) {
logger: log, logger: log,
db: db, db: db,
mu: sync.Mutex{}, mu: sync.Mutex{},
cooldowns: make(map[string]time.Time),
} }
// Ger data from DB // Ger data from DB
if err := db.GetJSON(ConfigKey, &manager.config); err != nil { if err := db.GetJSON(ConfigKey, &manager.config); err != nil {
@ -255,6 +258,19 @@ func (m *Manager) saveQueue() error {
return m.db.PutJSON(QueueKey, m.queue) return m.db.PutJSON(QueueKey, m.queue)
} }
func (m *Manager) GetRewardCooldown(rewardID string) time.Time {
m.mu.Lock()
defer m.mu.Unlock()
cooldown, ok := m.cooldowns[rewardID]
if !ok {
// Return zero time for a reward with no cooldown
return time.Time{}
}
return cooldown
}
func (m *Manager) AddRedeem(redeem Redeem) error { func (m *Manager) AddRedeem(redeem Redeem) error {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
@ -267,11 +283,21 @@ func (m *Manager) AddRedeem(redeem Redeem) error {
return err return err
} }
// Add cooldown if applicable
if redeem.Reward.Cooldown > 0 {
m.cooldowns[redeem.Reward.ID] = time.Now().Add(time.Second * time.Duration(redeem.Reward.Cooldown))
}
// Save points // Save points
return m.saveQueue() return m.saveQueue()
} }
func (m *Manager) PerformRedeem(redeem Redeem) error { func (m *Manager) PerformRedeem(redeem Redeem) error {
// Check cooldown
if time.Now().Before(m.GetRewardCooldown(redeem.Reward.ID)) {
return ErrRedeemInCooldown
}
// Add redeem // Add redeem
err := m.AddRedeem(redeem) err := m.AddRedeem(redeem)
if err != nil { if err != nil {

View file

@ -102,7 +102,14 @@ func cmdRedeemReward(bot *Bot, message irc.PrivateMessage) {
Reward: reward, Reward: reward,
RequestText: text, RequestText: text,
}); err != nil { }); err != nil {
switch err {
case loyalty.ErrRedeemInCooldown:
nextAvailable := bot.Loyalty.GetRewardCooldown(reward.ID)
bot.Client.Say(message.Channel, fmt.Sprintf("%s: That reward is in cooldown (available in %s)", message.User.DisplayName,
time.Until(nextAvailable).Truncate(time.Second)))
default:
bot.logger.WithError(err).Error("error while performing redeem") bot.logger.WithError(err).Error("error while performing redeem")
}
return return
} }