Description
This javascript code uses tmi.js
to listen for chat messages in a Twitch chat and perform certain actions depending on their content. In the sample code provided, if the message starts with !add
, !taskadd
, !addtask
or !taska
it will call the addTask
command handler which tries to add the task to a list of tasks. If the user who typed the message has a task already, it will not allow that and if the task is empty it will respond with an informative message.
const options = {
options: { debug: true },
connection: {
reconnect: true,
secure: true
},
identity: {
username: TWITCH_BOT_USERNAME,
password: TWITCH_BOT_PASSWORD
},
channels: [TWITCH_CHANNEL]
};
const client = new Client(options);
client.connect().catch(console.error);
const extractCommandAndMessage = (message) => {
const index = message.trim().indexOf(' ');
if (index === -1) {
return { command: message, message: '' };
}
return {
command: message.trim().substring(0, index),
message: message.substring(index + 1)
};
};
const addTaskCommands = ['!add', '!taskadd', '!addtask', '!taska'];
const twitchMessageHandler = async (channel, user, message, self) => {
if (self) return;
const { command, message: text } = extractCommandAndMessage(message);
if (!command) return;
const lowerCaseCommand = command.toLowerCase();
const username = user['display-name'];
// Tasks
if (addTaskCommands.includes(lowerCaseCommand)) {
addTask(channel, text, username, client);
}
if (editTaskCommands.includes(lowerCaseCommand)) {
editTask(channel, username, client, text);
}
if (completeTaskCommands.includes(lowerCaseCommand)) {
completeTask(channel, username, client);
}
}
const tasks = {};
const addTask = (channel, task, username, client) => {
if (!task) {
client.say(channel, 'Try adding a task after the command.');
return;
}
if (username in tasks) {
client.say(channel, 'You need to !complete or !edit your current task.');
return;
}
client.say(channel, `@${username}, adding "${task}" to the list...`);
tasks[username] = task;
};
const editTask = (channel, username, client, task) => {
if (!(username in tasks)) {
client.say(channel, 'You need to add a task in order to edit it.');
return;
}
if (!task) {
client.say(channel, 'You need to include a description of your new task');
return;
}
client.say(channel, `Okay ${username}, your task now says "${task}"`);
tasks[username] = task;
};
const completeTask = (channel, username, client) => {
if (!(username in tasks)) {
client.say(channel, 'You need to add a task first.');
return;
}
delete tasks[username];
taskCounter++;
if (!(username in individualcounters)) {
individualcounters[username] = 1;
} else {
individualcounters[username] += 1;
}
const userTaskCount = individualcounters[username];
client.say(
channel,
`BOOM ${username}! You completed your task! You have kicked ${userTaskCount} goal${
userTaskCount === 1 ? '' : 's'
} this stream!`
);
individualcounters[username] = userTaskCount;
};
client.on('message', twitchMessageHandler);
Specific issues
Message handler vs command handler responsibility
The addTask command handler is responsible for adding the task and responding with the client, when in reality its core and only function should be adding tasks as its name suggests. Is it a good idea to instead throw exceptions or return a Result object from the addTask
function and handle them in the higher level message handler? Something more like;
const twitchMessageHandler = async (channel, user, message, self) => {
...
if (addTaskCommands.includes(lowerCaseCommand)) {
if (!text) {
client.say(channel, 'Try adding a task after the command.');
}
try {
addTask(channel, text, username, client);
client.say(channel, `@${username}, adding "${text}" to the list...`);
} catch (ex) {
if (ex.name === 'UsernameAlreadyHasTask') {
client.say(
channel,
'You need to !complete or !edit your current task.'
);
}
console.log(ex)
}
}
...
}
const addTask = (task, username) => {
if (!task) {
throw NoTaskProvided();
}
if (username in tasks) {
throw UsernameAlreadyHasTask();
}
tasks[username] = task;
};
Perhaps theres a better way to separate the concerns from here?
Persistence
It's pretty obvious that the persistence method for tasks is a simple in memory JS object. Would it make sense to introduce a repository to handle task persistence something like;
// repository/task.js
const saveTask = (user, task) => {
// DB implementation or the in memory option
}
Conclusion
Besides those two specific questions, any other general improvements and tips would be greatly appreciated. I don't like how the twitchMessageHandler
function works, but it was the simplest method to implement. I am fairly new to javascript and the learning I have been doing have been towards a functional-like method which is very new to me so there are some learning difficulties.