diff --git a/README.md b/README.md index b1d1870..c74b38e 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ Completely customizable Twitch Bot. - [ ] Predictions - [ ] Event-based Action - [ ] Simple answer - - [ ] raided + - [x] join + - [x] raided - [ ] resub - [ ] submysterygift - [ ] subgift @@ -59,4 +60,24 @@ schedulers: - 'Text rolling 1' - 'Text rolling 2' - 'Text rolling 3' +events: + - name: 'join' + messages: + - 'Less noise {{ Username }} is coming!' + - 'Ah! We are talking about you {{ Username }} !' + - name: 'raided' + messages: + - 'Thanks to @{{ Username }} for this raid of {{ Viewers }} viewers !' + - name: 'resub' + messages: + - 'Thanks {{ Username }} for your {{ Months }} with us ! -- {{ Username }} say: {{ Message }}' + - name: 'submysterygift' + messages: + - '{{ Username }} is rich and he just offered {{ OfferedSubs }} subscription! Thank him in the chat! (with a total of {{ GiftCount }} subscription offered)' + - name: 'subgift' + messages: + - 'Hey ! {{ Username }} is {{ GiftCount }}x more generous with {{ RecipientUsername }} !' + - name: 'subscription' + messages: + - 'I know someone from sub, but, I say anything, alright {{ Username }} ?' ``` diff --git a/package-lock.json b/package-lock.json index 8de73ed..1104e88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2615,6 +2615,18 @@ "dev": true, "optional": true }, + "handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -3897,8 +3909,7 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "mixin-deep": { "version": "1.3.2", @@ -3975,6 +3986,11 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -5048,8 +5064,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "source-map-resolve": { "version": "0.5.3", @@ -5535,6 +5550,12 @@ "integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==", "dev": true }, + "uglify-js": { + "version": "3.13.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.3.tgz", + "integrity": "sha512-otIc7O9LyxpUcQoXzj2hL4LPWKklO6LJWoJUzNa8A17Xgi4fOeDC8FBDOLHnC/Slo1CQgsZMcM6as0M76BZaig==", + "optional": true + }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -5747,6 +5768,11 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", diff --git a/package.json b/package.json index 722a853..27d81c1 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "repository": "https://github.com/Ealenn/LarbinBot", "dependencies": { "figlet": "^1.5.0", + "handlebars": "^4.7.7", "reflect-metadata": "^0.1.13", "tmi.js": "^1.5.0", "tsyringe": "^4.5.0", diff --git a/src/Configuration.ts b/src/Configuration.ts index 7d5015d..6826c0e 100644 --- a/src/Configuration.ts +++ b/src/Configuration.ts @@ -41,9 +41,9 @@ export class Configuration implements IConfiguration { // Twitch this.Twitch = { - Username: process.env.TWITCH_USERNAME as string || '', - Password: process.env.TWITCH_PASSWORD as string || '', - Channel: process.env.TWITCH_CHANNEL as string || '' + Username: process.env.LARBIN_TWITCH_USERNAME as string || '', + Password: process.env.LARBIN_TWITCH_PASSWORD as string || '', + Channel: process.env.LARBIN_TWITCH_CHANNEL as string || '' }; } } diff --git a/src/LarbinBot.ts b/src/LarbinBot.ts index 94e364a..f4fdf2f 100644 --- a/src/LarbinBot.ts +++ b/src/LarbinBot.ts @@ -1,7 +1,8 @@ import { inject, injectable } from 'tsyringe'; import { IConfiguration } from './Configuration'; -import { WriterCommand } from './lib/Commands/WriterCommand'; -import { WriterScheduler } from './lib/Schedulers/WriterScheduler'; +import { WriterCommand } from './lib/Commands'; +import { IEvent, IEventParams } from './lib/Events'; +import { IScheduler } from './lib/Schedulers'; import { ILoggerService } from './services/LoggerService'; import { ITwitchService } from './services/TwitchService'; import { IYamlService } from './services/YamlService'; @@ -44,11 +45,18 @@ export class LarbinBot implements ILarbinBot { // Schedulers const schedulers = this._yamlService.getSchedulers(); - schedulers.forEach((scheduler: WriterScheduler) => { + schedulers.forEach((scheduler: IScheduler) => { this._twitchService.AddScheduler(scheduler); this._loggerService.Debug(`Scheduler ${scheduler.Id} every ${scheduler.Minutes} minutes Added.`); }); + // Events + const events = this._yamlService.getEvents(); + events.forEach((event: IEvent) => { + this._twitchService.AddEvent(event); + this._loggerService.Debug(`Event ${event.Type} Added.`); + }); + // Start this._twitchService.Listen(); } diff --git a/src/lib/Commands/WriterCommand.ts b/src/lib/Commands/WriterCommand.ts index 5de3426..5056ba9 100644 --- a/src/lib/Commands/WriterCommand.ts +++ b/src/lib/Commands/WriterCommand.ts @@ -2,7 +2,7 @@ import { ICommand } from '.'; import { ITwitchService } from '../../services/TwitchService'; /** - * Simple Writer Command + * Writer Command */ export class WriterCommand implements ICommand { private _trigger: string; diff --git a/src/lib/Commands/index.ts b/src/lib/Commands/index.ts index 1f1b71b..7552688 100644 --- a/src/lib/Commands/index.ts +++ b/src/lib/Commands/index.ts @@ -1,7 +1,15 @@ import { ChatUserstate } from 'tmi.js'; import { ITwitchService } from '../../services/TwitchService'; +/** + * Base Command + */ export interface ICommand { Trigger: string; Action(twitchService: ITwitchService, state: ChatUserstate): void; } + +/** + * Commands + */ +export * from './WriterCommand' diff --git a/src/lib/Events/RandomMessageEvent.ts b/src/lib/Events/RandomMessageEvent.ts new file mode 100644 index 0000000..f269dfb --- /dev/null +++ b/src/lib/Events/RandomMessageEvent.ts @@ -0,0 +1,26 @@ +import { EventType, IEvent, IEventParams } from '.'; +import Handlebars from 'handlebars'; +import { ITwitchService } from '../../services/TwitchService'; + +export class RandomMessageEvent implements IEvent { + public Type: EventType; + private _messages: Array; + + constructor(type: EventType, messages: Array) + { + this.Type = type; + this._messages = messages; + } + + protected getMessage() : string { + return this._messages[Math.floor(Math.random() * this._messages.length)]; + } + + public Action(twitchService: ITwitchService, params: T): void { + const message = this.getMessage(); + const Template = Handlebars.compile(message); + twitchService.Write( + Template(params) + ); + } +} diff --git a/src/lib/Events/index.ts b/src/lib/Events/index.ts new file mode 100644 index 0000000..1c7e503 --- /dev/null +++ b/src/lib/Events/index.ts @@ -0,0 +1,72 @@ +import { ITwitchService } from '../../services/TwitchService'; + +/** + * Event Type + */ +export enum EventType { + RAIDED = 'raided', + JOIN = 'join', + RESUB = 'resub', + SUBMYSTERYGIFT = 'submysterygift', + SUBGIFT = 'subgift', + SUBSCRIPTION = 'subscription' +} + +/** + * Base Event + */ +export interface IEvent { + Type: EventType; + Action(twitchService: ITwitchService, params: T): void; +} + +/** + * Event Params + */ +export interface IEventParams { + Channel: string; +} + +export class RaidedEventParams implements IEventParams { + Channel: string; + Username: string; + Viewers: number; +} + +export class JoinEventParams implements IEventParams { + Channel: string; + Username: string; +} + +export class ResubEventParams implements IEventParams { + Channel: string; + Username: string; + Months: number; + Message: string; +} + +export class SubGiftEventParams implements IEventParams { + Channel: string; + Username: string; + StreakMonths: number; + RecipientUsername: string; + GiftCount: number; +} + +export class SubMysteryGiftEventParams implements IEventParams { + Channel: string; + Username: string; + OfferedSubs: number; + GiftCount: number; +} + +export class SubscriptionEventParams implements IEventParams { + Channel: string; + Username: string; + Message: string; +} + +/** + * Events + */ +export * from './RandomMessageEvent'; diff --git a/src/lib/Schedulers/WriterScheduler.ts b/src/lib/Schedulers/RoundRobinScheduler.ts similarity index 87% rename from src/lib/Schedulers/WriterScheduler.ts rename to src/lib/Schedulers/RoundRobinScheduler.ts index a59524d..89f92cc 100644 --- a/src/lib/Schedulers/WriterScheduler.ts +++ b/src/lib/Schedulers/RoundRobinScheduler.ts @@ -2,9 +2,9 @@ import { IScheduler } from '.'; import { ITwitchService } from '../../services/TwitchService'; /** - * Simple Writer Scheduler + * Round Robin Scheduler */ -export class WriterScheduler implements IScheduler { +export class RoundRobinScheduler implements IScheduler { private _id: string; public get Id(): string { return this._id; @@ -26,7 +26,7 @@ export class WriterScheduler implements IScheduler { this._messages = messages; } - private getMessage(): string { + protected getMessage(): string { const message = this._messages[this._messageIndex]; this._messageIndex = this._messageIndex + 1; if (this._messageIndex >= this._messages.length) { diff --git a/src/lib/Schedulers/index.ts b/src/lib/Schedulers/index.ts index a09dbf3..2718750 100644 --- a/src/lib/Schedulers/index.ts +++ b/src/lib/Schedulers/index.ts @@ -1,7 +1,15 @@ import { ITwitchService } from '../../services/TwitchService'; +/** + * Base Scheduler + */ export interface IScheduler { Id: string; Minutes: number; Action(twitchService: ITwitchService): void; } + +/** + * Schedulers + */ +export * from './RoundRobinScheduler'; diff --git a/src/mappers/EventTypeParamsMapper.ts b/src/mappers/EventTypeParamsMapper.ts new file mode 100644 index 0000000..f0b1fc4 --- /dev/null +++ b/src/mappers/EventTypeParamsMapper.ts @@ -0,0 +1,59 @@ +import { EventType, IEventParams, ResubEventParams } from '../lib/Events'; +import { JoinEventParams, RaidedEventParams, SubMysteryGiftEventParams, SubscriptionEventParams } from '../lib/Events'; + +/** + * Convert tmi.js args to EventTypeParams T + * https://github.com/tmijs/docs/blob/gh-pages/_posts/v1.4.2/2019-03-03-Events.md + * @param eventType + * @param args + * @returns T + */ +export function EventTypeParamsMapper(eventType: EventType, args: any[]): T { + let params = { + Channel: args[0] as string + }; + + switch (eventType) { + case EventType.JOIN: + params = Object.assign(params, { + Username: args[1] as string + } as JoinEventParams); + break; + case EventType.RAIDED: + params = Object.assign(params, { + Username: args[1] as string, + Viewers: args[2] as number + } as RaidedEventParams); + break; + case EventType.RESUB: + params = Object.assign(params, { + Username: args[1], + Months: args[2], + Message: args[3] + }) as ResubEventParams; + break; + case EventType.SUBGIFT: + params = Object.assign(params, { + Username: args[1], + StreakMonths: args[2], + RecipientUsername: args[3], + GiftCount: args[5]['msg-param-sender-count'] + }); + break; + case EventType.SUBMYSTERYGIFT: + params = Object.assign(params, { + Username: args[1], + OfferedSubs: args[2], + GiftCount: args[3] + }) as SubMysteryGiftEventParams; + break; + case EventType.SUBSCRIPTION: + params = Object.assign(params, { + Username: args[1], + Message: args[3] + } as SubscriptionEventParams); + break; + } + + return params as T; +} diff --git a/src/services/TwitchService.ts b/src/services/TwitchService.ts index acb12f2..adf7d2f 100644 --- a/src/services/TwitchService.ts +++ b/src/services/TwitchService.ts @@ -2,7 +2,9 @@ import { Client, Options } from 'tmi.js' import { inject, singleton } from 'tsyringe'; import { IConfiguration } from '../Configuration'; import { ICommand } from '../lib/Commands'; +import { IEvent, IEventParams } from '../lib/Events'; import { IScheduler } from '../lib/Schedulers'; +import { EventTypeParamsMapper } from '../mappers/EventTypeParamsMapper'; import { ILoggerService } from './LoggerService'; /** @@ -12,6 +14,7 @@ export interface ITwitchService { Write(message: string): void; AddCommand(command: ICommand): ITwitchService; AddScheduler(scheduler: IScheduler): ITwitchService; + AddEvent(event: IEvent): ITwitchService; Listen(): void; } @@ -62,15 +65,22 @@ export class TwitchService implements ITwitchService { this._client.say(this._configuration.Twitch.Channel, message); } - public AddCommand(command: ICommand) : ITwitchService { + public AddCommand(command: ICommand): ITwitchService { this._commands.push(command); return this; } - public AddScheduler(scheduler: IScheduler) : ITwitchService { + public AddScheduler(scheduler: IScheduler): ITwitchService { setInterval((s) => { s.Action(this); }, scheduler.Minutes * 60000, scheduler); return this; } + + public AddEvent(event: IEvent): ITwitchService { + this._client.on(event.Type, (...args: any[]) => { + event.Action(this, EventTypeParamsMapper(event.Type, args)); + }); + return this; + } } diff --git a/src/services/YamlService.ts b/src/services/YamlService.ts index 8730727..3025a64 100644 --- a/src/services/YamlService.ts +++ b/src/services/YamlService.ts @@ -3,16 +3,19 @@ import { IConfiguration } from '../Configuration'; import YAML from 'yaml'; import FS from 'fs'; import Path from 'path'; -import { WriterCommand } from '../lib/Commands/WriterCommand'; +import { WriterCommand } from '../lib/Commands'; import { ILoggerService } from './LoggerService'; -import { WriterScheduler } from '../lib/Schedulers/WriterScheduler'; +import { EventType, IEvent, IEventParams } from '../lib/Events'; +import { JoinEventParams, RaidedEventParams, RandomMessageEvent } from '../lib/Events'; +import { IScheduler, RoundRobinScheduler } from '../lib/Schedulers'; /** * Provides tools for Yaml validation/parser */ export interface IYamlService { getCommands(): Array; - getSchedulers(): Array; + getSchedulers(): Array; + getEvents(): Array>; } @singleton() @@ -63,9 +66,9 @@ export class YamlService implements IYamlService { return commands; } - public getSchedulers(): Array { + public getSchedulers(): Array { const yamlContent = this.getYamlContent(); - const schedulers = new Array(); + const schedulers = new Array(); if (!yamlContent || !yamlContent.schedulers) { return schedulers; @@ -76,10 +79,27 @@ export class YamlService implements IYamlService { const id = Math.floor( Math.random() * (9999999 - 1111111) + 1111111 ); - schedulers.push(new WriterScheduler(`${element.id}#${id}`, element.minutes, element.messages)); + schedulers.push(new RoundRobinScheduler(`${element.id}#${id}`, element.minutes, element.messages)); } }); return schedulers; } + + public getEvents(): Array> { + const yamlContent = this.getYamlContent(); + const events = new Array>(); + + if (!yamlContent || !yamlContent.events) { + return events; + } + + yamlContent.events.forEach((element: any) => { + if (element.name && element.messages) { + events.push(new RandomMessageEvent(element.name, element.messages)); + } + }); + + return events; + } } diff --git a/tests/Configuration.spec.ts b/tests/Configuration.spec.ts index 8e15534..f1a1a20 100644 --- a/tests/Configuration.spec.ts +++ b/tests/Configuration.spec.ts @@ -39,9 +39,9 @@ describe('Configuration', function () { it('Twitch - With environment', async function () { // Arrange - process.env.TWITCH_USERNAME = 'John'; - process.env.TWITCH_PASSWORD = 'Smith'; - process.env.TWITCH_CHANNEL = 'Twitch' + process.env.LARBIN_TWITCH_USERNAME = 'John'; + process.env.LARBIN_TWITCH_PASSWORD = 'Smith'; + process.env.LARBIN_TWITCH_CHANNEL = 'Twitch' // Act const configuration = new Configuration(); @@ -56,9 +56,9 @@ describe('Configuration', function () { it('Twitch - Without environment', async function () { // Arrange - delete process.env.TWITCH_USERNAME; - delete process.env.TWITCH_PASSWORD; - delete process.env.TWITCH_CHANNEL; + delete process.env.LARBIN_TWITCH_USERNAME; + delete process.env.LARBIN_TWITCH_PASSWORD; + delete process.env.LARBIN_TWITCH_CHANNEL; // Act const configuration = new Configuration();