Forward irc errors to the client, improve command validation and feedback, handle topic changes

This commit is contained in:
Ken-Håvard Lieng 2017-05-28 07:20:43 +02:00
parent 993d29242e
commit aa59e71745
17 changed files with 328 additions and 96 deletions

View file

@ -428,11 +428,23 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
color: #999;
}
.message-error {
color: #F6546A;
}
.message-prompt {
font-weight: 700;
font-style: italic;
color: #6BB758;
}
.message-action {
color: #FF6698;
}
.message-time {
font-style: normal;
font-weight: 400;
color: #999;
}

View file

@ -1,9 +1,10 @@
import createCommandMiddleware from './middleware/command';
import createCommandMiddleware, { beforeHandler, notFoundHandler } from './middleware/command';
import { COMMAND } from './state/actions';
import { join, part, invite, kick } from './state/channels';
import { sendMessage, addMessage, raw } from './state/messages';
import { join, part, invite, kick, setTopic } from './state/channels';
import { sendMessage, raw } from './state/messages';
import { setNick, disconnect, whois, away } from './state/servers';
import { select } from './state/tab';
import { find } from './util';
const help = [
'/join <channel> - Join a channel',
@ -11,35 +12,50 @@ const help = [
'/nick <nick> - Change nick',
'/quit - Disconnect from the current server',
'/me <message> - Send action message',
'/topic - Show topic for the current channel',
'/topic [topic] - Show or set topic in the current channel',
'/msg <target> <message> - Send message to the specified channel or user',
'/say <message> - Send message to the current chat',
'/invite <user> [channel] - Invite user to the current or specified channel',
'/kick <user> - Kick user from the current channel',
'/whois <user> - Get information about user',
'/invite <nick> [channel] - Invite user to the current or specified channel',
'/kick <nick> - Kick user from the current channel',
'/whois <nick> - Get information about user',
'/away [message] - Set or clear away message',
'/raw [message] - Send raw IRC message to the current server'
'/raw [message] - Send raw IRC message to the current server',
'/help [command]... - Print help for all or the specified command(s)'
];
const text = content => ({ content });
const error = content => ({ content, type: 'error' });
const prompt = content => ({ content, type: 'prompt' });
const findHelp = cmd => find(help, line => line.slice(1, line.indexOf(' ')) === cmd);
export default createCommandMiddleware(COMMAND, {
join({ dispatch, server }, channel) {
if (channel) {
if (channel[0] !== '#') {
return error('Bad channel name');
}
dispatch(join([channel], server));
dispatch(select(server, channel));
} else {
return error('Missing channel');
}
},
part({ dispatch, server, channel }, partChannel) {
part({ dispatch, server, channel, isChannel }, partChannel) {
if (partChannel) {
dispatch(part([partChannel], server));
} else {
} else if (isChannel) {
dispatch(part([channel], server));
} else {
return error('This is not a channel');
}
},
nick({ dispatch, server }, nick) {
if (nick) {
dispatch(setNick(nick, server));
} else {
return error('Missing nick');
}
},
@ -47,74 +63,127 @@ export default createCommandMiddleware(COMMAND, {
dispatch(disconnect(server));
},
me({ dispatch, server, channel }, ...params) {
if (params.length > 0) {
dispatch(sendMessage(`\x01ACTION ${params.join(' ')}\x01`, channel, server));
me({ dispatch, server, channel }, ...message) {
const msg = message.join(' ');
if (msg !== '') {
dispatch(sendMessage(`\x01ACTION ${msg}\x01`, channel, server));
} else {
return error('Messages can not be empty');
}
},
topic({ dispatch, getState, server, channel }) {
const topic = getState().channels.getIn([server, channel, 'topic']);
if (topic) {
dispatch(addMessage({
server,
to: channel,
content: topic
}));
topic({ dispatch, getState, server, channel }, ...newTopic) {
if (newTopic.length > 0) {
dispatch(setTopic(newTopic.join(' '), channel, server));
} else {
const topic = getState().channels.getIn([server, channel, 'topic']);
if (topic) {
return text(topic);
}
return 'No topic set';
}
},
msg({ dispatch, server }, target, ...message) {
if (target && message) {
if (!target) {
return error('Missing nick/channel');
}
const msg = message.join(' ');
if (msg !== '') {
dispatch(sendMessage(message.join(' '), target, server));
dispatch(select(server, target));
} else {
return error('Messages can not be empty');
}
},
say({ dispatch, server, channel }, ...message) {
if (channel && message) {
if (!channel) {
return error('Messages can only be sent to channels or users');
}
const msg = message.join(' ');
if (msg !== '') {
dispatch(sendMessage(message.join(' '), channel, server));
} else {
return error('Messages can not be empty');
}
},
invite({ dispatch, server, channel }, user, inviteChannel) {
invite({ dispatch, server, channel, isChannel }, user, inviteChannel) {
if (!inviteChannel && !isChannel) {
return error('This is not a channel');
}
if (user && inviteChannel) {
dispatch(invite(user, inviteChannel, server));
} else if (user && channel) {
dispatch(invite(user, channel, server));
} else {
return error('Missing nick');
}
},
kick({ dispatch, server, channel }, user) {
if (user && channel) {
kick({ dispatch, server, channel, isChannel }, user) {
if (!isChannel) {
return error('This is not a channel');
}
if (user) {
dispatch(kick(user, channel, server));
} else {
return error('Missing nick');
}
},
whois({ dispatch, server }, user) {
if (user) {
dispatch(whois(user, server));
} else {
return error('Missing nick');
}
},
away({ dispatch, server }, message) {
dispatch(away(message, server));
away({ dispatch, server }, ...message) {
const msg = message.join(' ');
dispatch(away(msg, server));
if (msg !== '') {
return 'Away message set';
}
return 'Away message cleared';
},
raw({ dispatch, server }, ...message) {
if (message) {
const cmd = message.join(' ');
if (message.length > 0 && message[0] !== '') {
const cmd = `${message[0].toUpperCase()} ${message.slice(1).join(' ')}`;
dispatch(raw(cmd, server));
return `=> ${cmd}`;
return prompt(`=> ${cmd}`);
}
return [
prompt('=> /raw'),
error('Missing message')
];
},
help(_, ...commands) {
if (commands.length > 0) {
const cmdHelp = commands.filter(findHelp).map(findHelp);
if (cmdHelp.length > 0) {
return text(cmdHelp);
}
return error('Unable to find any help :(');
}
return text(help);
},
[beforeHandler](_, command, ...params) {
if (command !== 'raw') {
return prompt(`=> /${command} ${params.join(' ')}`);
}
},
help() {
return help;
},
commandNotFound(_, command) {
return `The command /${command} was not found`;
[notFoundHandler](_, command) {
return error(`=> /${command}: No such command`);
}
});

View file

@ -1,9 +1,26 @@
import { inform } from '../state/messages';
import { addMessages, inform, print } from '../state/messages';
import { isChannel } from '../util';
const notFound = 'commandNotFound';
export const beforeHandler = '_before';
export const notFoundHandler = 'commandNotFound';
function createContext({ dispatch, getState }, { server, channel }) {
return { dispatch, getState, server, channel };
return { dispatch, getState, server, channel, isChannel: isChannel(channel) };
}
// TODO: Pull this out as convenience action
function process({ dispatch, server, channel }, result) {
if (typeof result === 'string') {
dispatch(inform(result, server, channel));
} else if (Array.isArray(result)) {
if (typeof result[0] === 'string') {
dispatch(inform(result, server, channel));
} else if (typeof result[0] === 'object') {
dispatch(addMessages(result, server, channel));
}
} else if (typeof result === 'object' && result) {
dispatch(print(result.content, server, channel, result.type));
}
}
export default function createCommandMiddleware(type, handlers) {
@ -13,16 +30,15 @@ export default function createCommandMiddleware(type, handlers) {
const command = words[0];
const params = words.slice(1);
let result;
if (command in handlers) {
result = handlers[command](createContext(store, action), ...params);
} else if (notFound in handlers) {
result = handlers[notFound](createContext(store, action), command);
}
if (typeof result === 'string' || Array.isArray(result)) {
store.dispatch(inform(result, action.server, action.channel));
const ctx = createContext(store, action);
if (beforeHandler in handlers) {
process(ctx, handlers[beforeHandler](ctx, command, ...params));
}
process(ctx, handlers[command](ctx, ...params));
} else if (notFoundHandler in handlers) {
const ctx = createContext(store, action);
process(ctx, handlers[notFoundHandler](ctx, command));
}
}

View file

@ -1,5 +1,5 @@
import { socketAction } from '../state/actions';
import { broadcast, inform, addMessage, addMessages } from '../state/messages';
import { broadcast, inform, print, addMessage, addMessages } from '../state/messages';
import { select } from '../state/tab';
import { normalizeChannel } from '../util';
import { replace } from '../util/router';
@ -71,25 +71,39 @@ export default function handleSocket({ socket, store: { dispatch, getState } })
dispatch(broadcast(`${data.old} changed nick to ${data.new}`, data.server, channels));
},
topic({ server, channel, topic, nick }) {
if (nick) {
if (topic) {
dispatch(inform(`${nick} changed the topic to:`, server, channel));
dispatch(print(topic, server, channel));
} else {
dispatch(inform(`${nick} cleared the topic`, server, channel));
}
}
},
motd({ content, server }) {
dispatch(addMessages(content.map(line => ({ content: line })), server));
},
whois(data) {
const tab = getState().tab.selected;
if (data.nick) {
const tab = getState().tab.selected;
dispatch(inform([
`Nick: ${data.nick}`,
`Username: ${data.username}`,
`Realname: ${data.realname}`,
`Host: ${data.host}`,
`Server: ${data.server}`,
`Channels: ${data.channels}`
], tab.server, tab.name));
dispatch(print([
`Nick: ${data.nick}`,
`Username: ${data.username}`,
`Realname: ${data.realname}`,
`Host: ${data.host}`,
`Server: ${data.server}`,
`Channels: ${data.channels}`
], tab.server, tab.name));
}
},
print({ server, message }) {
dispatch(inform(message, server));
print(message) {
const tab = getState().tab.selected;
dispatch(addMessage(message, tab.server, tab.name));
}
};

View file

@ -2,6 +2,7 @@ export const INVITE = 'INVITE';
export const JOIN = 'JOIN';
export const KICK = 'KICK';
export const PART = 'PART';
export const SET_TOPIC = 'SET_TOPIC';
export const SET_ENVIRONMENT = 'SET_ENVIRONMENT';

View file

@ -269,3 +269,16 @@ export function kick(user, channel, server) {
}
};
}
export function setTopic(topic, channel, server) {
return {
type: actions.SET_TOPIC,
topic,
channel,
server,
socket: {
type: 'topic',
data: { topic, channel, server }
}
};
}

View file

@ -223,20 +223,24 @@ export function broadcast(message, server, channels) {
})), server);
}
export function inform(message, server, channel) {
export function print(message, server, channel, type) {
if (Array.isArray(message)) {
return addMessages(message.map(line => ({
content: line,
type: 'info'
type
})), server, channel);
}
return addMessage({
content: message,
type: 'info'
type
}, server, channel);
}
export function inform(message, server, channel) {
return print(message, server, channel, 'info');
}
export function runCommand(command, channel, server) {
return {
type: actions.COMMAND,

View file

@ -11,6 +11,11 @@ export function normalizeChannel(channel) {
return channel.split('#').join('').toLowerCase();
}
export function isChannel(name) {
// TODO: Handle other channel types
return typeof name === 'string' && name[0] === '#';
}
export function timestamp(date = new Date()) {
const h = padStart(date.getHours(), 2, '0');
const m = padStart(date.getMinutes(), 2, '0');