Skip to content

Commit

Permalink
feat(points): add !points undo (#3593)
Browse files Browse the repository at this point in the history
* feat(points): add !points undo

Fixes #3590

* fixup

* fix

* cleanup points changelog
  • Loading branch information
sogehige committed Apr 29, 2020
1 parent 3290a9d commit 783e7cc
Show file tree
Hide file tree
Showing 10 changed files with 339 additions and 3 deletions.
2 changes: 2 additions & 0 deletions docs/_master/systems/points.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
### Commands | OWNER
- !points add [username] [number]
- add [number] points to specified [username]
- !points undo [username]
- revert last add, remove or set points operation within 10minutess
- !points remove [username] [number]
- remove [number] points to specified [username]
- !points online [number]
Expand Down
2 changes: 2 additions & 0 deletions locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,7 @@
},
"points": {
"success": {
"undo": "$sender, příkaz '$command' pro body $username byly vráceny ($updatedValue $updatedValuePointsLocale to $originalValue $originalValuePointsLocale).",
"set": "$username ma nyni nastaveno $amount $pointsName",
"give": "$sender prave dal svych $amount $pointsName uzivateli $username",
"online": {
Expand All @@ -899,6 +900,7 @@
"remove": "A jeje, $amount $pointsName bylo odebrano uzivateli $username!"
},
"failed": {
"undo": "$sender, uživatel nenalezen v databázi nebo uživatel nemá body k vrácení.",
"set": "{core.command-parse} $command [username] [amount]",
"give": "{core.command-parse} $command [username] [amount]",
"giveNotEnough": "Promin, $sender, ale nemas $amount $pointsName abys je daroval uzivateli $username",
Expand Down
2 changes: 2 additions & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,7 @@
},
"points": {
"success": {
"undo": "$sender, points '$command' for $username was reverted ($updatedValue $updatedValuePointsLocale to $originalValue $originalValuePointsLocale).",
"set": "$username was set to $amount $pointsName",
"give": "$sender just gave his $amount $pointsName to $username",
"online": {
Expand All @@ -888,6 +889,7 @@
"remove": "Ouch, $amount $pointsName was removed from $username!"
},
"failed": {
"undo": "$sender, username wasn't found in database or user have no undo operations",
"set": "{core.command-parse} $command [username] [amount]",
"give": "{core.command-parse} $command [username] [amount]",
"giveNotEnough": "Sorry, $sender, you don't have $amount $pointsName to give it to $username",
Expand Down
26 changes: 26 additions & 0 deletions src/bot/database/entity/points.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { EntitySchema } from 'typeorm';
import { ColumnNumericTransformer } from './_transformer';

export interface PointsChangelogInterface {
id: number;
userId: number;
originalValue: number;
updatedValue: number;
updatedAt: number;
command: 'set' | 'add' | 'remove';
}

export const PointsChangelog = new EntitySchema<Readonly<Required<PointsChangelogInterface>>>({
name: 'points_changelog',
columns: {
id: { type: Number, primary: true, generated: 'increment' },
userId: { type: Number },
originalValue: { type: Number },
updatedValue: { type: Number },
updatedAt: { type: 'bigint', transformer: new ColumnNumericTransformer() },
command: { type: String },
},
indices: [
{ name: 'IDX_points_changelog_userId', columns: ['userId'] },
],
});
15 changes: 15 additions & 0 deletions src/bot/database/migration/mysql/1587985735225-pointsChangelog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {MigrationInterface, QueryRunner} from 'typeorm';

export class pointsChangelog1587985735225 implements MigrationInterface {
name = 'pointsChangelog1587985735225';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('CREATE TABLE `points_changelog` (`id` int NOT NULL AUTO_INCREMENT, `userId` int NOT NULL, `originalValue` int NOT NULL, `updatedValue` int NOT NULL, `updatedAt` bigint NOT NULL, `command` varchar(255) NOT NULL, INDEX `IDX_points_changelog_userId` (`userId`), PRIMARY KEY (`id`)) ENGINE=InnoDB', undefined);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `IDX_points_changelog_userId` ON `points_changelog`', undefined);
await queryRunner.query('DROP TABLE `points_changelog`', undefined);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {MigrationInterface, QueryRunner} from 'typeorm';

export class pointsChangelog1587985404134 implements MigrationInterface {
name = 'pointsChangelog1587985404134';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "points_changelog" ("id" SERIAL NOT NULL, "userId" integer NOT NULL, "originalValue" integer NOT NULL, "updatedValue" integer NOT NULL, "updatedAt" bigint NOT NULL, "command" character varying NOT NULL, CONSTRAINT "PK_0c0431424ad9af4002e606a5337" PRIMARY KEY ("id"))`, undefined);
await queryRunner.query(`CREATE INDEX "IDX_points_changelog_userId" ON "points_changelog" ("userId") `, undefined);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_points_changelog_userId"`, undefined);
await queryRunner.query(`DROP TABLE "points_changelog"`, undefined);
}

}
16 changes: 16 additions & 0 deletions src/bot/database/migration/sqlite/1587985914346-pointsChangelog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {MigrationInterface, QueryRunner} from 'typeorm';

export class pointsChangelog1587985914346 implements MigrationInterface {
name = 'pointsChangelog1587985914346';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "points_changelog" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "userId" integer NOT NULL, "originalValue" integer NOT NULL, "updatedValue" integer NOT NULL, "updatedAt" bigint NOT NULL, "command" varchar NOT NULL)`, undefined);
await queryRunner.query(`CREATE INDEX "IDX_points_changelog_userId" ON "points_changelog" ("userId") `, undefined);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_points_changelog_userId"`, undefined);
await queryRunner.query(`DROP TABLE "points_changelog"`, undefined);
}

}
75 changes: 73 additions & 2 deletions src/bot/systems/points.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import { permission } from '../helpers/permissions';
import System from './_interface';
import { debug, error, warning } from '../helpers/log';
import { adminEndpoint } from '../helpers/socket';
import { FindConditions, getConnection, getRepository } from 'typeorm';
import { FindConditions, getConnection, getRepository, LessThanOrEqual } from 'typeorm';
import { User, UserInterface } from '../database/entity/user';
import { PointsChangelog } from '../database/entity/points';
import { getAllOnlineUsernames } from '../helpers/getAllOnlineUsernames';
import { onChange, onLoad } from '../decorators/on';
import permissions from '../permissions';
Expand Down Expand Up @@ -99,6 +100,11 @@ class Points extends System {
return;
}

// cleanup all undoes (only 10minutes should be kept)
await getRepository(PointsChangelog).delete({
updatedAt: LessThanOrEqual(Date.now() - (10 * MINUTE)),
});

const [interval, offlineInterval, perInterval, perOfflineInterval, isOnline] = await Promise.all([
this.getPermissionBasedSettingsValue('interval'),
this.getPermissionBasedSettingsValue('offlineInterval'),
Expand Down Expand Up @@ -267,13 +273,60 @@ class Points extends System {
}
}

@command('!points undo')
@default_permission(permission.CASTERS)
async undo(opts: CommandOptions) {
try {
const [username] = new Expects(opts.parameters).username().toArray();
const userId = await users.getIdByName(username);
if (!userId) {
throw new Error(`User ${username} not found in database`);
}

const undoOperation = await getRepository(PointsChangelog).findOne({
where: { userId },
order: { updatedAt: 'DESC' },
});
if (!undoOperation) {
throw new Error(`No undo operation found for ` + username);
}

await getRepository(PointsChangelog).delete({ id: undoOperation.id });
await getRepository(User).update({ userId }, { points: undoOperation.originalValue });

return [{
response: prepare('points.success.undo', {
username,
command: undoOperation.command,
originalValue: undoOperation.originalValue,
originalValuePointsLocale: await this.getPointsName(undoOperation.originalValue),
updatedValue: undoOperation.updatedValue,
updatedValuePointsLocale: await this.getPointsName(undoOperation.updatedValue),
}), ...opts }];
} catch (err) {
error(err);
return [{ response: translate('points.failed.undo').replace('$command', opts.command), ...opts }];
}
}

@command('!points set')
@default_permission(permission.CASTERS)
async set (opts: CommandOptions): Promise<CommandResponse[]> {
try {
const [username, points] = new Expects(opts.parameters).username().points({ all: false }).toArray();

const originalUser = await getRepository(User).findOne({username});
if (!originalUser) {
throw new Error(`User ${username} not found in database.`);
}
await getRepository(User).update({ username }, { points });
await getRepository(PointsChangelog).insert({
userId: originalUser.userId,
updatedAt: Date.now(),
command: 'set',
originalValue: originalUser.points,
updatedValue: points,
});

const response = prepare('points.success.set', {
amount: points,
Expand Down Expand Up @@ -530,12 +583,21 @@ class Points extends System {
if (!user) {
await getRepository(User).save({
userId: Number(await api.getIdFromTwitch(username)),
username, points,
username,
});
return this.add(opts);
} else {
await getRepository(User).save({ ...user, points: user.points + points });
}

await getRepository(PointsChangelog).insert({
userId: user.userId,
command: 'add',
originalValue: user.points,
updatedValue: user.points + points,
updatedAt: Date.now(),
});

const response = prepare('points.success.add', {
amount: points,
username: username,
Expand Down Expand Up @@ -568,13 +630,22 @@ class Points extends System {
await getRepository(User).save({...user, points: Math.max(user.points - points, 0)});
}

await getRepository(PointsChangelog).insert({
userId: user.userId,
command: 'remove',
originalValue: user.points,
updatedValue: points === 'all' ? 0 : Math.max(user.points - points, 0),
updatedAt: Date.now(),
});

const response = prepare('points.success.remove', {
amount: points,
username: username,
pointsName: await this.getPointsName(points === 'all' ? 0 : points),
});
return [{ response, ...opts }];
} catch (err) {
error(err);
return [{ response: translate('points.failed.remove').replace('$command', opts.command), ...opts }];
}
}
Expand Down
3 changes: 2 additions & 1 deletion test/helpers/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const { Price } = require('../../dest/database/entity/price');
const { Rank } = require('../../dest/database/entity/rank');
const { Timer, TimerResponse } = require('../../dest/database/entity/timer');
const { Poll, PollVote } = require('../../dest/database/entity/poll');
const { PointsChangelog } = require('../../dest/database/entity/points');
const { Duel } = require('../../dest/database/entity/duel');
const { Variable, VariableHistory, VariableURL } = require('../../dest/database/entity/variable');
const { Event, EventOperation } = require('../../dest/database/entity/event');
Expand All @@ -44,7 +45,7 @@ module.exports = {
debug('test', chalk.bgRed('*** Cleaning up collections ***'));
await waitMs(400); // wait little bit for transactions to be done

const entities = [SongRequest, RaffleParticipant, Rank, PermissionCommands, Event, EventOperation, Variable, VariableHistory, VariableURL, Raffle, Duel, PollVote, Poll, TimerResponse, Timer, BetsParticipations, UserTip, UserBit, CommandsResponses, User, ModerationPermit, Alias, Bets, Commands, CommandsCount, Quotes, Settings, Cooldown, Keyword, Price];
const entities = [PointsChangelog, SongRequest, RaffleParticipant, Rank, PermissionCommands, Event, EventOperation, Variable, VariableHistory, VariableURL, Raffle, Duel, PollVote, Poll, TimerResponse, Timer, BetsParticipations, UserTip, UserBit, CommandsResponses, User, ModerationPermit, Alias, Bets, Commands, CommandsCount, Quotes, Settings, Cooldown, Keyword, Price];
if (['postgres', 'mysql'].includes((await getManager()).connection.options.type)) {
const metadatas = [];
for (const entity of entities) {
Expand Down

0 comments on commit 783e7cc

Please sign in to comment.