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:
parent
2513c2bc5a
commit
599e6a5540
9 changed files with 139 additions and 26 deletions
13
frontend/package-lock.json
generated
13
frontend/package-lock.json
generated
|
@ -6524,6 +6524,11 @@
|
|||
"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": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz",
|
||||
|
@ -8370,6 +8375,14 @@
|
|||
"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": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"@types/react-dom": "^17.0.4",
|
||||
"bulma": "^0.9.2",
|
||||
"parcel": "^2.0.0-beta.2",
|
||||
"pretty-ms": "^7.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-redux": "^7.2.4",
|
||||
|
|
14
frontend/src/lib/time-utils.ts
Normal file
14
frontend/src/lib/time-utils.ts
Normal 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 };
|
|
@ -82,6 +82,7 @@ export interface LoyaltyReward {
|
|||
image: string;
|
||||
price: number;
|
||||
required_info?: string;
|
||||
cooldown: number;
|
||||
}
|
||||
|
||||
export interface LoyaltyGoal {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { RouteComponentProps } from '@reach/router';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import prettyTime from 'pretty-ms';
|
||||
import { useModule } from '../../../lib/react-utils';
|
||||
import { RootState } from '../../../store';
|
||||
import {
|
||||
|
@ -9,6 +10,7 @@ import {
|
|||
modules,
|
||||
} from '../../../store/api/reducer';
|
||||
import Modal from '../../components/Modal';
|
||||
import { getInterval } from '../../../lib/time-utils';
|
||||
|
||||
interface RewardItemProps {
|
||||
item: LoyaltyReward;
|
||||
|
@ -71,6 +73,11 @@ function RewardItem({
|
|||
{expanded ? (
|
||||
<div className="content">
|
||||
{item.description}
|
||||
{item.cooldown > 0 ? (
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<b>Cooldown:</b> {prettyTime(item.cooldown * 1000)}
|
||||
</div>
|
||||
) : null}
|
||||
{item.required_info ? (
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<b>Required info:</b> {item.required_info}
|
||||
|
@ -126,12 +133,19 @@ function RewardModal({
|
|||
initialData?.description ?? '',
|
||||
);
|
||||
const [price, setPrice] = useState(initialData?.price ?? 0);
|
||||
const [extraRequired, setExtraRequired] = useState(
|
||||
initialData?.required_info !== null,
|
||||
);
|
||||
const [extraDetails, setExtraDetails] = useState(
|
||||
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) =>
|
||||
setID(newID.toLowerCase().replace(/[^a-zA-Z0-9]/gi, '-'));
|
||||
|
@ -152,6 +166,7 @@ function RewardModal({
|
|||
enabled: initialData?.enabled ?? false,
|
||||
image,
|
||||
required_info: extraRequired ? extraDetails : undefined,
|
||||
cooldown,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -311,6 +326,50 @@ function RewardModal({
|
|||
</div>
|
||||
</>
|
||||
) : 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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,18 +2,9 @@ import { RouteComponentProps } from '@reach/router';
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useModule } from '../../../lib/react-utils';
|
||||
import { getInterval } from '../../../lib/time-utils';
|
||||
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(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
props: RouteComponentProps<unknown>,
|
||||
|
|
|
@ -30,6 +30,7 @@ type Reward struct {
|
|||
Image string `json:"image"`
|
||||
Price int64 `json:"price"`
|
||||
CustomRequest string `json:"required_info,omitempty"`
|
||||
Cooldown int64 `json:"cooldown"`
|
||||
}
|
||||
|
||||
type Goal struct {
|
||||
|
|
|
@ -16,19 +16,21 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
ErrRedeemInCooldown = errors.New("redeem is on cooldown")
|
||||
ErrGoalNotFound = errors.New("goal not found")
|
||||
ErrGoalAlreadyReached = errors.New("goal already reached")
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
points map[string]PointsEntry
|
||||
config Config
|
||||
rewards RewardStorage
|
||||
goals GoalStorage
|
||||
queue RedeemQueueStorage
|
||||
mu sync.Mutex
|
||||
db *database.DB
|
||||
logger logrus.FieldLogger
|
||||
points map[string]PointsEntry
|
||||
config Config
|
||||
rewards RewardStorage
|
||||
goals GoalStorage
|
||||
queue RedeemQueueStorage
|
||||
mu sync.Mutex
|
||||
db *database.DB
|
||||
logger logrus.FieldLogger
|
||||
cooldowns map[string]time.Time
|
||||
}
|
||||
|
||||
func NewManager(db *database.DB, log logrus.FieldLogger) (*Manager, error) {
|
||||
|
@ -44,9 +46,10 @@ func NewManager(db *database.DB, log logrus.FieldLogger) (*Manager, error) {
|
|||
}
|
||||
|
||||
manager := &Manager{
|
||||
logger: log,
|
||||
db: db,
|
||||
mu: sync.Mutex{},
|
||||
logger: log,
|
||||
db: db,
|
||||
mu: sync.Mutex{},
|
||||
cooldowns: make(map[string]time.Time),
|
||||
}
|
||||
// Ger data from DB
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
@ -267,11 +283,21 @@ func (m *Manager) AddRedeem(redeem Redeem) error {
|
|||
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
|
||||
return m.saveQueue()
|
||||
}
|
||||
|
||||
func (m *Manager) PerformRedeem(redeem Redeem) error {
|
||||
// Check cooldown
|
||||
if time.Now().Before(m.GetRewardCooldown(redeem.Reward.ID)) {
|
||||
return ErrRedeemInCooldown
|
||||
}
|
||||
|
||||
// Add redeem
|
||||
err := m.AddRedeem(redeem)
|
||||
if err != nil {
|
||||
|
|
|
@ -102,7 +102,14 @@ func cmdRedeemReward(bot *Bot, message irc.PrivateMessage) {
|
|||
Reward: reward,
|
||||
RequestText: text,
|
||||
}); err != nil {
|
||||
bot.logger.WithError(err).Error("error while performing redeem")
|
||||
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")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue