Handle kick, rename server to network

This commit is contained in:
Ken-Håvard Lieng 2020-06-15 10:58:51 +02:00
parent a33157ff84
commit 6985dd16da
65 changed files with 2650 additions and 2179 deletions

File diff suppressed because one or more lines are too long

View File

@ -295,14 +295,14 @@ i[class*=' icon-']:before {
border-left: 5px solid #6bb758; border-left: 5px solid #6bb758;
} }
.tab-server { .tab-network {
display: flex; display: flex;
align-items: center; align-items: center;
color: #999; color: #999;
margin-top: 10px !important; margin-top: 10px !important;
} }
.tab-server .tab-content { .tab-network .tab-content {
flex: 1; flex: 1;
margin-right: 5px; margin-right: 5px;
} }
@ -498,12 +498,12 @@ input::-webkit-inner-spin-button {
cursor: pointer; cursor: pointer;
} }
.chat-server .userlist, .chat-network .userlist,
.chat-private .userlist { .chat-private .userlist {
display: none; display: none;
} }
.chat-server .userlist-bar, .chat-network .userlist-bar,
.chat-private .userlist-bar { .chat-private .userlist-bar {
display: none; display: none;
} }
@ -520,7 +520,7 @@ input::-webkit-inner-spin-button {
display: none; display: none;
} }
.chat-server .button-userlist, .chat-network .button-userlist,
.chat-private .button-userlist { .chat-private .button-userlist {
display: none; display: none;
} }
@ -596,7 +596,7 @@ input.chat-title {
background: #f0f0f0; background: #f0f0f0;
} }
.chat-server .search { .chat-network .search {
display: none; display: none;
} }

View File

