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"
|
"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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
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;
|
image: string;
|
||||||
price: number;
|
price: number;
|
||||||
required_info?: string;
|
required_info?: string;
|
||||||
|
cooldown: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoyaltyGoal {
|
export interface LoyaltyGoal {
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -16,19 +16,21 @@ 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")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
points map[string]PointsEntry
|
points map[string]PointsEntry
|
||||||
config Config
|
config Config
|
||||||
rewards RewardStorage
|
rewards RewardStorage
|
||||||
goals GoalStorage
|
goals GoalStorage
|
||||||
queue RedeemQueueStorage
|
queue RedeemQueueStorage
|
||||||
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) {
|
||||||
|
@ -44,9 +46,10 @@ func NewManager(db *database.DB, log logrus.FieldLogger) (*Manager, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
manager := &Manager{
|
manager := &Manager{
|
||||||
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 {
|
||||||
|
|
|
@ -102,7 +102,14 @@ func cmdRedeemReward(bot *Bot, message irc.PrivateMessage) {
|
||||||
Reward: reward,
|
Reward: reward,
|
||||||
RequestText: text,
|
RequestText: text,
|
||||||
}); err != nil {
|
}); 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue