Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(points): add !points undo #3593

Merged
merged 4 commits into from
Apr 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
}

}
74 changes: 72 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,59 @@ 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 });

sendMessage(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.sender, opts.attr);
} catch (err) {
error(err);
sendMessage(translate('points.failed.undo').replace('$command', opts.command), opts.sender, opts.attr);
}
}

@command('!points set')
@default_permission(permission.CASTERS)
async set (opts: CommandOptions) {
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 message = await prepare('points.success.set', {
amount: points,
Expand Down Expand Up @@ -532,12 +584,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 message = await prepare('points.success.add', {
amount: points,
username: username,
Expand Down Expand Up @@ -571,13 +632,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 message = await prepare('points.success.remove', {
amount: points,
username: username,
pointsName: await this.getPointsName(points === 'all' ? 0 : points),
});
sendMessage(message, opts.sender, opts.attr);
} catch (err) {
error(err);
sendMessage(translate('points.failed.remove').replace('$command', opts.command), opts.sender, opts.attr);
}
}
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