@ -1,8 +1,8 @@
import { COMMAND } from 'state/actions'; import { COMMAND } from 'state/actions';
import { join, part, invite, kick, setTopic } from 'state/channels'; import { join, part, invite, kick, setTopic } from 'state/channels';
import { sendMessage, raw } from 'state/messages'; import { sendMessage, raw } from 'state/messages';
import { setNick, disconnect, whois, away } from 'state/networks';
import { openPrivateChat } from 'state/privateChats'; import { openPrivateChat } from 'state/privateChats';
import { setNick, disconnect, whois, away } from 'state/servers';
import { select } from 'state/tab'; import { select } from 'state/tab';
import { find, isChannel } from 'utils'; import { find, isChannel } from 'utils';
import createCommandMiddleware, { import createCommandMiddleware, {
@ -14,7 +14,7 @@ const help = [
'/join <channel> - Join a channel', '/join <channel> - Join a channel',
'/part [channel] - Leave the current or specified channel', '/part [channel] - Leave the current or specified channel',
'/nick <nick> - Change nick', '/nick <nick> - Change nick',
'/quit - Disconnect from the current server', '/quit - Disconnect from the current network',
'/me <message> - Send action message', '/me <message> - Send action message',
'/topic [topic] - Show or set topic in the current channel', '/topic [topic] - Show or set topic in the current channel',
'/msg <target> <message> - Send message to the specified channel or user', '/msg <target> <message> - Send message to the specified channel or user',
@ -23,7 +23,7 @@ const help = [
'/kick <nick> - Kick user from the current channel', '/kick <nick> - Kick user from the current channel',
'/whois <nick> - Get information about user', '/whois <nick> - Get information about user',
'/away [message] - Set or clear away message', '/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 network',
'/help [command]... - Print help for all or the specified command(s)' '/help [command]... - Print help for all or the specified command(s)'
]; ];
@ -34,56 +34,56 @@ const findHelp = cmd =>
find(help, line => line.slice(1, line.indexOf(' ')) === cmd); find(help, line => line.slice(1, line.indexOf(' ')) === cmd);
export default createCommandMiddleware(COMMAND, { export default createCommandMiddleware(COMMAND, {
join({ dispatch, server }, channel) { join({ dispatch, network }, channel) {
if (channel) { if (channel) {
if (channel[0] !== '#') { if (channel[0] !== '#') {
return error('Bad channel name'); return error('Bad channel name');
} }
dispatch(join([channel], server)); dispatch(join([channel], network));
dispatch(select(server, channel)); dispatch(select(network, channel));
} else { } else {
return error('Missing channel'); return error('Missing channel');
} }
}, },
part({ dispatch, server, channel, inChannel }, partChannel) { part({ dispatch, network, channel, inChannel }, partChannel) {
if (partChannel) { if (partChannel) {
dispatch(part([partChannel], server)); dispatch(part([partChannel], network));
} else if (inChannel) { } else if (inChannel) {
dispatch(part([channel], server)); dispatch(part([channel], network));
} else { } else {
return error('This is not a channel'); return error('This is not a channel');
} }
}, },
nick({ dispatch, server }, nick) { nick({ dispatch, network }, nick) {
if (nick) { if (nick) {
dispatch(setNick(nick, server)); dispatch(setNick(nick, network));
} else { } else {
return error('Missing nick'); return error('Missing nick');
} }
}, },
quit({ dispatch, server }) { quit({ dispatch, network }) {
dispatch(disconnect(server)); dispatch(disconnect(network));
}, },
me({ dispatch, server, channel }, ...message) { me({ dispatch, network, channel }, ...message) {
const msg = message.join(' '); const msg = message.join(' ');
if (msg !== '') { if (msg !== '') {
dispatch(sendMessage(`\x01ACTION ${msg}\x01`, channel, server)); dispatch(sendMessage(`\x01ACTION ${msg}\x01`, channel, network));
} else { } else {
return error('Messages can not be empty'); return error('Messages can not be empty');
} }
}, },
topic({ dispatch, getState, server, channel }, ...newTopic) { topic({ dispatch, getState, network, channel }, ...newTopic) {
if (newTopic.length > 0) { if (newTopic.length > 0) {
dispatch(setTopic(newTopic.join(' '), channel, server)); dispatch(setTopic(newTopic.join(' '), channel, network));
return; return;
} }
if (channel) { if (channel) {
const { topic } = getState().channels[server][channel]; const { topic } = getState().channels[network][channel];
if (topic) { if (topic) {
return text(topic); return text(topic);
} }
@ -91,83 +91,83 @@ export default createCommandMiddleware(COMMAND, {
return 'No topic set'; return 'No topic set';
}, },
msg({ dispatch, server }, target, ...message) { msg({ dispatch, network }, target, ...message) {
if (!target) { if (!target) {
return error('Missing nick/channel'); return error('Missing nick/channel');
} }
const msg = message.join(' '); const msg = message.join(' ');
if (msg !== '') { if (msg !== '') {
dispatch(sendMessage(message.join(' '), target, server)); dispatch(sendMessage(message.join(' '), target, network));
if (!isChannel(target)) { if (!isChannel(target)) {
dispatch(openPrivateChat(server, target)); dispatch(openPrivateChat(network, target));
} }
dispatch(select(server, target)); dispatch(select(network, target));
} else { } else {
return error('Messages can not be empty'); return error('Messages can not be empty');
} }
}, },
say({ dispatch, server, channel }, ...message) { say({ dispatch, network, channel }, ...message) {
if (!channel) { if (!channel) {
return error('Messages can only be sent to channels or users'); return error('Messages can only be sent to channels or users');
} }
const msg = message.join(' '); const msg = message.join(' ');
if (msg !== '') { if (msg !== '') {
dispatch(sendMessage(message.join(' '), channel, server)); dispatch(sendMessage(message.join(' '), channel, network));
} else { } else {
return error('Messages can not be empty'); return error('Messages can not be empty');
} }
}, },
invite({ dispatch, server, channel, inChannel }, user, inviteChannel) { invite({ dispatch, network, channel, inChannel }, user, inviteChannel) {
if (!inviteChannel && !inChannel) { if (!inviteChannel && !inChannel) {
return error('This is not a channel'); return error('This is not a channel');
} }
if (user && inviteChannel) { if (user && inviteChannel) {
dispatch(invite(user, inviteChannel, server)); dispatch(invite(user, inviteChannel, network));
} else if (user && channel) { } else if (user && channel) {
dispatch(invite(user, channel, server)); dispatch(invite(user, channel, network));
} else { } else {
return error('Missing nick'); return error('Missing nick');
} }
}, },
kick({ dispatch, server, channel, inChannel }, user) { kick({ dispatch, network, channel, inChannel }, user) {
if (!inChannel) { if (!inChannel) {
return error('This is not a channel'); return error('This is not a channel');
} }
if (user) { if (user) {
dispatch(kick(user, channel, server)); dispatch(kick(user, channel, network));
} else { } else {
return error('Missing nick'); return error('Missing nick');
} }
}, },
whois({ dispatch, server }, user) { whois({ dispatch, network }, user) {
if (user) { if (user) {
dispatch(whois(user, server)); dispatch(whois(user, network));
} else { } else {
return error('Missing nick'); return error('Missing nick');
} }
}, },
away({ dispatch, server }, ...message) { away({ dispatch, network }, ...message) {
const msg = message.join(' '); const msg = message.join(' ');
dispatch(away(msg, server)); dispatch(away(msg, network));
if (msg !== '') { if (msg !== '') {
return 'Away message set'; return 'Away message set';
} }
return 'Away message cleared'; return 'Away message cleared';
}, },
raw({ dispatch, server }, ...message) { raw({ dispatch, network }, ...message) {
if (message.length > 0 && message[0] !== '') { if (message.length > 0 && message[0] !== '') {
const cmd = `${message[0].toUpperCase()} ${message.slice(1).join(' ')}`; const cmd = `${message[0].toUpperCase()} ${message.slice(1).join(' ')}`;
dispatch(raw(cmd, server)); dispatch(raw(cmd, network));
return prompt(`=> ${cmd}`); return prompt(`=> ${cmd}`);
} }
return [prompt('=> /raw'), error('Missing message')]; return [prompt('=> /raw'), error('Missing message')];

View File

@ -15,7 +15,7 @@ const App = ({
connected, connected,
tab, tab,
channels, channels,
servers, networks,
privateChats, privateChats,
showTabList, showTabList,
select, select,
@ -62,7 +62,7 @@ const App = ({
<TabList <TabList
tab={tab} tab={tab}
channels={channels} channels={channels}
servers={servers} networks={networks}
privateChats={privateChats} privateChats={privateChats}
showTabList={showTabList} showTabList={showTabList}
select={select} select={select}

View File

@ -7,7 +7,7 @@ import TabListItem from 'containers/TabListItem';
import { count } from 'utils'; import { count } from 'utils';
export default class TabList extends PureComponent { export default class TabList extends PureComponent {
handleTabClick = (server, target) => this.props.select(server, target); handleTabClick = (network, target) => this.props.select(network, target);
handleConnectClick = () => this.props.push('/connect'); handleConnectClick = () => this.props.push('/connect');
@ -17,7 +17,7 @@ export default class TabList extends PureComponent {
const { const {
tab, tab,
channels, channels,
servers, networks,
privateChats, privateChats,
showTabList, showTabList,
openModal openModal
@ -28,21 +28,21 @@ export default class TabList extends PureComponent {
'off-canvas': showTabList 'off-canvas': showTabList
}); });
channels.forEach(server => { channels.forEach(network => {
const { address } = server; const { address } = network;
const srv = servers[address]; const srv = networks[address];
tabs.push( tabs.push(
<TabListItem <TabListItem
key={address} key={address}
server={address} network={address}
content={srv.name} content={srv.name}
selected={tab.server === address && !tab.name} selected={tab.network === address && !tab.name}
connected={srv.status.connected} connected={srv.connected}
onClick={this.handleTabClick} onClick={this.handleTabClick}
/> />
); );
const chanCount = count(server.channels, c => c.joined); const chanCount = count(network.channels, c => c.joined);
const chanLimit = const chanLimit =
get(srv.features, ['CHANLIMIT', '#'], 0) || srv.features.MAXCHANNELS; get(srv.features, ['CHANLIMIT', '#'], 0) || srv.features.MAXCHANNELS;
@ -68,15 +68,15 @@ export default class TabList extends PureComponent {
</div> </div>
); );
server.channels.forEach(({ name, joined }) => network.channels.forEach(({ name, joined }) =>
tabs.push( tabs.push(
<TabListItem <TabListItem
key={address + name} key={address + name}
server={address} network={address}
target={name} target={name}
content={name} content={name}
joined={joined} joined={joined}
selected={tab.server === address && tab.name === name} selected={tab.network === address && tab.name === name}
onClick={this.handleTabClick} onClick={this.handleTabClick}
/> />
) )
@ -99,10 +99,10 @@ export default class TabList extends PureComponent {
tabs.push( tabs.push(
<TabListItem <TabListItem
key={address + nick} key={address + nick}
server={address} network={address}
target={nick} target={nick}
content={nick} content={nick}
selected={tab.server === address && tab.name === nick} selected={tab.network === address && tab.name === nick}
onClick={this.handleTabClick} onClick={this.handleTabClick}
/> />
) )

View File

@ -4,7 +4,7 @@ import classnames from 'classnames';
const TabListItem = ({ const TabListItem = ({
target, target,
content, content,
server, network,
selected, selected,
connected, connected,
joined, joined,
@ -12,7 +12,7 @@ const TabListItem = ({
onClick onClick
}) => { }) => {
const className = classnames({ const className = classnames({
'tab-server': !target, 'tab-network': !target,
success: !target && connected, success: !target && connected,
error: (!target && !connected) || (!joined && error), error: (!target && !connected) || (!joined && error),
disabled: !!target && !error && joined === false, disabled: !!target && !error && joined === false,
@ -20,7 +20,7 @@ const TabListItem = ({
}); });
return ( return (
<p className={className} onClick={() => onClick(server, target)}> <p className={className} onClick={() => onClick(network, target)}>
<span className="tab-content">{content}</span> <span className="tab-content">{content}</span>
</p> </p>
); );

View File

@ -9,10 +9,10 @@ import { select } from 'state/tab';
import { searchChannels } from 'state/channelSearch'; import { searchChannels } from 'state/channelSearch';
import { linkify } from 'utils'; import { linkify } from 'utils';
const Channel = memo(({ server, name, topic, userCount, joined }) => { const Channel = memo(({ network, name, topic, userCount, joined }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const handleClick = () => dispatch(join([name], server)); const handleClick = () => dispatch(join([name], network));
return ( return (
<div className="modal-channel-result"> <div className="modal-channel-result">
@ -40,7 +40,7 @@ const Channel = memo(({ server, name, topic, userCount, joined }) => {
}); });
const AddChannel = () => { const AddChannel = () => {
const [modal, server, closeModal] = useModal('channel'); const [modal, network, closeModal] = useModal('channel');
const channels = useSelector(state => state.channels); const channels = useSelector(state => state.channels);
const search = useSelector(state => state.channelSearch); const search = useSelector(state => state.channelSearch);
@ -53,7 +53,7 @@ const AddChannel = () => {
useEffect(() => { useEffect(() => {
if (modal.isOpen) { if (modal.isOpen) {
dispatch(searchChannels(server, '')); dispatch(searchChannels(network, ''));
setTimeout(() => inputEl.current.focus(), 0); setTimeout(() => inputEl.current.focus(), 0);
} else { } else {
prevSearch.current = ''; prevSearch.current = '';
@ -74,7 +74,7 @@ const AddChannel = () => {
if (nextQ !== prevSearch.current) { if (nextQ !== prevSearch.current) {
prevSearch.current = nextQ; prevSearch.current = nextQ;
dispatch(searchChannels(server, nextQ)); dispatch(searchChannels(network, nextQ));
} }
} }
}; };
@ -90,14 +90,14 @@ const AddChannel = () => {
channel = `#${channel}`; channel = `#${channel}`;
} }
dispatch(join([channel], server)); dispatch(join([channel], network));
dispatch(select(server, channel)); dispatch(select(network, channel));
} }
} }
}; };
const handleLoadMore = () => const handleLoadMore = () =>
dispatch(searchChannels(server, q, search.results.length)); dispatch(searchChannels(network, q, search.results.length));
let hasMore = !search.end; let hasMore = !search.end;
if (hasMore) { if (hasMore) {
@ -131,9 +131,9 @@ const AddChannel = () => {
<div ref={resultsEl} className="modal-channel-results"> <div ref={resultsEl} className="modal-channel-results">
{search.results.map(channel => ( {search.results.map(channel => (
<Channel <Channel
key={`${server} ${channel.name}`} key={`${network} ${channel.name}`}
server={server} network={network}
joined={channels[server]?.[channel.name]?.joined} joined={channels[network]?.[channel.name]?.joined}
{...channel} {...channel}
/> />
))} ))}

View File

@ -11,40 +11,40 @@ export default class Chat extends Component {
const { tab, part, closePrivateChat, disconnect } = this.props; const { tab, part, closePrivateChat, disconnect } = this.props;
if (isChannel(tab)) { if (isChannel(tab)) {
part([tab.name], tab.server); part([tab.name], tab.network);
} else if (tab.name) { } else if (tab.name) {
closePrivateChat(tab.server, tab.name); closePrivateChat(tab.network, tab.name);
} else { } else {
disconnect(tab.server); disconnect(tab.network);
} }
}; };
handleSearch = phrase => { handleSearch = phrase => {
const { tab, searchMessages } = this.props; const { tab, searchMessages } = this.props;
if (isChannel(tab)) { if (isChannel(tab)) {
searchMessages(tab.server, tab.name, phrase); searchMessages(tab.network, tab.name, phrase);
} }
}; };
handleNickClick = nick => { handleNickClick = nick => {
const { tab, openPrivateChat, select } = this.props; const { tab, openPrivateChat, select } = this.props;
openPrivateChat(tab.server, nick); openPrivateChat(tab.network, nick);
select(tab.server, nick); select(tab.network, nick);
}; };
handleTitleChange = title => { handleTitleChange = title => {
const { setServerName, tab } = this.props; const { setNetworkName, tab } = this.props;
setServerName(title, tab.server); setNetworkName(title, tab.network);
}; };
handleNickChange = nick => { handleNickChange = nick => {
const { setNick, tab } = this.props; const { setNick, tab } = this.props;
setNick(nick, tab.server, true); setNick(nick, tab.network, true);
}; };
handleNickEditDone = nick => { handleNickEditDone = nick => {
const { setNick, tab } = this.props; const { setNick, tab } = this.props;
setNick(nick, tab.server); setNick(nick, tab.network);
}; };
render() { render() {
@ -57,7 +57,7 @@ export default class Chat extends Component {
nick, nick,
search, search,
showUserList, showUserList,
status, error,
tab, tab,
title, title,
users, users,
@ -77,14 +77,14 @@ export default class Chat extends Component {
} else if (tab.name) { } else if (tab.name) {
chatClass = 'chat-private'; chatClass = 'chat-private';
} else { } else {
chatClass = 'chat-server'; chatClass = 'chat-network';
} }
return ( return (
<div className={chatClass}> <div className={chatClass}>
<ChatTitle <ChatTitle
channel={channel} channel={channel}
status={status} error={error}
tab={tab} tab={tab}
title={title} title={title}
openModal={openModal} openModal={openModal}

View File

@ -3,11 +3,11 @@ import { FiUsers, FiSearch, FiX } from 'react-icons/fi';
import Navicon from 'components/ui/Navicon'; import Navicon from 'components/ui/Navicon';
import Button from 'components/ui/Button'; import Button from 'components/ui/Button';
import Editable from 'components/ui/Editable'; import Editable from 'components/ui/Editable';
import { isValidServerName } from 'state/servers'; import { isValidNetworkName } from 'state/networks';
import { isChannel } from 'utils'; import { isChannel } from 'utils';
const ChatTitle = ({ const ChatTitle = ({
status, error,
title, title,
tab, tab,
channel, channel,
@ -26,11 +26,9 @@ const ChatTitle = ({
closeTitle = 'Disconnect'; closeTitle = 'Disconnect';
} }
let serverError = null; let networkError = null;
if (!tab.name && status.error) { if (!tab.name && error) {
serverError = ( networkError = <span className="chat-topic error">Error: {error}</span>;
<span className="chat-topic error">Error: {status.error}</span>
);
} }
return ( return (
@ -41,13 +39,13 @@ const ChatTitle = ({
className="chat-title" className="chat-title"
editable={!tab.name} editable={!tab.name}
value={title} value={title}
validate={isValidServerName} validate={isValidNetworkName}
onChange={onTitleChange} onChange={onTitleChange}
> >
<span className="chat-title">{title}</span> <span className="chat-title">{title}</span>
</Editable> </Editable>
<div className="chat-topic-wrap"> <div className="chat-topic-wrap">
{channel && channel.topic && ( {channel?.topic && (
<span <span
className="chat-topic" className="chat-topic"
onClick={() => openModal('topic', channel.name)} onClick={() => openModal('topic', channel.name)}
@ -55,7 +53,7 @@ const ChatTitle = ({
{channel.topic} {channel.topic}
</span> </span>
)} )}
{serverError} {networkError}
</div> </div>
{tab.name && ( {tab.name && (
<Button <Button
@ -80,7 +78,7 @@ const ChatTitle = ({
</div> </div>
<div className="userlist-bar"> <div className="userlist-bar">
<FiUsers /> <FiUsers />
{channel && channel.users.length} {channel?.users.length}
</div> </div>
</div> </div>
); );

View File

@ -36,7 +36,7 @@ export default class MessageBox extends PureComponent {
addMore = debounce(() => { addMore = debounce(() => {
const { tab, onAddMore } = this.props; const { tab, onAddMore } = this.props;
this.ready = true; this.ready = true;
onAddMore(tab.server, tab.name); onAddMore(tab.network, tab.name);
}, scrollbackDebounce); }, scrollbackDebounce);
constructor(props) { constructor(props) {
@ -130,7 +130,7 @@ export default class MessageBox extends PureComponent {
updateScrollKey = () => { updateScrollKey = () => {
const { tab } = this.props; const { tab } = this.props;
this.scrollKey = `msg:${tab.server}:${tab.name}`; this.scrollKey = `msg:${tab.network}:${tab.name}`;
return this.scrollKey; return this.scrollKey;
}; };
@ -222,7 +222,7 @@ export default class MessageBox extends PureComponent {
if (this.shouldAdd) { if (this.shouldAdd) {
const { tab, onAddMore } = this.props; const { tab, onAddMore } = this.props;
this.shouldAdd = false; this.shouldAdd = false;
onAddMore(tab.server, tab.name); onAddMore(tab.network, tab.name);
} }
}; };

View File

@ -21,9 +21,9 @@ const MessageInput = ({
const handleKey = e => { const handleKey = e => {
if (e.key === 'Enter' && e.target.value) { if (e.key === 'Enter' && e.target.value) {
if (e.target.value[0] === '/') { if (e.target.value[0] === '/') {
onCommand(e.target.value, tab.name, tab.server); onCommand(e.target.value, tab.name, tab.network);
} else if (tab.name) { } else if (tab.name) {
onMessage(e.target.value, tab.name, tab.server); onMessage(e.target.value, tab.name, tab.network);
} }
add(e.target.value); add(e.target.value);

View File

@ -4,7 +4,7 @@ import { getConnected } from 'state/app';
import { getSortedChannels } from 'state/channels'; import { getSortedChannels } from 'state/channels';
import { openModal, getHasOpenModals } from 'state/modals'; import { openModal, getHasOpenModals } from 'state/modals';
import { getPrivateChats } from 'state/privateChats'; import { getPrivateChats } from 'state/privateChats';
import { getServers } from 'state/servers'; import { getNetworks } from 'state/networks';
import { getSelectedTab, select } from 'state/tab'; import { getSelectedTab, select } from 'state/tab';
import { getShowTabList, hideMenu } from 'state/ui'; import { getShowTabList, hideMenu } from 'state/ui';
import connect from 'utils/connect'; import connect from 'utils/connect';
@ -14,7 +14,7 @@ const mapState = createStructuredSelector({
channels: getSortedChannels, channels: getSortedChannels,
connected: getConnected, connected: getConnected,
privateChats: getPrivateChats, privateChats: getPrivateChats,
servers: getServers, networks: getNetworks,
showTabList: getShowTabList, showTabList: getShowTabList,
tab: getSelectedTab, tab: getSelectedTab,
newVersionAvailable: state => state.app.newVersionAvailable, newVersionAvailable: state => state.app.newVersionAvailable,

View File

@ -27,11 +27,11 @@ import { openPrivateChat, closePrivateChat } from 'state/privateChats';
import { getSearch, searchMessages, toggleSearch } from 'state/search'; import { getSearch, searchMessages, toggleSearch } from 'state/search';
import { import {
getCurrentNick, getCurrentNick,
getCurrentServerStatus, getCurrentNetworkError,
disconnect, disconnect,
setNick, setNick,
setServerName setNetworkName
} from 'state/servers'; } from 'state/networks';
import { getSettings } from 'state/settings'; import { getSettings } from 'state/settings';
import { getSelectedTab, select } from 'state/tab'; import { getSelectedTab, select } from 'state/tab';
import { getShowUserList, toggleUserList } from 'state/ui'; import { getShowUserList, toggleUserList } from 'state/ui';
@ -45,7 +45,7 @@ const mapState = createStructuredSelector({
nick: getCurrentNick, nick: getCurrentNick,
search: getSearch, search: getSearch,
showUserList: getShowUserList, showUserList: getShowUserList,
status: getCurrentServerStatus, error: getCurrentNetworkError,
tab: getSelectedTab, tab: getSelectedTab,
title: getSelectedTabTitle, title: getSelectedTabTitle,
users: getSelectedChannelUsers, users: getSelectedChannelUsers,
@ -67,7 +67,7 @@ const mapDispatch = dispatch => ({
select, select,
sendMessage, sendMessage,
setNick, setNick,
setServerName, setNetworkName,
toggleSearch, toggleSearch,
toggleUserList toggleUserList
}, },

View File

@ -2,7 +2,7 @@ import { createStructuredSelector } from 'reselect';
import Connect from 'components/pages/Connect'; import Connect from 'components/pages/Connect';
import { getConnectDefaults, getApp } from 'state/app'; import { getConnectDefaults, getApp } from 'state/app';
import { join } from 'state/channels'; import { join } from 'state/channels';
import { connect as connectServer } from 'state/servers'; import { connect as connectNetwork } from 'state/networks';
import { select } from 'state/tab'; import { select } from 'state/tab';
import connect from 'utils/connect'; import connect from 'utils/connect';
@ -14,7 +14,7 @@ const mapState = createStructuredSelector({
const mapDispatch = { const mapDispatch = {
join, join,
connect: connectServer, connect: connectNetwork,
select select
}; };

View File

@ -4,8 +4,8 @@ import TabListItem from 'components/TabListItem';
import connect from 'utils/connect'; import connect from 'utils/connect';
const mapState = createStructuredSelector({ const mapState = createStructuredSelector({
error: (state, { server, target }) => { error: (state, { network, target }) => {
const messages = get(state, ['messages', server, target]); const messages = get(state, ['messages', network, target]);
if (messages && messages.length > 0) { if (messages && messages.length > 0) {
return messages[messages.length - 1].type === 'error'; return messages[messages.length - 1].type === 'error';

View File

@ -4,22 +4,28 @@ import { isChannel } from 'utils';
export const beforeHandler = '_before'; export const beforeHandler = '_before';
export const notFoundHandler = 'commandNotFound'; export const notFoundHandler = 'commandNotFound';
function createContext({ dispatch, getState }, { server, channel }) { function createContext({ dispatch, getState }, { network, channel }) {
return { dispatch, getState, server, channel, inChannel: isChannel(channel) }; return {
dispatch,
getState,
network,
channel,
inChannel: isChannel(channel)
};
} }
// TODO: Pull this out as convenience action // TODO: Pull this out as convenience action
function process({ dispatch, server, channel }, result) { function process({ dispatch, network, channel }, result) {
if (typeof result === 'string') { if (typeof result === 'string') {
dispatch(inform(result, server, channel)); dispatch(inform(result, network, channel));
} else if (Array.isArray(result)) { } else if (Array.isArray(result)) {
if (typeof result[0] === 'string') { if (typeof result[0] === 'string') {
dispatch(inform(result, server, channel)); dispatch(inform(result, network, channel));
} else if (typeof result[0] === 'object') { } else if (typeof result[0] === 'object') {
dispatch(addMessages(result, server, channel)); dispatch(addMessages(result, network, channel));
} }
} else if (typeof result === 'object' && result) { } else if (typeof result === 'object' && result) {
dispatch(print(result.content, server, channel, result.type)); dispatch(print(result.content, network, channel, result.type));
} }
} }

View File

@ -10,7 +10,7 @@ const message = store => next => {
return action => { return action => {
if (action.type === ADD_MESSAGES && action.prepend) { if (action.type === ADD_MESSAGES && action.prepend) {
const key = `${action.server} ${action.channel}`; const key = `${action.network} ${action.channel}`;
if (ready[key]) { if (ready[key]) {
ready[key] = false; ready[key] = false;
@ -19,7 +19,7 @@ const message = store => next => {
cache[key] = action; cache[key] = action;
} else if (action.type === ADD_FETCHED_MESSAGES) { } else if (action.type === ADD_FETCHED_MESSAGES) {
const key = `${action.server} ${action.channel}`; const key = `${action.network} ${action.channel}`;
ready[key] = true; ready[key] = true;
if (cache[key]) { if (cache[key]) {

View File

@ -1,18 +1,18 @@
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
import { getRouter } from 'state'; import { getRouter } from 'state';
import { getCurrentServerName } from 'state/servers'; import { getCurrentNetworkName } from 'state/networks';
import { observe } from 'utils/observe'; import { observe } from 'utils/observe';
export default function documentTitle({ store }) { export default function documentTitle({ store }) {
observe(store, [getRouter, getCurrentServerName], (router, serverName) => { observe(store, [getRouter, getCurrentNetworkName], (router, networkName) => {
let title; let title;
if (router.route === 'chat') { if (router.route === 'chat') {
const { server, name } = router.params; const { network, name } = router.params;
if (name) { if (name) {
title = `${name} @ ${serverName || server}`; title = `${name} @ ${networkName || network}`;
} else { } else {
title = serverName || server; title = networkName || network;
} }
} else { } else {
title = capitalize(router.route); title = capitalize(router.route);

View File

@ -8,15 +8,15 @@ import { when } from 'utils/observe';
function loadState({ store }, env) { function loadState({ store }, env) {
store.dispatch(setSettings(env.settings, true)); store.dispatch(setSettings(env.settings, true));
if (env.servers) { if (env.networks) {
store.dispatch({ store.dispatch({
type: socketActions.SERVERS, type: socketActions.NETWORKS,
data: env.servers data: env.networks
}); });
when(store, getConnected, () => when(store, getConnected, () =>
// Cache top channels for each server // Cache top channels for each network
env.servers.forEach(({ host }) => env.networks.forEach(({ host }) =>
store.dispatch(searchChannels(host, '')) store.dispatch(searchChannels(host, ''))
) )
); );
@ -56,8 +56,8 @@ function loadState({ store }, env) {
// Wait until wrapWidth gets initialized so that height calculations // Wait until wrapWidth gets initialized so that height calculations
// only happen once for these messages // only happen once for these messages
when(store, getWrapWidth, () => { when(store, getWrapWidth, () => {
const { messages, server, to, next } = env.messages; const { messages, network, to, next } = env.messages;
store.dispatch(addMessages(messages, server, to, false, next)); store.dispatch(addMessages(messages, network, to, false, next));
}); });
} }
} }

View File

@ -1,4 +1,5 @@
import { socketAction } from 'state/actions'; import { socketAction } from 'state/actions';
import { kicked } from 'state/channels';
import { import {
print, print,
addMessage, addMessage,
@ -7,15 +8,15 @@ import {
broadcastEvent broadcastEvent
} from 'state/messages'; } from 'state/messages';
import { openModal } from 'state/modals'; import { openModal } from 'state/modals';
import { reconnect } from 'state/servers'; import { reconnect } from 'state/networks';
import { select } from 'state/tab'; import { select } from 'state/tab';
import { find } from 'utils'; import { find } from 'utils';
function findChannels(state, server, user) { function findChannels(state, network, user) {
const channels = []; const channels = [];
Object.keys(state.channels[server]).forEach(channel => { Object.keys(state.channels[network]).forEach(channel => {
if (find(state.channels[server][channel].users, u => u.nick === user)) { if (find(state.channels[network][channel].users, u => u.nick === user)) {
channels.push(channel); channels.push(channel);
} }
}); });
@ -29,51 +30,56 @@ export default function handleSocket({
}) { }) {
const handlers = { const handlers = {
message(message) { message(message) {
dispatch(addMessage(message, message.server, message.to)); dispatch(addMessage(message, message.network, message.to));
return false; return false;
}, },
pm(message) { pm(message) {
dispatch(addMessage(message, message.server, message.from)); dispatch(addMessage(message, message.network, message.from));
return false; return false;
}, },
messages({ messages, server, to, prepend, next }) { messages({ messages, network, to, prepend, next }) {
dispatch(addMessages(messages, server, to, prepend, next)); dispatch(addMessages(messages, network, to, prepend, next));
return false; return false;
}, },
join({ user, server, channels }) { join({ user, network, channels }) {
dispatch(addEvent(server, channels[0], 'join', user)); dispatch(addEvent(network, channels[0], 'join', user));
}, },
part({ user, server, channel, reason }) { part({ user, network, channel, reason }) {
dispatch(addEvent(server, channel, 'part', user, reason)); dispatch(addEvent(network, channel, 'part', user, reason));
}, },
quit({ user, server, reason }) { quit({ user, network, reason }) {
const channels = findChannels(getState(), server, user); const channels = findChannels(getState(), network, user);
dispatch(broadcastEvent(server, channels, 'quit', user, reason)); dispatch(broadcastEvent(network, channels, 'quit', user, reason));
}, },
nick({ server, oldNick, newNick }) { kick({ network, channel, sender, user, reason }) {
dispatch(kicked(network, channel, user));
dispatch(addEvent(network, channel, 'kick', user, sender, reason));
},
nick({ network, oldNick, newNick }) {
if (oldNick) { if (oldNick) {
const channels = findChannels(getState(), server, oldNick); const channels = findChannels(getState(), network, oldNick);
dispatch(broadcastEvent(server, channels, 'nick', oldNick, newNick)); dispatch(broadcastEvent(network, channels, 'nick', oldNick, newNick));
} }
}, },
topic({ server, channel, topic, nick }) { topic({ network, channel, topic, nick }) {
if (nick) { if (nick) {
dispatch(addEvent(server, channel, 'topic', nick, topic)); dispatch(addEvent(network, channel, 'topic', nick, topic));
} }
}, },
motd({ content, server }) { motd({ content, network }) {
dispatch( dispatch(
addMessages( addMessages(
content.map(line => ({ content: line })), content.map(line => ({ content: line })),
server network
) )
); );
return false; return false;
@ -92,7 +98,7 @@ export default function handleSocket({
`Server: ${data.server}`, `Server: ${data.server}`,
`Channels: ${data.channels}` `Channels: ${data.channels}`
], ],
tab.server, tab.network,
tab.name tab.name
) )
); );
@ -101,24 +107,26 @@ export default function handleSocket({
print(message) { print(message) {
const tab = getState().tab.selected; const tab = getState().tab.selected;
dispatch(addMessage(message, tab.server, tab.name)); dispatch(addMessage(message, tab.network, tab.name));
return false; return false;
}, },
error({ server, target, message }) { error({ network, target, message }) {
dispatch(addMessage({ content: message, type: 'error' }, server, target)); dispatch(
addMessage({ content: message, type: 'error' }, network, target)
);
return false; return false;
}, },
connection_update({ server, errorType }) { connection_update({ network, errorType }) {
if (errorType === 'verify') { if (errorType === 'verify') {
dispatch( dispatch(
openModal('confirm', { openModal('confirm', {
question: question:
'The server is using a self-signed certificate, continue anyway?', 'The network is using a self-signed certificate, continue anyway?',
onConfirm: () => onConfirm: () =>
dispatch( dispatch(
reconnect(server, { reconnect(network, {
skipVerify: true skipVerify: true
}) })
) )
@ -127,12 +135,12 @@ export default function handleSocket({
} }
}, },
dcc_send({ server, from, filename, url }) { dcc_send({ network, from, filename, url }) {
const serverName = getState().servers[server]?.name || server; const networkName = getState().networks[network]?.name || network;
dispatch( dispatch(
openModal('confirm', { openModal('confirm', {
question: `${from} on ${serverName} is sending you: ${filename}`, question: `${from} on ${networkName} is sending you: ${filename}`,
confirmation: 'Download', confirmation: 'Download',
onConfirm: () => { onConfirm: () => {
const a = document.createElement('a'); const a = document.createElement('a');
@ -148,8 +156,11 @@ export default function handleSocket({
channel_forward(forward) { channel_forward(forward) {
const { selected } = getState().tab; const { selected } = getState().tab;
if (selected.server === forward.server && selected.name === forward.old) { if (
dispatch(select(forward.server, forward.new, true)); selected.network === forward.network &&
selected.name === forward.old
) {
dispatch(select(forward.network, forward.new, true));
} }
} }
}; };

View File

@ -11,7 +11,7 @@ const saveTab = debounce(
export default function storage({ store }) { export default function storage({ store }) {
observe(store, getSelectedTab, tab => { observe(store, getSelectedTab, tab => {
if (tab.server) { if (tab.network) {
saveTab(tab); saveTab(tab);
} }
}); });

View File

@ -1,5 +1,5 @@
export default { export default {
connect: '/connect', connect: '/connect',
settings: '/settings', settings: '/settings',
chat: '/:server(/:name)' chat: '/:network(/:name)'
}; };

View File

@ -0,0 +1,20 @@
import { connect, setNetworkName } from '../networks';
describe('setNetworkName()', () => {
it('passes valid names to the network', () => {
const name = 'cake';
const network = 'srv';
expect(setNetworkName(name, network)).toMatchObject({
socket: {
type: 'set_network_name',
data: { name, network }
}
});
});
it('does not pass invalid names to the network', () => {
expect(setNetworkName('', 'srv').socket).toBeUndefined();
expect(setNetworkName(' ', 'srv').socket).toBeUndefined();
});
});

View File

@ -1,20 +0,0 @@
import { connect, setServerName } from '../servers';
describe('setServerName()', () => {
it('passes valid names to the server', () => {
const name = 'cake';
const server = 'srv';
expect(setServerName(name, server)).toMatchObject({
socket: {
type: 'set_server_name',
data: { name, server }
}
});
});
it('does not pass invalid names to the server', () => {
expect(setServerName('', 'srv').socket).toBeUndefined();
expect(setServerName(' ', 'srv').socket).toBeUndefined();
});
});

View File

@ -1,5 +1,5 @@
import reducer, { compareUsers, getSortedChannels } from '../channels'; import reducer, { compareUsers, getSortedChannels } from '../channels';
import { connect } from '../servers'; import { connect } from '../networks';
import * as actions from '../actions'; import * as actions from '../actions';
describe('channel reducer', () => { describe('channel reducer', () => {
@ -17,7 +17,7 @@ describe('channel reducer', () => {
state = reducer(state, { state = reducer(state, {
type: actions.PART, type: actions.PART,
server: 'srv1', network: 'srv1',
channels: ['chan1', 'chan3'] channels: ['chan1', 'chan3']
}); });
@ -38,7 +38,7 @@ describe('channel reducer', () => {
state = reducer(state, { state = reducer(state, {
type: actions.socket.PART, type: actions.socket.PART,
server: 'srv', network: 'srv',
channel: 'chan1', channel: 'chan1',
user: 'nick2' user: 'nick2'
}); });
@ -80,7 +80,7 @@ describe('channel reducer', () => {
state = reducer(state, { state = reducer(state, {
type: actions.socket.QUIT, type: actions.socket.QUIT,
server: 'srv', network: 'srv',
user: 'nick2' user: 'nick2'
}); });
@ -100,6 +100,67 @@ describe('channel reducer', () => {
}); });
}); });
it('handles KICKED', () => {
let state = reducer(
undefined,
connect({
host: 'srv',
nick: 'nick2'
})
);
state = reducer(state, socket_join('srv', 'chan1', 'nick1'));
state = reducer(state, socket_join('srv', 'chan1', 'nick2'));
state = reducer(state, socket_join('srv', 'chan2', 'nick2'));
state = reducer(state, {
type: actions.KICKED,
network: 'srv',
channel: 'chan2',
user: 'nick2',
self: true
});
expect(state).toEqual({
srv: {
chan1: {
name: 'chan1',
joined: true,
users: [
{ mode: '', nick: 'nick1', renderName: 'nick1' },
{ mode: '', nick: 'nick2', renderName: 'nick2' }
]
},
chan2: {
name: 'chan2',
joined: false,
users: []
}
}
});
state = reducer(state, {
type: actions.KICKED,
network: 'srv',
channel: 'chan1',
user: 'nick1'
});
expect(state).toEqual({
srv: {
chan1: {
name: 'chan1',
joined: true,
users: [{ mode: '', nick: 'nick2', renderName: 'nick2' }]
},
chan2: {
name: 'chan2',
joined: false,
users: []
}
}
});
});
it('handles SOCKET_NICK', () => { it('handles SOCKET_NICK', () => {
let state = reducer(undefined, socket_join('srv', 'chan1', 'nick1')); let state = reducer(undefined, socket_join('srv', 'chan1', 'nick1'));
state = reducer(state, socket_join('srv', 'chan1', 'nick2')); state = reducer(state, socket_join('srv', 'chan1', 'nick2'));
@ -107,7 +168,7 @@ describe('channel reducer', () => {
state = reducer(state, { state = reducer(state, {
type: actions.socket.NICK, type: actions.socket.NICK,
server: 'srv', network: 'srv',
oldNick: 'nick1', oldNick: 'nick1',
newNick: 'nick3' newNick: 'nick3'
}); });
@ -135,7 +196,7 @@ describe('channel reducer', () => {
let state = reducer(undefined, socket_join('srv', 'chan1', 'nick1')); let state = reducer(undefined, socket_join('srv', 'chan1', 'nick1'));
state = reducer(state, { state = reducer(state, {
type: actions.socket.USERS, type: actions.socket.USERS,
server: 'srv', network: 'srv',
channel: 'chan1', channel: 'chan1',
users: ['user3', 'user2', '@user4', 'user1', '+user5'] users: ['user3', 'user2', '@user4', 'user1', '+user5']
}); });
@ -161,7 +222,7 @@ describe('channel reducer', () => {
let state = reducer(undefined, socket_join('srv', 'chan1', 'nick1')); let state = reducer(undefined, socket_join('srv', 'chan1', 'nick1'));
state = reducer(state, { state = reducer(state, {
type: actions.socket.TOPIC, type: actions.socket.TOPIC,
server: 'srv', network: 'srv',
channel: 'chan1', channel: 'chan1',
topic: 'the topic' topic: 'the topic'
}); });
@ -219,9 +280,9 @@ describe('channel reducer', () => {
const state = reducer(undefined, { const state = reducer(undefined, {
type: actions.socket.CHANNELS, type: actions.socket.CHANNELS,
data: [ data: [
{ server: 'srv', name: 'chan1', topic: 'the topic' }, { network: 'srv', name: 'chan1', topic: 'the topic' },
{ server: 'srv', name: 'chan2' }, { network: 'srv', name: 'chan2' },
{ server: 'srv2', name: 'chan1' } { network: 'srv2', name: 'chan1' }
] ]
}); });
@ -236,9 +297,9 @@ describe('channel reducer', () => {
}); });
}); });
it('handles SOCKET_SERVERS', () => { it('handles SOCKET_NETWORKS', () => {
const state = reducer(undefined, { const state = reducer(undefined, {
type: actions.socket.SERVERS, type: actions.socket.NETWORKS,
data: [{ host: '127.0.0.1' }, { host: 'thehost' }] data: [{ host: '127.0.0.1' }, { host: 'thehost' }]
}); });
@ -248,7 +309,7 @@ describe('channel reducer', () => {
}); });
}); });
it('optimistically adds the server on CONNECT', () => { it('optimistically adds the network on CONNECT', () => {
const state = reducer( const state = reducer(
undefined, undefined,
connect({ host: '127.0.0.1', nick: 'nick' }) connect({ host: '127.0.0.1', nick: 'nick' })
@ -259,7 +320,7 @@ describe('channel reducer', () => {
}); });
}); });
it('removes the server on DISCONNECT', () => { it('removes the network on DISCONNECT', () => {
let state = { let state = {
srv: {}, srv: {},
srv2: {} srv2: {}
@ -267,7 +328,7 @@ describe('channel reducer', () => {
state = reducer(state, { state = reducer(state, {
type: actions.DISCONNECT, type: actions.DISCONNECT,
server: 'srv2' network: 'srv2'
}); });
expect(state).toEqual({ expect(state).toEqual({
@ -276,19 +337,19 @@ describe('channel reducer', () => {
}); });
}); });
function socket_join(server, channel, user) { function socket_join(network, channel, user) {
return { return {
type: actions.socket.JOIN, type: actions.socket.JOIN,
server, network,
user, user,
channels: [channel] channels: [channel]
}; };
} }
function socket_mode(server, channel, user, add, remove) { function socket_mode(network, channel, user, add, remove) {
return { return {
type: actions.socket.MODE, type: actions.socket.MODE,
server, network,
channel, channel,
user, user,
add, add,
@ -323,7 +384,7 @@ describe('compareUsers()', () => {
}); });
describe('getSortedChannels', () => { describe('getSortedChannels', () => {
it('sorts servers and channels', () => { it('sorts networks and channels', () => {
expect( expect(
getSortedChannels({ getSortedChannels({
channels: { channels: {

View File

@ -7,7 +7,7 @@ describe('message reducer', () => {
it('adds the message on ADD_MESSAGE', () => { it('adds the message on ADD_MESSAGE', () => {
const state = reducer(undefined, { const state = reducer(undefined, {
type: actions.ADD_MESSAGE, type: actions.ADD_MESSAGE,
server: 'srv', network: 'srv',
tab: '#chan1', tab: '#chan1',
message: { message: {
from: 'foo', from: 'foo',
@ -30,7 +30,7 @@ describe('message reducer', () => {
it('adds all the messages on ADD_MESSAGES', () => { it('adds all the messages on ADD_MESSAGES', () => {
const state = reducer(undefined, { const state = reducer(undefined, {
type: actions.ADD_MESSAGES, type: actions.ADD_MESSAGES,
server: 'srv', network: 'srv',
tab: '#chan1', tab: '#chan1',
messages: [ messages: [
{ {
@ -80,7 +80,7 @@ describe('message reducer', () => {
state = reducer(state, { state = reducer(state, {
type: actions.ADD_MESSAGES, type: actions.ADD_MESSAGES,
server: 'srv', network: 'srv',
tab: '#chan1', tab: '#chan1',
prepend: true, prepend: true,
messages: [ messages: [
@ -105,7 +105,7 @@ describe('message reducer', () => {
state = reducer(state, { state = reducer(state, {
type: actions.ADD_MESSAGES, type: actions.ADD_MESSAGES,
server: 'srv', network: 'srv',
tab: '#chan1', tab: '#chan1',
prepend: true, prepend: true,
messages: [ messages: [
@ -136,7 +136,7 @@ describe('message reducer', () => {
state = reducer(state, { state = reducer(state, {
type: actions.ADD_MESSAGE, type: actions.ADD_MESSAGE,
server: 'srv', network: 'srv',
tab: '#chan1', tab: '#chan1',
message: { id: 1, date: new Date(1990, 0, 2) } message: { id: 1, date: new Date(1990, 0, 2) }
}); });
@ -157,7 +157,7 @@ describe('message reducer', () => {
state = reducer(state, { state = reducer(state, {
type: actions.ADD_MESSAGES, type: actions.ADD_MESSAGES,
server: 'srv', network: 'srv',
tab: '#chan1', tab: '#chan1',
messages: [ messages: [
{ id: 1, time: unix(new Date(1990, 0, 2)) }, { id: 1, time: unix(new Date(1990, 0, 2)) },
@ -202,7 +202,7 @@ describe('message reducer', () => {
expect(messages.srv['#chan3'][0].content).toBe('test'); expect(messages.srv['#chan3'][0].content).toBe('test');
}); });
it('deletes all messages related to server when disconnecting', () => { it('deletes all messages related to network when disconnecting', () => {
let state = { let state = {
srv: { srv: {
'#chan1': [{ content: 'msg1' }, { content: 'msg2' }], '#chan1': [{ content: 'msg1' }, { content: 'msg2' }],
@ -215,7 +215,7 @@ describe('message reducer', () => {
state = reducer(state, { state = reducer(state, {
type: actions.DISCONNECT, type: actions.DISCONNECT,
server: 'srv' network: 'srv'
}); });
expect(state).toEqual({ expect(state).toEqual({
@ -238,7 +238,7 @@ describe('message reducer', () => {
state = reducer(state, { state = reducer(state, {
type: actions.PART, type: actions.PART,
server: 'srv', network: 'srv',
channels: ['#chan1'] channels: ['#chan1']
}); });
@ -265,7 +265,7 @@ describe('message reducer', () => {
state = reducer(state, { state = reducer(state, {
type: actions.CLOSE_PRIVATE_CHAT, type: actions.CLOSE_PRIVATE_CHAT,
server: 'srv', network: 'srv',
nick: 'bob' nick: 'bob'
}); });

View File

@ -1,8 +1,8 @@
import reducer, { connect, setServerName } from '../servers'; import reducer, { connect, setNetworkName } from '../networks';
import * as actions from '../actions'; import * as actions from '../actions';
describe('server reducer', () => { describe('network reducer', () => {
it('adds the server on CONNECT', () => { it('adds the network on CONNECT', () => {
let state = reducer( let state = reducer(
undefined, undefined,
connect({ host: '127.0.0.1', nick: 'nick' }) connect({ host: '127.0.0.1', nick: 'nick' })
@ -13,10 +13,8 @@ describe('server reducer', () => {
name: '127.0.0.1', name: '127.0.0.1',
nick: 'nick', nick: 'nick',
editedNick: null, editedNick: null,
status: { connected: false,
connected: false, error: null,
error: null
},
features: {} features: {}
} }
}); });
@ -28,10 +26,8 @@ describe('server reducer', () => {
name: '127.0.0.1', name: '127.0.0.1',
nick: 'nick', nick: 'nick',
editedNick: null, editedNick: null,
status: { connected: false,
connected: false, error: null,
error: null
},
features: {} features: {}
} }
}); });
@ -46,26 +42,22 @@ describe('server reducer', () => {
name: '127.0.0.1', name: '127.0.0.1',
nick: 'nick', nick: 'nick',
editedNick: null, editedNick: null,
status: { connected: false,
connected: false, error: null,
error: null
},
features: {} features: {}
}, },
'127.0.0.2': { '127.0.0.2': {
name: 'srv', name: 'srv',
nick: 'nick', nick: 'nick',
editedNick: null, editedNick: null,
status: { connected: false,
connected: false, error: null,
error: null
},
features: {} features: {}
} }
}); });
}); });
it('removes the server on DISCONNECT', () => { it('removes the network on DISCONNECT', () => {
let state = { let state = {
srv: {}, srv: {},
srv2: {} srv2: {}
@ -73,7 +65,7 @@ describe('server reducer', () => {
state = reducer(state, { state = reducer(state, {
type: actions.DISCONNECT, type: actions.DISCONNECT,
server: 'srv2' network: 'srv2'
}); });
expect(state).toEqual({ expect(state).toEqual({
@ -81,14 +73,14 @@ describe('server reducer', () => {
}); });
}); });
it('handles SET_SERVER_NAME', () => { it('handles SET_NETWORK_NAME', () => {
let state = { let state = {
srv: { srv: {
name: 'cake' name: 'cake'
} }
}; };
state = reducer(state, setServerName('pie', 'srv')); state = reducer(state, setNetworkName('pie', 'srv'));
expect(state).toEqual({ expect(state).toEqual({
srv: { srv: {
@ -104,7 +96,7 @@ describe('server reducer', () => {
); );
state = reducer(state, { state = reducer(state, {
type: actions.SET_NICK, type: actions.SET_NICK,
server: '127.0.0.1', network: '127.0.0.1',
nick: 'nick2', nick: 'nick2',
editing: true editing: true
}); });
@ -125,13 +117,13 @@ describe('server reducer', () => {
); );
state = reducer(state, { state = reducer(state, {
type: actions.SET_NICK, type: actions.SET_NICK,
server: '127.0.0.1', network: '127.0.0.1',
nick: 'nick2', nick: 'nick2',
editing: true editing: true
}); });
state = reducer(state, { state = reducer(state, {
type: actions.SET_NICK, type: actions.SET_NICK,
server: '127.0.0.1', network: '127.0.0.1',
nick: '' nick: ''
}); });
@ -151,7 +143,7 @@ describe('server reducer', () => {
); );
state = reducer(state, { state = reducer(state, {
type: actions.socket.NICK, type: actions.socket.NICK,
server: '127.0.0.1', network: '127.0.0.1',
oldNick: 'nick', oldNick: 'nick',
newNick: 'nick2' newNick: 'nick2'
}); });
@ -172,13 +164,13 @@ describe('server reducer', () => {
); );
state = reducer(state, { state = reducer(state, {
type: actions.SET_NICK, type: actions.SET_NICK,
server: '127.0.0.1', network: '127.0.0.1',
nick: 'nick2', nick: 'nick2',
editing: true editing: true
}); });
state = reducer(state, { state = reducer(state, {
type: actions.socket.NICK_FAIL, type: actions.socket.NICK_FAIL,
server: '127.0.0.1' network: '127.0.0.1'
}); });
expect(state).toMatchObject({ expect(state).toMatchObject({
@ -190,25 +182,21 @@ describe('server reducer', () => {
}); });
}); });
it('adds the servers on SOCKET_SERVERS', () => { it('adds the networks on SOCKET_NETWORKS', () => {
let state = reducer(undefined, { let state = reducer(undefined, {
type: actions.socket.SERVERS, type: actions.socket.NETWORKS,
data: [ data: [
{ {
host: '127.0.0.1', host: '127.0.0.1',
name: 'stuff', name: 'stuff',
nick: 'nick', nick: 'nick',
status: { connected: true
connected: true
}
}, },
{ {
host: '127.0.0.2', host: '127.0.0.2',
name: 'stuffz', name: 'stuffz',
nick: 'nick2', nick: 'nick2',
status: { connected: false
connected: false
}
} }
] ]
}); });
@ -218,18 +206,14 @@ describe('server reducer', () => {
name: 'stuff', name: 'stuff',
nick: 'nick', nick: 'nick',
editedNick: null, editedNick: null,
status: { connected: true,
connected: true
},
features: {} features: {}
}, },
'127.0.0.2': { '127.0.0.2': {
name: 'stuffz', name: 'stuffz',
nick: 'nick2', nick: 'nick2',
editedNick: null, editedNick: null,
status: { connected: false,
connected: false
},
features: {} features: {}
} }
}); });
@ -242,7 +226,7 @@ describe('server reducer', () => {
); );
state = reducer(state, { state = reducer(state, {
type: actions.socket.CONNECTION_UPDATE, type: actions.socket.CONNECTION_UPDATE,
server: '127.0.0.1', network: '127.0.0.1',
connected: true connected: true
}); });
@ -251,16 +235,14 @@ describe('server reducer', () => {
name: '127.0.0.1', name: '127.0.0.1',
nick: 'nick', nick: 'nick',
editedNick: null, editedNick: null,
status: { connected: true,
connected: true
},
features: {} features: {}
} }
}); });
state = reducer(state, { state = reducer(state, {
type: actions.socket.CONNECTION_UPDATE, type: actions.socket.CONNECTION_UPDATE,
server: '127.0.0.1', network: '127.0.0.1',
connected: false, connected: false,
error: 'Bad stuff happened' error: 'Bad stuff happened'
}); });
@ -270,10 +252,8 @@ describe('server reducer', () => {
name: '127.0.0.1', name: '127.0.0.1',
nick: 'nick', nick: 'nick',
editedNick: null, editedNick: null,
status: { connected: false,
connected: false, error: 'Bad stuff happened',
error: 'Bad stuff happened'
},
features: {} features: {}
} }
}); });

View File

@ -7,17 +7,17 @@ describe('tab reducer', () => {
let state = reducer(undefined, setSelectedTab('srv', '#chan')); let state = reducer(undefined, setSelectedTab('srv', '#chan'));
expect(state).toEqual({ expect(state).toEqual({
selected: { server: 'srv', name: '#chan' }, selected: { network: 'srv', name: '#chan' },
history: [{ server: 'srv', name: '#chan' }] history: [{ network: 'srv', name: '#chan' }]
}); });
state = reducer(state, setSelectedTab('srv', 'user1')); state = reducer(state, setSelectedTab('srv', 'user1'));
expect(state).toEqual({ expect(state).toEqual({
selected: { server: 'srv', name: 'user1' }, selected: { network: 'srv', name: 'user1' },
history: [ history: [
{ server: 'srv', name: '#chan' }, { network: 'srv', name: '#chan' },
{ server: 'srv', name: 'user1' } { network: 'srv', name: 'user1' }
] ]
}); });
}); });
@ -30,15 +30,15 @@ describe('tab reducer', () => {
state = reducer(state, { state = reducer(state, {
type: actions.PART, type: actions.PART,
server: 'srv', network: 'srv',
channels: ['#chan'] channels: ['#chan']
}); });
expect(state).toEqual({ expect(state).toEqual({
selected: { server: 'srv', name: '#chan3' }, selected: { network: 'srv', name: '#chan3' },
history: [ history: [
{ server: 'srv1', name: 'bob' }, { network: 'srv1', name: 'bob' },
{ server: 'srv', name: '#chan3' } { network: 'srv', name: '#chan3' }
] ]
}); });
}); });
@ -51,21 +51,21 @@ describe('tab reducer', () => {
state = reducer(state, { state = reducer(state, {
type: actions.CLOSE_PRIVATE_CHAT, type: actions.CLOSE_PRIVATE_CHAT,
server: 'srv1', network: 'srv1',
nick: 'bob' nick: 'bob'
}); });
expect(state).toEqual({ expect(state).toEqual({
selected: { server: 'srv', name: '#chan3' }, selected: { network: 'srv', name: '#chan3' },
history: [ history: [
{ server: 'srv', name: '#chan' }, { network: 'srv', name: '#chan' },
{ server: 'srv', name: '#chan' }, { network: 'srv', name: '#chan' },
{ server: 'srv', name: '#chan3' } { network: 'srv', name: '#chan3' }
] ]
}); });
}); });
it('removes all tabs related to server from history on DISCONNECT', () => { it('removes all tabs related to network from history on DISCONNECT', () => {
let state = reducer(undefined, setSelectedTab('srv', '#chan')); let state = reducer(undefined, setSelectedTab('srv', '#chan'));
state = reducer(state, setSelectedTab('srv1', 'bob')); state = reducer(state, setSelectedTab('srv1', 'bob'));
state = reducer(state, setSelectedTab('srv', '#chan')); state = reducer(state, setSelectedTab('srv', '#chan'));
@ -73,12 +73,12 @@ describe('tab reducer', () => {
state = reducer(state, { state = reducer(state, {
type: actions.DISCONNECT, type: actions.DISCONNECT,
server: 'srv' network: 'srv'
}); });
expect(state).toEqual({ expect(state).toEqual({
selected: { server: 'srv', name: '#chan3' }, selected: { network: 'srv', name: '#chan3' },
history: [{ server: 'srv1', name: 'bob' }] history: [{ network: 'srv1', name: 'bob' }]
}); });
}); });
@ -89,7 +89,7 @@ describe('tab reducer', () => {
expect(state).toEqual({ expect(state).toEqual({
selected: {}, selected: {},
history: [{ server: 'srv', name: '#chan' }] history: [{ network: 'srv', name: '#chan' }]
}); });
}); });
@ -99,7 +99,7 @@ describe('tab reducer', () => {
locationChanged( locationChanged(
'chat', 'chat',
{ {
server: 'srv', network: 'srv',
name: '#chan' name: '#chan'
}, },
{} {}
@ -107,8 +107,8 @@ describe('tab reducer', () => {
); );
expect(state).toEqual({ expect(state).toEqual({
selected: { server: 'srv', name: '#chan' }, selected: { network: 'srv', name: '#chan' },
history: [{ server: 'srv', name: '#chan' }] history: [{ network: 'srv', name: '#chan' }]
}); });
}); });
}); });

View File

@ -3,6 +3,7 @@ export const APP_SET = 'APP_SET';
export const INVITE = 'INVITE'; export const INVITE = 'INVITE';
export const JOIN = 'JOIN'; export const JOIN = 'JOIN';
export const KICK = 'KICK'; export const KICK = 'KICK';
export const KICKED = 'KICKED';
export const PART = 'PART'; export const PART = 'PART';
export const SET_TOPIC = 'SET_TOPIC'; export const SET_TOPIC = 'SET_TOPIC';
@ -36,7 +37,7 @@ export const CONNECT = 'CONNECT';
export const DISCONNECT = 'DISCONNECT'; export const DISCONNECT = 'DISCONNECT';
export const RECONNECT = 'RECONNECT'; export const RECONNECT = 'RECONNECT';
export const SET_NICK = 'SET_NICK'; export const SET_NICK = 'SET_NICK';
export const SET_SERVER_NAME = 'SET_SERVER_NAME'; export const SET_NETWORK_NAME = 'SET_NETWORK_NAME';
export const WHOIS = 'WHOIS'; export const WHOIS = 'WHOIS';
export const SET_CERT = 'SET_CERT'; export const SET_CERT = 'SET_CERT';
@ -83,7 +84,7 @@ export const socket = createSocketActions([
'pm', 'pm',
'quit', 'quit',
'search', 'search',
'servers', 'networks',
'topic', 'topic',
'users' 'users'
]); ]);

View File

@ -8,7 +8,7 @@ const initialState = {
}; };
export default createReducer(initialState, { export default createReducer(initialState, {
[actions.socket.CHANNEL_SEARCH](state, { results, start, server, q }) { [actions.socket.CHANNEL_SEARCH](state, { results, start, network, q }) {
if (results) { if (results) {
state.end = false; state.end = false;
@ -18,7 +18,7 @@ export default createReducer(initialState, {
state.results = results; state.results = results;
if (!q) { if (!q) {
state.topCache[server] = results; state.topCache[network] = results;
} }
} }
} else { } else {
@ -34,14 +34,14 @@ export default createReducer(initialState, {
} }
}); });
export function searchChannels(server, q, start) { export function searchChannels(network, q, start) {
return { return {
type: actions.CHANNEL_SEARCH, type: actions.CHANNEL_SEARCH,
server, network,
q, q,
socket: { socket: {
type: 'channel_search', type: 'channel_search',
data: { server, q, start } data: { network, q, start }
} }
}; };
} }

View File

@ -56,13 +56,18 @@ function removeUser(users, nick) {
} }
} }
function init(state, server, channel) { function init(state, network, channel) {
if (!state[server]) { if (!state[network]) {
state[server] = {}; state[network] = {};
} }
if (channel && !state[server][channel]) { if (channel && !state[network][channel]) {
state[server][channel] = { name: channel, users: [], joined: false }; state[network][channel] = {
name: channel,
users: [],
joined: false
};
} }
return state[network][channel];
} }
export function compareUsers(a, b) { export function compareUsers(a, b) {
@ -93,18 +98,18 @@ export const getChannels = state => state.channels;
export const getSortedChannels = createSelector(getChannels, channels => export const getSortedChannels = createSelector(getChannels, channels =>
sortBy( sortBy(
Object.keys(channels).map(server => ({ Object.keys(channels).map(network => ({
address: server, address: network,
channels: sortBy(channels[server], channel => channel.name.toLowerCase()) channels: sortBy(channels[network], channel => channel.name.toLowerCase())
})), })),
server => server.address.toLowerCase() network => network.address.toLowerCase()
) )
); );
export const getSelectedChannel = createSelector( export const getSelectedChannel = createSelector(
getSelectedTab, getSelectedTab,
getChannels, getChannels,
(tab, channels) => get(channels, [tab.server, tab.name]) (tab, channels) => get(channels, [tab.network, tab.name])
); );
export const getSelectedChannelUsers = createSelector( export const getSelectedChannelUsers = createSelector(
@ -120,43 +125,53 @@ export const getSelectedChannelUsers = createSelector(
export default createReducer( export default createReducer(
{}, {},
{ {
[actions.JOIN](state, { server, channels }) { [actions.JOIN](state, { network, channels }) {
channels.forEach(channel => init(state, server, channel)); channels.forEach(channel => init(state, network, channel));
}, },
[actions.PART](state, { server, channels }) { [actions.PART](state, { network, channels }) {
channels.forEach(channel => delete state[server][channel]); channels.forEach(channel => delete state[network][channel]);
}, },
[actions.socket.JOIN](state, { server, channels, user }) { [actions.socket.JOIN](state, { network, channels, user }) {
const channel = channels[0]; const channel = channels[0];
init(state, server, channel); const chan = init(state, network, channel);
state[server][channel].name = channel; chan.name = channel;
state[server][channel].joined = true; chan.joined = true;
state[server][channel].users.push(createUser(user)); chan.users.push(createUser(user));
}, },
[actions.socket.CHANNEL_FORWARD](state, action) { [actions.socket.CHANNEL_FORWARD](state, action) {
init(state, action.server, action.new); init(state, action.network, action.new);
delete state[action.server][action.old]; delete state[action.network][action.old];
}, },
[actions.socket.PART](state, { server, channel, user }) { [actions.socket.PART](state, { network, channel, user }) {
if (state[server][channel]) { if (state[network][channel]) {
removeUser(state[server][channel].users, user); removeUser(state[network][channel].users, user);
} }
}, },
[actions.socket.QUIT](state, { server, user }) { [actions.socket.QUIT](state, { network, user }) {
Object.keys(state[server]).forEach(channel => { Object.keys(state[network]).forEach(channel => {
removeUser(state[server][channel].users, user); removeUser(state[network][channel].users, user);
}); });
}, },
[actions.socket.NICK](state, { server, oldNick, newNick }) { [actions.KICKED](state, { network, channel, user, self }) {
Object.keys(state[server]).forEach(channel => { const chan = state[network][channel];
if (self) {
chan.joined = false;
chan.users = [];
} else {
removeUser(chan.users, user);
}
},
[actions.socket.NICK](state, { network, oldNick, newNick }) {
Object.keys(state[network]).forEach(channel => {
const user = find( const user = find(
state[server][channel].users, state[network][channel].users,
u => u.nick === oldNick u => u.nick === oldNick
); );
if (user) { if (user) {
@ -166,16 +181,16 @@ export default createReducer(
}); });
}, },
[actions.socket.USERS](state, { server, channel, users }) { [actions.socket.USERS](state, { network, channel, users }) {
state[server][channel].users = users.map(nick => loadUser(nick)); state[network][channel].users = users.map(nick => loadUser(nick));
}, },
[actions.socket.TOPIC](state, { server, channel, topic }) { [actions.socket.TOPIC](state, { network, channel, topic }) {
state[server][channel].topic = topic; state[network][channel].topic = topic;
}, },
[actions.socket.MODE](state, { server, channel, user, remove, add }) { [actions.socket.MODE](state, { network, channel, user, remove, add }) {
const u = find(state[server][channel].users, v => v.nick === user); const u = find(state[network][channel].users, v => v.nick === user);
if (u) { if (u) {
if (remove) { if (remove) {
let j = remove.length; let j = remove.length;
@ -194,15 +209,15 @@ export default createReducer(
[actions.socket.CHANNELS](state, { data }) { [actions.socket.CHANNELS](state, { data }) {
if (data) { if (data) {
data.forEach(({ server, name, topic }) => { data.forEach(({ network, name, topic }) => {
init(state, server, name); const chan = init(state, network, name);
state[server][name].joined = true; chan.joined = true;
state[server][name].topic = topic; chan.topic = topic;
}); });
} }
}, },
[actions.socket.SERVERS](state, { data }) { [actions.socket.NETWORKS](state, { data }) {
if (data) { if (data) {
data.forEach(({ host }) => init(state, host)); data.forEach(({ host }) => init(state, host));
} }
@ -212,33 +227,33 @@ export default createReducer(
init(state, host); init(state, host);
}, },
[actions.DISCONNECT](state, { server }) { [actions.DISCONNECT](state, { network }) {
delete state[server]; delete state[network];
} }
} }
); );
export function join(channels, server) { export function join(channels, network) {
return { return {
type: actions.JOIN, type: actions.JOIN,
channels, channels,
server, network,
socket: { socket: {
type: 'join', type: 'join',
data: { channels, server } data: { channels, network }
} }
}; };
} }
export function part(channels, server) { export function part(channels, network) {
return (dispatch, getState) => { return (dispatch, getState) => {
const action = { const action = {
type: actions.PART, type: actions.PART,
channels, channels,
server network
}; };
const state = getState().channels[server]; const state = getState().channels[network];
const joined = channels.filter(c => state[c] && state[c].joined); const joined = channels.filter(c => state[c] && state[c].joined);
if (joined.length > 0) { if (joined.length > 0) {
@ -246,7 +261,7 @@ export function part(channels, server) {
type: 'part', type: 'part',
data: { data: {
channels: joined, channels: joined,
server network
} }
}; };
} }
@ -256,41 +271,55 @@ export function part(channels, server) {
}; };
} }
export function invite(user, channel, server) { export function invite(user, channel, network) {
return { return {
type: actions.INVITE, type: actions.INVITE,
user, user,
channel, channel,
server, network,
socket: { socket: {
type: 'invite', type: 'invite',
data: { user, channel, server } data: { user, channel, network }
} }
}; };
} }
export function kick(user, channel, server) { export function kick(user, channel, network) {
return { return {
type: actions.KICK, type: actions.KICK,
user, user,
channel, channel,
server, network,
socket: { socket: {
type: 'kick', type: 'kick',
data: { user, channel, server } data: { user, channel, network }
} }
}; };
} }
export function setTopic(topic, channel, server) { export function kicked(network, channel, user) {
return (dispatch, getState) => {
const nick = getState().networks[network]?.nick;
dispatch({
type: actions.KICKED,
network,
channel,
user,
self: nick === user
});
};
}
export function setTopic(topic, channel, network) {
return { return {
type: actions.SET_TOPIC, type: actions.SET_TOPIC,
topic, topic,
channel, channel,
server, network,
socket: { socket: {
type: 'topic', type: 'topic',
data: { topic, channel, server } data: { topic, channel, network }
} }
}; };
} }

View File

@ -5,9 +5,9 @@ import channelSearch from './channelSearch';
import input from './input'; import input from './input';
import messages from './messages'; import messages from './messages';
import modals from './modals'; import modals from './modals';
import networks from './networks';
import privateChats from './privateChats'; import privateChats from './privateChats';
import search from './search'; import search from './search';
import servers from './servers';
import settings from './settings'; import settings from './settings';
import tab from './tab'; import tab from './tab';
import ui from './ui'; import ui from './ui';
@ -24,9 +24,9 @@ export default function createReducer(router) {
input, input,
messages, messages,
modals, modals,
networks,
privateChats, privateChats,
search, search,
servers,
settings, settings,
tab, tab,
ui ui

View File

@ -23,9 +23,9 @@ export const getSelectedMessages = createSelector(
getSelectedTab, getSelectedTab,
getMessages, getMessages,
(tab, messages) => { (tab, messages) => {
const target = tab.name || tab.server; const target = tab.name || tab.network;
if (has(messages, [tab.server, target])) { if (has(messages, [tab.network, target])) {
return messages[tab.server][target]; return messages[tab.network][target];
} }
return []; return [];
} }
@ -39,12 +39,12 @@ export const getHasMoreMessages = createSelector(
} }
); );
function init(state, server, tab) { function init(state, network, tab) {
if (!state[server]) { if (!state[network]) {
state[server] = {}; state[network] = {};
} }
if (!state[server][tab]) { if (!state[network][tab]) {
state[server][tab] = []; state[network][tab] = [];
} }
} }
@ -117,6 +117,13 @@ function renderEvents(events) {
return [renderNick(oldNick), ' changed nick to ', renderNick(newNick)]; return [renderNick(oldNick), ' changed nick to ', renderNick(newNick)];
} }
if (first.type === 'kick') {
const [kicked, by] = first.params;
return [renderNick(by), ' kicked ', renderNick(kicked)];
}
if (first.type === 'topic') { if (first.type === 'topic') {
const [nick, newTopic] = first.params; const [nick, newTopic] = first.params;
const topic = colorify(linkify(newTopic)); const topic = colorify(linkify(newTopic));
@ -176,14 +183,14 @@ let nextID = 0;
function initMessage( function initMessage(
state, state,
message, message,
server, network,
tab, tab,
wrapWidth, wrapWidth,
charWidth, charWidth,
windowWidth, windowWidth,
prepend prepend
) { ) {
const messages = state[server][tab]; const messages = state[network][tab];
if (messages.length > 0 && !prepend) { if (messages.length > 0 && !prepend) {
const lastMessage = messages[messages.length - 1]; const lastMessage = messages[messages.length - 1];
@ -280,7 +287,7 @@ function isSameDay(d1, d2) {
function reducerPrependMessages( function reducerPrependMessages(
state, state,
messages, messages,
server, network,
tab, tab,
wrapWidth, wrapWidth,
charWidth, charWidth,
@ -293,7 +300,7 @@ function reducerPrependMessages(
initMessage( initMessage(
state, state,
message, message,
server, network,
tab, tab,
wrapWidth, wrapWidth,
charWidth, charWidth,
@ -307,7 +314,7 @@ function reducerPrependMessages(
msgs.push(message); msgs.push(message);
} }
const m = state[server][tab]; const m = state[network][tab];
if (m.length > 0) { if (m.length > 0) {
const lastNewMessage = msgs[msgs.length - 1]; const lastNewMessage = msgs[msgs.length - 1];
@ -323,8 +330,8 @@ function reducerPrependMessages(
m.unshift(...msgs); m.unshift(...msgs);
} }
function reducerAddMessage(message, server, tab, state) { function reducerAddMessage(message, network, tab, state) {
const messages = state[server][tab]; const messages = state[network][tab];
if (messages.length > 0) { if (messages.length > 0) {
const lastMessage = messages[messages.length - 1]; const lastMessage = messages[messages.length - 1];
@ -341,34 +348,34 @@ export default createReducer(
{ {
[actions.ADD_MESSAGE]( [actions.ADD_MESSAGE](
state, state,
{ server, tab, message, wrapWidth, charWidth, windowWidth } { network, tab, message, wrapWidth, charWidth, windowWidth }
) { ) {
init(state, server, tab); init(state, network, tab);
const shouldAdd = initMessage( const shouldAdd = initMessage(
state, state,
message, message,
server, network,
tab, tab,
wrapWidth, wrapWidth,
charWidth, charWidth,
windowWidth windowWidth
); );
if (shouldAdd) { if (shouldAdd) {
reducerAddMessage(message, server, tab, state); reducerAddMessage(message, network, tab, state);
} }
}, },
[actions.ADD_MESSAGES]( [actions.ADD_MESSAGES](
state, state,
{ server, tab, messages, prepend, wrapWidth, charWidth, windowWidth } { network, tab, messages, prepend, wrapWidth, charWidth, windowWidth }
) { ) {
if (prepend) { if (prepend) {
init(state, server, tab); init(state, network, tab);
reducerPrependMessages( reducerPrependMessages(
state, state,
messages, messages,
server, network,
tab, tab,
wrapWidth, wrapWidth,
charWidth, charWidth,
@ -376,45 +383,45 @@ export default createReducer(
); );
} else { } else {
if (!messages[0].tab) { if (!messages[0].tab) {
init(state, server, tab); init(state, network, tab);
} }
messages.forEach(message => { messages.forEach(message => {
if (message.tab) { if (message.tab) {
init(state, server, message.tab); init(state, network, message.tab);
} }
const shouldAdd = initMessage( const shouldAdd = initMessage(
state, state,
message, message,
server, network,
message.tab || tab, message.tab || tab,
wrapWidth, wrapWidth,
charWidth, charWidth,
windowWidth windowWidth
); );
if (shouldAdd) { if (shouldAdd) {
reducerAddMessage(message, server, message.tab || tab, state); reducerAddMessage(message, network, message.tab || tab, state);
} }
}); });
} }
}, },
[actions.DISCONNECT](state, { server }) { [actions.DISCONNECT](state, { network }) {
delete state[server]; delete state[network];
}, },
[actions.PART](state, { server, channels }) { [actions.PART](state, { network, channels }) {
channels.forEach(channel => delete state[server][channel]); channels.forEach(channel => delete state[network][channel]);
}, },
[actions.CLOSE_PRIVATE_CHAT](state, { server, nick }) { [actions.CLOSE_PRIVATE_CHAT](state, { network, nick }) {
delete state[server][nick]; delete state[network][nick];
}, },
[actions.socket.CHANNEL_FORWARD](state, { server, old }) { [actions.socket.CHANNEL_FORWARD](state, { network, old }) {
if (state[server]) { if (state[network]) {
delete state[server][old]; delete state[network][old];
} }
}, },
@ -422,9 +429,9 @@ export default createReducer(
state, state,
{ wrapWidth, charWidth, windowWidth } { wrapWidth, charWidth, windowWidth }
) { ) {
Object.keys(state).forEach(server => Object.keys(state).forEach(network =>
Object.keys(state[server]).forEach(target => Object.keys(state[network]).forEach(target =>
state[server][target].forEach(message => { state[network][target].forEach(message => {
if (message.type === 'date') { if (message.type === 'date') {
return; return;
} }
@ -441,7 +448,7 @@ export default createReducer(
); );
}, },
[actions.socket.SERVERS](state, { data }) { [actions.socket.NETWORKS](state, { data }) {
if (data) { if (data) {
data.forEach(({ host }) => { data.forEach(({ host }) => {
state[host] = {}; state[host] = {};
@ -451,9 +458,9 @@ export default createReducer(
} }
); );
export function getMessageTab(server, to) { export function getMessageTab(network, to) {
if (!to || to === '*' || (!isChannel(to) && to.indexOf('.') !== -1)) { if (!to || to === '*' || (!isChannel(to) && to.indexOf('.') !== -1)) {
return server; return network;
} }
return to; return to;
} }
@ -474,7 +481,7 @@ export function fetchMessages() {
socket: { socket: {
type: 'fetch_messages', type: 'fetch_messages',
data: { data: {
server: tab.server, network: tab.network,
channel: tab.name, channel: tab.name,
next: first.id next: first.id
} }
@ -484,10 +491,10 @@ export function fetchMessages() {
}; };
} }
export function addFetchedMessages(server, tab) { export function addFetchedMessages(network, tab) {
return { return {
type: actions.ADD_FETCHED_MESSAGES, type: actions.ADD_FETCHED_MESSAGES,
server, network,
tab tab
}; };
} }
@ -501,17 +508,17 @@ export function updateMessageHeight(wrapWidth, charWidth, windowWidth) {
}; };
} }
export function sendMessage(content, to, server) { export function sendMessage(content, to, network) {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
const { wrapWidth, charWidth, windowWidth } = getApp(state); const { wrapWidth, charWidth, windowWidth } = getApp(state);
dispatch({ dispatch({
type: actions.ADD_MESSAGE, type: actions.ADD_MESSAGE,
server, network,
tab: to, tab: to,
message: { message: {
from: state.servers[server].nick, from: state.networks[network].nick,
content content
}, },
wrapWidth, wrapWidth,
@ -519,21 +526,21 @@ export function sendMessage(content, to, server) {
windowWidth, windowWidth,
socket: { socket: {
type: 'message', type: 'message',
data: { content, to, server } data: { content, to, network }
} }
}); });
}; };
} }
export function addMessage(message, server, to) { export function addMessage(message, network, to) {
const tab = getMessageTab(server, to); const tab = getMessageTab(network, to);
return (dispatch, getState) => { return (dispatch, getState) => {
const { wrapWidth, charWidth, windowWidth } = getApp(getState()); const { wrapWidth, charWidth, windowWidth } = getApp(getState());
dispatch({ dispatch({
type: actions.ADD_MESSAGE, type: actions.ADD_MESSAGE,
server, network,
tab, tab,
message, message,
wrapWidth, wrapWidth,
@ -543,8 +550,8 @@ export function addMessage(message, server, to) {
}; };
} }
export function addMessages(messages, server, to, prepend, next) { export function addMessages(messages, network, to, prepend, next) {
const tab = getMessageTab(server, to); const tab = getMessageTab(network, to);
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
@ -558,7 +565,7 @@ export function addMessages(messages, server, to, prepend, next) {
dispatch({ dispatch({
type: actions.ADD_MESSAGES, type: actions.ADD_MESSAGES,
server, network,
tab, tab,
messages, messages,
prepend, prepend,
@ -569,7 +576,7 @@ export function addMessages(messages, server, to, prepend, next) {
}; };
} }
export function addEvent(server, tab, type, ...params) { export function addEvent(network, tab, type, ...params) {
return addMessage( return addMessage(
{ {
type: 'info', type: 'info',
@ -581,12 +588,12 @@ export function addEvent(server, tab, type, ...params) {
} }
] ]
}, },
server, network,
tab tab
); );
} }
export function broadcastEvent(server, channels, type, ...params) { export function broadcastEvent(network, channels, type, ...params) {
const now = unix(); const now = unix();
return addMessages( return addMessages(
@ -601,29 +608,29 @@ export function broadcastEvent(server, channels, type, ...params) {
} }
] ]
})), })),
server network
); );
} }
export function broadcast(message, server, channels) { export function broadcast(message, network, channels) {
return addMessages( return addMessages(
channels.map(channel => ({ channels.map(channel => ({
tab: channel, tab: channel,
content: message, content: message,
type: 'info' type: 'info'
})), })),
server network
); );
} }
export function print(message, server, channel, type) { export function print(message, network, channel, type) {
if (Array.isArray(message)) { if (Array.isArray(message)) {
return addMessages( return addMessages(
message.map(line => ({ message.map(line => ({
content: line, content: line,
type type
})), })),
server, network,
channel channel
); );
} }
@ -633,32 +640,32 @@ export function print(message, server, channel, type) {
content: message, content: message,
type type
}, },
server, network,
channel channel
); );
} }
export function inform(message, server, channel) { export function inform(message, network, channel) {
return print(message, server, channel, 'info'); return print(message, network, channel, 'info');
} }
export function runCommand(command, channel, server) { export function runCommand(command, channel, network) {
return { return {
type: actions.COMMAND, type: actions.COMMAND,
command, command,
channel, channel,
server network
}; };
} }
export function raw(message, server) { export function raw(message, network) {
return { return {
type: actions.RAW, type: actions.RAW,
message, message,
server, network,
socket: { socket: {
type: 'raw', type: 'raw',
data: { message, server } data: { message, network }
} }
}; };
} }

229
client/js/state/networks.js Normal file
View File

@ -0,0 +1,229 @@
import { createSelector } from 'reselect';
import get from 'lodash/get';
import createReducer from 'utils/createReducer';
import { getSelectedTab, updateSelection } from './tab';
import * as actions from './actions';
export const getNetworks = state => state.networks;
export const getCurrentNick = createSelector(
getNetworks,
getSelectedTab,
(networks, tab) => {
if (!networks[tab.network]) {
return;
}
const { editedNick } = networks[tab.network];
if (editedNick === null) {
return networks[tab.network].nick;
}
return editedNick;
}
);
export const getCurrentNetworkName = createSelector(
getNetworks,
getSelectedTab,
(networks, tab) => get(networks, [tab.network, 'name'])
);
export const getCurrentNetworkError = createSelector(
getNetworks,
getSelectedTab,
(networks, tab) => get(networks, [tab.network, 'error'], null)
);
export default createReducer(
{},
{
[actions.CONNECT](state, { host, nick, name }) {
if (!state[host]) {
state[host] = {
nick,
editedNick: null,
name: name || host,
connected: false,
error: null,
features: {}
};
}
},
[actions.DISCONNECT](state, { network }) {
delete state[network];
},
[actions.SET_NETWORK_NAME](state, { network, name }) {
state[network].name = name;
},
[actions.SET_NICK](state, { network, nick, editing }) {
if (editing) {
state[network].editedNick = nick;
} else if (nick === '') {
state[network].editedNick = null;
}
},
[actions.socket.NICK](state, { network, oldNick, newNick }) {
if (!oldNick || oldNick === state[network].nick) {
state[network].nick = newNick;
state[network].editedNick = null;
}
},
[actions.socket.NICK_FAIL](state, { network }) {
state[network].editedNick = null;
},
[actions.socket.NETWORKS](state, { data }) {
if (data) {
data.forEach(
({ host, name = host, nick, connected, error, features = {} }) => {
state[host] = {
name,
nick,
connected,
error,
features,
editedNick: null
};
}
);
}
},
[actions.socket.CONNECTION_UPDATE](state, { network, connected, error }) {
if (state[network]) {
state[network].connected = connected;
state[network].error = error;
}
},
[actions.socket.FEATURES](state, { network, features }) {
const srv = state[network];
if (srv) {
srv.features = features;
if (features.NETWORK && srv.name === network) {
srv.name = features.NETWORK;
}
}
}
}
);
export function connect(config) {
return {
type: actions.CONNECT,
...config,
socket: {
type: 'connect',
data: config
}
};
}
export function disconnect(network) {
return dispatch => {
dispatch({
type: actions.DISCONNECT,
network,
socket: {
type: 'quit',
data: { network }
}
});
dispatch(updateSelection());
};
}
export function reconnect(network, settings) {
return {
type: actions.RECONNECT,
network,
settings,
socket: {
type: 'reconnect',
data: {
...settings,
network
}
}
};
}
export function whois(user, network) {
return {
type: actions.WHOIS,
user,
network,
socket: {
type: 'whois',
data: { user, network }
}
};
}
export function away(message, network) {
return {
type: actions.AWAY,
message,
network,
socket: {
type: 'away',
data: { message, network }
}
};
}
export function setNick(nick, network, editing) {
nick = nick.trim().replace(' ', '');
const action = {
type: actions.SET_NICK,
nick,
network,
editing
};
if (!editing && nick !== '') {
action.socket = {
type: 'nick',
data: {
newNick: nick,
network
}
};
}
return action;
}
export function isValidNetworkName(name) {
return name.trim() !== '';
}
export function setNetworkName(name, network) {
const action = {
type: actions.SET_NETWORK_NAME,
name,
network
};
if (isValidNetworkName(name)) {
action.socket = {
type: 'set_network_name',
data: {
name,
network
},
debounce: {
delay: 500,
key: `network_name:${network}`
}
};
}
return action;
}

View File

@ -5,13 +5,13 @@ import * as actions from './actions';
export const getPrivateChats = state => state.privateChats; export const getPrivateChats = state => state.privateChats;
function open(state, server, nick) { function open(state, network, nick) {
if (!state[server]) { if (!state[network]) {
state[server] = []; state[network] = [];
} }
if (!state[server].includes(nick)) { if (!state[network].includes(nick)) {
state[server].push(nick); state[network].push(nick);
state[server] = sortBy(state[server], v => v.toLowerCase()); state[network] = sortBy(state[network], v => v.toLowerCase());
} }
} }
@ -19,63 +19,63 @@ export default createReducer(
{}, {},
{ {
[actions.OPEN_PRIVATE_CHAT](state, action) { [actions.OPEN_PRIVATE_CHAT](state, action) {
open(state, action.server, action.nick); open(state, action.network, action.nick);
}, },
[actions.CLOSE_PRIVATE_CHAT](state, { server, nick }) { [actions.CLOSE_PRIVATE_CHAT](state, { network, nick }) {
const i = state[server]?.findIndex(n => n === nick); const i = state[network]?.findIndex(n => n === nick);
if (i !== -1) { if (i !== -1) {
state[server].splice(i, 1); state[network].splice(i, 1);
} }
}, },
[actions.PRIVATE_CHATS](state, { privateChats }) { [actions.PRIVATE_CHATS](state, { privateChats }) {
privateChats.forEach(({ server, name }) => { privateChats.forEach(({ network, name }) => {
if (!state[server]) { if (!state[network]) {
state[server] = []; state[network] = [];
} }
state[server].push(name); state[network].push(name);
}); });
}, },
[actions.socket.PM](state, action) { [actions.socket.PM](state, action) {
if (action.from.indexOf('.') === -1) { if (action.from.indexOf('.') === -1) {
open(state, action.server, action.from); open(state, action.network, action.from);
} }
}, },
[actions.DISCONNECT](state, { server }) { [actions.DISCONNECT](state, { network }) {
delete state[server]; delete state[network];
} }
} }
); );
export function openPrivateChat(server, nick) { export function openPrivateChat(network, nick) {
return (dispatch, getState) => { return (dispatch, getState) => {
if (!getState().privateChats[server]?.includes(nick)) { if (!getState().privateChats[network]?.includes(nick)) {
dispatch({ dispatch({
type: actions.OPEN_PRIVATE_CHAT, type: actions.OPEN_PRIVATE_CHAT,
server, network,
nick, nick,
socket: { socket: {
type: 'open_dm', type: 'open_dm',
data: { server, name: nick } data: { network, name: nick }
} }
}); });
} }
}; };
} }
export function closePrivateChat(server, nick) { export function closePrivateChat(network, nick) {
return dispatch => { return dispatch => {
dispatch({ dispatch({
type: actions.CLOSE_PRIVATE_CHAT, type: actions.CLOSE_PRIVATE_CHAT,
server, network,
nick, nick,
socket: { socket: {
type: 'close_dm', type: 'close_dm',
data: { server, name: nick } data: { network, name: nick }
} }
}); });
dispatch(updateSelection()); dispatch(updateSelection());

View File

@ -18,15 +18,15 @@ export default createReducer(initialState, {
} }
}); });
export function searchMessages(server, channel, phrase) { export function searchMessages(network, channel, phrase) {
return { return {
type: actions.SEARCH_MESSAGES, type: actions.SEARCH_MESSAGES,
server, network,
channel, channel,
phrase, phrase,
socket: { socket: {
type: 'search', type: 'search',
data: { server, channel, phrase } data: { network, channel, phrase }
} }
}; };
} }

View File

@ -1,11 +1,11 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import get from 'lodash/get'; import get from 'lodash/get';
import { getServers } from './servers'; import { getNetworks } from './networks';
import { getSelectedTab } from './tab'; import { getSelectedTab } from './tab';
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export const getSelectedTabTitle = createSelector( export const getSelectedTabTitle = createSelector(
getSelectedTab, getSelectedTab,
getServers, getNetworks,
(tab, servers) => tab.name || get(servers, [tab.server, 'name']) (tab, networks) => tab.name || get(networks, [tab.network, 'name'])
); );

View File

@ -1,222 +0,0 @@
import { createSelector } from 'reselect';
import get from 'lodash/get';
import createReducer from 'utils/createReducer';
import { getSelectedTab, updateSelection } from './tab';
import * as actions from './actions';
export const getServers = state => state.servers;
export const getCurrentNick = createSelector(
getServers,
getSelectedTab,
(servers, tab) => {
if (!servers[tab.server]) {
return;
}
const { editedNick } = servers[tab.server];
if (editedNick === null) {
return servers[tab.server].nick;
}
return editedNick;
}
);
export const getCurrentServerName = createSelector(
getServers,
getSelectedTab,
(servers, tab) => get(servers, [tab.server, 'name'])
);
export const getCurrentServerStatus = createSelector(
getServers,
getSelectedTab,
(servers, tab) => get(servers, [tab.server, 'status'], {})
);
export default createReducer(
{},
{
[actions.CONNECT](state, { host, nick, name }) {
if (!state[host]) {
state[host] = {
nick,
editedNick: null,
name: name || host,
status: {
connected: false,
error: null
},
features: {}
};
}
},
[actions.DISCONNECT](state, { server }) {
delete state[server];
},
[actions.SET_SERVER_NAME](state, { server, name }) {
state[server].name = name;
},
[actions.SET_NICK](state, { server, nick, editing }) {
if (editing) {
state[server].editedNick = nick;
} else if (nick === '') {
state[server].editedNick = null;
}
},
[actions.socket.NICK](state, { server, oldNick, newNick }) {
if (!oldNick || oldNick === state[server].nick) {
state[server].nick = newNick;
state[server].editedNick = null;
}
},
[actions.socket.NICK_FAIL](state, { server }) {
state[server].editedNick = null;
},
[actions.socket.SERVERS](state, { data }) {
if (data) {
data.forEach(({ host, name = host, nick, status, features = {} }) => {
state[host] = { name, nick, status, features, editedNick: null };
});
}
},
[actions.socket.CONNECTION_UPDATE](state, { server, connected, error }) {
if (state[server]) {
state[server].status.connected = connected;
state[server].status.error = error;
}
},
[actions.socket.FEATURES](state, { server, features }) {
const srv = state[server];
if (srv) {
srv.features = features;
if (features.NETWORK && srv.name === server) {
srv.name = features.NETWORK;
}
}
}
}
);
export function connect(config) {
return {
type: actions.CONNECT,
...config,
socket: {
type: 'connect',
data: config
}
};
}
export function disconnect(server) {
return dispatch => {
dispatch({
type: actions.DISCONNECT,
server,
socket: {
type: 'quit',
data: { server }
}
});
dispatch(updateSelection());
};
}
export function reconnect(server, settings) {
return {
type: actions.RECONNECT,
server,
settings,
socket: {
type: 'reconnect',
data: {
...settings,
server
}
}
};
}
export function whois(user, server) {
return {
type: actions.WHOIS,
user,
server,
socket: {
type: 'whois',
data: { user, server }
}
};
}
export function away(message, server) {
return {
type: actions.AWAY,
message,
server,
socket: {
type: 'away',
data: { message, server }
}
};
}
export function setNick(nick, server, editing) {
nick = nick.trim().replace(' ', '');
const action = {
type: actions.SET_NICK,
nick,
server,
editing
};
if (!editing && nick !== '') {
action.socket = {
type: 'nick',
data: {
newNick: nick,
server
}
};
}
return action;
}
export function isValidServerName(name) {
return name.trim() !== '';
}
export function setServerName(name, server) {
const action = {
type: actions.SET_SERVER_NAME,
name,
server
};
if (isValidServerName(name)) {
action.socket = {
type: 'set_server_name',
data: {
name,
server
},
debounce: {
delay: 500,
key: `server_name:${server}`
}
};
}
return action;
}

View File

@ -12,7 +12,7 @@ const initialState = {
function selectTab(state, action) { function selectTab(state, action) {
state.selected = { state.selected = {
server: action.server, network: action.network,
name: action.name name: action.name
}; };
state.history.push(state.selected); state.history.push(state.selected);
@ -25,18 +25,19 @@ export default createReducer(initialState, {
[actions.PART](state, action) { [actions.PART](state, action) {
state.history = state.history.filter( state.history = state.history.filter(
tab => !(tab.server === action.server && tab.name === action.channels[0]) tab =>
!(tab.network === action.network && tab.name === action.channels[0])
); );
}, },
[actions.CLOSE_PRIVATE_CHAT](state, action) { [actions.CLOSE_PRIVATE_CHAT](state, action) {
state.history = state.history.filter( state.history = state.history.filter(
tab => !(tab.server === action.server && tab.name === action.nick) tab => !(tab.network === action.network && tab.name === action.nick)
); );
}, },
[actions.DISCONNECT](state, action) { [actions.DISCONNECT](state, action) {
state.history = state.history.filter(tab => tab.server !== action.server); state.history = state.history.filter(tab => tab.network !== action.network);
}, },
[LOCATION_CHANGED](state, action) { [LOCATION_CHANGED](state, action) {
@ -49,30 +50,30 @@ export default createReducer(initialState, {
} }
}); });
export function select(server, name, doReplace) { export function select(network, name, doReplace) {
const navigate = doReplace ? replace : push; const navigate = doReplace ? replace : push;
if (name) { if (name) {
return navigate(`/${server}/${encodeURIComponent(name)}`); return navigate(`/${network}/${encodeURIComponent(name)}`);
} }
return navigate(`/${server}`); return navigate(`/${network}`);
} }
export function tabExists( export function tabExists(
{ server, name }, { network, name },
{ servers, channels, privateChats } { networks, channels, privateChats }
) { ) {
return ( return (
(name && get(channels, [server, name])) || (name && get(channels, [network, name])) ||
(!name && server && servers[server]) || (!name && network && networks[network]) ||
(name && find(privateChats[server], nick => nick === name)) (name && find(privateChats[network], nick => nick === name))
); );
} }
function parseTabCookie() { function parseTabCookie() {
const cookie = Cookie.get('tab'); const cookie = Cookie.get('tab');
if (cookie) { if (cookie) {
const [server, name = null] = cookie.split(/;(.+)/); const [network, name = null] = cookie.split(/;(.+)/);
return { server, name }; return { network, name };
} }
return null; return null;
} }
@ -88,35 +89,35 @@ export function updateSelection(tryCookie) {
if (tryCookie) { if (tryCookie) {
const tab = parseTabCookie(); const tab = parseTabCookie();
if (tab && tabExists(tab, state)) { if (tab && tabExists(tab, state)) {
return dispatch(select(tab.server, tab.name, true)); return dispatch(select(tab.network, tab.name, true));
} }
} }
const { servers } = state; const { networks } = state;
const { history } = state.tab; const { history } = state.tab;
const { server } = state.tab.selected; const { network } = state.tab.selected;
const serverAddrs = Object.keys(servers); const networkAddrs = Object.keys(networks);
if (serverAddrs.length === 0) { if (networkAddrs.length === 0) {
dispatch(replace('/connect')); dispatch(replace('/connect'));
} else if ( } else if (
history.length > 0 && history.length > 0 &&
tabExists(history[history.length - 1], state) tabExists(history[history.length - 1], state)
) { ) {
const tab = history[history.length - 1]; const tab = history[history.length - 1];
dispatch(select(tab.server, tab.name, true)); dispatch(select(tab.network, tab.name, true));
} else if (servers[server]) { } else if (networks[network]) {
dispatch(select(server, null, true)); dispatch(select(network, null, true));
} else { } else {
dispatch(select(serverAddrs.sort()[0], null, true)); dispatch(select(networkAddrs.sort()[0], null, true));
} }
}; };
} }
export function setSelectedTab(server, name = null) { export function setSelectedTab(network, name = null) {
return { return {
type: actions.SELECT_TAB, type: actions.SELECT_TAB,
server, network,
name name
}; };
} }

View File

@ -19,17 +19,17 @@ export function isChannel(name) {
return typeof name === 'string' && name[0] === '#'; return typeof name === 'string' && name[0] === '#';
} }
export function stringifyTab(server, name) { export function stringifyTab(network, name) {
if (typeof server === 'object') { if (typeof network === 'object') {
if (server.name) { if (network.name) {
return `${server.server};${server.name}`; return `${network.network};${network.name}`;
} }
return server.server; return network.network;
} }
if (name) { if (name) {
return `${server};${name}`; return `${network};${name}`;
} }
return server; return network;
} }
function isString(s, maxLength) { function isString(s, maxLength) {

View File

@ -95,7 +95,7 @@
"test:watch": "jest --watch", "test:watch": "jest --watch",
"gen:install": "cross-env GO111MODULE=off go get -u github.com/andyleap/gencode github.com/mailru/easyjson/...", "gen:install": "cross-env GO111MODULE=off go get -u github.com/andyleap/gencode github.com/mailru/easyjson/...",
"gen:binary": "gencode go -package storage -schema ../storage/storage.schema -unsafe", "gen:binary": "gencode go -package storage -schema ../storage/storage.schema -unsafe",
"gen:json": "cross-env GO111MODULE=off easyjson -all -lower_camel_case -omit_empty ../server/json.go ../server/index_data.go && cross-env GO111MODULE=off easyjson -lower_camel_case -omit_empty ../storage/user.go" "gen:json": "cross-env GO111MODULE=off easyjson -all -lower_camel_case -omit_empty ../server/json.go ../server/index_data.go ../storage/network.go && cross-env GO111MODULE=off easyjson -lower_camel_case -omit_empty ../storage/user.go"
}, },
"jest": { "jest": {
"moduleNameMapper": { "moduleNameMapper": {

View File

@ -309,6 +309,7 @@ func (c *Client) removeChannels(channels ...string) {
for i, ch := range c.channels { for i, ch := range c.channels {
if c.EqualFold(removeCh, ch) { if c.EqualFold(removeCh, ch) {
c.channels = append(c.channels[:i], c.channels[i+1:]...) c.channels = append(c.channels[:i], c.channels[i+1:]...)
break
} }
} }
} }

View File

@ -10,6 +10,7 @@ const (
QUIT = "QUIT" QUIT = "QUIT"
JOIN = "JOIN" JOIN = "JOIN"
PART = "PART" PART = "PART"
KICK = "KICK"
TOPIC = "TOPIC" TOPIC = "TOPIC"
NAMES = "NAMES" NAMES = "NAMES"
LIST = "LIST" LIST = "LIST"

View File

@ -38,6 +38,18 @@ func (c *Client) handleMessage(msg *Message) {
case QUIT: case QUIT:
msg.meta = c.state.removeUserAll(msg.Sender) msg.meta = c.state.removeUserAll(msg.Sender)
case KICK:
if len(msg.Params) > 1 {
channel, nick := msg.Params[0], msg.Params[1]
if c.Is(nick) {
c.removeChannels(channel)
c.state.removeChannel(channel)
} else {
c.state.removeUser(nick, channel)
}
}
case NICK: case NICK:
if c.Is(msg.Sender) { if c.Is(msg.Sender) {
c.setNick(msg.LastParam()) c.setNick(msg.LastParam())
@ -55,7 +67,7 @@ func (c *Client) handleMessage(msg *Message) {
target := msg.Params[0] target := msg.Params[0]
if len(msg.Params) > 2 && isChannel(target) { if len(msg.Params) > 2 && isChannel(target) {
mode := ParseMode(msg.Params[1]) mode := ParseMode(msg.Params[1])
mode.Server = c.Host() mode.Network = c.Host()
mode.Channel = target mode.Channel = target
mode.User = msg.Params[2] mode.User = msg.Params[2]
@ -102,19 +114,23 @@ func (c *Client) handleMessage(msg *Message) {
} }
case RPL_NAMREPLY: case RPL_NAMREPLY:
channel := msg.Params[2] if len(msg.Params) > 2 {
users := strings.Split(strings.TrimSuffix(msg.LastParam(), " "), " ") channel := msg.Params[2]
users := strings.Split(strings.TrimSuffix(msg.LastParam(), " "), " ")
userBuffer := c.state.userBuffers[channel] userBuffer := c.state.userBuffers[channel]
c.state.userBuffers[channel] = append(userBuffer, users...) c.state.userBuffers[channel] = append(userBuffer, users...)
}
case RPL_ENDOFNAMES: case RPL_ENDOFNAMES:
channel := msg.Params[1] if len(msg.Params) > 1 {
users := c.state.userBuffers[channel] channel := msg.Params[1]
users := c.state.userBuffers[channel]
c.state.setUsers(users, channel) c.state.setUsers(users, channel)
delete(c.state.userBuffers, channel) delete(c.state.userBuffers, channel)
msg.meta = users msg.meta = users
}
case ERROR: case ERROR:
c.Messages <- msg c.Messages <- msg
@ -128,7 +144,7 @@ func (c *Client) handleMessage(msg *Message) {
} }
type Mode struct { type Mode struct {
Server string Network string
Channel string Channel string
User string User string
Add string Add string

View File

@ -23,7 +23,7 @@ type dispatchVersion struct {
type indexData struct { type indexData struct {
Defaults connectDefaults Defaults connectDefaults
Servers []Server Networks []*storage.Network
Channels []*storage.Channel Channels []*storage.Channel
OpenDMs []storage.Tab OpenDMs []storage.Tab
HexIP bool HexIP bool
@ -42,8 +42,11 @@ func (d *Dispatch) getIndexData(r *http.Request, state *State) *indexData {
cfg := d.Config() cfg := d.Config()
data := indexData{ data := indexData{
Defaults: connectDefaults{Defaults: &cfg.Defaults}, Defaults: connectDefaults{
HexIP: cfg.HexIP, Defaults: &cfg.Defaults,
ServerPassword: cfg.Defaults.ServerPassword != "",
},
HexIP: cfg.HexIP,
Version: dispatchVersion{ Version: dispatchVersion{
Tag: version.Tag, Tag: version.Tag,
Commit: version.Commit, Commit: version.Commit,
@ -51,77 +54,53 @@ func (d *Dispatch) getIndexData(r *http.Request, state *State) *indexData {
}, },
} }
data.Defaults.ServerPassword = cfg.Defaults.ServerPassword != ""
if state == nil { if state == nil {
data.Settings = storage.DefaultClientSettings() data.Settings = storage.DefaultClientSettings()
return &data return &data
} }
data.Settings = state.user.GetClientSettings() data.Settings = state.user.ClientSettings()
servers, err := state.user.GetServers() state.lock.Lock()
if err != nil { for _, network := range state.networks {
return nil network = network.Copy()
network.Password = ""
network.Username = ""
network.Realname = ""
data.Networks = append(data.Networks, network)
data.Channels = append(data.Channels, network.Channels()...)
} }
connections := state.getConnectionStates() state.lock.Unlock()
for _, server := range servers {
server.Password = ""
server.Username = ""
server.Realname = ""
s := Server{ openDMs, err := state.user.OpenDMs()
Server: server, if err == nil {
Status: newConnectionUpdate(server.Host, connections[server.Host]), data.OpenDMs = openDMs
}
if i, ok := state.irc[server.Host]; ok {
s.Features = i.Features.Map()
}
data.Servers = append(data.Servers, s)
} }
channels, err := state.user.GetChannels()
if err != nil {
return nil
}
for i, channel := range channels {
if client, ok := state.getIRC(channel.Server); ok {
channels[i].Topic = client.ChannelTopic(channel.Name)
}
}
data.Channels = channels
openDMs, err := state.user.GetOpenDMs()
if err != nil {
return nil
}
data.OpenDMs = openDMs
tab, err := tabFromRequest(r) tab, err := tabFromRequest(r)
if err == nil && hasTab(channels, openDMs, tab.Server, tab.Name) { if err == nil && hasTab(data.Channels, openDMs, tab.Network, tab.Name) {
data.addUsersAndMessages(tab.Server, tab.Name, state) data.addUsersAndMessages(tab.Network, tab.Name, state)
} }
return &data return &data
} }
func (d *indexData) addUsersAndMessages(server, name string, state *State) { func (d *indexData) addUsersAndMessages(network, name string, state *State) {
if i, ok := state.getIRC(server); ok && isChannel(name) { if i, ok := state.client(network); ok && isChannel(name) {
if users := i.ChannelUsers(name); len(users) > 0 { if users := i.ChannelUsers(name); len(users) > 0 {
d.Users = &Userlist{ d.Users = &Userlist{
Server: server, Network: network,
Channel: name, Channel: name,
Users: users, Users: users,
} }
} }
} }
messages, hasMore, err := state.user.GetLastMessages(server, name, 50) messages, hasMore, err := state.user.LastMessages(network, name, 50)
if err == nil && len(messages) > 0 { if err == nil && len(messages) > 0 {
m := Messages{ m := Messages{
Server: server, Network: network,
To: name, To: name,
Messages: messages, Messages: messages,
} }
@ -134,16 +113,16 @@ func (d *indexData) addUsersAndMessages(server, name string, state *State) {
} }
} }
func hasTab(channels []*storage.Channel, openDMs []storage.Tab, server, name string) bool { func hasTab(channels []*storage.Channel, openDMs []storage.Tab, network, name string) bool {
if name != "" { if name != "" {
for _, ch := range channels { for _, ch := range channels {
if server == ch.Server && name == ch.Name { if network == ch.Network && name == ch.Name {
return true return true
} }
} }
for _, tab := range openDMs { for _, tab := range openDMs {
if server == tab.Server && name == tab.Name { if network == tab.Network && name == tab.Name {
return true return true
} }
} }
@ -179,7 +158,7 @@ func tabFromRequest(r *http.Request) (Tab, error) {
parts := strings.SplitN(v, ";", 2) parts := strings.SplitN(v, ";", 2)
if len(parts) == 2 { if len(parts) == 2 {
tab.Server = parts[0] tab.Network = parts[0]
tab.Name = parts[1] tab.Name = parts[1]
} }
} else { } else {
@ -194,7 +173,7 @@ func tabFromRequest(r *http.Request) (Tab, error) {
tab.Name = name tab.Name = name
} }
tab.Server = parts[0] tab.Network = parts[0]
} }
} }

View File

@ -42,27 +42,35 @@ func easyjson7e607aefDecodeGithubComKhliengDispatchServer(in *jlexer.Lexer, out
if data := in.Raw(); in.Ok() { if data := in.Raw(); in.Ok() {
in.AddError((out.Defaults).UnmarshalJSON(data)) in.AddError((out.Defaults).UnmarshalJSON(data))
} }
case "servers": case "networks":
if in.IsNull() { if in.IsNull() {
in.Skip() in.Skip()
out.Servers = nil out.Networks = nil
} else { } else {
in.Delim('[') in.Delim('[')
if out.Servers == nil { if out.Networks == nil {
if !in.IsDelim(']') { if !in.IsDelim(']') {
out.Servers = make([]Server, 0, 0) out.Networks = make([]*storage.Network, 0, 8)
} else { } else {
out.Servers = []Server{} out.Networks = []*storage.Network{}
} }
} else { } else {
out.Servers = (out.Servers)[:0] out.Networks = (out.Networks)[:0]
} }
for !in.IsDelim(']') { for !in.IsDelim(']') {
var v1 Server var v1 *storage.Network
if data := in.Raw(); in.Ok() { if in.IsNull() {
in.AddError((v1).UnmarshalJSON(data)) in.Skip()
v1 = nil
} else {
if v1 == nil {
v1 = new(storage.Network)
}
if data := in.Raw(); in.Ok() {
in.AddError((*v1).UnmarshalJSON(data))
}
} }
out.Servers = append(out.Servers, v1) out.Networks = append(out.Networks, v1)
in.WantComma() in.WantComma()
} }
in.Delim(']') in.Delim(']')
@ -91,7 +99,9 @@ func easyjson7e607aefDecodeGithubComKhliengDispatchServer(in *jlexer.Lexer, out
if v2 == nil { if v2 == nil {
v2 = new(storage.Channel) v2 = new(storage.Channel)
} }
easyjson7e607aefDecodeGithubComKhliengDispatchStorage(in, v2) if data := in.Raw(); in.Ok() {
in.AddError((*v2).UnmarshalJSON(data))
}
} }
out.Channels = append(out.Channels, v2) out.Channels = append(out.Channels, v2)
in.WantComma() in.WantComma()
@ -115,7 +125,7 @@ func easyjson7e607aefDecodeGithubComKhliengDispatchServer(in *jlexer.Lexer, out
} }
for !in.IsDelim(']') { for !in.IsDelim(']') {
var v3 storage.Tab var v3 storage.Tab
easyjson7e607aefDecodeGithubComKhliengDispatchStorage1(in, &v3) easyjson7e607aefDecodeGithubComKhliengDispatchStorage(in, &v3)
out.OpenDMs = append(out.OpenDMs, v3) out.OpenDMs = append(out.OpenDMs, v3)
in.WantComma() in.WantComma()
} }
@ -183,8 +193,8 @@ func easyjson7e607aefEncodeGithubComKhliengDispatchServer(out *jwriter.Writer, i
out.RawString(prefix[1:]) out.RawString(prefix[1:])
out.Raw((in.Defaults).MarshalJSON()) out.Raw((in.Defaults).MarshalJSON())
} }
if len(in.Servers) != 0 { if len(in.Networks) != 0 {
const prefix string = ",\"servers\":" const prefix string = ",\"networks\":"
if first { if first {
first = false first = false
out.RawString(prefix[1:]) out.RawString(prefix[1:])
@ -193,11 +203,15 @@ func easyjson7e607aefEncodeGithubComKhliengDispatchServer(out *jwriter.Writer, i
} }
{ {
out.RawByte('[') out.RawByte('[')
for v4, v5 := range in.Servers { for v4, v5 := range in.Networks {
if v4 > 0 { if v4 > 0 {
out.RawByte(',') out.RawByte(',')
} }
out.Raw((v5).MarshalJSON()) if v5 == nil {
out.RawString("null")
} else {
out.Raw((*v5).MarshalJSON())
}
} }
out.RawByte(']') out.RawByte(']')
} }
@ -219,7 +233,7 @@ func easyjson7e607aefEncodeGithubComKhliengDispatchServer(out *jwriter.Writer, i
if v7 == nil { if v7 == nil {
out.RawString("null") out.RawString("null")
} else { } else {
easyjson7e607aefEncodeGithubComKhliengDispatchStorage(out, *v7) out.Raw((*v7).MarshalJSON())
} }
} }
out.RawByte(']') out.RawByte(']')
@ -239,7 +253,7 @@ func easyjson7e607aefEncodeGithubComKhliengDispatchServer(out *jwriter.Writer, i
if v8 > 0 { if v8 > 0 {
out.RawByte(',') out.RawByte(',')
} }
easyjson7e607aefEncodeGithubComKhliengDispatchStorage1(out, v9) easyjson7e607aefEncodeGithubComKhliengDispatchStorage(out, v9)
} }
out.RawByte(']') out.RawByte(']')
} }
@ -320,7 +334,7 @@ func (v *indexData) UnmarshalJSON(data []byte) error {
func (v *indexData) UnmarshalEasyJSON(l *jlexer.Lexer) { func (v *indexData) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjson7e607aefDecodeGithubComKhliengDispatchServer(l, v) easyjson7e607aefDecodeGithubComKhliengDispatchServer(l, v)
} }
func easyjson7e607aefDecodeGithubComKhliengDispatchStorage1(in *jlexer.Lexer, out *storage.Tab) { func easyjson7e607aefDecodeGithubComKhliengDispatchStorage(in *jlexer.Lexer, out *storage.Tab) {
isTopLevel := in.IsStart() isTopLevel := in.IsStart()
if in.IsNull() { if in.IsNull() {
if isTopLevel { if isTopLevel {
@ -339,8 +353,8 @@ func easyjson7e607aefDecodeGithubComKhliengDispatchStorage1(in *jlexer.Lexer, ou
continue continue
} }
switch key { switch key {
case "server": case "network":
out.Server = string(in.String()) out.Network = string(in.String())
case "name": case "name":
out.Name = string(in.String()) out.Name = string(in.String())
default: default:
@ -353,15 +367,15 @@ func easyjson7e607aefDecodeGithubComKhliengDispatchStorage1(in *jlexer.Lexer, ou
in.Consumed() in.Consumed()
} }
} }
func easyjson7e607aefEncodeGithubComKhliengDispatchStorage1(out *jwriter.Writer, in storage.Tab) { func easyjson7e607aefEncodeGithubComKhliengDispatchStorage(out *jwriter.Writer, in storage.Tab) {
out.RawByte('{') out.RawByte('{')
first := true first := true
_ = first _ = first
if in.Server != "" { if in.Network != "" {
const prefix string = ",\"server\":" const prefix string = ",\"network\":"
first = false first = false
out.RawString(prefix[1:]) out.RawString(prefix[1:])
out.String(string(in.Server)) out.String(string(in.Network))
} }
if in.Name != "" { if in.Name != "" {
const prefix string = ",\"name\":" const prefix string = ",\"name\":"
@ -375,73 +389,6 @@ func easyjson7e607aefEncodeGithubComKhliengDispatchStorage1(out *jwriter.Writer,
} }
out.RawByte('}') out.RawByte('}')
} }
func easyjson7e607aefDecodeGithubComKhliengDispatchStorage(in *jlexer.Lexer, out *storage.Channel) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeFieldName(false)
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "server":
out.Server = string(in.String())
case "name":
out.Name = string(in.String())
case "topic":
out.Topic = string(in.String())
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjson7e607aefEncodeGithubComKhliengDispatchStorage(out *jwriter.Writer, in storage.Channel) {
out.RawByte('{')
first := true
_ = first
if in.Server != "" {
const prefix string = ",\"server\":"
first = false
out.RawString(prefix[1:])
out.String(string(in.Server))
}
if in.Name != "" {
const prefix string = ",\"name\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.Name))
}
if in.Topic != "" {
const prefix string = ",\"topic\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.Topic))
}
out.RawByte('}')
}
func easyjson7e607aefDecodeGithubComKhliengDispatchServer1(in *jlexer.Lexer, out *dispatchVersion) { func easyjson7e607aefDecodeGithubComKhliengDispatchServer1(in *jlexer.Lexer, out *dispatchVersion) {
isTopLevel := in.IsStart() isTopLevel := in.IsStart()
if in.IsNull() { if in.IsNull() {

View File

@ -19,25 +19,25 @@ func TestGetTabFromPath(t *testing.T) {
URL: &url.URL{Path: "/init"}, URL: &url.URL{Path: "/init"},
Header: http.Header{"Referer": []string{"/chat.freenode.net/%23r%2Fstuff%2F"}}, Header: http.Header{"Referer": []string{"/chat.freenode.net/%23r%2Fstuff%2F"}},
}, },
Tab{storage.Tab{Server: "chat.freenode.net", Name: "#r/stuff/"}}, Tab{storage.Tab{Network: "chat.freenode.net", Name: "#r/stuff/"}},
}, { }, {
&http.Request{ &http.Request{
URL: &url.URL{Path: "/init"}, URL: &url.URL{Path: "/init"},
Header: http.Header{"Referer": []string{"/chat.freenode.net/%23r%2Fstuff"}}, Header: http.Header{"Referer": []string{"/chat.freenode.net/%23r%2Fstuff"}},
}, },
Tab{storage.Tab{Server: "chat.freenode.net", Name: "#r/stuff"}}, Tab{storage.Tab{Network: "chat.freenode.net", Name: "#r/stuff"}},
}, { }, {
&http.Request{ &http.Request{
URL: &url.URL{Path: "/init"}, URL: &url.URL{Path: "/init"},
Header: http.Header{"Referer": []string{"/chat.freenode.net/%23stuff"}}, Header: http.Header{"Referer": []string{"/chat.freenode.net/%23stuff"}},
}, },
Tab{storage.Tab{Server: "chat.freenode.net", Name: "#stuff"}}, Tab{storage.Tab{Network: "chat.freenode.net", Name: "#stuff"}},
}, { }, {
&http.Request{ &http.Request{
URL: &url.URL{Path: "/init"}, URL: &url.URL{Path: "/init"},
Header: http.Header{"Referer": []string{"/chat.freenode.net/stuff"}}, Header: http.Header{"Referer": []string{"/chat.freenode.net/stuff"}},
}, },
Tab{storage.Tab{Server: "chat.freenode.net", Name: "stuff"}}, Tab{storage.Tab{Network: "chat.freenode.net", Name: "stuff"}},
}, { }, {
&http.Request{ &http.Request{
URL: &url.URL{Path: "/init"}, URL: &url.URL{Path: "/init"},
@ -48,7 +48,7 @@ func TestGetTabFromPath(t *testing.T) {
&http.Request{ &http.Request{
URL: &url.URL{Path: "/ws/chat.freenode.net"}, URL: &url.URL{Path: "/ws/chat.freenode.net"},
}, },
Tab{storage.Tab{Server: "chat.freenode.net"}}, Tab{storage.Tab{Network: "chat.freenode.net"}},
}, },
} }

View File

@ -7,7 +7,6 @@ import (
"github.com/khlieng/dispatch/pkg/irc" "github.com/khlieng/dispatch/pkg/irc"
"github.com/khlieng/dispatch/storage" "github.com/khlieng/dispatch/storage"
"github.com/khlieng/dispatch/version"
) )
func createNickInUseHandler(i *irc.Client, state *State) func(string) string { func createNickInUseHandler(i *irc.Client, state *State) func(string) string {
@ -16,12 +15,12 @@ func createNickInUseHandler(i *irc.Client, state *State) func(string) string {
if newNick == i.GetNick() { if newNick == i.GetNick() {
state.sendJSON("nick_fail", NickFail{ state.sendJSON("nick_fail", NickFail{
Server: i.Host(), Network: i.Host(),
}) })
} }
state.sendJSON("error", IRCError{ state.sendJSON("error", IRCError{
Server: i.Host(), Network: i.Host(),
Message: fmt.Sprintf("Nickname %s is unavailable, trying %s instead", nick, newNick), Message: fmt.Sprintf("Nickname %s is unavailable, trying %s instead", nick, newNick),
}) })
@ -29,23 +28,11 @@ func createNickInUseHandler(i *irc.Client, state *State) func(string) string {
} }
} }
func connectIRC(server *storage.Server, state *State, srcIP []byte) *irc.Client { func connectIRC(network *storage.Network, state *State, srcIP []byte) *irc.Client {
cfg := state.srv.Config() cfg := state.srv.Config()
ircCfg := network.IRCConfig()
ircCfg := irc.Config{ if ircCfg.TLS {
Host: server.Host,
Port: server.Port,
TLS: server.TLS,
Nick: server.Nick,
Username: server.Username,
Realname: server.Realname,
Account: server.Account,
Password: server.Password,
Version: fmt.Sprintf("Dispatch %s (git: %s)", version.Tag, version.Commit),
Source: "https://github.com/khlieng/dispatch",
}
if server.TLS {
ircCfg.TLSConfig = &tls.Config{ ircCfg.TLSConfig = &tls.Config{
InsecureSkipVerify: !cfg.VerifyCertificates, InsecureSkipVerify: !cfg.VerifyCertificates,
} }
@ -59,18 +46,16 @@ func connectIRC(server *storage.Server, state *State, srcIP []byte) *irc.Client
ircCfg.Username = hex.EncodeToString(srcIP) ircCfg.Username = hex.EncodeToString(srcIP)
} }
if server.ServerPassword == "" && if ircCfg.ServerPassword == "" &&
cfg.Defaults.ServerPassword != "" && cfg.Defaults.ServerPassword != "" &&
server.Host == cfg.Defaults.Host { ircCfg.Host == cfg.Defaults.Host {
ircCfg.ServerPassword = cfg.Defaults.ServerPassword ircCfg.ServerPassword = cfg.Defaults.ServerPassword
} else {
ircCfg.ServerPassword = server.ServerPassword
} }
i := irc.NewClient(&ircCfg) i := irc.NewClient(ircCfg)
i.Config.HandleNickInUse = createNickInUseHandler(i, state) i.Config.HandleNickInUse = createNickInUseHandler(i, state)
state.setIRC(server.Host, i) state.setNetwork(network.Host, state.user.NewNetwork(network, i))
i.Connect() i.Connect()
go newIRCHandler(i, state).run() go newIRCHandler(i, state).run()

View File

@ -50,7 +50,7 @@ func (i *ircHandler) run() {
select { select {
case msg, ok := <-i.client.Messages: case msg, ok := <-i.client.Messages:
if !ok { if !ok {
i.state.deleteIRC(i.client.Host()) i.state.deleteNetwork(i.client.Host())
return return
} }
@ -58,7 +58,14 @@ func (i *ircHandler) run() {
case state := <-i.client.ConnectionChanged: case state := <-i.client.ConnectionChanged:
i.state.sendJSON("connection_update", newConnectionUpdate(i.client.Host(), state)) i.state.sendJSON("connection_update", newConnectionUpdate(i.client.Host(), state))
i.state.setConnectionState(i.client.Host(), state)
if network, ok := i.state.network(i.client.Host()); ok {
var err string
if state.Error != nil {
err = state.Error.Error()
}
network.SetStatus(state.Connected, err)
}
if state.Error != nil && (lastConnErr == nil || if state.Error != nil && (lastConnErr == nil ||
state.Error.Error() != lastConnErr.Error()) { state.Error.Error() != lastConnErr.Error()) {
@ -87,7 +94,7 @@ func (i *ircHandler) run() {
func (i *ircHandler) dispatchMessage(msg *irc.Message) { func (i *ircHandler) dispatchMessage(msg *irc.Message) {
if msg.Command[0] == '4' && !isExcludedError(msg.Command) { if msg.Command[0] == '4' && !isExcludedError(msg.Command) {
err := IRCError{ err := IRCError{
Server: i.client.Host(), Network: i.client.Host(),
Message: msg.LastParam(), Message: msg.LastParam(),
} }
@ -109,25 +116,30 @@ func (i *ircHandler) dispatchMessage(msg *irc.Message) {
} }
func (i *ircHandler) nick(msg *irc.Message) { func (i *ircHandler) nick(msg *irc.Message) {
i.state.sendJSON("nick", Nick{ nick := Nick{
Server: i.client.Host(), Network: i.client.Host(),
Old: msg.Sender, Old: msg.Sender,
New: msg.LastParam(), New: msg.LastParam(),
}) }
if i.client.Is(msg.LastParam()) { i.state.sendJSON("nick", nick)
go i.state.user.SetNick(msg.LastParam(), i.client.Host())
if i.client.Is(nick.New) {
if network, ok := i.state.network(nick.Network); ok {
network.SetNick(nick.New)
go network.Save()
}
} }
channels := irc.GetNickChannels(msg) channels := irc.GetNickChannels(msg)
go i.state.user.LogEvent(i.client.Host(), "nick", []string{msg.Sender, msg.LastParam()}, channels...) go i.state.user.LogEvent(nick.Network, "nick", []string{nick.Old, nick.New}, channels...)
} }
func (i *ircHandler) join(msg *irc.Message) { func (i *ircHandler) join(msg *irc.Message) {
host := i.client.Host() host := i.client.Host()
i.state.sendJSON("join", Join{ i.state.sendJSON("join", Join{
Server: host, Network: host,
User: msg.Sender, User: msg.Sender,
Channels: msg.Params, Channels: msg.Params,
}) })
@ -138,37 +150,74 @@ func (i *ircHandler) join(msg *irc.Message) {
// In case no topic is set and there's a cached one that needs to be cleared // In case no topic is set and there's a cached one that needs to be cleared
i.client.Topic(channel) i.client.Topic(channel)
i.state.sendLastMessages(host, channel, 50) if network, ok := i.state.network(host); ok {
if ch := network.Channel(channel); ch != nil {
ch.SetJoined(true)
} else {
i.state.sendLastMessages(host, channel, 50)
go i.state.user.AddChannel(&storage.Channel{ ch = network.NewChannel(channel)
Server: host, ch.SetJoined(true)
Name: channel, network.AddChannel(ch)
}) go ch.Save()
}
}
} }
go i.state.user.LogEvent(host, "join", []string{msg.Sender}, channel) go i.state.user.LogEvent(host, "join", []string{msg.Sender}, channel)
} }
func (i *ircHandler) part(msg *irc.Message) { func (i *ircHandler) part(msg *irc.Message) {
host := i.client.Host()
channel := msg.Params[0]
part := Part{ part := Part{
Server: host, Network: i.client.Host(),
User: msg.Sender, User: msg.Sender,
Channel: channel, Channel: msg.Params[0],
} }
params := []string{part.User}
if len(msg.Params) == 2 { if len(msg.Params) == 2 {
part.Reason = msg.Params[1] part.Reason = msg.Params[1]
params = append(params, part.Reason)
} }
i.state.sendJSON("part", part) i.state.sendJSON("part", part)
if i.client.Is(msg.Sender) { if i.client.Is(msg.Sender) {
go i.state.user.RemoveChannel(host, part.Channel) go i.state.user.RemoveChannel(part.Network, part.Channel)
} }
go i.state.user.LogEvent(host, "part", []string{msg.Sender}, channel) go i.state.user.LogEvent(part.Network, "part", params, part.Channel)
}
func (i *ircHandler) kick(msg *irc.Message) {
if len(msg.Params) < 2 {
return
}
kick := Kick{
Network: i.client.Host(),
Channel: msg.Params[0],
Sender: msg.Sender,
User: msg.Params[1],
}
params := []string{kick.User, kick.Sender}
if len(msg.Params) > 2 {
kick.Reason = msg.Params[2]
params = append(params, kick.Reason)
}
i.state.sendJSON("kick", kick)
go i.state.user.LogEvent(kick.Network, "kick", params, kick.Channel)
if i.client.Is(kick.User) {
if network, ok := i.state.network(kick.Network); ok {
network.Channel(kick.Channel).SetJoined(false)
}
}
} }
func (i *ircHandler) mode(msg *irc.Message) { func (i *ircHandler) mode(msg *irc.Message) {
@ -193,7 +242,7 @@ func (i *ircHandler) message(msg *irc.Message) {
message := Message{ message := Message{
ID: betterguid.New(), ID: betterguid.New(),
Server: i.client.Host(), Network: i.client.Host(),
From: msg.Sender, From: msg.Sender,
Content: msg.LastParam(), Content: msg.LastParam(),
} }
@ -215,7 +264,7 @@ func (i *ircHandler) message(msg *irc.Message) {
if target != "*" && !msg.IsFromServer() { if target != "*" && !msg.IsFromServer() {
go i.state.user.LogMessage(&storage.Message{ go i.state.user.LogMessage(&storage.Message{
ID: message.ID, ID: message.ID,
Server: message.Server, Network: message.Network,
From: message.From, From: message.From,
To: target, To: target,
Content: message.Content, Content: message.Content,
@ -225,9 +274,9 @@ func (i *ircHandler) message(msg *irc.Message) {
func (i *ircHandler) quit(msg *irc.Message) { func (i *ircHandler) quit(msg *irc.Message) {
i.state.sendJSON("quit", Quit{ i.state.sendJSON("quit", Quit{
Server: i.client.Host(), Network: i.client.Host(),
User: msg.Sender, User: msg.Sender,
Reason: msg.LastParam(), Reason: msg.LastParam(),
}) })
channels := irc.GetQuitChannels(msg) channels := irc.GetQuitChannels(msg)
@ -238,8 +287,8 @@ func (i *ircHandler) quit(msg *irc.Message) {
func (i *ircHandler) info(msg *irc.Message) { func (i *ircHandler) info(msg *irc.Message) {
if msg.Command == irc.RPL_WELCOME { if msg.Command == irc.RPL_WELCOME {
i.state.sendJSON("nick", Nick{ i.state.sendJSON("nick", Nick{
Server: i.client.Host(), Network: i.client.Host(),
New: msg.Params[0], New: msg.Params[0],
}) })
_, needsUpdate := channelIndexes.Get(i.client.Host()) _, needsUpdate := channelIndexes.Get(i.client.Host())
@ -252,25 +301,27 @@ func (i *ircHandler) info(msg *irc.Message) {
} }
i.state.sendJSON("pm", Message{ i.state.sendJSON("pm", Message{
Server: i.client.Host(), Network: i.client.Host(),
From: msg.Sender, From: msg.Sender,
Content: strings.Join(msg.Params[1:], " "), Content: strings.Join(msg.Params[1:], " "),
}) })
} }
func (i *ircHandler) features(msg *irc.Message) { func (i *ircHandler) features(msg *irc.Message) {
features := i.client.Features.Map()
i.state.sendJSON("features", Features{ i.state.sendJSON("features", Features{
Server: i.client.Host(), Network: i.client.Host(),
Features: i.client.Features.Map(), Features: features,
}) })
if name := i.client.Features.String("NETWORK"); name != "" { if network, ok := i.state.network(i.client.Host()); ok {
go func() { network.SetFeatures(features)
server, err := i.state.user.GetServer(i.client.Host())
if err == nil && server.Name == "" { if name := i.client.Features.String("NETWORK"); name != "" {
i.state.user.SetServerName(name, server.Host) network.SetName(name)
} go network.Save()
}() }
} }
} }
@ -310,30 +361,41 @@ func (i *ircHandler) topic(msg *irc.Message) {
} }
i.state.sendJSON("topic", Topic{ i.state.sendJSON("topic", Topic{
Server: i.client.Host(), Network: i.client.Host(),
Channel: channel, Channel: channel,
Topic: msg.LastParam(), Topic: msg.LastParam(),
Nick: nick, Nick: nick,
}) })
if network, ok := i.state.network(i.client.Host()); ok {
network.Channel(channel).SetTopic(msg.LastParam())
}
} }
func (i *ircHandler) noTopic(msg *irc.Message) { func (i *ircHandler) noTopic(msg *irc.Message) {
channel := msg.Params[1]
i.state.sendJSON("topic", Topic{ i.state.sendJSON("topic", Topic{
Server: i.client.Host(), Network: i.client.Host(),
Channel: msg.Params[1], Channel: channel,
}) })
if network, ok := i.state.network(i.client.Host()); ok {
network.Channel(channel).SetTopic("")
}
} }
func (i *ircHandler) namesEnd(msg *irc.Message) { func (i *ircHandler) namesEnd(msg *irc.Message) {
i.state.sendJSON("users", Userlist{ i.state.sendJSON("users", Userlist{
Server: i.client.Host(), Network: i.client.Host(),
Channel: msg.Params[1], Channel: msg.Params[1],
Users: irc.GetNamreplyUsers(msg), Users: irc.GetNamreplyUsers(msg),
}) })
} }
func (i *ircHandler) motdStart(msg *irc.Message) { func (i *ircHandler) motdStart(msg *irc.Message) {
i.motdBuffer.Server = i.client.Host() i.motdBuffer.Network = i.client.Host()
i.motdBuffer.Title = msg.LastParam() i.motdBuffer.Title = msg.LastParam()
} }
@ -376,23 +438,23 @@ func (i *ircHandler) listEnd(msg *irc.Message) {
func (i *ircHandler) badNick(msg *irc.Message) { func (i *ircHandler) badNick(msg *irc.Message) {
i.state.sendJSON("nick_fail", NickFail{ i.state.sendJSON("nick_fail", NickFail{
Server: i.client.Host(), Network: i.client.Host(),
}) })
} }
func (i *ircHandler) forward(msg *irc.Message) { func (i *ircHandler) forward(msg *irc.Message) {
if len(msg.Params) > 2 { if len(msg.Params) > 2 {
i.state.sendJSON("channel_forward", ChannelForward{ i.state.sendJSON("channel_forward", ChannelForward{
Server: i.client.Host(), Network: i.client.Host(),
Old: msg.Params[1], Old: msg.Params[1],
New: msg.Params[2], New: msg.Params[2],
}) })
} }
} }
func (i *ircHandler) error(msg *irc.Message) { func (i *ircHandler) error(msg *irc.Message) {
i.state.sendJSON("error", IRCError{ i.state.sendJSON("error", IRCError{
Server: i.client.Host(), Network: i.client.Host(),
Message: msg.LastParam(), Message: msg.LastParam(),
}) })
} }
@ -413,7 +475,7 @@ func (i *ircHandler) receiveDCCSend(pack *irc.DCCSend, msg *irc.Message) {
i.state.setPendingDCC(pack.File, pack) i.state.setPendingDCC(pack.File, pack)
i.state.sendJSON("dcc_send", DCCSend{ i.state.sendJSON("dcc_send", DCCSend{
Server: i.client.Host(), Network: i.client.Host(),
From: msg.Sender, From: msg.Sender,
Filename: pack.File, Filename: pack.File,
URL: fmt.Sprintf("%s://%s/downloads/%s/%s", URL: fmt.Sprintf("%s://%s/downloads/%s/%s",
@ -431,6 +493,7 @@ func (i *ircHandler) initHandlers() {
irc.NICK: i.nick, irc.NICK: i.nick,
irc.JOIN: i.join, irc.JOIN: i.join,
irc.PART: i.part, irc.PART: i.part,
irc.KICK: i.kick,
irc.MODE: i.mode, irc.MODE: i.mode,
irc.PRIVMSG: i.message, irc.PRIVMSG: i.message,
irc.NOTICE: i.message, irc.NOTICE: i.message,
@ -469,16 +532,16 @@ func (i *ircHandler) log(v ...interface{}) {
func (i *ircHandler) sendDCCInfo(message string, log bool, a ...interface{}) { func (i *ircHandler) sendDCCInfo(message string, log bool, a ...interface{}) {
msg := Message{ msg := Message{
Server: i.client.Host(), Network: i.client.Host(),
From: "@dcc", From: "@dcc",
Content: fmt.Sprintf(message, a...), Content: fmt.Sprintf(message, a...),
} }
i.state.sendJSON("pm", msg) i.state.sendJSON("pm", msg)
if log { if log {
i.state.user.AddOpenDM(msg.Server, msg.From) i.state.user.AddOpenDM(msg.Network, msg.From)
i.state.user.LogMessage(&storage.Message{ i.state.user.LogMessage(&storage.Message{
Server: msg.Server, Network: msg.Network,
From: msg.From, From: msg.From,
Content: msg.Content, Content: msg.Content,
}) })

View File

@ -76,9 +76,9 @@ func TestHandleIRCNick(t *testing.T) {
}) })
checkResponse(t, "nick", Nick{ checkResponse(t, "nick", Nick{
Server: "host.com", Network: "host.com",
Old: "old", Old: "old",
New: "new", New: "new",
}, res) }, res)
} }
@ -90,7 +90,7 @@ func TestHandleIRCJoin(t *testing.T) {
}) })
checkResponse(t, "join", Join{ checkResponse(t, "join", Join{
Server: "host.com", Network: "host.com",
User: "joining", User: "joining",
Channels: []string{"#chan"}, Channels: []string{"#chan"},
}, res) }, res)
@ -104,7 +104,7 @@ func TestHandleIRCPart(t *testing.T) {
}) })
checkResponse(t, "part", Part{ checkResponse(t, "part", Part{
Server: "host.com", Network: "host.com",
User: "parting", User: "parting",
Channel: "#chan", Channel: "#chan",
Reason: "the reason", Reason: "the reason",
@ -117,7 +117,7 @@ func TestHandleIRCPart(t *testing.T) {
}) })
checkResponse(t, "part", Part{ checkResponse(t, "part", Part{
Server: "host.com", Network: "host.com",
User: "parting", User: "parting",
Channel: "#chan", Channel: "#chan",
}, res) }, res)
@ -159,9 +159,9 @@ func TestHandleIRCQuit(t *testing.T) {
}) })
checkResponse(t, "quit", Quit{ checkResponse(t, "quit", Quit{
Server: "host.com", Network: "host.com",
User: "nick", User: "nick",
Reason: "the reason", Reason: "the reason",
}, res) }, res)
} }
@ -173,12 +173,12 @@ func TestHandleIRCWelcome(t *testing.T) {
}) })
checkResponse(t, "nick", Nick{ checkResponse(t, "nick", Nick{
Server: "host.com", Network: "host.com",
New: "nick", New: "nick",
}, <-res) }, <-res)
checkResponse(t, "pm", Message{ checkResponse(t, "pm", Message{
Server: "host.com", Network: "host.com",
From: "nick", From: "nick",
Content: "some text", Content: "some text",
}, <-res) }, <-res)
@ -224,7 +224,7 @@ func TestHandleIRCTopic(t *testing.T) {
}) })
checkResponse(t, "topic", Topic{ checkResponse(t, "topic", Topic{
Server: "host.com", Network: "host.com",
Channel: "#chan", Channel: "#chan",
Topic: "the topic", Topic: "the topic",
}, res) }, res)
@ -236,7 +236,7 @@ func TestHandleIRCTopic(t *testing.T) {
}) })
checkResponse(t, "topic", Topic{ checkResponse(t, "topic", Topic{
Server: "host.com", Network: "host.com",
Channel: "#chan", Channel: "#chan",
Topic: "the topic", Topic: "the topic",
Nick: "bob", Nick: "bob",
@ -250,7 +250,7 @@ func TestHandleIRCNoTopic(t *testing.T) {
}) })
checkResponse(t, "topic", Topic{ checkResponse(t, "topic", Topic{
Server: "host.com", Network: "host.com",
Channel: "#chan", Channel: "#chan",
}, res) }, res)
} }
@ -279,7 +279,7 @@ func TestHandleIRCMotd(t *testing.T) {
i.dispatchMessage(&irc.Message{Command: irc.RPL_ENDOFMOTD}) i.dispatchMessage(&irc.Message{Command: irc.RPL_ENDOFMOTD})
checkResponse(t, "motd", MOTD{ checkResponse(t, "motd", MOTD{
Server: "host.com", Network: "host.com",
Title: "motd title", Title: "motd title",
Content: []string{"line 1", "line 2"}, Content: []string{"line 1", "line 2"},
}, <-s.broadcast) }, <-s.broadcast)
@ -302,6 +302,6 @@ func TestHandleIRCBadNick(t *testing.T) {
<-s.broadcast <-s.broadcast
checkResponse(t, "nick_fail", NickFail{ checkResponse(t, "nick_fail", NickFail{
Server: "host.com", Network: "host.com",
}, <-s.broadcast) }, <-s.broadcast)
} }

View File

@ -19,37 +19,31 @@ type WSResponse struct {
Data interface{} Data interface{}
} }
type Server struct {
*storage.Server
Status ConnectionUpdate
Features map[string]interface{}
}
type Features struct { type Features struct {
Server string Network string
Features map[string]interface{} Features map[string]interface{}
} }
type ServerName struct { type NetworkName struct {
Server string Network string
Name string Name string
} }
type ReconnectSettings struct { type ReconnectSettings struct {
Server string Network string
SkipVerify bool SkipVerify bool
} }
type ConnectionUpdate struct { type ConnectionUpdate struct {
Server string Network string
Connected bool Connected bool
Error string Error string
ErrorType string ErrorType string
} }
func newConnectionUpdate(server string, state irc.ConnectionState) ConnectionUpdate { func newConnectionUpdate(network string, state irc.ConnectionState) ConnectionUpdate {
status := ConnectionUpdate{ status := ConnectionUpdate{
Server: server, Network: network,
Connected: state.Connected, Connected: state.Connected,
} }
if state.Error != nil { if state.Error != nil {
@ -62,23 +56,23 @@ func newConnectionUpdate(server string, state irc.ConnectionState) ConnectionUpd
} }
type Nick struct { type Nick struct {
Server string Network string
Old string `json:"oldNick,omitempty"` Old string `json:"oldNick,omitempty"`
New string `json:"newNick,omitempty"` New string `json:"newNick,omitempty"`
} }
type NickFail struct { type NickFail struct {
Server string Network string
} }
type Join struct { type Join struct {
Server string Network string
User string User string
Channels []string Channels []string
} }
type Part struct { type Part struct {
Server string Network string
User string User string
Channel string Channel string
Channels []string Channels []string
@ -90,14 +84,14 @@ type Mode struct {
} }
type Quit struct { type Quit struct {
Server string Network string
User string User string
Reason string Reason string
} }
type Message struct { type Message struct {
ID string ID string
Server string Network string
From string From string
To string To string
Content string Content string
@ -105,7 +99,7 @@ type Message struct {
} }
type Messages struct { type Messages struct {
Server string Network string
To string To string
Messages []storage.Message Messages []storage.Message
Prepend bool Prepend bool
@ -113,39 +107,41 @@ type Messages struct {
} }
type Topic struct { type Topic struct {
Server string Network string
Channel string Channel string
Topic string Topic string
Nick string Nick string
} }
type Userlist struct { type Userlist struct {
Server string Network string
Channel string Channel string
Users []string Users []string
} }
type MOTD struct { type MOTD struct {
Server string Network string
Title string Title string
Content []string Content []string
} }
type Invite struct { type Invite struct {
Server string Network string
Channel string Channel string
User string User string
} }
type Kick struct { type Kick struct {
Server string Network string
Channel string Channel string
Sender string
User string User string
Reason string
} }
type Whois struct { type Whois struct {
Server string Network string
User string User string
} }
type WhoisReply struct { type WhoisReply struct {
@ -158,23 +154,23 @@ type WhoisReply struct {
} }
type Away struct { type Away struct {
Server string Network string
Message string Message string
} }
type Raw struct { type Raw struct {
Server string Network string
Message string Message string
} }
type SearchRequest struct { type SearchRequest struct {
Server string Network string
Channel string Channel string
Phrase string Phrase string
} }
type SearchResult struct { type SearchResult struct {
Server string Network string
Channel string Channel string
Results []storage.Message Results []storage.Message
} }
@ -185,26 +181,26 @@ type ClientCert struct {
} }
type FetchMessages struct { type FetchMessages struct {
Server string Network string
Channel string Channel string
Next string Next string
} }
type Error struct { type Error struct {
Server string Network string
Message string Message string
} }
type IRCError struct { type IRCError struct {
Server string Network string
Target string Target string
Message string Message string
} }
type ChannelSearch struct { type ChannelSearch struct {
Server string Network string
Q string Q string
Start int Start int
} }
type ChannelSearchResult struct { type ChannelSearchResult struct {
@ -213,13 +209,13 @@ type ChannelSearchResult struct {
} }
type ChannelForward struct { type ChannelForward struct {
Server string Network string
Old string Old string
New string New string
} }
type DCCSend struct { type DCCSend struct {
Server string Network string
From string From string
Filename string Filename string
URL string URL string

File diff suppressed because it is too large Load Diff

View File

@ -87,22 +87,23 @@ func (d *Dispatch) loadUser(user *storage.User) {
d.states.set(state) d.states.set(state)
go state.run() go state.run()
channels, err := user.GetChannels() networks, err := user.Networks()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
servers, err := user.GetServers() channels, err := user.Channels()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
for _, server := range servers { for _, network := range networks {
i := connectIRC(server, state, user.GetLastIP()) i := connectIRC(network, state, user.GetLastIP())
var joining []string var joining []string
for _, channel := range channels { for _, channel := range channels {
if channel.Server == server.Host { if channel.Network == network.Host {
network.AddChannel(network.NewChannel(channel.Name))
joining = append(joining, channel.Name) joining = append(joining, channel.Name)
} }
} }
@ -115,7 +116,7 @@ func (d *Dispatch) startHTTP() {
port := cfg.Port port := cfg.Port
if cfg.Dev { if cfg.Dev {
// The node dev server will proxy index page requests and // The node dev network will proxy index page requests and
// websocket connections to this port // websocket connections to this port
port = "1337" port = "1337"
} }
@ -190,7 +191,7 @@ func (d *Dispatch) ServeHTTP(w http.ResponseWriter, r *http.Request) {
filename := params[2] filename := params[2]
w.Header().Set("Content-Disposition", "attachment; filename="+filename) w.Header().Set("Content-Disposition", "attachment; filename="+filename)
if pack, ok := state.getPendingDCC(filename); ok { if pack, ok := state.pendingDCC(filename); ok {
state.deletePendingDCC(filename) state.deletePendingDCC(filename)
w.Header().Set("Content-Length", strconv.FormatUint(pack.Length, 10)) w.Header().Set("Content-Length", strconv.FormatUint(pack.Length, 10))

View File

@ -20,26 +20,23 @@ const (
type State struct { type State struct {
stateData stateData
irc map[string]*irc.Client networks map[string]*storage.Network
connectionState map[string]irc.ConnectionState
pendingDCCSends map[string]*irc.DCCSend pendingDCCSends map[string]*irc.DCCSend
ircLock sync.Mutex
ws map[string]*wsConn ws map[string]*wsConn
wsLock sync.Mutex
broadcast chan WSResponse broadcast chan WSResponse
srv *Dispatch srv *Dispatch
user *storage.User user *storage.User
expiration *time.Timer expiration *time.Timer
reset chan time.Duration reset chan time.Duration
lock sync.Mutex
} }
func NewState(user *storage.User, srv *Dispatch) *State { func NewState(user *storage.User, srv *Dispatch) *State {
return &State{ return &State{
stateData: stateData{m: map[string]interface{}{}}, stateData: stateData{m: map[string]interface{}{}},
irc: make(map[string]*irc.Client), networks: make(map[string]*storage.Network),
connectionState: make(map[string]irc.ConnectionState),
pendingDCCSends: make(map[string]*irc.DCCSend), pendingDCCSends: make(map[string]*irc.DCCSend),
ws: make(map[string]*wsConn), ws: make(map[string]*wsConn),
broadcast: make(chan WSResponse, 32), broadcast: make(chan WSResponse, 32),
@ -50,99 +47,84 @@ func NewState(user *storage.User, srv *Dispatch) *State {
} }
} }
func (s *State) getIRC(server string) (*irc.Client, bool) { func (s *State) network(host string) (*storage.Network, bool) {
s.ircLock.Lock() s.lock.Lock()
i, ok := s.irc[server] n, ok := s.networks[host]
s.ircLock.Unlock() s.lock.Unlock()
return i, ok return n, ok
} }
func (s *State) setIRC(server string, i *irc.Client) { func (s *State) client(host string) (*irc.Client, bool) {
s.ircLock.Lock() if network, ok := s.network(host); ok {
s.irc[server] = i return network.Client(), true
s.connectionState[server] = irc.ConnectionState{
Connected: false,
} }
s.ircLock.Unlock() return nil, false
}
func (s *State) setNetwork(host string, network *storage.Network) {
s.lock.Lock()
s.networks[host] = network
s.lock.Unlock()
s.reset <- 0 s.reset <- 0
} }
func (s *State) deleteIRC(server string) { func (s *State) deleteNetwork(host string) {
s.ircLock.Lock() s.lock.Lock()
delete(s.irc, server) delete(s.networks, host)
delete(s.connectionState, server) s.lock.Unlock()
s.ircLock.Unlock()
s.resetExpirationIfEmpty() s.resetExpirationIfEmpty()
} }
func (s *State) numIRC() int { func (s *State) numIRC() int {
s.ircLock.Lock() s.lock.Lock()
n := len(s.irc) n := len(s.networks)
s.ircLock.Unlock() s.lock.Unlock()
return n return n
} }
func (s *State) getConnectionStates() map[string]irc.ConnectionState { func (s *State) pendingDCC(filename string) (*irc.DCCSend, bool) {
s.ircLock.Lock() s.lock.Lock()
state := make(map[string]irc.ConnectionState, len(s.connectionState))
for k, v := range s.connectionState {
state[k] = v
}
s.ircLock.Unlock()
return state
}
func (s *State) setConnectionState(server string, state irc.ConnectionState) {
s.ircLock.Lock()
s.connectionState[server] = state
s.ircLock.Unlock()
}
func (s *State) getPendingDCC(filename string) (*irc.DCCSend, bool) {
s.ircLock.Lock()
pack, ok := s.pendingDCCSends[filename] pack, ok := s.pendingDCCSends[filename]
s.ircLock.Unlock() s.lock.Unlock()
return pack, ok return pack, ok
} }
func (s *State) setPendingDCC(filename string, pack *irc.DCCSend) { func (s *State) setPendingDCC(filename string, pack *irc.DCCSend) {
s.ircLock.Lock() s.lock.Lock()
s.pendingDCCSends[filename] = pack s.pendingDCCSends[filename] = pack
s.ircLock.Unlock() s.lock.Unlock()
} }
func (s *State) deletePendingDCC(filename string) { func (s *State) deletePendingDCC(filename string) {
s.ircLock.Lock() s.lock.Lock()
delete(s.pendingDCCSends, filename) delete(s.pendingDCCSends, filename)
s.ircLock.Unlock() s.lock.Unlock()
} }
func (s *State) setWS(addr string, w *wsConn) { func (s *State) setWS(addr string, w *wsConn) {
s.wsLock.Lock() s.lock.Lock()
s.ws[addr] = w s.ws[addr] = w
s.wsLock.Unlock() s.lock.Unlock()
s.reset <- 0 s.reset <- 0
} }
func (s *State) deleteWS(addr string) { func (s *State) deleteWS(addr string) {
s.wsLock.Lock() s.lock.Lock()
delete(s.ws, addr) delete(s.ws, addr)
s.wsLock.Unlock() s.lock.Unlock()
s.resetExpirationIfEmpty() s.resetExpirationIfEmpty()
} }
func (s *State) numWS() int { func (s *State) numWS() int {
s.ircLock.Lock() s.lock.Lock()
n := len(s.ws) n := len(s.ws)
s.ircLock.Unlock() s.lock.Unlock()
return n return n
} }
@ -151,11 +133,11 @@ func (s *State) sendJSON(t string, v interface{}) {
s.broadcast <- WSResponse{t, v} s.broadcast <- WSResponse{t, v}
} }
func (s *State) sendLastMessages(server, channel string, count int) { func (s *State) sendLastMessages(network, channel string, count int) {
messages, hasMore, err := s.user.GetLastMessages(server, channel, count) messages, hasMore, err := s.user.LastMessages(network, channel, count)
if err == nil && len(messages) > 0 { if err == nil && len(messages) > 0 {
res := Messages{ res := Messages{
Server: server, Network: network,
To: channel, To: channel,
Messages: messages, Messages: messages,
} }
@ -168,11 +150,11 @@ func (s *State) sendLastMessages(server, channel string, count int) {
} }
} }
func (s *State) sendMessages(server, channel string, count int, fromID string) { func (s *State) sendMessages(network, channel string, count int, fromID string) {
messages, hasMore, err := s.user.GetMessages(server, channel, count, fromID) messages, hasMore, err := s.user.Messages(network, channel, count, fromID)
if err == nil && len(messages) > 0 { if err == nil && len(messages) > 0 {
res := Messages{ res := Messages{
Server: server, Network: network,
To: channel, To: channel,
Messages: messages, Messages: messages,
Prepend: true, Prepend: true,
@ -193,27 +175,25 @@ func (s *State) resetExpirationIfEmpty() {
} }
func (s *State) kill() { func (s *State) kill() {
s.wsLock.Lock() s.lock.Lock()
for _, ws := range s.ws { for _, ws := range s.ws {
ws.conn.Close() ws.conn.Close()
} }
s.wsLock.Unlock() for _, network := range s.networks {
s.ircLock.Lock() network.Client().Quit()
for _, i := range s.irc {
i.Quit()
} }
s.ircLock.Unlock() s.lock.Unlock()
} }
func (s *State) run() { func (s *State) run() {
for { for {
select { select {
case res := <-s.broadcast: case res := <-s.broadcast:
s.wsLock.Lock() s.lock.Lock()
for _, ws := range s.ws { for _, ws := range s.ws {
ws.out <- res ws.out <- res
} }
s.wsLock.Unlock() s.lock.Unlock()
case <-s.expiration.C: case <-s.expiration.C:
s.srv.states.delete(s.user.ID) s.srv.states.delete(s.user.ID)
@ -283,7 +263,7 @@ func newStateStore(sessionStore storage.SessionStore) *stateStore {
sessionStore: sessionStore, sessionStore: sessionStore,
} }
sessions, err := sessionStore.GetSessions() sessions, err := sessionStore.Sessions()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -24,13 +24,6 @@ func newWSHandler(conn *websocket.Conn, state *State, r *http.Request) *wsHandle
addr: conn.RemoteAddr(), addr: conn.RemoteAddr(),
} }
if r.Header.Get("X-Forwarded-For") != "" {
ip := net.ParseIP(r.Header.Get("X-Forwarded-For"))
if ip != nil {
h.addr.(*net.TCPAddr).IP = ip
}
}
h.init(r) h.init(r)
h.initHandlers() h.initHandlers()
return h return h
@ -61,6 +54,13 @@ func (h *wsHandler) dispatchRequest(req WSRequest) {
} }
func (h *wsHandler) init(r *http.Request) { func (h *wsHandler) init(r *http.Request) {
if r.Header.Get("X-Forwarded-For") != "" {
ip := net.ParseIP(r.Header.Get("X-Forwarded-For"))
if ip != nil {
h.addr.(*net.TCPAddr).IP = ip
}
}
h.state.setWS(h.addr.String(), h.ws) h.state.setWS(h.addr.String(), h.ws)
h.state.user.SetLastIP(addrToIPBytes(h.addr)) h.state.user.SetLastIP(addrToIPBytes(h.addr))
if r.TLS != nil { if r.TLS != nil {
@ -74,58 +74,65 @@ func (h *wsHandler) init(r *http.Request) {
h.state.numIRC(), "IRC connections |", h.state.numIRC(), "IRC connections |",
h.state.numWS(), "WebSocket connections") h.state.numWS(), "WebSocket connections")
tab, err := tabFromRequest(r) go h.sendData(r)
}
channels, err := h.state.user.GetChannels() func (h *wsHandler) sendData(r *http.Request) {
tab, err := tabFromRequest(r)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
} }
for _, channel := range channels { h.state.lock.Lock()
if channel.Server == tab.Server && channel.Name == tab.Name { for _, network := range h.state.networks {
// Userlist and messages for this channel gets embedded in the index page for _, channel := range network.ChannelNames() {
continue if network.Host == tab.Network && channel == tab.Name {
// Userlist and messages for this channel gets embedded in the index page
continue
}
if users := network.Client().ChannelUsers(channel); len(users) > 0 {
h.state.sendJSON("users", Userlist{
Network: network.Host,
Channel: channel,
Users: users,
})
}
h.state.sendLastMessages(network.Host, channel, 50)
} }
if i, ok := h.state.getIRC(channel.Server); ok {
h.state.sendJSON("users", Userlist{
Server: channel.Server,
Channel: channel.Name,
Users: i.ChannelUsers(channel.Name),
})
}
h.state.sendLastMessages(channel.Server, channel.Name, 50)
} }
h.state.lock.Unlock()
openDMs, err := h.state.user.GetOpenDMs() openDMs, err := h.state.user.OpenDMs()
if err != nil { if err != nil {
log.Println(err) log.Println(err)
} }
for _, openDM := range openDMs { for _, openDM := range openDMs {
if openDM.Server == tab.Server && openDM.Name == tab.Name { if openDM.Network == tab.Network && openDM.Name == tab.Name {
continue continue
} }
h.state.sendLastMessages(openDM.Server, openDM.Name, 50) h.state.sendLastMessages(openDM.Network, openDM.Name, 50)
} }
} }
func (h *wsHandler) connect(b []byte) { func (h *wsHandler) connect(b []byte) {
var data Server var network storage.Network
data.UnmarshalJSON(b) network.UnmarshalJSON(b)
data.Host = strings.ToLower(data.Host) network.Host = strings.ToLower(network.Host)
if _, ok := h.state.getIRC(data.Host); !ok { if _, ok := h.state.network(network.Host); !ok {
log.Println(h.addr, "[IRC] Add server", data.Host) log.Println(h.addr, "[IRC] Add server", network.Host)
connectIRC(data.Server, h.state, addrToIPBytes(h.addr)) connectIRC(&network, h.state, addrToIPBytes(h.addr))
go h.state.user.AddServer(data.Server) go network.Save()
} else { } else {
log.Println(h.addr, "[IRC]", data.Host, "already added") log.Println(h.addr, "[IRC]", network.Host, "already added")
} }
} }
@ -133,7 +140,7 @@ func (h *wsHandler) reconnect(b []byte) {
var data ReconnectSettings var data ReconnectSettings
data.UnmarshalJSON(b) data.UnmarshalJSON(b)
if i, ok := h.state.getIRC(data.Server); ok && !i.Connected() { if i, ok := h.state.client(data.Network); ok && !i.Connected() {
if i.Config.TLS { if i.Config.TLS {
i.Config.TLSConfig.InsecureSkipVerify = data.SkipVerify i.Config.TLSConfig.InsecureSkipVerify = data.SkipVerify
} }
@ -145,7 +152,7 @@ func (h *wsHandler) join(b []byte) {
var data Join var data Join
data.UnmarshalJSON(b) data.UnmarshalJSON(b)
if i, ok := h.state.getIRC(data.Server); ok { if i, ok := h.state.client(data.Network); ok {
i.Join(data.Channels...) i.Join(data.Channels...)
} }
} }
@ -154,33 +161,43 @@ func (h *wsHandler) part(b []byte) {
var data Part var data Part
data.UnmarshalJSON(b) data.UnmarshalJSON(b)
if i, ok := h.state.getIRC(data.Server); ok { if i, ok := h.state.client(data.Network); ok {
i.Part(data.Channels...) i.Part(data.Channels...)
} }
go func() {
if network, ok := h.state.network(data.Network); ok {
network.RemoveChannels(data.Channels...)
}
for _, channel := range data.Channels {
h.state.user.RemoveChannel(data.Network, channel)
}
}()
} }
func (h *wsHandler) quit(b []byte) { func (h *wsHandler) quit(b []byte) {
var data Quit var data Quit
data.UnmarshalJSON(b) data.UnmarshalJSON(b)
log.Println(h.addr, "[IRC] Remove server", data.Server) log.Println(h.addr, "[IRC] Remove server", data.Network)
if i, ok := h.state.getIRC(data.Server); ok { if i, ok := h.state.client(data.Network); ok {
h.state.deleteIRC(data.Server) h.state.deleteNetwork(data.Network)
i.Quit() i.Quit()
} }
go h.state.user.RemoveServer(data.Server) go h.state.user.RemoveNetwork(data.Network)
} }
func (h *wsHandler) message(b []byte) { func (h *wsHandler) message(b []byte) {
var data Message var data Message
data.UnmarshalJSON(b) data.UnmarshalJSON(b)
if i, ok := h.state.getIRC(data.Server); ok { if i, ok := h.state.client(data.Network); ok {
i.Privmsg(data.To, data.Content) i.Privmsg(data.To, data.Content)
go h.state.user.LogMessage(&storage.Message{ go h.state.user.LogMessage(&storage.Message{
Server: data.Server, Network: data.Network,
From: i.GetNick(), From: i.GetNick(),
To: data.To, To: data.To,
Content: data.Content, Content: data.Content,
@ -192,7 +209,7 @@ func (h *wsHandler) nick(b []byte) {
var data Nick var data Nick
data.UnmarshalJSON(b) data.UnmarshalJSON(b)
if i, ok := h.state.getIRC(data.Server); ok { if i, ok := h.state.client(data.Network); ok {
i.Nick(data.New) i.Nick(data.New)
} }
} }
@ -201,7 +218,7 @@ func (h *wsHandler) topic(b []byte) {
var data Topic var data Topic
data.UnmarshalJSON(b) data.UnmarshalJSON(b)
if i, ok := h.state.getIRC(data.Server); ok { if i, ok := h.state.client(data.Network); ok {
i.Topic(data.Channel, data.Topic) i.Topic(data.Channel, data.Topic)
} }
} }
@ -210,7 +227,7 @@ func (h *wsHandler) invite(b []byte) {
var data Invite var data Invite
data.UnmarshalJSON(b) data.UnmarshalJSON(b)
if i, ok := h.state.getIRC(data.Server); ok { if i, ok := h.state.client(data.Network); ok {
i.Invite(data.User, data.Channel) i.Invite(data.User, data.Channel)
} }
} }
@ -219,7 +236,7 @@ func (h *wsHandler) kick(b []byte) {
var data Invite var data Invite
data.UnmarshalJSON(b) data.UnmarshalJSON(b)
if i, ok := h.state.getIRC(data.Server); ok { if i, ok := h.state.client(data.Network); ok {
i.Kick(data.Channel, data.User) i.Kick(data.Channel, data.User)
} }
} }
@ -228,7 +245,7 @@ func (h *wsHandler) whois(b []byte) {
var data Whois var data Whois
data.UnmarshalJSON(b) data.UnmarshalJSON(b)
if i, ok := h.state.getIRC(data.Server); ok { if i, ok := h.state.client(data.Network); ok {
i.Whois(data.User) i.Whois(data.User)
} }
} }
@ -237,7 +254,7 @@ func (h *wsHandler) away(b []byte) {
var data Away var data Away
data.UnmarshalJSON(b) data.UnmarshalJSON(b)
if i, ok := h.state.getIRC(data.Server); ok { if i, ok := h.state.client(data.Network); ok {
i.Away(data.Message) i.Away(data.Message)
} }
} }
@ -246,7 +263,7 @@ func (h *wsHandler) raw(b []byte) {
var data Raw var data Raw
data.UnmarshalJSON(b) data.UnmarshalJSON(b)
if i, ok := h.state.getIRC(data.Server); ok { if i, ok := h.state.client(data.Network); ok {
i.Write(data.Message) i.Write(data.Message)
} }
} }
@ -256,14 +273,14 @@ func (h *wsHandler) search(b []byte) {
var data SearchRequest var data SearchRequest
data.UnmarshalJSON(b) data.UnmarshalJSON(b)
results, err := h.state.user.SearchMessages(data.Server, data.Channel, data.Phrase) results, err := h.state.user.SearchMessages(data.Network, data.Channel, data.Phrase)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return return
} }
h.state.sendJSON("search", SearchResult{ h.state.sendJSON("search", SearchResult{
Server: data.Server, Network: data.Network,
Channel: data.Channel, Channel: data.Channel,
Results: results, Results: results,
}) })
@ -287,15 +304,18 @@ func (h *wsHandler) fetchMessages(b []byte) {
var data FetchMessages var data FetchMessages
data.UnmarshalJSON(b) data.UnmarshalJSON(b)
h.state.sendMessages(data.Server, data.Channel, 200, data.Next) h.state.sendMessages(data.Network, data.Channel, 200, data.Next)
} }
func (h *wsHandler) setServerName(b []byte) { func (h *wsHandler) setNetworkName(b []byte) {
var data ServerName var data NetworkName
data.UnmarshalJSON(b) data.UnmarshalJSON(b)
if isValidServerName(data.Name) { if isValidNetworkName(data.Name) {
h.state.user.SetServerName(data.Name, data.Server) if network, ok := h.state.network(data.Network); ok {
network.SetName(data.Name)
go network.Save()
}
} }
} }
@ -310,7 +330,7 @@ func (h *wsHandler) channelSearch(b []byte) {
var data ChannelSearch var data ChannelSearch
data.UnmarshalJSON(b) data.UnmarshalJSON(b)
index, needsUpdate := channelIndexes.Get(data.Server) index, needsUpdate := channelIndexes.Get(data.Network)
if index != nil { if index != nil {
n := 10 n := 10
if data.Start > 0 { if data.Start > 0 {
@ -323,8 +343,8 @@ func (h *wsHandler) channelSearch(b []byte) {
}) })
} }
if i, ok := h.state.getIRC(data.Server); ok && needsUpdate { if i, ok := h.state.client(data.Network); ok && needsUpdate {
h.state.Set("update_chanlist_"+data.Server, true) h.state.Set("update_chanlist_"+data.Network, true)
i.List() i.List()
} }
} }
@ -333,43 +353,43 @@ func (h *wsHandler) openDM(b []byte) {
var data Tab var data Tab
data.UnmarshalJSON(b) data.UnmarshalJSON(b)
h.state.sendLastMessages(data.Server, data.Name, 50) h.state.sendLastMessages(data.Network, data.Name, 50)
h.state.user.AddOpenDM(data.Server, data.Name) h.state.user.AddOpenDM(data.Network, data.Name)
} }
func (h *wsHandler) closeDM(b []byte) { func (h *wsHandler) closeDM(b []byte) {
var data Tab var data Tab
data.UnmarshalJSON(b) data.UnmarshalJSON(b)
h.state.user.RemoveOpenDM(data.Server, data.Name) h.state.user.RemoveOpenDM(data.Network, data.Name)
} }
func (h *wsHandler) initHandlers() { func (h *wsHandler) initHandlers() {
h.handlers = map[string]func([]byte){ h.handlers = map[string]func([]byte){
"connect": h.connect, "connect": h.connect,
"reconnect": h.reconnect, "reconnect": h.reconnect,
"join": h.join, "join": h.join,
"part": h.part, "part": h.part,
"quit": h.quit, "quit": h.quit,
"message": h.message, "message": h.message,
"nick": h.nick, "nick": h.nick,
"topic": h.topic, "topic": h.topic,
"invite": h.invite, "invite": h.invite,
"kick": h.kick, "kick": h.kick,
"whois": h.whois, "whois": h.whois,
"away": h.away, "away": h.away,
"raw": h.raw, "raw": h.raw,
"search": h.search, "search": h.search,
"cert": h.cert, "cert": h.cert,
"fetch_messages": h.fetchMessages, "fetch_messages": h.fetchMessages,
"set_server_name": h.setServerName, "set_network_name": h.setNetworkName,
"settings_set": h.setSettings, "settings_set": h.setSettings,
"channel_search": h.channelSearch, "channel_search": h.channelSearch,
"open_dm": h.openDM, "open_dm": h.openDM,
"close_dm": h.closeDM, "close_dm": h.closeDM,
} }
} }
func isValidServerName(name string) bool { func isValidNetworkName(name string) bool {
return strings.TrimSpace(name) != "" return strings.TrimSpace(name) != ""
} }

View File

@ -48,8 +48,8 @@ func (b *Bleve) Index(id string, message *storage.Message) error {
return b.index.Index(id, message) return b.index.Index(id, message)
} }
func (b *Bleve) SearchMessages(server, channel, q string) ([]string, error) { func (b *Bleve) SearchMessages(network, channel, q string) ([]string, error) {
serverQuery := bleve.NewMatchQuery(server) serverQuery := bleve.NewMatchQuery(network)
serverQuery.SetField("server") serverQuery.SetField("server")
channelQuery := bleve.NewMatchQuery(channel) channelQuery := bleve.NewMatchQuery(channel)
channelQuery.SetField("to") channelQuery.SetField("to")

View File

@ -13,7 +13,7 @@ import (
var ( var (
bucketUsers = []byte("Users") bucketUsers = []byte("Users")
bucketServers = []byte("Servers") bucketNetworks = []byte("Networks")
bucketChannels = []byte("Channels") bucketChannels = []byte("Channels")
bucketOpenDMs = []byte("OpenDMs") bucketOpenDMs = []byte("OpenDMs")
bucketMessages = []byte("Messages") bucketMessages = []byte("Messages")
@ -33,7 +33,7 @@ func New(path string) (*BoltStore, error) {
db.Update(func(tx *bolt.Tx) error { db.Update(func(tx *bolt.Tx) error {
tx.CreateBucketIfNotExists(bucketUsers) tx.CreateBucketIfNotExists(bucketUsers)
tx.CreateBucketIfNotExists(bucketServers) tx.CreateBucketIfNotExists(bucketNetworks)
tx.CreateBucketIfNotExists(bucketChannels) tx.CreateBucketIfNotExists(bucketChannels)
tx.CreateBucketIfNotExists(bucketOpenDMs) tx.CreateBucketIfNotExists(bucketOpenDMs)
tx.CreateBucketIfNotExists(bucketMessages) tx.CreateBucketIfNotExists(bucketMessages)
@ -50,7 +50,7 @@ func (s *BoltStore) Close() {
s.db.Close() s.db.Close()
} }
func (s *BoltStore) GetUsers() ([]*storage.User, error) { func (s *BoltStore) Users() ([]*storage.User, error) {
var users []*storage.User var users []*storage.User
s.db.View(func(tx *bolt.Tx) error { s.db.View(func(tx *bolt.Tx) error {
@ -99,69 +99,69 @@ func (s *BoltStore) DeleteUser(user *storage.User) error {
} }
return deletePrefix(user.IDBytes, return deletePrefix(user.IDBytes,
tx.Bucket(bucketServers), tx.Bucket(bucketNetworks),
tx.Bucket(bucketChannels), tx.Bucket(bucketChannels),
tx.Bucket(bucketOpenDMs), tx.Bucket(bucketOpenDMs),
) )
}) })
} }
func (s *BoltStore) GetServer(user *storage.User, address string) (*storage.Server, error) { func (s *BoltStore) Network(user *storage.User, address string) (*storage.Network, error) {
var server *storage.Server var network *storage.Network
err := s.db.View(func(tx *bolt.Tx) error { err := s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketServers) b := tx.Bucket(bucketNetworks)
id := serverID(user, address) id := networkID(user, address)
v := b.Get(id) v := b.Get(id)
if v == nil { if v == nil {
return storage.ErrNotFound return storage.ErrNotFound
} else { } else {
server = &storage.Server{} network = &storage.Network{}
server.Unmarshal(v) network.Unmarshal(v)
return nil return nil
} }
}) })
return server, err return network, err
} }
func (s *BoltStore) GetServers(user *storage.User) ([]*storage.Server, error) { func (s *BoltStore) Networks(user *storage.User) ([]*storage.Network, error) {
var servers []*storage.Server var networks []*storage.Network
s.db.View(func(tx *bolt.Tx) error { s.db.View(func(tx *bolt.Tx) error {
c := tx.Bucket(bucketServers).Cursor() c := tx.Bucket(bucketNetworks).Cursor()
for k, v := c.Seek(user.IDBytes); bytes.HasPrefix(k, user.IDBytes); k, v = c.Next() { for k, v := c.Seek(user.IDBytes); bytes.HasPrefix(k, user.IDBytes); k, v = c.Next() {
server := storage.Server{} network := storage.Network{}
server.Unmarshal(v) network.Unmarshal(v)
servers = append(servers, &server) networks = append(networks, &network)
} }
return nil return nil
}) })
return servers, nil return networks, nil
} }
func (s *BoltStore) SaveServer(user *storage.User, server *storage.Server) error { func (s *BoltStore) SaveNetwork(user *storage.User, network *storage.Network) error {
return s.db.Batch(func(tx *bolt.Tx) error { return s.db.Batch(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketServers) b := tx.Bucket(bucketNetworks)
data, _ := server.Marshal(nil) data, _ := network.Marshal(nil)
return b.Put(serverID(user, server.Host), data) return b.Put(networkID(user, network.Host), data)
}) })
} }
func (s *BoltStore) RemoveServer(user *storage.User, address string) error { func (s *BoltStore) RemoveNetwork(user *storage.User, address string) error {
return s.db.Batch(func(tx *bolt.Tx) error { return s.db.Batch(func(tx *bolt.Tx) error {
serverID := serverID(user, address) networkID := networkID(user, address)
err := tx.Bucket(bucketServers).Delete(serverID) err := tx.Bucket(bucketNetworks).Delete(networkID)
if err != nil { if err != nil {
return err return err
} }
return deletePrefix(serverID, return deletePrefix(networkID,
tx.Bucket(bucketChannels), tx.Bucket(bucketChannels),
tx.Bucket(bucketOpenDMs), tx.Bucket(bucketOpenDMs),
) )
@ -170,16 +170,16 @@ func (s *BoltStore) RemoveServer(user *storage.User, address string) error {
func (s *BoltStore) SetNick(user *storage.User, nick, address string) error { func (s *BoltStore) SetNick(user *storage.User, nick, address string) error {
return s.db.Batch(func(tx *bolt.Tx) error { return s.db.Batch(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketServers) b := tx.Bucket(bucketNetworks)
id := serverID(user, address) id := networkID(user, address)
server := storage.Server{} network := storage.Network{}
v := b.Get(id) v := b.Get(id)
if v != nil { if v != nil {
server.Unmarshal(v) network.Unmarshal(v)
server.Nick = nick network.Nick = nick
data, _ := server.Marshal(nil) data, _ := network.Marshal(nil)
return b.Put(id, data) return b.Put(id, data)
} }
@ -187,18 +187,18 @@ func (s *BoltStore) SetNick(user *storage.User, nick, address string) error {
}) })
} }
func (s *BoltStore) SetServerName(user *storage.User, name, address string) error { func (s *BoltStore) SetNetworkName(user *storage.User, name, address string) error {
return s.db.Batch(func(tx *bolt.Tx) error { return s.db.Batch(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketServers) b := tx.Bucket(bucketNetworks)
id := serverID(user, address) id := networkID(user, address)
server := storage.Server{} network := storage.Network{}
v := b.Get(id) v := b.Get(id)
if v != nil { if v != nil {
server.Unmarshal(v) network.Unmarshal(v)
server.Name = name network.Name = name
data, _ := server.Marshal(nil) data, _ := network.Marshal(nil)
return b.Put(id, data) return b.Put(id, data)
} }
@ -206,7 +206,7 @@ func (s *BoltStore) SetServerName(user *storage.User, name, address string) erro
}) })
} }
func (s *BoltStore) GetChannels(user *storage.User) ([]*storage.Channel, error) { func (s *BoltStore) Channels(user *storage.User) ([]*storage.Channel, error) {
var channels []*storage.Channel var channels []*storage.Channel
s.db.View(func(tx *bolt.Tx) error { s.db.View(func(tx *bolt.Tx) error {
@ -224,25 +224,36 @@ func (s *BoltStore) GetChannels(user *storage.User) ([]*storage.Channel, error)
return channels, nil return channels, nil
} }
func (s *BoltStore) AddChannel(user *storage.User, channel *storage.Channel) error { func (s *BoltStore) SaveChannel(user *storage.User, channel *storage.Channel) error {
return s.db.Batch(func(tx *bolt.Tx) error { return s.db.Batch(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketChannels) b := tx.Bucket(bucketChannels)
data, _ := channel.Marshal(nil) data, _ := channel.Marshal(nil)
return b.Put(channelID(user, channel.Server, channel.Name), data) return b.Put(channelID(user, channel.Network, channel.Name), data)
}) })
} }
func (s *BoltStore) RemoveChannel(user *storage.User, server, channel string) error { func (s *BoltStore) RemoveChannel(user *storage.User, network, channel string) error {
return s.db.Batch(func(tx *bolt.Tx) error { return s.db.Batch(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketChannels) b := tx.Bucket(bucketChannels)
id := channelID(user, server, channel) id := channelID(user, network, channel)
return b.Delete(id) return b.Delete(id)
}) })
} }
func (s *BoltStore) GetOpenDMs(user *storage.User) ([]storage.Tab, error) { func (s *BoltStore) HasChannel(user *storage.User, network, channel string) bool {
has := false
s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketChannels)
has = b.Get(channelID(user, network, channel)) != nil
return nil
})
return has
}
func (s *BoltStore) OpenDMs(user *storage.User) ([]storage.Tab, error) {
var openDMs []storage.Tab var openDMs []storage.Tab
s.db.View(func(tx *bolt.Tx) error { s.db.View(func(tx *bolt.Tx) error {
@ -251,8 +262,8 @@ func (s *BoltStore) GetOpenDMs(user *storage.User) ([]storage.Tab, error) {
for k, _ := c.Seek(user.IDBytes); bytes.HasPrefix(k, user.IDBytes); k, _ = c.Next() { for k, _ := c.Seek(user.IDBytes); bytes.HasPrefix(k, user.IDBytes); k, _ = c.Next() {
tab := bytes.Split(k[8:], []byte{0}) tab := bytes.Split(k[8:], []byte{0})
openDMs = append(openDMs, storage.Tab{ openDMs = append(openDMs, storage.Tab{
Server: string(tab[0]), Network: string(tab[0]),
Name: string(tab[1]), Name: string(tab[1]),
}) })
} }
@ -262,24 +273,24 @@ func (s *BoltStore) GetOpenDMs(user *storage.User) ([]storage.Tab, error) {
return openDMs, nil return openDMs, nil
} }
func (s *BoltStore) AddOpenDM(user *storage.User, server, nick string) error { func (s *BoltStore) AddOpenDM(user *storage.User, network, nick string) error {
return s.db.Batch(func(tx *bolt.Tx) error { return s.db.Batch(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketOpenDMs) b := tx.Bucket(bucketOpenDMs)
return b.Put(channelID(user, server, nick), nil) return b.Put(channelID(user, network, nick), nil)
}) })
} }
func (s *BoltStore) RemoveOpenDM(user *storage.User, server, nick string) error { func (s *BoltStore) RemoveOpenDM(user *storage.User, network, nick string) error {
return s.db.Batch(func(tx *bolt.Tx) error { return s.db.Batch(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketOpenDMs) b := tx.Bucket(bucketOpenDMs)
return b.Delete(channelID(user, server, nick)) return b.Delete(channelID(user, network, nick))
}) })
} }
func (s *BoltStore) logMessage(tx *bolt.Tx, message *storage.Message) error { func (s *BoltStore) logMessage(tx *bolt.Tx, message *storage.Message) error {
b, err := tx.Bucket(bucketMessages).CreateBucketIfNotExists([]byte(message.Server + ":" + message.To)) b, err := tx.Bucket(bucketMessages).CreateBucketIfNotExists([]byte(message.Network + ":" + message.To))
if err != nil { if err != nil {
return err return err
} }
@ -311,12 +322,12 @@ func (s *BoltStore) LogMessages(messages []*storage.Message) error {
}) })
} }
func (s *BoltStore) GetMessages(server, channel string, count int, fromID string) ([]storage.Message, bool, error) { func (s *BoltStore) Messages(network, channel string, count int, fromID string) ([]storage.Message, bool, error) {
messages := make([]storage.Message, count) messages := make([]storage.Message, count)
hasMore := false hasMore := false
s.db.View(func(tx *bolt.Tx) error { s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketMessages).Bucket([]byte(server + ":" + channel)) b := tx.Bucket(bucketMessages).Bucket([]byte(network + ":" + channel))
if b == nil { if b == nil {
return nil return nil
} }
@ -353,11 +364,11 @@ func (s *BoltStore) GetMessages(server, channel string, count int, fromID string
return nil, false, nil return nil, false, nil
} }
func (s *BoltStore) GetMessagesByID(server, channel string, ids []string) ([]storage.Message, error) { func (s *BoltStore) MessagesByID(network, channel string, ids []string) ([]storage.Message, error) {
messages := make([]storage.Message, len(ids)) messages := make([]storage.Message, len(ids))
err := s.db.View(func(tx *bolt.Tx) error { err := s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketMessages).Bucket([]byte(server + ":" + channel)) b := tx.Bucket(bucketMessages).Bucket([]byte(network + ":" + channel))
for i, id := range ids { for i, id := range ids {
messages[i].Unmarshal(b.Get([]byte(id))) messages[i].Unmarshal(b.Get([]byte(id)))
@ -367,7 +378,7 @@ func (s *BoltStore) GetMessagesByID(server, channel string, ids []string) ([]sto
return messages, err return messages, err
} }
func (s *BoltStore) GetSessions() ([]*session.Session, error) { func (s *BoltStore) Sessions() ([]*session.Session, error) {
var sessions []*session.Session var sessions []*session.Session
err := s.db.View(func(tx *bolt.Tx) error { err := s.db.View(func(tx *bolt.Tx) error {
@ -422,18 +433,18 @@ func deletePrefix(prefix []byte, buckets ...*bolt.Bucket) error {
return nil return nil
} }
func serverID(user *storage.User, address string) []byte { func networkID(user *storage.User, address string) []byte {
id := make([]byte, 8+len(address)) id := make([]byte, 8+len(address))
copy(id, user.IDBytes) copy(id, user.IDBytes)
copy(id[8:], address) copy(id[8:], address)
return id return id
} }
func channelID(user *storage.User, server, channel string) []byte { func channelID(user *storage.User, network, channel string) []byte {
id := make([]byte, 8+len(server)+1+len(channel)) id := make([]byte, 8+len(network)+1+len(channel))
copy(id, user.IDBytes) copy(id, user.IDBytes)
copy(id[8:], server) copy(id[8:], network)
copy(id[8+len(server)+1:], channel) copy(id[8+len(network)+1:], channel)
return id return id
} }

View File

@ -124,32 +124,32 @@ type managedChannelIndex struct {
updating bool updating bool
} }
func (m *ChannelIndexManager) Get(server string) (ChannelListIndex, bool) { func (m *ChannelIndexManager) Get(network string) (ChannelListIndex, bool) {
m.lock.Lock() m.lock.Lock()
defer m.lock.Unlock() defer m.lock.Unlock()
idx, ok := m.indexes[server] idx, ok := m.indexes[network]
if !ok { if !ok {
m.indexes[server] = &managedChannelIndex{ m.indexes[network] = &managedChannelIndex{
updating: true, updating: true,
} }
go m.timeoutUpdate(server) go m.timeoutUpdate(network)
return nil, true return nil, true
} }
if !idx.updating && time.Since(idx.updatedAt) > ChannelListUpdateInterval { if !idx.updating && time.Since(idx.updatedAt) > ChannelListUpdateInterval {
idx.updating = true idx.updating = true
go m.timeoutUpdate(server) go m.timeoutUpdate(network)
return idx.index, true return idx.index, true
} }
return idx.index, false return idx.index, false
} }
func (m *ChannelIndexManager) Set(server string, index ChannelListIndex) { func (m *ChannelIndexManager) Set(network string, index ChannelListIndex) {
if index.len() > 0 { if index.len() > 0 {
m.lock.Lock() m.lock.Lock()
m.indexes[server] = &managedChannelIndex{ m.indexes[network] = &managedChannelIndex{
index: index, index: index,
updatedAt: time.Now(), updatedAt: time.Now(),
} }
@ -157,12 +157,12 @@ func (m *ChannelIndexManager) Set(server string, index ChannelListIndex) {
} }
} }
func (m *ChannelIndexManager) timeoutUpdate(server string) { func (m *ChannelIndexManager) timeoutUpdate(network string) {
time.Sleep(ChannelListUpdateTimeout) time.Sleep(ChannelListUpdateTimeout)
m.lock.Lock() m.lock.Lock()
if m.indexes[server].updating { if m.indexes[network].updating {
m.indexes[server].updating = false m.indexes[network].updating = false
} }
m.lock.Unlock() m.lock.Unlock()
} }

216
storage/network.go Normal file
View File

@ -0,0 +1,216 @@
package storage
import (
"fmt"
"sync"
"github.com/khlieng/dispatch/pkg/irc"
"github.com/khlieng/dispatch/version"
)
type Network struct {
Name string
Host string
Port string
TLS bool
ServerPassword string
Nick string
Username string
Realname string
Account string
Password string
Features map[string]interface{}
Connected bool
Error string
user *User
client *irc.Client
channels map[string]*Channel
lock sync.Mutex
}
func (n *Network) Save() error {
return n.user.SaveNetwork(n.Copy())
}
func (n *Network) Copy() *Network {
n.lock.Lock()
network := Network{
Name: n.Name,
Host: n.Host,
Port: n.Port,
TLS: n.TLS,
ServerPassword: n.ServerPassword,
Nick: n.Nick,
Username: n.Username,
Realname: n.Realname,
Account: n.Account,
Password: n.Password,
Features: n.Features,
Connected: n.Connected,
Error: n.Error,
user: n.user,
client: n.client,
channels: n.channels,
}
n.lock.Unlock()
return &network
}
func (n *Network) Client() *irc.Client {
return n.client
}
func (n *Network) IRCConfig() *irc.Config {
return &irc.Config{
Host: n.Host,
Port: n.Port,
TLS: n.TLS,
Nick: n.Nick,
Username: n.Username,
Realname: n.Realname,
Account: n.Account,
Password: n.Password,
Version: fmt.Sprintf("Dispatch %s (git: %s)", version.Tag, version.Commit),
Source: "https://github.com/khlieng/dispatch",
}
}
func (n *Network) SetName(name string) {
n.lock.Lock()
n.Name = name
n.lock.Unlock()
}
func (n *Network) SetNick(nick string) {
n.lock.Lock()
n.Nick = nick
n.lock.Unlock()
}
func (n *Network) SetFeatures(features map[string]interface{}) {
n.lock.Lock()
n.Features = features
n.lock.Unlock()
}
func (n *Network) SetStatus(connected bool, err string) {
n.lock.Lock()
n.Connected = connected
n.Error = err
n.lock.Unlock()
}
func (n *Network) Channel(name string) *Channel {
n.lock.Lock()
ch := n.channels[name]
n.lock.Unlock()
return ch
}
func (n *Network) Channels() []*Channel {
n.lock.Lock()
channels := make([]*Channel, 0, len(n.channels))
for _, ch := range n.channels {
channels = append(channels, ch.Copy())
}
n.lock.Unlock()
return channels
}
func (n *Network) ChannelNames() []string {
n.lock.Lock()
names := make([]string, 0, len(n.channels))
for _, ch := range n.channels {
names = append(names, ch.Name)
}
n.lock.Unlock()
return names
}
func (n *Network) NewChannel(name string) *Channel {
return &Channel{
Network: n.Host,
Name: name,
user: n.user,
}
}
func (n *Network) AddChannel(channel *Channel) {
n.lock.Lock()
n.channels[channel.Name] = channel
n.lock.Unlock()
}
func (n *Network) RemoveChannels(channels ...string) {
n.lock.Lock()
for _, name := range channels {
delete(n.channels, name)
}
n.lock.Unlock()
}
type Channel struct {
Network string
Name string
Topic string
Joined bool
user *User
lock sync.Mutex
}
func (c *Channel) Save() error {
return c.user.SaveChannel(c.Copy())
}
func (c *Channel) Copy() *Channel {
c.lock.Lock()
ch := Channel{
Network: c.Network,
Name: c.Name,
Topic: c.Topic,
Joined: c.Joined,
user: c.user,
}
c.lock.Unlock()
return &ch
}
func (c *Channel) SetTopic(topic string) {
if c == nil {
return
}
c.lock.Lock()
c.Topic = topic
c.lock.Unlock()
}
func (c *Channel) IsJoined() bool {
if c == nil {
return false
}
c.lock.Lock()
joined := c.Joined
c.lock.Unlock()
return joined
}
func (c *Channel) SetJoined(joined bool) {
if c == nil {
return
}
c.lock.Lock()
c.Joined = joined
c.lock.Unlock()
}

377
storage/network_easyjson.go Normal file
View File

@ -0,0 +1,377 @@
//v1: false// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.
package storage
import (
json "encoding/json"
easyjson "github.com/mailru/easyjson"
jlexer "github.com/mailru/easyjson/jlexer"
jwriter "github.com/mailru/easyjson/jwriter"
)
// suppress unused package warning
var (
_ *json.RawMessage
_ *jlexer.Lexer
_ *jwriter.Writer
_ easyjson.Marshaler
)
func easyjsonC5839400DecodeGithubComKhliengDispatchStorage(in *jlexer.Lexer, out *Network) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeFieldName(false)
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "name":
out.Name = string(in.String())
case "host":
out.Host = string(in.String())
case "port":
out.Port = string(in.String())
case "tls":
out.TLS = bool(in.Bool())
case "serverPassword":
out.ServerPassword = string(in.String())
case "nick":
out.Nick = string(in.String())
case "username":
out.Username = string(in.String())
case "realname":
out.Realname = string(in.String())
case "account":
out.Account = string(in.String())
case "password":
out.Password = string(in.String())
case "features":
if in.IsNull() {
in.Skip()
} else {
in.Delim('{')
if !in.IsDelim('}') {
out.Features = make(map[string]interface{})
} else {
out.Features = nil
}
for !in.IsDelim('}') {
key := string(in.String())
in.WantColon()
var v1 interface{}
if m, ok := v1.(easyjson.Unmarshaler); ok {
m.UnmarshalEasyJSON(in)
} else if m, ok := v1.(json.Unmarshaler); ok {
_ = m.UnmarshalJSON(in.Raw())
} else {
v1 = in.Interface()
}
(out.Features)[key] = v1
in.WantComma()
}
in.Delim('}')
}
case "connected":
out.Connected = bool(in.Bool())
case "error":
out.Error = string(in.String())
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjsonC5839400EncodeGithubComKhliengDispatchStorage(out *jwriter.Writer, in Network) {
out.RawByte('{')
first := true
_ = first
if in.Name != "" {
const prefix string = ",\"name\":"
first = false
out.RawString(prefix[1:])
out.String(string(in.Name))
}
if in.Host != "" {
const prefix string = ",\"host\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.Host))
}
if in.Port != "" {
const prefix string = ",\"port\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.Port))
}
if in.TLS {
const prefix string = ",\"tls\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.Bool(bool(in.TLS))
}
if in.ServerPassword != "" {
const prefix string = ",\"serverPassword\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.ServerPassword))
}
if in.Nick != "" {
const prefix string = ",\"nick\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.Nick))
}
if in.Username != "" {
const prefix string = ",\"username\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.Username))
}
if in.Realname != "" {
const prefix string = ",\"realname\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.Realname))
}
if in.Account != "" {
const prefix string = ",\"account\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.Account))
}
if in.Password != "" {
const prefix string = ",\"password\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.Password))
}
if len(in.Features) != 0 {
const prefix string = ",\"features\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
{
out.RawByte('{')
v2First := true
for v2Name, v2Value := range in.Features {
if v2First {
v2First = false
} else {
out.RawByte(',')
}
out.String(string(v2Name))
out.RawByte(':')
if m, ok := v2Value.(easyjson.Marshaler); ok {
m.MarshalEasyJSON(out)
} else if m, ok := v2Value.(json.Marshaler); ok {
out.Raw(m.MarshalJSON())
} else {
out.Raw(json.Marshal(v2Value))
}
}
out.RawByte('}')
}
}
if in.Connected {
const prefix string = ",\"connected\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.Bool(bool(in.Connected))
}
if in.Error != "" {
const prefix string = ",\"error\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.Error))
}
out.RawByte('}')
}
// MarshalJSON supports json.Marshaler interface
func (v Network) MarshalJSON() ([]byte, error) {
w := jwriter.Writer{}
easyjsonC5839400EncodeGithubComKhliengDispatchStorage(&w, v)
return w.Buffer.BuildBytes(), w.Error
}
// MarshalEasyJSON supports easyjson.Marshaler interface
func (v Network) MarshalEasyJSON(w *jwriter.Writer) {
easyjsonC5839400EncodeGithubComKhliengDispatchStorage(w, v)
}
// UnmarshalJSON supports json.Unmarshaler interface
func (v *Network) UnmarshalJSON(data []byte) error {
r := jlexer.Lexer{Data: data}
easyjsonC5839400DecodeGithubComKhliengDispatchStorage(&r, v)
return r.Error()
}
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *Network) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjsonC5839400DecodeGithubComKhliengDispatchStorage(l, v)
}
func easyjsonC5839400DecodeGithubComKhliengDispatchStorage1(in *jlexer.Lexer, out *Channel) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeFieldName(false)
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "network":
out.Network = string(in.String())
case "name":
out.Name = string(in.String())
case "topic":
out.Topic = string(in.String())
case "joined":
out.Joined = bool(in.Bool())
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjsonC5839400EncodeGithubComKhliengDispatchStorage1(out *jwriter.Writer, in Channel) {
out.RawByte('{')
first := true
_ = first
if in.Network != "" {
const prefix string = ",\"network\":"
first = false
out.RawString(prefix[1:])
out.String(string(in.Network))
}
if in.Name != "" {
const prefix string = ",\"name\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.Name))
}
if in.Topic != "" {
const prefix string = ",\"topic\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.Topic))
}
if in.Joined {
const prefix string = ",\"joined\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.Bool(bool(in.Joined))
}
out.RawByte('}')
}
// MarshalJSON supports json.Marshaler interface
func (v Channel) MarshalJSON() ([]byte, error) {
w := jwriter.Writer{}
easyjsonC5839400EncodeGithubComKhliengDispatchStorage1(&w, v)
return w.Buffer.BuildBytes(), w.Error
}
// MarshalEasyJSON supports easyjson.Marshaler interface
func (v Channel) MarshalEasyJSON(w *jwriter.Writer) {
easyjsonC5839400EncodeGithubComKhliengDispatchStorage1(w, v)
}
// UnmarshalJSON supports json.Unmarshaler interface
func (v *Channel) UnmarshalJSON(data []byte) error {
r := jlexer.Lexer{Data: data}
easyjsonC5839400DecodeGithubComKhliengDispatchStorage1(&r, v)
return r.Error()
}
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *Channel) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjsonC5839400DecodeGithubComKhliengDispatchStorage1(l, v)
}

View File

@ -31,26 +31,27 @@ var (
) )
type Store interface { type Store interface {
GetUsers() ([]*User, error) Users() ([]*User, error)
SaveUser(user *User) error SaveUser(user *User) error
DeleteUser(user *User) error DeleteUser(user *User) error
GetServer(user *User, host string) (*Server, error) Network(user *User, host string) (*Network, error)
GetServers(user *User) ([]*Server, error) Networks(user *User) ([]*Network, error)
SaveServer(user *User, server *Server) error SaveNetwork(user *User, network *Network) error
RemoveServer(user *User, host string) error RemoveNetwork(user *User, host string) error
GetChannels(user *User) ([]*Channel, error) Channels(user *User) ([]*Channel, error)
AddChannel(user *User, channel *Channel) error HasChannel(user *User, network, channel string) bool
RemoveChannel(user *User, server, channel string) error SaveChannel(user *User, channel *Channel) error
RemoveChannel(user *User, network, channel string) error
GetOpenDMs(user *User) ([]Tab, error) OpenDMs(user *User) ([]Tab, error)
AddOpenDM(user *User, server, nick string) error AddOpenDM(user *User, network, nick string) error
RemoveOpenDM(user *User, server, nick string) error RemoveOpenDM(user *User, network, nick string) error
} }
type SessionStore interface { type SessionStore interface {
GetSessions() ([]*session.Session, error) Sessions() ([]*session.Session, error)
SaveSession(session *session.Session) error SaveSession(session *session.Session) error
DeleteSession(key string) error DeleteSession(key string) error
} }
@ -58,15 +59,15 @@ type SessionStore interface {
type MessageStore interface { type MessageStore interface {
LogMessage(message *Message) error LogMessage(message *Message) error
LogMessages(messages []*Message) error LogMessages(messages []*Message) error
GetMessages(server, channel string, count int, fromID string) ([]Message, bool, error) Messages(network, channel string, count int, fromID string) ([]Message, bool, error)
GetMessagesByID(server, channel string, ids []string) ([]Message, error) MessagesByID(network, channel string, ids []string) ([]Message, error)
Close() Close()
} }
type MessageStoreCreator func(*User) (MessageStore, error) type MessageStoreCreator func(*User) (MessageStore, error)
type MessageSearchProvider interface { type MessageSearchProvider interface {
SearchMessages(server, channel, q string) ([]string, error) SearchMessages(network, channel, q string) ([]string, error)
Index(id string, message *Message) error Index(id string, message *Message) error
Close() Close()
} }

View File

@ -9,7 +9,7 @@ struct ClientSettings {
ColoredNicks bool ColoredNicks bool
} }
struct Server { struct Network {
Name string Name string
Host string Host string
Port string Port string
@ -23,7 +23,7 @@ struct Server {
} }
struct Channel { struct Channel {
Server string Network string
Name string Name string
} }

View File

@ -238,7 +238,7 @@ func (d *ClientSettings) Unmarshal(buf []byte) (uint64, error) {
return i + 1, nil return i + 1, nil
} }
func (d *Server) Size() (s uint64) { func (d *Network) Size() (s uint64) {
{ {
l := uint64(len(d.Name)) l := uint64(len(d.Name))
@ -378,7 +378,7 @@ func (d *Server) Size() (s uint64) {
s += 1 s += 1
return return
} }
func (d *Server) Marshal(buf []byte) ([]byte, error) { func (d *Network) Marshal(buf []byte) ([]byte, error) {
size := d.Size() size := d.Size()
{ {
if uint64(cap(buf)) >= size { if uint64(cap(buf)) >= size {
@ -570,7 +570,7 @@ func (d *Server) Marshal(buf []byte) ([]byte, error) {
return buf[:i+1], nil return buf[:i+1], nil
} }
func (d *Server) Unmarshal(buf []byte) (uint64, error) { func (d *Network) Unmarshal(buf []byte) (uint64, error) {
i := uint64(0) i := uint64(0)
{ {
@ -762,7 +762,7 @@ func (d *Server) Unmarshal(buf []byte) (uint64, error) {
func (d *Channel) Size() (s uint64) { func (d *Channel) Size() (s uint64) {
{ {
l := uint64(len(d.Server)) l := uint64(len(d.Network))
{ {
@ -805,7 +805,7 @@ func (d *Channel) Marshal(buf []byte) ([]byte, error) {
i := uint64(0) i := uint64(0)
{ {
l := uint64(len(d.Server)) l := uint64(len(d.Network))
{ {
@ -820,7 +820,7 @@ func (d *Channel) Marshal(buf []byte) ([]byte, error) {
i++ i++
} }
copy(buf[i+0:], d.Server) copy(buf[i+0:], d.Network)
i += l i += l
} }
{ {
@ -865,7 +865,7 @@ func (d *Channel) Unmarshal(buf []byte) (uint64, error) {
l = t l = t
} }
d.Server = string(buf[i+0 : i+0+l]) d.Network = string(buf[i+0 : i+0+l])
i += l i += l
} }
{ {

View File

@ -6,6 +6,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/khlieng/dispatch/pkg/irc"
"github.com/kjk/betterguid" "github.com/kjk/betterguid"
) )
@ -58,7 +59,7 @@ func NewUser(store Store) (*User, error) {
} }
func LoadUsers(store Store) ([]*User, error) { func LoadUsers(store Store) ([]*User, error) {
users, err := store.GetUsers() users, err := store.Users()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -76,15 +77,15 @@ func LoadUsers(store Store) ([]*User, error) {
user.lastMessages = map[string]map[string]*Message{} user.lastMessages = map[string]map[string]*Message{}
user.loadCertificate() user.loadCertificate()
channels, err := user.GetChannels() channels, err := user.Channels()
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, channel := range channels { for _, channel := range channels {
messages, _, err := user.GetLastMessages(channel.Server, channel.Name, 1) messages, _, err := user.LastMessages(channel.Network, channel.Name, 1)
if err == nil && len(messages) == 1 { if err == nil && len(messages) == 1 {
user.lastMessages[channel.Server] = map[string]*Message{ user.lastMessages[channel.Network] = map[string]*Message{
channel.Name: &messages[0], channel.Name: &messages[0],
} }
} }
@ -131,7 +132,7 @@ func DefaultClientSettings() *ClientSettings {
} }
} }
func (u *User) GetClientSettings() *ClientSettings { func (u *User) ClientSettings() *ClientSettings {
u.lock.Lock() u.lock.Lock()
settings := *u.clientSettings settings := *u.clientSettings
u.lock.Unlock() u.lock.Unlock()
@ -158,91 +159,88 @@ func (u *User) UnmarshalClientSettingsJSON(b []byte) error {
return u.store.SaveUser(u) return u.store.SaveUser(u)
} }
type Server struct { func (u *User) NewNetwork(template *Network, client *irc.Client) *Network {
Name string if template == nil {
Host string template = &Network{}
Port string }
TLS bool
ServerPassword string template.user = u
Nick string template.client = client
Username string template.channels = map[string]*Channel{}
Realname string
Account string return template
Password string
} }
func (u *User) GetServer(address string) (*Server, error) { func (u *User) Network(address string) (*Network, error) {
return u.store.GetServer(u, address) return u.store.Network(u, address)
} }
func (u *User) GetServers() ([]*Server, error) { func (u *User) Networks() ([]*Network, error) {
return u.store.GetServers(u) return u.store.Networks(u)
} }
func (u *User) AddServer(server *Server) error { func (u *User) SaveNetwork(network *Network) error {
return u.store.SaveServer(u, server) return u.store.SaveNetwork(u, network)
} }
func (u *User) RemoveServer(address string) error { func (u *User) RemoveNetwork(address string) error {
return u.store.RemoveServer(u, address) return u.store.RemoveNetwork(u, address)
} }
func (u *User) SetNick(nick, address string) error { func (u *User) SetNick(nick, address string) error {
server, err := u.GetServer(address) network, err := u.Network(address)
if err != nil { if err != nil {
return err return err
} }
server.Nick = nick network.Nick = nick
return u.AddServer(server) return u.SaveNetwork(network)
} }
func (u *User) SetServerName(name, address string) error { func (u *User) SetNetworkName(name, address string) error {
server, err := u.GetServer(address) network, err := u.Network(address)
if err != nil { if err != nil {
return err return err
} }
server.Name = name network.Name = name
return u.AddServer(server) return u.SaveNetwork(network)
} }
type Channel struct { func (u *User) Channels() ([]*Channel, error) {
Server string return u.store.Channels(u)
Name string
Topic string
} }
func (u *User) GetChannels() ([]*Channel, error) { func (u *User) SaveChannel(channel *Channel) error {
return u.store.GetChannels(u) return u.store.SaveChannel(u, channel)
} }
func (u *User) AddChannel(channel *Channel) error { func (u *User) RemoveChannel(network, channel string) error {
return u.store.AddChannel(u, channel) return u.store.RemoveChannel(u, network, channel)
} }
func (u *User) RemoveChannel(server, channel string) error { func (u *User) HasChannel(network, channel string) bool {
return u.store.RemoveChannel(u, server, channel) return u.store.HasChannel(u, network, channel)
} }
type Tab struct { type Tab struct {
Server string Network string
Name string Name string
} }
func (u *User) GetOpenDMs() ([]Tab, error) { func (u *User) OpenDMs() ([]Tab, error) {
return u.store.GetOpenDMs(u) return u.store.OpenDMs(u)
} }
func (u *User) AddOpenDM(server, nick string) error { func (u *User) AddOpenDM(network, nick string) error {
return u.store.AddOpenDM(u, server, nick) return u.store.AddOpenDM(u, network, nick)
} }
func (u *User) RemoveOpenDM(server, nick string) error { func (u *User) RemoveOpenDM(network, nick string) error {
return u.store.RemoveOpenDM(u, server, nick) return u.store.RemoveOpenDM(u, network, nick)
} }
type Message struct { type Message struct {
ID string `json:"-" bleve:"-"` ID string `json:"-" bleve:"-"`
Server string `json:"-" bleve:"server"` Network string `json:"-" bleve:"server"`
From string `bleve:"-"` From string `bleve:"-"`
To string `json:"-" bleve:"to"` To string `json:"-" bleve:"to"`
Content string `bleve:"content"` Content string `bleve:"content"`
@ -267,7 +265,7 @@ func (u *User) LogMessage(msg *Message) error {
msg.To = msg.From msg.To = msg.From
} }
u.setLastMessage(msg.Server, msg.To, msg) u.setLastMessage(msg.Network, msg.To, msg)
err := u.messageLog.LogMessage(msg) err := u.messageLog.LogMessage(msg)
if err != nil { if err != nil {
@ -282,7 +280,7 @@ type Event struct {
Time int64 Time int64
} }
func (u *User) LogEvent(server, name string, params []string, channels ...string) error { func (u *User) LogEvent(network, name string, params []string, channels ...string) error {
now := time.Now().Unix() now := time.Now().Unix()
event := Event{ event := Event{
Type: name, Type: name,
@ -291,11 +289,11 @@ func (u *User) LogEvent(server, name string, params []string, channels ...string
} }
for _, channel := range channels { for _, channel := range channels {
lastMessage := u.getLastMessage(server, channel) lastMessage := u.getLastMessage(network, channel)
if lastMessage != nil && shouldCollapse(lastMessage, event) { if lastMessage != nil && shouldCollapse(lastMessage, event) {
lastMessage.Events = append(lastMessage.Events, event) lastMessage.Events = append(lastMessage.Events, event)
u.setLastMessage(server, channel, lastMessage) u.setLastMessage(network, channel, lastMessage)
err := u.messageLog.LogMessage(lastMessage) err := u.messageLog.LogMessage(lastMessage)
if err != nil { if err != nil {
@ -303,13 +301,13 @@ func (u *User) LogEvent(server, name string, params []string, channels ...string
} }
} else { } else {
msg := &Message{ msg := &Message{
ID: betterguid.New(), ID: betterguid.New(),
Server: server, Network: network,
To: channel, To: channel,
Time: now, Time: now,
Events: []Event{event}, Events: []Event{event},
} }
u.setLastMessage(server, channel, msg) u.setLastMessage(network, channel, msg)
err := u.messageLog.LogMessage(msg) err := u.messageLog.LogMessage(msg)
if err != nil { if err != nil {
@ -338,15 +336,15 @@ func shouldCollapse(msg *Message, event Event) bool {
return matches == 2 return matches == 2
} }
func (u *User) getLastMessage(server, channel string) *Message { func (u *User) getLastMessage(network, channel string) *Message {
u.lock.Lock() u.lock.Lock()
defer u.lock.Unlock() defer u.lock.Unlock()
if _, ok := u.lastMessages[server]; !ok { if _, ok := u.lastMessages[network]; !ok {
return nil return nil
} }
last := u.lastMessages[server][channel] last := u.lastMessages[network][channel]
if last != nil { if last != nil {
msg := *last msg := *last
return &msg return &msg
@ -354,30 +352,30 @@ func (u *User) getLastMessage(server, channel string) *Message {
return nil return nil
} }
func (u *User) setLastMessage(server, channel string, msg *Message) { func (u *User) setLastMessage(network, channel string, msg *Message) {
u.lock.Lock() u.lock.Lock()
if _, ok := u.lastMessages[server]; !ok { if _, ok := u.lastMessages[network]; !ok {
u.lastMessages[server] = map[string]*Message{} u.lastMessages[network] = map[string]*Message{}
} }
u.lastMessages[server][channel] = msg u.lastMessages[network][channel] = msg
u.lock.Unlock() u.lock.Unlock()
} }
func (u *User) GetMessages(server, channel string, count int, fromID string) ([]Message, bool, error) { func (u *User) Messages(network, channel string, count int, fromID string) ([]Message, bool, error) {
return u.messageLog.GetMessages(server, channel, count, fromID) return u.messageLog.Messages(network, channel, count, fromID)
} }
func (u *User) GetLastMessages(server, channel string, count int) ([]Message, bool, error) { func (u *User) LastMessages(network, channel string, count int) ([]Message, bool, error) {
return u.GetMessages(server, channel, count, "") return u.Messages(network, channel, count, "")
} }
func (u *User) SearchMessages(server, channel, q string) ([]Message, error) { func (u *User) SearchMessages(network, channel, q string) ([]Message, error) {
ids, err := u.messageIndex.SearchMessages(server, channel, q) ids, err := u.messageIndex.SearchMessages(network, channel, q)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return u.messageLog.GetMessagesByID(server, channel, ids) return u.messageLog.MessagesByID(network, channel, ids)
} }

View File

@ -34,23 +34,23 @@ func TestUser(t *testing.T) {
user, err := storage.NewUser(db) user, err := storage.NewUser(db)
assert.Nil(t, err) assert.Nil(t, err)
srv := &storage.Server{ srv := &storage.Network{
Name: "freenode", Name: "freenode",
Host: "irc.freenode.net", Host: "irc.freenode.net",
Nick: "test", Nick: "test",
} }
chan1 := &storage.Channel{ chan1 := &storage.Channel{
Server: srv.Host, Network: srv.Host,
Name: "#test", Name: "#test",
} }
chan2 := &storage.Channel{ chan2 := &storage.Channel{
Server: srv.Host, Network: srv.Host,
Name: "#testing", Name: "#testing",
} }
user.AddServer(srv) user.SaveNetwork(srv)
user.AddChannel(chan1) user.SaveChannel(chan1)
user.AddChannel(chan2) user.SaveChannel(chan2)
users, err := storage.LoadUsers(db) users, err := storage.LoadUsers(db)
assert.Nil(t, err) assert.Nil(t, err)
@ -59,52 +59,52 @@ func TestUser(t *testing.T) {
user = users[0] user = users[0]
assert.Equal(t, uint64(1), user.ID) assert.Equal(t, uint64(1), user.ID)
servers, err := user.GetServers() servers, err := user.Networks()
assert.Len(t, servers, 1) assert.Len(t, servers, 1)
assert.Equal(t, srv, servers[0]) assert.Equal(t, srv, servers[0])
channels, err := user.GetChannels() channels, err := user.Channels()
assert.Len(t, channels, 2) assert.Len(t, channels, 2)
assert.Equal(t, chan1, channels[0]) assert.Equal(t, chan1, channels[0])
assert.Equal(t, chan2, channels[1]) assert.Equal(t, chan2, channels[1])
user.SetNick("bob", srv.Host) user.SetNick("bob", srv.Host)
servers, err = user.GetServers() servers, err = user.Networks()
assert.Equal(t, "bob", servers[0].Nick) assert.Equal(t, "bob", servers[0].Nick)
user.SetServerName("cake", srv.Host) user.SetNetworkName("cake", srv.Host)
servers, err = user.GetServers() servers, err = user.Networks()
assert.Equal(t, "cake", servers[0].Name) assert.Equal(t, "cake", servers[0].Name)
user.RemoveChannel(srv.Host, chan1.Name) user.RemoveChannel(srv.Host, chan1.Name)
channels, err = user.GetChannels() channels, err = user.Channels()
assert.Len(t, channels, 1) assert.Len(t, channels, 1)
assert.Equal(t, chan2, channels[0]) assert.Equal(t, chan2, channels[0])
user.RemoveServer(srv.Host) user.RemoveNetwork(srv.Host)
servers, err = user.GetServers() servers, err = user.Networks()
assert.Len(t, servers, 0) assert.Len(t, servers, 0)
channels, err = user.GetChannels() channels, err = user.Channels()
assert.Len(t, channels, 0) assert.Len(t, channels, 0)
user.AddOpenDM(srv.Host, "cake") user.AddOpenDM(srv.Host, "cake")
openDMs, err := user.GetOpenDMs() openDMs, err := user.OpenDMs()
assert.Nil(t, err) assert.Nil(t, err)
assert.Len(t, openDMs, 1) assert.Len(t, openDMs, 1)
err = user.RemoveOpenDM(srv.Host, "cake") err = user.RemoveOpenDM(srv.Host, "cake")
assert.Nil(t, err) assert.Nil(t, err)
openDMs, err = user.GetOpenDMs() openDMs, err = user.OpenDMs()
assert.Nil(t, err) assert.Nil(t, err)
assert.Len(t, openDMs, 0) assert.Len(t, openDMs, 0)
settings := user.GetClientSettings() settings := user.ClientSettings()
assert.NotNil(t, settings) assert.NotNil(t, settings)
assert.Equal(t, storage.DefaultClientSettings(), settings) assert.Equal(t, storage.DefaultClientSettings(), settings)
settings.ColoredNicks = !settings.ColoredNicks settings.ColoredNicks = !settings.ColoredNicks
err = user.SetClientSettings(settings) err = user.SetClientSettings(settings)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, settings, user.GetClientSettings()) assert.Equal(t, settings, user.ClientSettings())
assert.NotEqual(t, settings, storage.DefaultClientSettings()) assert.NotEqual(t, settings, storage.DefaultClientSettings())
user.AddOpenDM(srv.Host, "cake") user.AddOpenDM(srv.Host, "cake")
@ -113,7 +113,7 @@ func TestUser(t *testing.T) {
_, err = os.Stat(storage.Path.User(user.Username)) _, err = os.Stat(storage.Path.User(user.Username))
assert.True(t, os.IsNotExist(err)) assert.True(t, os.IsNotExist(err))
openDMs, err = user.GetOpenDMs() openDMs, err = user.OpenDMs()
assert.Nil(t, err) assert.Nil(t, err)
assert.Len(t, openDMs, 0) assert.Len(t, openDMs, 0)
@ -143,12 +143,12 @@ func TestMessages(t *testing.T) {
os.MkdirAll(storage.Path.User(user.Username), 0700) os.MkdirAll(storage.Path.User(user.Username), 0700)
messages, hasMore, err := user.GetMessages("irc.freenode.net", "#go-nuts", 10, "6") messages, hasMore, err := user.Messages("irc.freenode.net", "#go-nuts", 10, "6")
assert.Nil(t, err) assert.Nil(t, err)
assert.False(t, hasMore) assert.False(t, hasMore)
assert.Len(t, messages, 0) assert.Len(t, messages, 0)
messages, hasMore, err = user.GetLastMessages("irc.freenode.net", "#go-nuts", 10) messages, hasMore, err = user.LastMessages("irc.freenode.net", "#go-nuts", 10)
assert.Nil(t, err) assert.Nil(t, err)
assert.False(t, hasMore) assert.False(t, hasMore)
assert.Len(t, messages, 0) assert.Len(t, messages, 0)
@ -163,7 +163,7 @@ func TestMessages(t *testing.T) {
ids = append(ids, id) ids = append(ids, id)
err = user.LogMessage(&storage.Message{ err = user.LogMessage(&storage.Message{
ID: id, ID: id,
Server: "irc.freenode.net", Network: "irc.freenode.net",
From: "nick", From: "nick",
To: "#go-nuts", To: "#go-nuts",
Content: "message" + strconv.Itoa(i), Content: "message" + strconv.Itoa(i),
@ -171,35 +171,35 @@ func TestMessages(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
} }
messages, hasMore, err = user.GetMessages("irc.freenode.net", "#go-nuts", 10, ids[4]) messages, hasMore, err = user.Messages("irc.freenode.net", "#go-nuts", 10, ids[4])
assert.Equal(t, "message0", messages[0].Content) assert.Equal(t, "message0", messages[0].Content)
assert.Equal(t, "message3", messages[3].Content) assert.Equal(t, "message3", messages[3].Content)
assert.Nil(t, err) assert.Nil(t, err)
assert.False(t, hasMore) assert.False(t, hasMore)
assert.Len(t, messages, 4) assert.Len(t, messages, 4)
messages, hasMore, err = user.GetMessages("irc.freenode.net", "#go-nuts", 10, betterguid.New()) messages, hasMore, err = user.Messages("irc.freenode.net", "#go-nuts", 10, betterguid.New())
assert.Equal(t, "message0", messages[0].Content) assert.Equal(t, "message0", messages[0].Content)
assert.Equal(t, "message4", messages[4].Content) assert.Equal(t, "message4", messages[4].Content)
assert.Nil(t, err) assert.Nil(t, err)
assert.False(t, hasMore) assert.False(t, hasMore)
assert.Len(t, messages, 5) assert.Len(t, messages, 5)
messages, hasMore, err = user.GetMessages("irc.freenode.net", "#go-nuts", 10, ids[2]) messages, hasMore, err = user.Messages("irc.freenode.net", "#go-nuts", 10, ids[2])
assert.Equal(t, "message0", messages[0].Content) assert.Equal(t, "message0", messages[0].Content)
assert.Equal(t, "message1", messages[1].Content) assert.Equal(t, "message1", messages[1].Content)
assert.Nil(t, err) assert.Nil(t, err)
assert.False(t, hasMore) assert.False(t, hasMore)
assert.Len(t, messages, 2) assert.Len(t, messages, 2)
messages, hasMore, err = user.GetLastMessages("irc.freenode.net", "#go-nuts", 10) messages, hasMore, err = user.LastMessages("irc.freenode.net", "#go-nuts", 10)
assert.Equal(t, "message0", messages[0].Content) assert.Equal(t, "message0", messages[0].Content)
assert.Equal(t, "message4", messages[4].Content) assert.Equal(t, "message4", messages[4].Content)
assert.Nil(t, err) assert.Nil(t, err)
assert.False(t, hasMore) assert.False(t, hasMore)
assert.Len(t, messages, 5) assert.Len(t, messages, 5)
messages, hasMore, err = user.GetLastMessages("irc.freenode.net", "#go-nuts", 4) messages, hasMore, err = user.LastMessages("irc.freenode.net", "#go-nuts", 4)
assert.Equal(t, "message1", messages[0].Content) assert.Equal(t, "message1", messages[0].Content)
assert.Equal(t, "message4", messages[3].Content) assert.Equal(t, "message4", messages[3].Content)
assert.Nil(t, err) assert.Nil(t, err)
@ -211,7 +211,7 @@ func TestMessages(t *testing.T) {
assert.True(t, len(messages) > 0) assert.True(t, len(messages) > 0)
user.LogEvent("irc.freenode.net", "join", []string{"bob"}, "#go-nuts") user.LogEvent("irc.freenode.net", "join", []string{"bob"}, "#go-nuts")
messages, hasMore, err = user.GetLastMessages("irc.freenode.net", "#go-nuts", 1) messages, hasMore, err = user.LastMessages("irc.freenode.net", "#go-nuts", 1)
assert.Zero(t, messages[0].Content) assert.Zero(t, messages[0].Content)
assert.Nil(t, err) assert.Nil(t, err)
assert.True(t, hasMore) assert.True(t, hasMore)
@ -220,7 +220,7 @@ func TestMessages(t *testing.T) {
assert.NotZero(t, messages[0].Events[0].Time) assert.NotZero(t, messages[0].Events[0].Time)
user.LogEvent("irc.freenode.net", "part", []string{"bob"}, "#go-nuts") user.LogEvent("irc.freenode.net", "part", []string{"bob"}, "#go-nuts")
messages, hasMore, err = user.GetLastMessages("irc.freenode.net", "#go-nuts", 1) messages, hasMore, err = user.LastMessages("irc.freenode.net", "#go-nuts", 1)
assert.Zero(t, messages[0].Content) assert.Zero(t, messages[0].Content)
assert.Nil(t, err) assert.Nil(t, err)
assert.True(t, hasMore) assert.True(t, hasMore)
@ -229,7 +229,7 @@ func TestMessages(t *testing.T) {
assert.NotZero(t, messages[0].Events[0].Time) assert.NotZero(t, messages[0].Events[0].Time)
user.LogEvent("irc.freenode.net", "nick", []string{"bob", "rob"}, "#go-nuts") user.LogEvent("irc.freenode.net", "nick", []string{"bob", "rob"}, "#go-nuts")
messages, hasMore, err = user.GetLastMessages("irc.freenode.net", "#go-nuts", 1) messages, hasMore, err = user.LastMessages("irc.freenode.net", "#go-nuts", 1)
assert.Zero(t, messages[0].Content) assert.Zero(t, messages[0].Content)
assert.Nil(t, err) assert.Nil(t, err)
assert.True(t, hasMore) assert.True(t, hasMore)
@ -238,7 +238,7 @@ func TestMessages(t *testing.T) {
assert.NotZero(t, messages[0].Events[0].Time) assert.NotZero(t, messages[0].Events[0].Time)
user.LogEvent("irc.freenode.net", "quit", []string{"rob", "bored"}, "#go-nuts") user.LogEvent("irc.freenode.net", "quit", []string{"rob", "bored"}, "#go-nuts")
messages, hasMore, err = user.GetLastMessages("irc.freenode.net", "#go-nuts", 1) messages, hasMore, err = user.LastMessages("irc.freenode.net", "#go-nuts", 1)
assert.Zero(t, messages[0].Content) assert.Zero(t, messages[0].Content)
assert.Nil(t, err) assert.Nil(t, err)
assert.True(t, hasMore) assert.True(t, hasMore)