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

View file

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

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;
price: number;
required_info?: string;
cooldown: number;
}
export interface LoyaltyGoal {

View file

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

View file

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

View file

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

View file

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

View file

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