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

Feature/11 - Multi server support #31

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,11 @@
const NotImplemented = require("@errors/notImplemented");

module.exports = class ActionHandlerBase {
constructor(logger, command) {
this.command = command;
constructor(logger, name) {
this.name = name;
this.logger = logger;
this.logPrefix = `[${this.constructor.name}] `;
this.logger.log(`${this.logPrefix}Initialising '!${command}' action handler`);
}

isMatch(action) {
return (this.command && this.command === action.command);
this.logger.log(`${this.logPrefix}Initialising '!${name}' action handler`);
}

async handle() {
Expand Down
22 changes: 22 additions & 0 deletions app/actionHandlers/actionHandlerMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

module.exports = class ActionHandlerMessage {
/**
* This class acts as an interface between the chatListeners and the actionHandlers.
* @constructor
*/
constructor() {
this.command = "";
this.data = "";
this.isBangCommand = false;

// Remaining properties are overridden by each *MessageResolver
this.server = null;
this.isBot = false;

this.userId = 0;
this.channelId = 0
this.nick = "";
this.timestamp = null;
}
}
31 changes: 31 additions & 0 deletions app/actionHandlers/actionHandlerResolver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use strict';

module.exports = class ActionHandlerResolver {
constructor({ logger, actions }) {
this.actions = actions;
this.logger = logger;
this.logPrefix = `[${this.constructor.name}] `;
}

async resolve(actionHandlerMessage) {
const m = actionHandlerMessage;
this.logger.log(`${this.logPrefix}Received isBangCommand: '${m.isBangCommand}'; command: '${m.command}'; data: '${m.data}'`);
if (m.isBangCommand === true && m.command === "generic") {
throw new Error("The generic action handler cannot be resolved with a bang command");
}

let resolvedAction = this.actions.find(a => a.name === (m.command));

if (resolvedAction) {
return resolvedAction;
}

resolvedAction = this.actions.find(a => a.name === "generic");

if (!resolvedAction) {
throw new Error("The generic action handler was not found");
}

return resolvedAction;
}
}
136 changes: 136 additions & 0 deletions app/actionHandlers/actionHandlerResolver.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
'use strict';

const ActionHandlerResolver = require("@actionHandlers/actionHandlerResolver");
const faker = require('faker');

describe("actionHandlerResolver", () => {
let logger;

beforeEach(() => {
logger = jasmine.createSpyObj("logger", ["log"]);
});

it("maps injected actions to actions property", () => {
// Arrange
const actions = [
{}, {}
];

// Act
const resolver = new ActionHandlerResolver({ logger, actions });

// Assert
expect(resolver.actions).toBe(actions);
});

it("maps injected logger to logger property", () => {
// Act
const resolver = new ActionHandlerResolver({ logger });

// Assert
expect(resolver.logger).toBe(logger);
});

it("resolve returns generic action handler if isBangCommand is false", async () => {
// Arrange
const actionHandlerMessage = jasmine.createSpyObj("actionHandlerMessage", null, {
isBangCommand: false,
command: "generic"
});
const genericActionHandler = jasmine.createSpyObj("genericActionHandler", null, {
name: "generic"
});
const actions = [
genericActionHandler
];
const resolver = new ActionHandlerResolver({ logger, actions });

// Act
const resolvedAction = await resolver.resolve(actionHandlerMessage)

// Assert
expect(resolvedAction).toBe(genericActionHandler);
});

it("resolve returns action handler specified in command if isBangCommand is true", async () => {
// Arrange
const actionHandlerMessage = jasmine.createSpyObj("actionHandlerMessage", null, {
isBangCommand: true,
command: "notes"
});
const notesActionHandler = jasmine.createSpyObj("notesActionHandler", null, {
name: "notes"
});

const actions = [
notesActionHandler
];
const resolver = new ActionHandlerResolver({ logger, actions });

// Act
const resolvedAction = await resolver.resolve(actionHandlerMessage)

// Assert
expect(resolvedAction).toBe(notesActionHandler);
});

it("resolve returns rejected promise if isBangCommand is true and command is generic", async () => {
// Arrange
const actionHandlerMessage = jasmine.createSpyObj("actionHandlerMessage", null, {
isBangCommand: true,
command: "generic"
});
const genericActionHandler = jasmine.createSpyObj("genericActionHandler", null, {
name: "generic"
});
const actions = [
genericActionHandler
];
const resolver = new ActionHandlerResolver({ logger, actions });

// Act & Assert
await expectAsync(resolver.resolve(actionHandlerMessage))
.toBeRejectedWithError("The generic action handler cannot be resolved with a bang command");
});

it("resolve returns generic action handler when isBangCommand is true and action handler does not exist", async () => {
// Arrange
const actionHandlerMessage = jasmine.createSpyObj("actionHandlerMessage", null, {
isBangCommand: true,
command: "notavalidaction"
});
const genericActionHandler = jasmine.createSpyObj("genericActionHandler", null, {
name: "generic"
});
const actions = [
genericActionHandler
];
const resolver = new ActionHandlerResolver({ logger, actions });

// Act
const resolvedAction = await resolver.resolve(actionHandlerMessage)

// Assert
expect(resolvedAction).toBe(genericActionHandler);
});

it("resolve rejects promise if generic action handler is not found", async () => {
// Arrange
const actionHandlerMessage = jasmine.createSpyObj("actionHandlerMessage", null, {
isBangCommand: true,
command: faker.random.word()
});
const anotherActionHandler = jasmine.createSpyObj("anotherActionHandler", null, {
name: "notgeneric"
});
const actions = [
anotherActionHandler
];
const resolver = new ActionHandlerResolver({ logger, actions });

// Act & Assert
await expectAsync(resolver.resolve(actionHandlerMessage))
.toBeRejectedWithError("The generic action handler was not found");
});

});
33 changes: 33 additions & 0 deletions app/actionHandlers/generic/genericActionHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use strict';

const ActionHandlerBase = require("@actionHandlers/actionHandlerBase");

module.exports = class GenericActionHandler extends ActionHandlerBase {
constructor({ logger }) {
super(logger, "generic");
}

async handle(actionHandlerMessage) {
if (!actionHandlerMessage) {
return;
}

// This action handler is called whenever a bang command is not found for a message received by any server.

// This is intended be used to log all chat messages from all
// listeners.
// This feature should be enabled or disabled via config.

// Do not uncomment the below line unless you want every single chat message from every server to be logged.

// This could potentially be used to redirect chat from all
// servers to a central monitoring destination i.e. if
// a streamer pushes their stream to mixer, twitch and
// YouTube, but monitor chat in one place.

// this.logger.log(`${this.logPrefix}Generic action handler received message: '${actionHandlerMessage.data}'`);

// DO NOT RETURN ANYTHING FROM THIS HANDLER!
// If you do, the bot will go into recursive meltdown
}
};
19 changes: 19 additions & 0 deletions app/actionHandlers/help/helpActionHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict';

const ActionHandlerBase = require("@actionHandlers/actionHandlerBase");

module.exports = class HelpActionHandler extends ActionHandlerBase {
constructor({ logger, helpActions }) {
super(logger, "help");
this.actions = helpActions;
this.help = "`!help` to show this message.";
}

async handle(actionHandlerMessage) {
if (!actionHandlerMessage) {
return;
}
let helpText = this.actions.map(a => a.help).join("\r\n");
return helpText;
}
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

const ActionHandlerBase = require("@actions/actionHandlerBase");
const ActionHandlerBase = require("@actionHandlers/actionHandlerBase");

module.exports = class NotesActionHandler extends ActionHandlerBase {
constructor({ logger, notesActionPersistenceHandler }) {
Expand All @@ -9,20 +9,23 @@ module.exports = class NotesActionHandler extends ActionHandlerBase {
this.help = "`!notes [message]` records a note.";
}

async handle(action, msg) {
if (!action || !msg) {
async handle(actionHandlerMessage) {
if (!actionHandlerMessage) {
return;
}
const userID = msg.author.id;
const channelID = msg.channel.id;
const nick = msg.author.username;
const timestamp = msg.createdAt;
if (!action.data) {
const userID = actionHandlerMessage.userId;
const channelID = actionHandlerMessage.channelId;
const nick = actionHandlerMessage.nick;
const timestamp = actionHandlerMessage.timestamp;
const data = actionHandlerMessage.data;
const server = actionHandlerMessage.server;

if (!data) {
return "I can't record an empty note! " + this.help;
}

try {
await this.persistenceHandler.insertNote(timestamp, userID, channelID, nick, action.data);
await this.persistenceHandler.insertNote(timestamp, userID, channelID, nick, data, server);
return "thanks, I've recorded that for you.";
} catch (e) {
console.error(e);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

const ActionHandlerBase = require("@actions/actionHandlerBase");
const ActionHandlerBase = require("@actionHandlers/actionHandlerBase");

module.exports = class QuoteActionHandler extends ActionHandlerBase {
constructor({ logger, notesActionPersistenceHandler }) {
Expand All @@ -9,12 +9,12 @@ module.exports = class QuoteActionHandler extends ActionHandlerBase {
this.help = "`!quote <search>` finds a note (if `search` is omitted, I'll just find a random note).";
}

async handle(action) {
if (!action) {
async handle(actionHandlerMessage) {
if (!actionHandlerMessage) {
return;
}

if (!action.data) {
if (!actionHandlerMessage.data) {
try {
const [rows] = await this.persistenceHandler.getRandomNote();
if (rows.length) {
Expand All @@ -28,7 +28,7 @@ module.exports = class QuoteActionHandler extends ActionHandlerBase {
}
} else {
try {
const [rows] = await this.persistenceHandler.getRandomNoteByContent(action.data);
const [rows] = await this.persistenceHandler.getRandomNoteByContent(actionHandlerMessage.data);
if (rows.length) {
return `\`${rows[0]["nick"]}\`: \`\`\`${rows[0].message}\`\`\``;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ module.exports = class NotesActionPersistenceHandler {
this.logger.log(`${this.logPrefix}Initialising action persistence handler`);
}

async insertNote(timestamp, userID, channelID, nick, message) {
async insertNote(timestamp, userID, channelID, nick, message, server) {
this.logger.log(`${this.logPrefix}Inserting new note`);
return await this.repository.insertNote(timestamp, userID, channelID, nick, message);
return await this.repository.insertNote(timestamp, userID, channelID, nick, message, server);
}

async getRandomNote() {
Expand Down
Loading