From 80795b0b88e177e0b643da879546a961acf6c244 Mon Sep 17 00:00:00 2001 From: Marchandise Rudy Date: Sat, 29 May 2021 23:54:11 +0200 Subject: [PATCH] feat(counter): command stats variables (#23) --- README.md | 3 +- src/lib/Commands/RandomMessageCommand.ts | 7 ++-- src/lib/Commands/RoundRobinMessageCommand.ts | 7 ++-- src/lib/Commands/index.ts | 11 +++++-- src/services/Model/CommandStats.ts | 14 ++++++++ src/services/TwitchService.ts | 16 ++++++--- .../lib/Commands/RandomMessageCommand.spec.ts | 25 ++++++++++++-- .../Commands/RoundRobinMessageCommand.spec.ts | 25 ++++++++++++-- tests/services/TwitchService.spec.ts | 33 +++++++++++++++---- www/src/views/Configuration.vue | 4 +++ 10 files changed, 123 insertions(+), 22 deletions(-) create mode 100644 src/services/Model/CommandStats.ts diff --git a/README.md b/README.md index 5acd5fb..801997b 100644 --- a/README.md +++ b/README.md @@ -80,11 +80,12 @@ commands: others: true # All messages: - 'My Facebook is https://facebook.com/example' + - 'Like my Facebook page https://facebook.com/example' - name: '!twitter' policies: others: true # All messages: - - 'My Twitter is https://twitter.com/example' + - 'My Twitter is https://twitter.com/example (This question has been asked {{ Count }} times)' schedulers: - id: 'social' # Required, is only used to make this scheduler unique minutes: 10 # Send message every minutes diff --git a/src/lib/Commands/RandomMessageCommand.ts b/src/lib/Commands/RandomMessageCommand.ts index b2cc033..7eb45bd 100644 --- a/src/lib/Commands/RandomMessageCommand.ts +++ b/src/lib/Commands/RandomMessageCommand.ts @@ -1,4 +1,6 @@ +import { ChatUserstate } from 'tmi.js'; import { BaseCommand, CommandPolicies } from '.'; +import { ICommandStats } from '../../services/Model/CommandStats'; import { ITwitchService } from '../../services/TwitchService'; /** @@ -20,7 +22,8 @@ export class RandomMessageCommand extends BaseCommand { return this._messages[Math.floor(Math.random() * this._messages.length)]; } - public Action(twitchService: ITwitchService): void { - twitchService.Write(this.getMessage()); + public Action(twitchService: ITwitchService, fullMessage: string, userState: ChatUserstate, stats: ICommandStats): void { + const messageWithStats = this._replaceWithVariables(this.getMessage(), stats); + twitchService.Write(messageWithStats); } } diff --git a/src/lib/Commands/RoundRobinMessageCommand.ts b/src/lib/Commands/RoundRobinMessageCommand.ts index 3455036..9b4b0f0 100644 --- a/src/lib/Commands/RoundRobinMessageCommand.ts +++ b/src/lib/Commands/RoundRobinMessageCommand.ts @@ -1,4 +1,6 @@ +import { ChatUserstate } from 'tmi.js'; import { BaseCommand, CommandPolicies } from '.'; +import { ICommandStats } from '../../services/Model/CommandStats'; import { ITwitchService } from '../../services/TwitchService'; /** @@ -26,7 +28,8 @@ export class RoundRobinMessageCommand extends BaseCommand { return message; } - public Action(twitchService: ITwitchService): void { - twitchService.Write(this.getMessage()); + public Action(twitchService: ITwitchService, fullMessage: string, userState: ChatUserstate, stats: ICommandStats): void { + const messageWithStats = this._replaceWithVariables(this.getMessage(), stats); + twitchService.Write(messageWithStats); } } diff --git a/src/lib/Commands/index.ts b/src/lib/Commands/index.ts index d22f89b..c184ce7 100644 --- a/src/lib/Commands/index.ts +++ b/src/lib/Commands/index.ts @@ -1,6 +1,8 @@ import { ChatUserstate, Userstate } from 'tmi.js'; import { Configuration } from '../../Configuration'; +import { ICommandStats } from '../../services/Model/CommandStats'; import { ITwitchService } from '../../services/TwitchService'; +import Handlebars from 'handlebars'; /** * Policies Command @@ -19,7 +21,7 @@ export class CommandPolicies { export interface ICommand { Trigger: string; Policies: CommandPolicies; - Action(twitchService: ITwitchService, fullMessage: string, state: ChatUserstate): void; + Action(twitchService: ITwitchService, fullMessage: string, state: ChatUserstate, stats: ICommandStats): void; CanAction(userState: Userstate, configuration: Configuration): boolean; } @@ -38,7 +40,7 @@ export abstract class BaseCommand implements ICommand { this._policies = policies; } - abstract Action(twitchService: ITwitchService, fullMessage: string, userState: ChatUserstate): void; + abstract Action(twitchService: ITwitchService, fullMessage: string, userState: ChatUserstate, stats: ICommandStats): void; public CanAction(userState: Userstate): boolean { if (this._policies.Sub && userState.subscriber) { return true; @@ -57,6 +59,11 @@ export abstract class BaseCommand implements ICommand { } return false; } + + protected _replaceWithVariables(message: string, params: any): string { + const Template = Handlebars.compile(message); + return Template(params); + } } /** diff --git a/src/services/Model/CommandStats.ts b/src/services/Model/CommandStats.ts new file mode 100644 index 0000000..e287df0 --- /dev/null +++ b/src/services/Model/CommandStats.ts @@ -0,0 +1,14 @@ +/** + * Command Stats Model + */ +export interface ICommandStats { + Count: number; + LastTrigger: Date; +} + +export function GenerateDefaultCommandStats(): ICommandStats { + return { + Count: 0, + LastTrigger: new Date() + }; +} diff --git a/src/services/TwitchService.ts b/src/services/TwitchService.ts index 4388883..d21c7d9 100644 --- a/src/services/TwitchService.ts +++ b/src/services/TwitchService.ts @@ -8,6 +8,7 @@ import { EventTypeParamsMapper } from '../mappers/EventTypeParamsMapper'; import { ILoggerService } from '.'; import { ITmiFactory } from '../factory/TmiFactory'; import { setInterval, clearInterval } from 'timers'; +import { GenerateDefaultCommandStats, ICommandStats } from './Model/CommandStats'; /** * Provides all twitch tools @@ -25,8 +26,8 @@ export interface ITwitchService { @singleton() export class TwitchService implements ITwitchService { - private _thresholdKeyDate: Map = new Map(); private _commands: Array = new Array(); + private _commandsStats: Map = new Map(); private _schedulers: Array = new Array(); private _schedulersIntervals: Array = new Array(); @@ -54,14 +55,15 @@ export class TwitchService implements ITwitchService { if (command != undefined) { if (command.CanAction(userstate, this._configuration)) { if (this._thresholdValidation(command.Trigger)) { - command.Action(this, message, userstate); + command.Action(this, message, userstate, this._commandsStats.get(command.Trigger) as ICommandStats); } } } } private _thresholdValidation(key: string): boolean { - const lastTrigger = this._thresholdKeyDate.get(key) ?? new Date(1970, 1); + let commandStats = this._commandsStats.get(key); + const lastTrigger = commandStats?.LastTrigger ?? new Date(1970, 1); const epochNow = new Date().getTime(); const epochLastTrigger = lastTrigger.getTime(); const thresholdInMiliseconds = this._configuration.App.ThresholdInSeconds * 1000; @@ -70,7 +72,13 @@ export class TwitchService implements ITwitchService { return false; } - this._thresholdKeyDate.set(key, new Date()); + if (!commandStats) { + commandStats = GenerateDefaultCommandStats(); + } + + commandStats.LastTrigger = new Date(); + commandStats.Count = commandStats.Count + 1; + this._commandsStats.set(key, commandStats); return true; } diff --git a/tests/lib/Commands/RandomMessageCommand.spec.ts b/tests/lib/Commands/RandomMessageCommand.spec.ts index d3dabeb..3f0b021 100644 --- a/tests/lib/Commands/RandomMessageCommand.spec.ts +++ b/tests/lib/Commands/RandomMessageCommand.spec.ts @@ -4,7 +4,6 @@ import { CommandPolicies, RandomMessageCommand } from '../../../src/lib/Commands import { ITwitchService } from '../../../src/services'; describe('Commands - RandomMessageCommand', function () { - it('Message', async function () { // Arrange const Trigger = '!command'; @@ -22,7 +21,7 @@ describe('Commands - RandomMessageCommand', function () { // Act for (let index = 0; index < 20; index++) { - command.Action(twitchService.object()); + command.Action(twitchService.object(), FullText, {}, { Count: 42, LastTrigger: new Date() }); } // Assert @@ -32,4 +31,26 @@ describe('Commands - RandomMessageCommand', function () { twitchService.verify(x => x.Write(messages[1]), Times.AtLeastOnce()); twitchService.verify(x => x.Write(messages[2]), Times.AtLeastOnce()); }); + + it('Stats', async function () { + // Arrange + const Trigger = '!command'; + const FullText = Trigger; + const Policies = new CommandPolicies(); + + const messages = ['example {{ Count }} stats']; + const twitchService = new Mock(); + twitchService + .setup(x => x.Write(It.IsAny())) + .callback((interaction) => { + expect(interaction.args[0]).toBe('example 42 stats'); + }); + const command = new RandomMessageCommand(Trigger, Policies, messages); + + // Act + command.Action(twitchService.object(), FullText, {}, { Count: 42, LastTrigger: new Date() }); + + // Assert + twitchService.verify(x => x.Write(It.IsAny()), Times.Once()); + }); }); diff --git a/tests/lib/Commands/RoundRobinMessageCommand.spec.ts b/tests/lib/Commands/RoundRobinMessageCommand.spec.ts index 838baf2..36001e4 100644 --- a/tests/lib/Commands/RoundRobinMessageCommand.spec.ts +++ b/tests/lib/Commands/RoundRobinMessageCommand.spec.ts @@ -4,7 +4,6 @@ import { CommandPolicies, RoundRobinMessageCommand } from '../../../src/lib/Comm import { ITwitchService } from '../../../src/services'; describe('Commands - RoundRobinMessageCommand', function () { - it('Message', async function () { // Arrange const Trigger = '!command'; @@ -22,7 +21,7 @@ describe('Commands - RoundRobinMessageCommand', function () { // Act for (let index = 0; index < 30; index++) { - command.Action(twitchService.object()); + command.Action(twitchService.object(), FullText, {}, { Count: 42, LastTrigger: new Date() }); } // Assert @@ -32,4 +31,26 @@ describe('Commands - RoundRobinMessageCommand', function () { twitchService.verify(x => x.Write(messages[1]), Times.Exactly(10)); twitchService.verify(x => x.Write(messages[2]), Times.Exactly(10)); }); + + it('Stats', async function () { + // Arrange + const Trigger = '!command'; + const FullText = Trigger; + const Policies = new CommandPolicies(); + + const messages = ['example {{ Count }} stats']; + const twitchService = new Mock(); + twitchService + .setup(x => x.Write(It.IsAny())) + .callback((interaction) => { + expect(interaction.args[0]).toBe('example 42 stats'); + }); + const command = new RoundRobinMessageCommand(Trigger, Policies, messages); + + // Act + command.Action(twitchService.object(), FullText, {}, { Count: 42, LastTrigger: new Date() }); + + // Assert + twitchService.verify(x => x.Write(It.IsAny()), Times.Once()); + }); }); diff --git a/tests/services/TwitchService.spec.ts b/tests/services/TwitchService.spec.ts index 8456084..40db60a 100644 --- a/tests/services/TwitchService.spec.ts +++ b/tests/services/TwitchService.spec.ts @@ -49,15 +49,16 @@ describe('Service - Twitch', function () { it('Threshold', async function () { // Arrange - const sleep = () => new Promise(resolve => setTimeout(resolve, 2000)); + const fakeCommand = '!TestThreshold'; + const sleep = () => new Promise(resolve => setTimeout(resolve, 2200)); // Act - const result1 = twitchService["_thresholdValidation"](mockCommand.Trigger); - const result2 = twitchService["_thresholdValidation"](mockCommand.Trigger); - const result3 = twitchService["_thresholdValidation"](mockCommand.Trigger); + const result1 = twitchService["_thresholdValidation"](fakeCommand); + const result2 = twitchService["_thresholdValidation"](fakeCommand); + const result3 = twitchService["_thresholdValidation"](fakeCommand); await sleep(); - const result4 = twitchService["_thresholdValidation"](mockCommand.Trigger); - const result5 = twitchService["_thresholdValidation"](mockCommand.Trigger); + const result4 = twitchService["_thresholdValidation"](fakeCommand); + const result5 = twitchService["_thresholdValidation"](fakeCommand); // Assert expect(result1).toBeTruthy(); @@ -65,7 +66,25 @@ describe('Service - Twitch', function () { expect(result3).toBeFalsy(); expect(result4).toBeTruthy(); expect(result5).toBeFalsy(); - }, 5 * 1000); + }, 10 * 1000); + + it('CommandStats', async function () { + // Arrange + const fakeCommand = '!TestCommandStats'; + const sleep = () => new Promise(resolve => setTimeout(resolve, 2200)); + + // Act + twitchService["_thresholdValidation"](fakeCommand); + await sleep(); + twitchService["_thresholdValidation"](fakeCommand); + await sleep(); + twitchService["_thresholdValidation"](fakeCommand); + const result = twitchService["_commandsStats"].get(fakeCommand); + + // Assert + expect(result).toBeDefined(); + expect(result?.Count).toBe(3); + }, 10 * 1000); it('Write', async function () { // Arrange diff --git a/www/src/views/Configuration.vue b/www/src/views/Configuration.vue index 793fbe3..311e378 100644 --- a/www/src/views/Configuration.vue +++ b/www/src/views/Configuration.vue @@ -11,6 +11,9 @@ # Debug mode DEBUG=true + # Single command threshold per second + LARBIN_THRESHOLD=5 + # Twitch Credentials LARBIN_TWITCH_USERNAME: Larbin LARBIN_TWITCH_PASSWORD: oic:password @@ -37,6 +40,7 @@ others: true # All messages: - 'My Facebook is https://facebook.com/example' + - 'Like Facebook page https://twitter.com/example (This question has been asked {{ Count }} times)' `" />