Compare commits
82 Commits
v0.6.4
...
i2p-suppor
Author | SHA1 | Date | |
---|---|---|---|
8f18eaa4cf | |||
f997149dd6 | |||
a02ad3a299 | |||
ea4f321fe7 | |||
1fe4c4d17e | |||
307573830a | |||
ca4db66308 | |||
d844f6ee1a | |||
02e9df865e | |||
45f8795fad | |||
e0ca9d5d8c | |||
972f568a00 | |||
b9f52a8761 | |||
ca23b3ded8 | |||
1c996822cd | |||
f89e6ae133 | |||
4694e66e98 | |||
fc937aaac8 | |||
fd5e50a2cb | |||
04e6e8c7a2 | |||
15ee5ce1c9 | |||
e0d2243248 | |||
67fe5d263d | |||
7040f1c8d0 | |||
2ea4584c97 | |||
fcf0c17682 | |||
6985dd16da | |||
a33157ff84 | |||
a4fe18c0f0 | |||
9d8d04fa7c | |||
876d9ebdd0 | |||
ead3b37cf9 | |||
edd4d6eadb | |||
e76beca4a0 | |||
8829793290 | |||
71b2136a9e | |||
e97c7f2ada | |||
9aac4f4e29 | |||
2f8dad2529 | |||
be8b785813 | |||
e937f5d8b9 | |||
99b3ff519b | |||
35727fb2b8 | |||
1abe280957 | |||
e33b9f05e4 | |||
973578bb49 | |||
4816fbfbca | |||
f0eada0f75 | |||
abbe739b04 | |||
63afd839be | |||
84a10efe36 | |||
902e4da46f | |||
e05118a29b | |||
a90e8d4b2f | |||
2d68f04ab2 | |||
b92f5cfb43 | |||
fa99a96733 | |||
6b5bf4ced1 | |||
0c902f8ac8 | |||
ed432881ef | |||
75c9560dfb | |||
1532b2a8c8 | |||
4b8491cf99 | |||
2509420ba5 | |||
ed2e56948e | |||
9581a63e81 | |||
8fa91ac470 | |||
eb7545455c | |||
eab788a782 | |||
dcbf3397c1 | |||
1773fef8ef | |||
08ffc79a65 | |||
2a72b198e3 | |||
0d085a2b4d | |||
497f9e882c | |||
7d97d10e76 | |||
8305dd561d | |||
e97bb519ed | |||
18acde5b2b | |||
bab4732221 | |||
79af695d17 | |||
d98312f99b |
@ -15,10 +15,10 @@ matrix:
|
||||
install:
|
||||
- GO111MODULE=off go get github.com/jteeuwen/go-bindata/...
|
||||
- cd client
|
||||
- nvm install 12.4.0
|
||||
- nvm use 12.4.0
|
||||
- nvm install node
|
||||
- nvm use node
|
||||
- npm install -g yarn
|
||||
- yarn global add gulp
|
||||
- yarn global add gulp-cli
|
||||
- yarn
|
||||
|
||||
script:
|
||||
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"javascript.preferences.importModuleSpecifier": "non-relative"
|
||||
}
|
@ -10,6 +10,9 @@
|
||||
- Persistent connections
|
||||
- Multiple servers and users
|
||||
- Automatic HTTPS through Let's Encrypt
|
||||
- Single binary with no dependencies
|
||||
- DCC downloads
|
||||
- SASL
|
||||
- Client certificates
|
||||
|
||||
## Usage
|
||||
@ -25,7 +28,7 @@ There is a few different ways of getting it:
|
||||
|
||||
### 2. Go
|
||||
|
||||
This requires a [Go environment](http://golang.org/doc/install), version 1.10 or greater.
|
||||
This requires a [Go environment](http://golang.org/doc/install), version 1.11 or greater.
|
||||
|
||||
Fetch, compile and run dispatch:
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
@ -4,6 +4,7 @@
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"plugins": ["babel"],
|
||||
"rules": {
|
||||
"consistent-return": 0,
|
||||
"jsx-a11y/click-events-have-key-events": 0,
|
||||
@ -19,7 +20,10 @@
|
||||
"react/jsx-props-no-spreading": 0,
|
||||
"react/prop-types": 0,
|
||||
"react/state-in-constructor": 0,
|
||||
"react/static-property-placement": 0
|
||||
"react/static-property-placement": 0,
|
||||
|
||||
"no-unused-expressions": 0,
|
||||
"babel/no-unused-expressions": 2
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
|
@ -2,12 +2,34 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Roboto Mono, monospace;
|
||||
background: #f0f0f0;
|
||||
color: #222;
|
||||
scrollbar-color: #ccc rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #ccc;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #999;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:active {
|
||||
background: #666;
|
||||
}
|
||||
|
||||
h1,
|
||||
@ -180,7 +202,7 @@ i[class*=' icon-']:before {
|
||||
padding: 25px 15px 10px;
|
||||
}
|
||||
|
||||
.textinput span {
|
||||
.textinput-label {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@ -261,6 +283,19 @@ i[class*=' icon-']:before {
|
||||
font-family: Montserrat, sans-serif;
|
||||
transition: transform 0.2s;
|
||||
user-select: none;
|
||||
scrollbar-color: #333 rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.tablist ::-webkit-scrollbar-thumb {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.tablist ::-webkit-scrollbar-thumb:hover {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.tablist ::-webkit-scrollbar-thumb:active {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.tab-container {
|
||||
@ -295,18 +330,26 @@ i[class*=' icon-']:before {
|
||||
border-left: 5px solid #6bb758;
|
||||
}
|
||||
|
||||
.tab-server {
|
||||
.tab-network {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #999;
|
||||
margin-top: 10px !important;
|
||||
}
|
||||
|
||||
.tab-server .tab-content {
|
||||
.tab-network .tab-content {
|
||||
flex: 1;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.tab-prefix {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.error .tab-prefix {
|
||||
color: #f6546a;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
margin: 5px;
|
||||
margin-left: 15px;
|
||||
@ -317,6 +360,10 @@ i[class*=' icon-']:before {
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.tab-label-channels {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tab-label span {
|
||||
flex: 1;
|
||||
}
|
||||
@ -329,7 +376,7 @@ i[class*=' icon-']:before {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.tab-label button:hover {
|
||||
.tab-label:hover button {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
@ -408,6 +455,17 @@ i[class*=' icon-']:before {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.connect-section {
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 15px;
|
||||
margin: 15px 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.connect-section h2 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
appearance: textfield;
|
||||
}
|
||||
@ -441,13 +499,17 @@ input::-webkit-inner-spin-button {
|
||||
.connect-form-address label {
|
||||
margin-top: 5px;
|
||||
font: 12px 'Montserrat', sans-serif;
|
||||
padding: 10px;
|
||||
padding: 10px 0;
|
||||
padding-bottom: 0;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.connect-form-ssl {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.connect-form-button-optionals {
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
@ -483,12 +545,12 @@ input::-webkit-inner-spin-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-server .userlist,
|
||||
.chat-network .userlist,
|
||||
.chat-private .userlist {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chat-server .userlist-bar,
|
||||
.chat-network .userlist-bar,
|
||||
.chat-private .userlist-bar {
|
||||
display: none;
|
||||
}
|
||||
@ -505,7 +567,7 @@ input::-webkit-inner-spin-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chat-server .button-userlist,
|
||||
.chat-network .button-userlist,
|
||||
.chat-private .button-userlist {
|
||||
display: none;
|
||||
}
|
||||
@ -581,7 +643,7 @@ input.chat-title {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.chat-server .search {
|
||||
.chat-network .search {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -710,6 +772,11 @@ input.chat-title {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.message-events-more {
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.message-input-wrap {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@ -802,7 +869,6 @@ input.message-input-nick.invalid {
|
||||
}
|
||||
|
||||
.settings h2 {
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
var path = require('path');
|
||||
var exec = require('child_process').exec;
|
||||
var url = require('url');
|
||||
|
||||
var gulp = require('gulp');
|
||||
var gutil = require('gulp-util');
|
||||
@ -41,7 +39,9 @@ function js(cb) {
|
||||
process.env['NODE_ENV'] = 'production';
|
||||
|
||||
compiler.run(function (err, stats) {
|
||||
if (err) throw new gutil.PluginError('webpack', err);
|
||||
if (err) {
|
||||
throw new gutil.PluginError('webpack', err);
|
||||
}
|
||||
|
||||
gutil.log(
|
||||
'[webpack]',
|
||||
@ -50,7 +50,9 @@ function js(cb) {
|
||||
})
|
||||
);
|
||||
|
||||
if (stats.hasErrors()) process.exit(1);
|
||||
if (stats.hasErrors()) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
cb();
|
||||
});
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { COMMAND } from 'state/actions';
|
||||
import { join, part, invite, kick, setTopic } from 'state/channels';
|
||||
import { sendMessage, raw } from 'state/messages';
|
||||
import { setNick, disconnect, whois, away } from 'state/servers';
|
||||
import { setNick, disconnect, whois, away } from 'state/networks';
|
||||
import { openPrivateChat } from 'state/privateChats';
|
||||
import { select } from 'state/tab';
|
||||
import { find } from 'utils';
|
||||
import { find, isChannel } from 'utils';
|
||||
import createCommandMiddleware, {
|
||||
beforeHandler,
|
||||
notFoundHandler
|
||||
@ -13,7 +14,7 @@ const help = [
|
||||
'/join <channel> - Join a channel',
|
||||
'/part [channel] - Leave the current or specified channel',
|
||||
'/nick <nick> - Change nick',
|
||||
'/quit - Disconnect from the current server',
|
||||
'/quit - Disconnect from the current network',
|
||||
'/me <message> - Send action message',
|
||||
'/topic [topic] - Show or set topic in the current channel',
|
||||
'/msg <target> <message> - Send message to the specified channel or user',
|
||||
@ -22,7 +23,7 @@ const help = [
|
||||
'/kick <nick> - Kick user from the current channel',
|
||||
'/whois <nick> - Get information about user',
|
||||
'/away [message] - Set or clear away message',
|
||||
'/raw [message] - Send raw IRC message to the current server',
|
||||
'/raw [message] - Send raw IRC message to the current network',
|
||||
'/help [command]... - Print help for all or the specified command(s)'
|
||||
];
|
||||
|
||||
@ -33,56 +34,55 @@ const findHelp = cmd =>
|
||||
find(help, line => line.slice(1, line.indexOf(' ')) === cmd);
|
||||
|
||||
export default createCommandMiddleware(COMMAND, {
|
||||
join({ dispatch, server }, channel) {
|
||||
join({ dispatch, network }, channel) {
|
||||
if (channel) {
|
||||
if (channel[0] !== '#') {
|
||||
return error('Bad channel name');
|
||||
}
|
||||
dispatch(join([channel], server));
|
||||
dispatch(select(server, channel));
|
||||
dispatch(join([channel], network));
|
||||
} else {
|
||||
return error('Missing channel');
|
||||
}
|
||||
},
|
||||
|
||||
part({ dispatch, server, channel, isChannel }, partChannel) {
|
||||
part({ dispatch, network, channel, inChannel }, partChannel) {
|
||||
if (partChannel) {
|
||||
dispatch(part([partChannel], server));
|
||||
} else if (isChannel) {
|
||||
dispatch(part([channel], server));
|
||||
dispatch(part([partChannel], network));
|
||||
} else if (inChannel) {
|
||||
dispatch(part([channel], network));
|
||||
} else {
|
||||
return error('This is not a channel');
|
||||
}
|
||||
},
|
||||
|
||||
nick({ dispatch, server }, nick) {
|
||||
nick({ dispatch, network }, nick) {
|
||||
if (nick) {
|
||||
dispatch(setNick(nick, server));
|
||||
dispatch(setNick(nick, network));
|
||||
} else {
|
||||
return error('Missing nick');
|
||||
}
|
||||
},
|
||||
|
||||
quit({ dispatch, server }) {
|
||||
dispatch(disconnect(server));
|
||||
quit({ dispatch, network }) {
|
||||
dispatch(disconnect(network));
|
||||
},
|
||||
|
||||
me({ dispatch, server, channel }, ...message) {
|
||||
me({ dispatch, network, channel }, ...message) {
|
||||
const msg = message.join(' ');
|
||||
if (msg !== '') {
|
||||
dispatch(sendMessage(`\x01ACTION ${msg}\x01`, channel, server));
|
||||
dispatch(sendMessage(`\x01ACTION ${msg}\x01`, channel, network));
|
||||
} else {
|
||||
return error('Messages can not be empty');
|
||||
}
|
||||
},
|
||||
|
||||
topic({ dispatch, getState, server, channel }, ...newTopic) {
|
||||
topic({ dispatch, getState, network, channel }, ...newTopic) {
|
||||
if (newTopic.length > 0) {
|
||||
dispatch(setTopic(newTopic.join(' '), channel, server));
|
||||
dispatch(setTopic(newTopic.join(' '), channel, network));
|
||||
return;
|
||||
}
|
||||
if (channel) {
|
||||
const { topic } = getState().channels[server][channel];
|
||||
const { topic } = getState().channels[network][channel];
|
||||
if (topic) {
|
||||
return text(topic);
|
||||
}
|
||||
@ -90,80 +90,83 @@ export default createCommandMiddleware(COMMAND, {
|
||||
return 'No topic set';
|
||||
},
|
||||
|
||||
msg({ dispatch, server }, target, ...message) {
|
||||
msg({ dispatch, network }, target, ...message) {
|
||||
if (!target) {
|
||||
return error('Missing nick/channel');
|
||||
}
|
||||
|
||||
const msg = message.join(' ');
|
||||
if (msg !== '') {
|
||||
dispatch(sendMessage(message.join(' '), target, server));
|
||||
dispatch(select(server, target));
|
||||
dispatch(sendMessage(message.join(' '), target, network));
|
||||
if (!isChannel(target)) {
|
||||
dispatch(openPrivateChat(network, target));
|
||||
}
|
||||
dispatch(select(network, target));
|
||||
} else {
|
||||
return error('Messages can not be empty');
|
||||
}
|
||||
},
|
||||
|
||||
say({ dispatch, server, channel }, ...message) {
|
||||
say({ dispatch, network, channel }, ...message) {
|
||||
if (!channel) {
|
||||
return error('Messages can only be sent to channels or users');
|
||||
}
|
||||
|
||||
const msg = message.join(' ');
|
||||
if (msg !== '') {
|
||||
dispatch(sendMessage(message.join(' '), channel, server));
|
||||
dispatch(sendMessage(message.join(' '), channel, network));
|
||||
} else {
|
||||
return error('Messages can not be empty');
|
||||
}
|
||||
},
|
||||
|
||||
invite({ dispatch, server, channel, isChannel }, user, inviteChannel) {
|
||||
if (!inviteChannel && !isChannel) {
|
||||
invite({ dispatch, network, channel, inChannel }, user, inviteChannel) {
|
||||
if (!inviteChannel && !inChannel) {
|
||||
return error('This is not a channel');
|
||||
}
|
||||
|
||||
if (user && inviteChannel) {
|
||||
dispatch(invite(user, inviteChannel, server));
|
||||
dispatch(invite(user, inviteChannel, network));
|
||||
} else if (user && channel) {
|
||||
dispatch(invite(user, channel, server));
|
||||
dispatch(invite(user, channel, network));
|
||||
} else {
|
||||
return error('Missing nick');
|
||||
}
|
||||
},
|
||||
|
||||
kick({ dispatch, server, channel, isChannel }, user) {
|
||||
if (!isChannel) {
|
||||
kick({ dispatch, network, channel, inChannel }, user) {
|
||||
if (!inChannel) {
|
||||
return error('This is not a channel');
|
||||
}
|
||||
|
||||
if (user) {
|
||||
dispatch(kick(user, channel, server));
|
||||
dispatch(kick(user, channel, network));
|
||||
} else {
|
||||
return error('Missing nick');
|
||||
}
|
||||
},
|
||||
|
||||
whois({ dispatch, server }, user) {
|
||||
whois({ dispatch, network }, user) {
|
||||
if (user) {
|
||||
dispatch(whois(user, server));
|
||||
dispatch(whois(user, network));
|
||||
} else {
|
||||
return error('Missing nick');
|
||||
}
|
||||
},
|
||||
|
||||
away({ dispatch, server }, ...message) {
|
||||
away({ dispatch, network }, ...message) {
|
||||
const msg = message.join(' ');
|
||||
dispatch(away(msg, server));
|
||||
dispatch(away(msg, network));
|
||||
if (msg !== '') {
|
||||
return 'Away message set';
|
||||
}
|
||||
return 'Away message cleared';
|
||||
},
|
||||
|
||||
raw({ dispatch, server }, ...message) {
|
||||
raw({ dispatch, network }, ...message) {
|
||||
if (message.length > 0 && message[0] !== '') {
|
||||
const cmd = `${message[0].toUpperCase()} ${message.slice(1).join(' ')}`;
|
||||
dispatch(raw(cmd, server));
|
||||
dispatch(raw(cmd, network));
|
||||
return prompt(`=> ${cmd}`);
|
||||
}
|
||||
return [prompt('=> /raw'), error('Missing message')];
|
||||
|
@ -6,14 +6,16 @@ import cn from 'classnames';
|
||||
|
||||
const Modals = lazy(() => import('components/modals'));
|
||||
const Chat = lazy(() => import('containers/Chat'));
|
||||
const Connect = lazy(() => import('containers/Connect'));
|
||||
const Connect = lazy(() =>
|
||||
import(/* webpackChunkName: "connect" */ 'containers/Connect')
|
||||
);
|
||||
const Settings = lazy(() => import('containers/Settings'));
|
||||
|
||||
const App = ({
|
||||
connected,
|
||||
tab,
|
||||
channels,
|
||||
servers,
|
||||
networks,
|
||||
privateChats,
|
||||
showTabList,
|
||||
select,
|
||||
@ -60,7 +62,7 @@ const App = ({
|
||||
<TabList
|
||||
tab={tab}
|
||||
channels={channels}
|
||||
servers={servers}
|
||||
networks={networks}
|
||||
privateChats={privateChats}
|
||||
showTabList={showTabList}
|
||||
select={select}
|
||||
|
@ -7,7 +7,7 @@ import TabListItem from 'containers/TabListItem';
|
||||
import { count } from 'utils';
|
||||
|
||||
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');
|
||||
|
||||
@ -17,7 +17,7 @@ export default class TabList extends PureComponent {
|
||||
const {
|
||||
tab,
|
||||
channels,
|
||||
servers,
|
||||
networks,
|
||||
privateChats,
|
||||
showTabList,
|
||||
openModal
|
||||
@ -28,21 +28,21 @@ export default class TabList extends PureComponent {
|
||||
'off-canvas': showTabList
|
||||
});
|
||||
|
||||
channels.forEach(server => {
|
||||
const { address } = server;
|
||||
const srv = servers[address];
|
||||
channels.forEach(network => {
|
||||
const { address } = network;
|
||||
const srv = networks[address];
|
||||
tabs.push(
|
||||
<TabListItem
|
||||
key={address}
|
||||
server={address}
|
||||
network={address}
|
||||
content={srv.name}
|
||||
selected={tab.server === address && !tab.name}
|
||||
connected={srv.status.connected}
|
||||
selected={tab.network === address && !tab.name}
|
||||
connected={srv.connected}
|
||||
onClick={this.handleTabClick}
|
||||
/>
|
||||
);
|
||||
|
||||
const chanCount = count(server.channels, c => c.joined);
|
||||
const chanCount = count(network.channels, c => c.joined);
|
||||
const chanLimit =
|
||||
get(srv.features, ['CHANLIMIT', '#'], 0) || srv.features.MAXCHANNELS;
|
||||
|
||||
@ -60,7 +60,7 @@ export default class TabList extends PureComponent {
|
||||
tabs.push(
|
||||
<div
|
||||
key={`${address}-chans}`}
|
||||
className="tab-label"
|
||||
className="tab-label tab-label-channels"
|
||||
onClick={() => openModal('channel', address)}
|
||||
>
|
||||
<span>CHANNELS {chanLabel}</span>
|
||||
@ -68,15 +68,15 @@ export default class TabList extends PureComponent {
|
||||
</div>
|
||||
);
|
||||
|
||||
server.channels.forEach(({ name, joined }) =>
|
||||
network.channels.forEach(({ name, joined }) =>
|
||||
tabs.push(
|
||||
<TabListItem
|
||||
key={address + name}
|
||||
server={address}
|
||||
network={address}
|
||||
target={name}
|
||||
content={name}
|
||||
joined={joined}
|
||||
selected={tab.server === address && tab.name === name}
|
||||
selected={tab.network === address && tab.name === name}
|
||||
onClick={this.handleTabClick}
|
||||
/>
|
||||
)
|
||||
@ -99,10 +99,10 @@ export default class TabList extends PureComponent {
|
||||
tabs.push(
|
||||
<TabListItem
|
||||
key={address + nick}
|
||||
server={address}
|
||||
network={address}
|
||||
target={nick}
|
||||
content={nick}
|
||||
selected={tab.server === address && tab.name === nick}
|
||||
selected={tab.network === address && tab.name === nick}
|
||||
onClick={this.handleTabClick}
|
||||
/>
|
||||
)
|
||||
@ -114,9 +114,17 @@ export default class TabList extends PureComponent {
|
||||
<div className={className}>
|
||||
<div className="tab-container">{tabs}</div>
|
||||
<div className="side-buttons">
|
||||
<Button icon={FiPlus} onClick={this.handleConnectClick} />
|
||||
<Button icon={FiUser} />
|
||||
<Button icon={FiSettings} onClick={this.handleSettingsClick} />
|
||||
<Button
|
||||
icon={FiPlus}
|
||||
aria-label="Connect"
|
||||
onClick={this.handleConnectClick}
|
||||
/>
|
||||
<Button icon={FiUser} aria-label="User" />
|
||||
<Button
|
||||
icon={FiSettings}
|
||||
aria-label="Settings"
|
||||
onClick={this.handleSettingsClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,10 +1,22 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
function splitContent(content) {
|
||||
let start = 0;
|
||||
while (content[start] === '#') {
|
||||
start++;
|
||||
}
|
||||
|
||||
if (start > 0) {
|
||||
return [content.slice(0, start), content.slice(start)];
|
||||
}
|
||||
return [null, content];
|
||||
}
|
||||
|
||||
const TabListItem = ({
|
||||
target,
|
||||
content,
|
||||
server,
|
||||
network,
|
||||
selected,
|
||||
connected,
|
||||
joined,
|
||||
@ -12,16 +24,21 @@ const TabListItem = ({
|
||||
onClick
|
||||
}) => {
|
||||
const className = classnames({
|
||||
'tab-server': !target,
|
||||
'tab-network': !target,
|
||||
success: !target && connected,
|
||||
error: (!target && !connected) || (!joined && error),
|
||||
disabled: !!target && !error && joined === false,
|
||||
selected
|
||||
});
|
||||
|
||||
const [prefix, name] = splitContent(content);
|
||||
|
||||
return (
|
||||
<p className={className} onClick={() => onClick(server, target)}>
|
||||
<span className="tab-content">{content}</span>
|
||||
<p className={className} onClick={() => onClick(network, target)}>
|
||||
<span className="tab-content">
|
||||
{prefix && <span className="tab-prefix">{prefix}</span>}
|
||||
{name}
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
72
client/js/components/Text.js
Normal file
72
client/js/components/Text.js
Normal file
@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import stringToRGB from 'utils/color';
|
||||
|
||||
function nickStyle(nick, color) {
|
||||
const style = {
|
||||
fontWeight: 400
|
||||
};
|
||||
|
||||
if (color) {
|
||||
style.color = stringToRGB(nick);
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
function renderBlock(block, coloredNick, key) {
|
||||
switch (block.type) {
|
||||
case 'text':
|
||||
return block.text;
|
||||
|
||||
case 'link':
|
||||
return (
|
||||
<a target="_blank" rel="noopener noreferrer" href={block.url} key={key}>
|
||||
{block.text}
|
||||
</a>
|
||||
);
|
||||
|
||||
case 'format':
|
||||
return (
|
||||
<span style={block.style} key={key}>
|
||||
{block.text}
|
||||
</span>
|
||||
);
|
||||
|
||||
case 'nick':
|
||||
return (
|
||||
<span
|
||||
className="message-sender"
|
||||
style={nickStyle(block.text, coloredNick)}
|
||||
key={key}
|
||||
>
|
||||
{block.text}
|
||||
</span>
|
||||
);
|
||||
|
||||
case 'events':
|
||||
return (
|
||||
<span className="message-events-more" key={key}>
|
||||
{block.text}
|
||||
</span>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const Text = ({ children, coloredNick }) => {
|
||||
if (!children) {
|
||||
return null;
|
||||
}
|
||||
if (children.length > 1) {
|
||||
let key = 0;
|
||||
return children.map(block => renderBlock(block, coloredNick, key++));
|
||||
}
|
||||
if (children.length === 1) {
|
||||
return renderBlock(children[0], coloredNick);
|
||||
}
|
||||
return children;
|
||||
};
|
||||
|
||||
export default Text;
|
@ -2,17 +2,18 @@ import React, { memo, useState, useEffect, useRef } from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { FiUsers, FiX } from 'react-icons/fi';
|
||||
import Text from 'components/Text';
|
||||
import useModal from 'components/modals/useModal';
|
||||
import Button from 'components/ui/Button';
|
||||
import { join } from 'state/channels';
|
||||
import { select } from 'state/tab';
|
||||
import { searchChannels } from 'state/channelSearch';
|
||||
import { linkify } from 'utils';
|
||||
import colorify from 'utils/colorify';
|
||||
|
||||
const Channel = memo(({ server, name, topic, userCount, joined }) => {
|
||||
const Channel = memo(({ network, name, topic, userCount, joined }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleClick = () => dispatch(join([name], server));
|
||||
const handleClick = () => dispatch(join([name], network));
|
||||
|
||||
return (
|
||||
<div className="modal-channel-result">
|
||||
@ -34,13 +35,15 @@ const Channel = memo(({ server, name, topic, userCount, joined }) => {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="modal-channel-topic">{linkify(topic)}</p>
|
||||
<p className="modal-channel-topic">
|
||||
<Text>{colorify(linkify(topic))}</Text>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const AddChannel = () => {
|
||||
const [modal, server, closeModal] = useModal('channel');
|
||||
const [modal, network, closeModal] = useModal('channel');
|
||||
|
||||
const channels = useSelector(state => state.channels);
|
||||
const search = useSelector(state => state.channelSearch);
|
||||
@ -53,9 +56,10 @@ const AddChannel = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (modal.isOpen) {
|
||||
dispatch(searchChannels(server, ''));
|
||||
dispatch(searchChannels(network, ''));
|
||||
setTimeout(() => inputEl.current.focus(), 0);
|
||||
} else {
|
||||
prevSearch.current = '';
|
||||
setQ('');
|
||||
}
|
||||
}, [modal.isOpen]);
|
||||
@ -73,7 +77,7 @@ const AddChannel = () => {
|
||||
|
||||
if (nextQ !== prevSearch.current) {
|
||||
prevSearch.current = nextQ;
|
||||
dispatch(searchChannels(server, nextQ));
|
||||
dispatch(searchChannels(network, nextQ));
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -89,14 +93,13 @@ const AddChannel = () => {
|
||||
channel = `#${channel}`;
|
||||
}
|
||||
|
||||
dispatch(join([channel], server));
|
||||
dispatch(select(server, channel));
|
||||
dispatch(join([channel], network));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadMore = () =>
|
||||
dispatch(searchChannels(server, q, search.results.length));
|
||||
dispatch(searchChannels(network, q, search.results.length));
|
||||
|
||||
let hasMore = !search.end;
|
||||
if (hasMore) {
|
||||
@ -130,9 +133,9 @@ const AddChannel = () => {
|
||||
<div ref={resultsEl} className="modal-channel-results">
|
||||
{search.results.map(channel => (
|
||||
<Channel
|
||||
key={`${server} ${channel.name}`}
|
||||
server={server}
|
||||
joined={channels[server]?.[channel.name]?.joined}
|
||||
key={`${network} ${channel.name}`}
|
||||
network={network}
|
||||
joined={channels[network]?.[channel.name]?.joined}
|
||||
{...channel}
|
||||
/>
|
||||
))}
|
||||
|
@ -2,10 +2,12 @@ import React from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { FiX } from 'react-icons/fi';
|
||||
import Text from 'components/Text';
|
||||
import Button from 'components/ui/Button';
|
||||
import useModal from 'components/modals/useModal';
|
||||
import { getSelectedChannel } from 'state/channels';
|
||||
import { linkify } from 'utils';
|
||||
import colorify from 'utils/colorify';
|
||||
|
||||
const Topic = () => {
|
||||
const [modal, channel, closeModal] = useModal('topic');
|
||||
@ -18,7 +20,9 @@ const Topic = () => {
|
||||
<h2>Topic in {channel}</h2>
|
||||
<Button icon={FiX} className="modal-close" onClick={closeModal} />
|
||||
</div>
|
||||
<p className="modal-content">{linkify(topic)}</p>
|
||||
<p className="modal-content">
|
||||
<Text>{colorify(linkify(topic))}</Text>
|
||||
</p>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
@ -11,40 +11,40 @@ export default class Chat extends Component {
|
||||
const { tab, part, closePrivateChat, disconnect } = this.props;
|
||||
|
||||
if (isChannel(tab)) {
|
||||
part([tab.name], tab.server);
|
||||
part([tab.name], tab.network);
|
||||
} else if (tab.name) {
|
||||
closePrivateChat(tab.server, tab.name);
|
||||
closePrivateChat(tab.network, tab.name);
|
||||
} else {
|
||||
disconnect(tab.server);
|
||||
disconnect(tab.network);
|
||||
}
|
||||
};
|
||||
|
||||
handleSearch = phrase => {
|
||||
const { tab, searchMessages } = this.props;
|
||||
if (isChannel(tab)) {
|
||||
searchMessages(tab.server, tab.name, phrase);
|
||||
searchMessages(tab.network, tab.name, phrase);
|
||||
}
|
||||
};
|
||||
|
||||
handleNickClick = nick => {
|
||||
const { tab, openPrivateChat, select } = this.props;
|
||||
openPrivateChat(tab.server, nick);
|
||||
select(tab.server, nick);
|
||||
openPrivateChat(tab.network, nick);
|
||||
select(tab.network, nick);
|
||||
};
|
||||
|
||||
handleTitleChange = title => {
|
||||
const { setServerName, tab } = this.props;
|
||||
setServerName(title, tab.server);
|
||||
const { setNetworkName, tab } = this.props;
|
||||
setNetworkName(title, tab.network);
|
||||
};
|
||||
|
||||
handleNickChange = nick => {
|
||||
const { setNick, tab } = this.props;
|
||||
setNick(nick, tab.server, true);
|
||||
setNick(nick, tab.network, true);
|
||||
};
|
||||
|
||||
handleNickEditDone = nick => {
|
||||
const { setNick, tab } = this.props;
|
||||
setNick(nick, tab.server);
|
||||
setNick(nick, tab.network);
|
||||
};
|
||||
|
||||
render() {
|
||||
@ -57,7 +57,7 @@ export default class Chat extends Component {
|
||||
nick,
|
||||
search,
|
||||
showUserList,
|
||||
status,
|
||||
error,
|
||||
tab,
|
||||
title,
|
||||
users,
|
||||
@ -77,14 +77,14 @@ export default class Chat extends Component {
|
||||
} else if (tab.name) {
|
||||
chatClass = 'chat-private';
|
||||
} else {
|
||||
chatClass = 'chat-server';
|
||||
chatClass = 'chat-network';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={chatClass}>
|
||||
<ChatTitle
|
||||
channel={channel}
|
||||
status={status}
|
||||
error={error}
|
||||
tab={tab}
|
||||
title={title}
|
||||
openModal={openModal}
|
||||
|
@ -3,11 +3,11 @@ import { FiUsers, FiSearch, FiX } from 'react-icons/fi';
|
||||
import Navicon from 'components/ui/Navicon';
|
||||
import Button from 'components/ui/Button';
|
||||
import Editable from 'components/ui/Editable';
|
||||
import { isValidServerName } from 'state/servers';
|
||||
import { isValidNetworkName } from 'state/networks';
|
||||
import { isChannel } from 'utils';
|
||||
|
||||
const ChatTitle = ({
|
||||
status,
|
||||
error,
|
||||
title,
|
||||
tab,
|
||||
channel,
|
||||
@ -26,11 +26,9 @@ const ChatTitle = ({
|
||||
closeTitle = 'Disconnect';
|
||||
}
|
||||
|
||||
let serverError = null;
|
||||
if (!tab.name && status.error) {
|
||||
serverError = (
|
||||
<span className="chat-topic error">Error: {status.error}</span>
|
||||
);
|
||||
let networkError = null;
|
||||
if (!tab.name && error) {
|
||||
networkError = <span className="chat-topic error">Error: {error}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -41,13 +39,13 @@ const ChatTitle = ({
|
||||
className="chat-title"
|
||||
editable={!tab.name}
|
||||
value={title}
|
||||
validate={isValidServerName}
|
||||
validate={isValidNetworkName}
|
||||
onChange={onTitleChange}
|
||||
>
|
||||
<span className="chat-title">{title}</span>
|
||||
</Editable>
|
||||
<div className="chat-topic-wrap">
|
||||
{channel && channel.topic && (
|
||||
{channel?.topic && (
|
||||
<span
|
||||
className="chat-topic"
|
||||
onClick={() => openModal('topic', channel.name)}
|
||||
@ -55,21 +53,32 @@ const ChatTitle = ({
|
||||
{channel.topic}
|
||||
</span>
|
||||
)}
|
||||
{serverError}
|
||||
{networkError}
|
||||
</div>
|
||||
{tab.name && (
|
||||
<Button icon={FiSearch} title="Search" onClick={onToggleSearch} />
|
||||
<Button
|
||||
icon={FiSearch}
|
||||
title="Search"
|
||||
aria-label="Search"
|
||||
onClick={onToggleSearch}
|
||||
/>
|
||||
)}
|
||||
<Button icon={FiX} title={closeTitle} onClick={onCloseClick} />
|
||||
<Button
|
||||
icon={FiX}
|
||||
title={closeTitle}
|
||||
aria-label={closeTitle}
|
||||
onClick={onCloseClick}
|
||||
/>
|
||||
<Button
|
||||
icon={FiUsers}
|
||||
className="button-userlist"
|
||||
aria-label="Users"
|
||||
onClick={onToggleUserList}
|
||||
/>
|
||||
</div>
|
||||
<div className="userlist-bar">
|
||||
<FiUsers />
|
||||
{channel && channel.users.length}
|
||||
{channel?.users.length}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,25 +1,25 @@
|
||||
import React, { memo } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Text from 'components/Text';
|
||||
import stringToRGB from 'utils/color';
|
||||
|
||||
const Message = ({ message, coloredNick, style, onNickClick }) => {
|
||||
const Message = ({ message, coloredNick, onNickClick }) => {
|
||||
const className = classnames('message', {
|
||||
[`message-${message.type}`]: message.type
|
||||
});
|
||||
|
||||
if (message.type === 'date') {
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
<div className={className}>
|
||||
{message.content}
|
||||
<hr />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
style = {
|
||||
...style,
|
||||
paddingLeft: `${window.messageIndent + 15}px`,
|
||||
textIndent: `-${window.messageIndent}px`
|
||||
const style = {
|
||||
paddingLeft: `${message.indent + 15}px`,
|
||||
textIndent: `-${message.indent}px`
|
||||
};
|
||||
|
||||
const senderStyle = {};
|
||||
@ -39,7 +39,10 @@ const Message = ({ message, coloredNick, style, onNickClick }) => {
|
||||
{message.from}
|
||||
</span>
|
||||
)}
|
||||
<span> {message.content}</span>
|
||||
<span>
|
||||
{' '}
|
||||
<Text coloredNick={coloredNick}>{message.content}</Text>
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
@ -36,7 +36,7 @@ export default class MessageBox extends PureComponent {
|
||||
addMore = debounce(() => {
|
||||
const { tab, onAddMore } = this.props;
|
||||
this.ready = true;
|
||||
onAddMore(tab.server, tab.name);
|
||||
onAddMore(tab.network, tab.name);
|
||||
}, scrollbackDebounce);
|
||||
|
||||
constructor(props) {
|
||||
@ -130,7 +130,7 @@ export default class MessageBox extends PureComponent {
|
||||
|
||||
updateScrollKey = () => {
|
||||
const { tab } = this.props;
|
||||
this.scrollKey = `msg:${tab.server}:${tab.name}`;
|
||||
this.scrollKey = `msg:${tab.network}:${tab.name}`;
|
||||
return this.scrollKey;
|
||||
};
|
||||
|
||||
@ -222,7 +222,7 @@ export default class MessageBox extends PureComponent {
|
||||
if (this.shouldAdd) {
|
||||
const { tab, onAddMore } = this.props;
|
||||
this.shouldAdd = false;
|
||||
onAddMore(tab.server, tab.name);
|
||||
onAddMore(tab.network, tab.name);
|
||||
}
|
||||
};
|
||||
|
||||
@ -247,12 +247,13 @@ export default class MessageBox extends PureComponent {
|
||||
const message = messages[index - 1];
|
||||
|
||||
return (
|
||||
<Message
|
||||
message={message}
|
||||
coloredNick={coloredNicks}
|
||||
style={style}
|
||||
onNickClick={onNickClick}
|
||||
/>
|
||||
<div style={style}>
|
||||
<Message
|
||||
message={message}
|
||||
coloredNick={coloredNicks}
|
||||
onNickClick={onNickClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -21,9 +21,9 @@ const MessageInput = ({
|
||||
const handleKey = e => {
|
||||
if (e.key === 'Enter' && e.target.value) {
|
||||
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) {
|
||||
onMessage(e.target.value, tab.name, tab.server);
|
||||
onMessage(e.target.value, tab.name, tab.network);
|
||||
}
|
||||
|
||||
add(e.target.value);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { memo } from 'react';
|
||||
import Text from 'components/Text';
|
||||
import { timestamp, linkify } from 'utils';
|
||||
|
||||
const SearchResult = ({ result }) => {
|
||||
@ -16,7 +17,10 @@ const SearchResult = ({ result }) => {
|
||||
{' '}
|
||||
<span className="message-sender">{result.from}</span>
|
||||
</span>
|
||||
<span> {linkify(result.content)}</span>
|
||||
<span>
|
||||
{' '}
|
||||
<Text>{linkify(result.content)}</Text>
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
@ -1,19 +1,39 @@
|
||||
import React, { Component } from 'react';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Form, withFormik } from 'formik';
|
||||
import { FiMoreHorizontal } from 'react-icons/fi';
|
||||
import Navicon from 'components/ui/Navicon';
|
||||
import Button from 'components/ui/Button';
|
||||
import Checkbox from 'components/ui/formik/Checkbox';
|
||||
import TextInput from 'components/ui/TextInput';
|
||||
import Error from 'components/ui/formik/Error';
|
||||
import { isValidNick, isValidChannel, isValidUsername, isInt } from 'utils';
|
||||
import { FiMoreHorizontal } from 'react-icons/fi';
|
||||
|
||||
const getSortedDefaultChannels = createSelector(
|
||||
defaults => defaults.channels,
|
||||
channels => channels.split(',').sort()
|
||||
);
|
||||
|
||||
const transformChannels = channels => {
|
||||
const comma = channels[channels.length - 1] === ',';
|
||||
|
||||
channels = channels
|
||||
.split(',')
|
||||
.map(channel => {
|
||||
channel = channel.trim();
|
||||
if (channel) {
|
||||
if (isValidChannel(channel, false) && channel[0] !== '#') {
|
||||
channel = `#${channel}`;
|
||||
}
|
||||
}
|
||||
return channel;
|
||||
})
|
||||
.filter(s => s)
|
||||
.join(',');
|
||||
|
||||
return comma ? `${channels},` : channels;
|
||||
};
|
||||
|
||||
class Connect extends Component {
|
||||
state = {
|
||||
showOptionals: false
|
||||
@ -21,10 +41,10 @@ class Connect extends Component {
|
||||
|
||||
handleSSLChange = e => {
|
||||
const { values, setFieldValue } = this.props;
|
||||
if (e.target.checked && values.port === 6667) {
|
||||
setFieldValue('port', 6697, false);
|
||||
} else if (!e.target.checked && values.port === 6697) {
|
||||
setFieldValue('port', 6667, false);
|
||||
if (e.target.checked && values.port === '6667') {
|
||||
setFieldValue('port', '6697', false);
|
||||
} else if (!e.target.checked && values.port === '6697') {
|
||||
setFieldValue('port', '6667', false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -36,41 +56,31 @@ class Connect extends Component {
|
||||
const { hexIP } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<div className="connect-section">
|
||||
<h2>SASL</h2>
|
||||
<TextInput name="account" />
|
||||
<TextInput name="password" type="password" noTrim />
|
||||
</div>
|
||||
{!hexIP && <TextInput name="username" />}
|
||||
<TextInput name="password" type="password" noTrim />
|
||||
<TextInput
|
||||
name="serverPassword"
|
||||
label="Server Password"
|
||||
type="password"
|
||||
noTrim
|
||||
/>
|
||||
<TextInput name="realname" noTrim />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
transformPort = port => {
|
||||
if (!port) {
|
||||
return this.props.values.tls ? 6697 : 6667;
|
||||
return this.props.values.tls ? '6697' : '6667';
|
||||
}
|
||||
return port;
|
||||
};
|
||||
|
||||
transformChannels = channels => {
|
||||
const comma = channels[channels.length - 1] === ',';
|
||||
|
||||
channels = channels
|
||||
.split(',')
|
||||
.map(channel => {
|
||||
channel = channel.trim();
|
||||
if (channel) {
|
||||
if (isValidChannel(channel, false) && channel[0] !== '#') {
|
||||
channel = `#${channel}`;
|
||||
}
|
||||
}
|
||||
return channel;
|
||||
})
|
||||
.filter(s => s)
|
||||
.join(',');
|
||||
|
||||
return comma ? `${channels},` : channels;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { defaults, values } = this.props;
|
||||
const { readOnly, showDetails } = defaults;
|
||||
@ -108,6 +118,7 @@ class Connect extends Component {
|
||||
noError
|
||||
/>
|
||||
<Checkbox
|
||||
classNameLabel="connect-form-ssl"
|
||||
name="tls"
|
||||
label="SSL"
|
||||
topLabel
|
||||
@ -117,11 +128,12 @@ class Connect extends Component {
|
||||
<Error name="host" />
|
||||
<Error name="port" />
|
||||
<TextInput name="nick" />
|
||||
<TextInput name="channels" transform={this.transformChannels} />
|
||||
<TextInput name="channels" transform={transformChannels} />
|
||||
{this.state.showOptionals && this.renderOptionals()}
|
||||
<Button
|
||||
className="connect-form-button-optionals"
|
||||
icon={FiMoreHorizontal}
|
||||
aria-label="Show more"
|
||||
onClick={this.handleShowClick}
|
||||
/>
|
||||
<Button type="submit">Connect</Button>
|
||||
@ -140,24 +152,40 @@ class Connect extends Component {
|
||||
|
||||
export default withFormik({
|
||||
enableReinitialize: true,
|
||||
mapPropsToValues: ({ defaults }) => {
|
||||
let port = 6667;
|
||||
if (defaults.port) {
|
||||
({ port } = defaults);
|
||||
mapPropsToValues: ({ defaults, query }) => {
|
||||
let port = '6667';
|
||||
if (query.port || defaults.port) {
|
||||
port = query.port || defaults.port;
|
||||
} else if (defaults.ssl) {
|
||||
port = 6697;
|
||||
port = '6697';
|
||||
}
|
||||
|
||||
let { channels } = query;
|
||||
if (channels) {
|
||||
channels = transformChannels(channels);
|
||||
}
|
||||
|
||||
let ssl;
|
||||
if (query.ssl === 'true') {
|
||||
ssl = true;
|
||||
} else if (query.ssl === 'false') {
|
||||
ssl = false;
|
||||
} else {
|
||||
ssl = defaults.ssl || false;
|
||||
}
|
||||
|
||||
return {
|
||||
name: defaults.name,
|
||||
host: defaults.host,
|
||||
name: query.name || defaults.name,
|
||||
host: query.host || defaults.host,
|
||||
port,
|
||||
nick: '',
|
||||
channels: defaults.channels.join(','),
|
||||
username: '',
|
||||
password: defaults.password ? ' ' : '',
|
||||
realname: '',
|
||||
tls: defaults.ssl || false
|
||||
nick: query.nick || localStorage.lastNick || '',
|
||||
channels: channels || defaults.channels.join(','),
|
||||
account: '',
|
||||
password: '',
|
||||
username: query.username || '',
|
||||
serverPassword: defaults.serverPassword ? ' ' : '',
|
||||
realname: query.realname || localStorage.lastRealname || '',
|
||||
tls: ssl
|
||||
};
|
||||
},
|
||||
validate: values => {
|
||||
@ -208,7 +236,12 @@ export default withFormik({
|
||||
select(values.host);
|
||||
|
||||
if (channels.length > 0) {
|
||||
join(channels, values.host);
|
||||
join(channels, values.host, false);
|
||||
}
|
||||
|
||||
localStorage.lastNick = values.nick;
|
||||
if (values.realname) {
|
||||
localStorage.lastRealname = values.realname;
|
||||
}
|
||||
}
|
||||
})(Connect);
|
||||
|
@ -41,7 +41,7 @@ const Settings = ({
|
||||
<Checkbox
|
||||
name="coloredNicks"
|
||||
label="Colored nicks"
|
||||
checked={settings.coloredNicks}
|
||||
checked={!!settings.coloredNicks}
|
||||
onChange={e => setSetting('coloredNicks', e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const Checkbox = ({ name, label, topLabel, ...props }) => (
|
||||
const Checkbox = ({ name, label, topLabel, classNameLabel, ...props }) => (
|
||||
<label
|
||||
className={classnames('checkbox', {
|
||||
className={classnames('checkbox', classNameLabel, {
|
||||
'top-label': topLabel
|
||||
})}
|
||||
htmlFor={name}
|
||||
|
@ -4,24 +4,6 @@ import classnames from 'classnames';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import Error from 'components/ui/formik/Error';
|
||||
|
||||
const getValue = (e, trim) => {
|
||||
let v = e.target.value;
|
||||
|
||||
if (trim) {
|
||||
v = v.trim();
|
||||
}
|
||||
|
||||
if (e.target.type === 'number') {
|
||||
v = parseFloat(v);
|
||||
/* eslint-disable-next-line no-self-compare */
|
||||
if (v !== v) {
|
||||
v = '';
|
||||
}
|
||||
}
|
||||
|
||||
return v;
|
||||
};
|
||||
|
||||
export default class TextInput extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -76,6 +58,7 @@ export default class TextInput extends PureComponent {
|
||||
className={field.value && 'value'}
|
||||
type="text"
|
||||
name={name}
|
||||
id={name}
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
autoComplete="off"
|
||||
@ -85,7 +68,12 @@ export default class TextInput extends PureComponent {
|
||||
{...field}
|
||||
{...props}
|
||||
onChange={e => {
|
||||
let v = getValue(e, !noTrim);
|
||||
let v = e.target.value;
|
||||
|
||||
if (!noTrim) {
|
||||
v = v.trim();
|
||||
}
|
||||
|
||||
if (transform) {
|
||||
v = transform(v);
|
||||
}
|
||||
@ -99,30 +87,31 @@ export default class TextInput extends PureComponent {
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
if (blurTransform) {
|
||||
const v = blurTransform(getValue(e));
|
||||
|
||||
if (v && v !== field.value) {
|
||||
form.setFieldValue(name, v, false);
|
||||
}
|
||||
}
|
||||
|
||||
field.onBlur(e);
|
||||
if (props.onBlur) {
|
||||
props.onBlur(e);
|
||||
}
|
||||
|
||||
if (blurTransform) {
|
||||
const v = blurTransform(e.target.value);
|
||||
|
||||
if (v && v !== field.value) {
|
||||
form.setFieldValue(name, v);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className={classnames('textinput-1', {
|
||||
<label
|
||||
htmlFor={name}
|
||||
className={classnames('textinput-label', 'textinput-1', {
|
||||
value: field.value,
|
||||
error: form.touched[name] && form.errors[name]
|
||||
})}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</label>
|
||||
<span
|
||||
className={classnames('textinput-2', {
|
||||
className={classnames('textinput-label', 'textinput-2', {
|
||||
value: field.value,
|
||||
error: form.touched[name] && form.errors[name]
|
||||
})}
|
||||
|
@ -4,7 +4,7 @@ import { getConnected } from 'state/app';
|
||||
import { getSortedChannels } from 'state/channels';
|
||||
import { openModal, getHasOpenModals } from 'state/modals';
|
||||
import { getPrivateChats } from 'state/privateChats';
|
||||
import { getServers } from 'state/servers';
|
||||
import { getNetworks } from 'state/networks';
|
||||
import { getSelectedTab, select } from 'state/tab';
|
||||
import { getShowTabList, hideMenu } from 'state/ui';
|
||||
import connect from 'utils/connect';
|
||||
@ -14,7 +14,7 @@ const mapState = createStructuredSelector({
|
||||
channels: getSortedChannels,
|
||||
connected: getConnected,
|
||||
privateChats: getPrivateChats,
|
||||
servers: getServers,
|
||||
networks: getNetworks,
|
||||
showTabList: getShowTabList,
|
||||
tab: getSelectedTab,
|
||||
newVersionAvailable: state => state.app.newVersionAvailable,
|
||||
|
@ -27,11 +27,11 @@ import { openPrivateChat, closePrivateChat } from 'state/privateChats';
|
||||
import { getSearch, searchMessages, toggleSearch } from 'state/search';
|
||||
import {
|
||||
getCurrentNick,
|
||||
getCurrentServerStatus,
|
||||
getCurrentNetworkError,
|
||||
disconnect,
|
||||
setNick,
|
||||
setServerName
|
||||
} from 'state/servers';
|
||||
setNetworkName
|
||||
} from 'state/networks';
|
||||
import { getSettings } from 'state/settings';
|
||||
import { getSelectedTab, select } from 'state/tab';
|
||||
import { getShowUserList, toggleUserList } from 'state/ui';
|
||||
@ -45,7 +45,7 @@ const mapState = createStructuredSelector({
|
||||
nick: getCurrentNick,
|
||||
search: getSearch,
|
||||
showUserList: getShowUserList,
|
||||
status: getCurrentServerStatus,
|
||||
error: getCurrentNetworkError,
|
||||
tab: getSelectedTab,
|
||||
title: getSelectedTabTitle,
|
||||
users: getSelectedChannelUsers,
|
||||
@ -67,7 +67,7 @@ const mapDispatch = dispatch => ({
|
||||
select,
|
||||
sendMessage,
|
||||
setNick,
|
||||
setServerName,
|
||||
setNetworkName,
|
||||
toggleSearch,
|
||||
toggleUserList
|
||||
},
|
||||
|
@ -2,18 +2,19 @@ import { createStructuredSelector } from 'reselect';
|
||||
import Connect from 'components/pages/Connect';
|
||||
import { getConnectDefaults, getApp } from 'state/app';
|
||||
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 connect from 'utils/connect';
|
||||
|
||||
const mapState = createStructuredSelector({
|
||||
defaults: getConnectDefaults,
|
||||
hexIP: state => getApp(state).hexIP
|
||||
hexIP: state => getApp(state).hexIP,
|
||||
query: state => state.router.query
|
||||
});
|
||||
|
||||
const mapDispatch = {
|
||||
join,
|
||||
connect: connectServer,
|
||||
connect: connectNetwork,
|
||||
select
|
||||
};
|
||||
|
||||
|
@ -4,8 +4,8 @@ import TabListItem from 'components/TabListItem';
|
||||
import connect from 'utils/connect';
|
||||
|
||||
const mapState = createStructuredSelector({
|
||||
error: (state, { server, target }) => {
|
||||
const messages = get(state, ['messages', server, target]);
|
||||
error: (state, { network, target }) => {
|
||||
const messages = get(state, ['messages', network, target]);
|
||||
|
||||
if (messages && messages.length > 0) {
|
||||
return messages[messages.length - 1].type === 'error';
|
||||
|
@ -4,22 +4,27 @@ import { isChannel } from 'utils';
|
||||
export const beforeHandler = '_before';
|
||||
export const notFoundHandler = 'commandNotFound';
|
||||
|
||||
function createContext({ dispatch, getState }, { server, channel }) {
|
||||
return { dispatch, getState, server, channel, isChannel: isChannel(channel) };
|
||||
function createContext({ dispatch, getState }, { network, channel }) {
|
||||
return {
|
||||
dispatch,
|
||||
getState,
|
||||
network,
|
||||
channel,
|
||||
inChannel: isChannel(channel)
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Pull this out as convenience action
|
||||
function process({ dispatch, server, channel }, result) {
|
||||
function process({ dispatch, network, channel }, result) {
|
||||
if (typeof result === 'string') {
|
||||
dispatch(inform(result, server, channel));
|
||||
dispatch(inform(result, network, channel));
|
||||
} else if (Array.isArray(result)) {
|
||||
if (typeof result[0] === 'string') {
|
||||
dispatch(inform(result, server, channel));
|
||||
dispatch(inform(result, network, channel));
|
||||
} else if (typeof result[0] === 'object') {
|
||||
dispatch(addMessages(result, server, channel));
|
||||
dispatch(addMessages(result, network, channel));
|
||||
}
|
||||
} else if (typeof result === 'object' && result) {
|
||||
dispatch(print(result.content, server, channel, result.type));
|
||||
dispatch(print(result.content, network, channel, result.type));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ const message = store => next => {
|
||||
|
||||
return action => {
|
||||
if (action.type === ADD_MESSAGES && action.prepend) {
|
||||
const key = `${action.server} ${action.channel}`;
|
||||
const key = `${action.network} ${action.channel}`;
|
||||
|
||||
if (ready[key]) {
|
||||
ready[key] = false;
|
||||
@ -19,7 +19,7 @@ const message = store => next => {
|
||||
|
||||
cache[key] = action;
|
||||
} else if (action.type === ADD_FETCHED_MESSAGES) {
|
||||
const key = `${action.server} ${action.channel}`;
|
||||
const key = `${action.network} ${action.channel}`;
|
||||
ready[key] = true;
|
||||
|
||||
if (cache[key]) {
|
||||
|
@ -1,18 +1,18 @@
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import { getRouter } from 'state';
|
||||
import { getCurrentServerName } from 'state/servers';
|
||||
import { getCurrentNetworkName } from 'state/networks';
|
||||
import { observe } from 'utils/observe';
|
||||
|
||||
export default function documentTitle({ store }) {
|
||||
observe(store, [getRouter, getCurrentServerName], (router, serverName) => {
|
||||
observe(store, [getRouter, getCurrentNetworkName], (router, networkName) => {
|
||||
let title;
|
||||
|
||||
if (router.route === 'chat') {
|
||||
const { server, name } = router.params;
|
||||
const { network, name } = router.params;
|
||||
if (name) {
|
||||
title = `${name} @ ${serverName || server}`;
|
||||
title = `${name} @ ${networkName || network}`;
|
||||
} else {
|
||||
title = serverName || server;
|
||||
title = networkName || network;
|
||||
}
|
||||
} else {
|
||||
title = capitalize(router.route);
|
||||
|
@ -1,22 +1,15 @@
|
||||
import FontFaceObserver from 'fontfaceobserver';
|
||||
import { setCharWidth } from 'state/app';
|
||||
import { stringWidth } from 'utils';
|
||||
|
||||
export default function fonts({ store }) {
|
||||
export default async function fonts({ store }) {
|
||||
let { charWidth } = localStorage;
|
||||
if (charWidth) {
|
||||
store.dispatch(setCharWidth(parseFloat(charWidth)));
|
||||
} else {
|
||||
await document.fonts.load('16px Roboto Mono');
|
||||
|
||||
charWidth = stringWidth(' ', '16px Roboto Mono');
|
||||
store.dispatch(setCharWidth(charWidth));
|
||||
localStorage.charWidth = charWidth;
|
||||
}
|
||||
|
||||
new FontFaceObserver('Roboto Mono').load().then(() => {
|
||||
if (!charWidth) {
|
||||
charWidth = stringWidth(' ', '16px Roboto Mono');
|
||||
store.dispatch(setCharWidth(charWidth));
|
||||
localStorage.charWidth = charWidth;
|
||||
}
|
||||
});
|
||||
|
||||
new FontFaceObserver('Montserrat').load();
|
||||
new FontFaceObserver('Montserrat', { weight: 700 }).load();
|
||||
new FontFaceObserver('Roboto Mono', { weight: 700 }).load();
|
||||
}
|
||||
|
@ -1,58 +1,42 @@
|
||||
import { socket as socketActions } from 'state/actions';
|
||||
import { getConnected, getWrapWidth, appSet } from 'state/app';
|
||||
import { INIT } from 'state/actions';
|
||||
import { getConnected, getWrapWidth } from 'state/app';
|
||||
import { searchChannels } from 'state/channelSearch';
|
||||
import { addMessages } from 'state/messages';
|
||||
import { setSettings } from 'state/settings';
|
||||
import { when } from 'utils/observe';
|
||||
|
||||
function loadState({ store }, env) {
|
||||
store.dispatch(setSettings(env.settings, true));
|
||||
|
||||
if (env.servers) {
|
||||
store.dispatch({
|
||||
type: socketActions.SERVERS,
|
||||
data: env.servers
|
||||
});
|
||||
|
||||
when(store, getConnected, () =>
|
||||
// Cache top channels for each server
|
||||
env.servers.forEach(({ host }) =>
|
||||
store.dispatch(searchChannels(host, ''))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (env.channels) {
|
||||
store.dispatch({
|
||||
type: socketActions.CHANNELS,
|
||||
data: env.channels
|
||||
});
|
||||
}
|
||||
|
||||
if (env.users) {
|
||||
store.dispatch({
|
||||
type: socketActions.USERS,
|
||||
...env.users
|
||||
});
|
||||
}
|
||||
|
||||
store.dispatch(
|
||||
appSet({
|
||||
store.dispatch({
|
||||
type: INIT,
|
||||
settings: env.settings,
|
||||
networks: env.networks,
|
||||
channels: env.channels,
|
||||
openDMs: env.openDMs,
|
||||
users: env.users,
|
||||
app: {
|
||||
connectDefaults: env.defaults,
|
||||
initialized: true,
|
||||
hexIP: env.hexIP,
|
||||
version: env.version
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (env.messages) {
|
||||
// Wait until wrapWidth gets initialized so that height calculations
|
||||
// only happen once for these messages
|
||||
when(store, getWrapWidth, () => {
|
||||
const { messages, server, to, next } = env.messages;
|
||||
store.dispatch(addMessages(messages, server, to, false, next));
|
||||
const { messages, network, to, next } = env.messages;
|
||||
store.dispatch(addMessages(messages, network, to, false, next));
|
||||
});
|
||||
}
|
||||
|
||||
if (env.networks) {
|
||||
when(store, getConnected, () =>
|
||||
// Cache top channels for each network
|
||||
env.networks.forEach(({ host }) =>
|
||||
store.dispatch(searchChannels(host, ''))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
|
@ -1,26 +1,22 @@
|
||||
import { socketAction } from 'state/actions';
|
||||
import { setConnected } from 'state/app';
|
||||
import { kicked } from 'state/channels';
|
||||
import {
|
||||
broadcast,
|
||||
inform,
|
||||
print,
|
||||
addMessage,
|
||||
addMessages
|
||||
addMessages,
|
||||
addEvent,
|
||||
broadcastEvent
|
||||
} from 'state/messages';
|
||||
import { openModal } from 'state/modals';
|
||||
import { reconnect } from 'state/servers';
|
||||
import { reconnect } from 'state/networks';
|
||||
import { select } from 'state/tab';
|
||||
import { find } from 'utils';
|
||||
|
||||
function withReason(message, reason) {
|
||||
return message + (reason ? ` (${reason})` : '');
|
||||
}
|
||||
|
||||
function findChannels(state, server, user) {
|
||||
function findChannels(state, network, user) {
|
||||
const channels = [];
|
||||
|
||||
Object.keys(state.channels[server]).forEach(channel => {
|
||||
if (find(state.channels[server][channel].users, u => u.nick === user)) {
|
||||
Object.keys(state.channels[network]).forEach(channel => {
|
||||
if (find(state.channels[network][channel].users, u => u.nick === user)) {
|
||||
channels.push(channel);
|
||||
}
|
||||
});
|
||||
@ -34,59 +30,59 @@ export default function handleSocket({
|
||||
}) {
|
||||
const handlers = {
|
||||
message(message) {
|
||||
dispatch(addMessage(message, message.server, message.to));
|
||||
dispatch(addMessage(message, message.network, message.to));
|
||||
return false;
|
||||
},
|
||||
|
||||
pm(message) {
|
||||
dispatch(addMessage(message, message.server, message.from));
|
||||
dispatch(addMessage(message, message.network, message.from));
|
||||
return false;
|
||||
},
|
||||
|
||||
messages({ messages, server, to, prepend, next }) {
|
||||
dispatch(addMessages(messages, server, to, prepend, next));
|
||||
messages({ messages, network, to, prepend, next }) {
|
||||
dispatch(addMessages(messages, network, to, prepend, next));
|
||||
return false;
|
||||
},
|
||||
|
||||
join({ user, server, channels }) {
|
||||
dispatch(inform(`${user} joined the channel`, server, channels[0]));
|
||||
join({ user, network, channels }) {
|
||||
dispatch(addEvent(network, channels[0], 'join', user));
|
||||
},
|
||||
|
||||
part({ user, server, channel, reason }) {
|
||||
dispatch(
|
||||
inform(withReason(`${user} left the channel`, reason), server, channel)
|
||||
);
|
||||
part({ user, network, channel, reason }) {
|
||||
dispatch(addEvent(network, channel, 'part', user, reason));
|
||||
},
|
||||
|
||||
quit({ user, server, reason }) {
|
||||
const channels = findChannels(getState(), server, user);
|
||||
dispatch(broadcast(withReason(`${user} quit`, reason), server, channels));
|
||||
quit({ user, network, reason }) {
|
||||
const channels = findChannels(getState(), network, user);
|
||||
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) {
|
||||
const channels = findChannels(getState(), server, oldNick);
|
||||
dispatch(
|
||||
broadcast(`${oldNick} changed nick to ${newNick}`, server, channels)
|
||||
);
|
||||
const channels = findChannels(getState(), network, oldNick);
|
||||
dispatch(broadcastEvent(network, channels, 'nick', oldNick, newNick));
|
||||
}
|
||||
},
|
||||
|
||||
topic({ server, channel, topic, nick }) {
|
||||
topic({ network, channel, topic, nick }) {
|
||||
if (nick) {
|
||||
if (topic) {
|
||||
dispatch(inform(`${nick} changed the topic to:`, server, channel));
|
||||
dispatch(print(topic, server, channel));
|
||||
} else {
|
||||
dispatch(inform(`${nick} cleared the topic`, server, channel));
|
||||
}
|
||||
dispatch(addEvent(network, channel, 'topic', nick, topic));
|
||||
}
|
||||
},
|
||||
|
||||
motd({ content, server }) {
|
||||
motd({ content, network }) {
|
||||
dispatch(
|
||||
addMessages(
|
||||
content.map(line => ({ content: line })),
|
||||
server
|
||||
network
|
||||
)
|
||||
);
|
||||
return false;
|
||||
},
|
||||
|
||||
whois(data) {
|
||||
@ -102,30 +98,47 @@ export default function handleSocket({
|
||||
`Server: ${data.server}`,
|
||||
`Channels: ${data.channels}`
|
||||
],
|
||||
tab.server,
|
||||
tab.network,
|
||||
tab.name
|
||||
)
|
||||
);
|
||||
return false;
|
||||
},
|
||||
|
||||
print(message) {
|
||||
const tab = getState().tab.selected;
|
||||
dispatch(addMessage(message, tab.server, tab.name));
|
||||
dispatch(addMessage(message, tab.network, tab.name));
|
||||
return false;
|
||||
},
|
||||
|
||||
error({ server, target, message }) {
|
||||
dispatch(addMessage({ content: message, type: 'error' }, server, target));
|
||||
error({ network, target, message }) {
|
||||
const state = getState();
|
||||
const tab = state.tab.selected;
|
||||
|
||||
if (network === tab.network) {
|
||||
// Print it in the current channel if the error happened on
|
||||
// the current network
|
||||
target = tab.name;
|
||||
} else if (!state.channels[network]?.[target]) {
|
||||
// Print it the network tab if the target does not exist
|
||||
target = null;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
addMessage({ content: message, type: 'error' }, network, target)
|
||||
);
|
||||
return false;
|
||||
},
|
||||
|
||||
connection_update({ server, errorType }) {
|
||||
connection_update({ network, errorType }) {
|
||||
if (errorType === 'verify') {
|
||||
dispatch(
|
||||
openModal('confirm', {
|
||||
question:
|
||||
'The server is using a self-signed certificate, continue anyway?',
|
||||
'The network is using a self-signed certificate, continue anyway?',
|
||||
onConfirm: () =>
|
||||
dispatch(
|
||||
reconnect(server, {
|
||||
reconnect(network, {
|
||||
skipVerify: true
|
||||
})
|
||||
)
|
||||
@ -134,8 +147,20 @@ export default function handleSocket({
|
||||
}
|
||||
},
|
||||
|
||||
_connected(connected) {
|
||||
dispatch(setConnected(connected));
|
||||
dcc_send({ network, from, filename, size, url }) {
|
||||
const networkName = getState().networks[network]?.name || network;
|
||||
|
||||
dispatch(
|
||||
openModal('confirm', {
|
||||
question: `${from} on ${networkName} is sending you (${size}): ${filename}`,
|
||||
confirmation: 'Download',
|
||||
onConfirm: () => {
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.click();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -143,8 +168,11 @@ export default function handleSocket({
|
||||
channel_forward(forward) {
|
||||
const { selected } = getState().tab;
|
||||
|
||||
if (selected.server === forward.server && selected.name === forward.old) {
|
||||
dispatch(select(forward.server, forward.new, true));
|
||||
if (
|
||||
selected.network === forward.network &&
|
||||
selected.name === forward.old
|
||||
) {
|
||||
dispatch(select(forward.network, forward.new, true));
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -157,18 +185,12 @@ export default function handleSocket({
|
||||
action = { ...data, type: socketAction(type) };
|
||||
}
|
||||
|
||||
if (type in handlers) {
|
||||
handlers[type](data);
|
||||
}
|
||||
|
||||
if (type.charAt(0) === '_') {
|
||||
if (handlers[type]?.(data) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(action);
|
||||
|
||||
if (type in afterHandlers) {
|
||||
afterHandlers[type](data);
|
||||
}
|
||||
afterHandlers[type]?.(data);
|
||||
});
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Cookie from 'js-cookie';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { getSelectedTab } from 'state/tab';
|
||||
import { isChannel, stringifyTab } from 'utils';
|
||||
import { stringifyTab } from 'utils';
|
||||
import { observe } from 'utils/observe';
|
||||
|
||||
const saveTab = debounce(
|
||||
@ -11,7 +11,7 @@ const saveTab = debounce(
|
||||
|
||||
export default function storage({ store }) {
|
||||
observe(store, getSelectedTab, tab => {
|
||||
if (isChannel(tab) || (tab.server && !tab.name)) {
|
||||
if (tab.network) {
|
||||
saveTab(tab);
|
||||
}
|
||||
});
|
||||
|
@ -10,7 +10,6 @@ const smallScreen = 600;
|
||||
|
||||
export default function widthUpdates({ store }) {
|
||||
when(store, getCharWidth, charWidth => {
|
||||
window.messageIndent = 6 * charWidth;
|
||||
const scrollBarWidth = measureScrollBarWidth();
|
||||
let prevWrapWidth;
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
export default {
|
||||
connect: '/connect',
|
||||
settings: '/settings',
|
||||
chat: '/:server(/:name)'
|
||||
chat: '/:network(/:name)'
|
||||
};
|
||||
|
20
client/js/state/__tests__/actions-networks.test.js
Normal file
20
client/js/state/__tests__/actions-networks.test.js
Normal 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();
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
import reducer, { compareUsers, getSortedChannels } from '../channels';
|
||||
import { connect } from '../servers';
|
||||
import { connect } from '../networks';
|
||||
import * as actions from '../actions';
|
||||
|
||||
describe('channel reducer', () => {
|
||||
@ -17,7 +17,7 @@ describe('channel reducer', () => {
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.PART,
|
||||
server: 'srv1',
|
||||
network: 'srv1',
|
||||
channels: ['chan1', 'chan3']
|
||||
});
|
||||
|
||||
@ -38,7 +38,7 @@ describe('channel reducer', () => {
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.socket.PART,
|
||||
server: 'srv',
|
||||
network: 'srv',
|
||||
channel: 'chan1',
|
||||
user: 'nick2'
|
||||
});
|
||||
@ -80,7 +80,7 @@ describe('channel reducer', () => {
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.socket.QUIT,
|
||||
server: 'srv',
|
||||
network: 'srv',
|
||||
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', () => {
|
||||
let state = reducer(undefined, socket_join('srv', 'chan1', 'nick1'));
|
||||
state = reducer(state, socket_join('srv', 'chan1', 'nick2'));
|
||||
@ -107,7 +168,7 @@ describe('channel reducer', () => {
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.socket.NICK,
|
||||
server: 'srv',
|
||||
network: 'srv',
|
||||
oldNick: 'nick1',
|
||||
newNick: 'nick3'
|
||||
});
|
||||
@ -135,7 +196,7 @@ describe('channel reducer', () => {
|
||||
let state = reducer(undefined, socket_join('srv', 'chan1', 'nick1'));
|
||||
state = reducer(state, {
|
||||
type: actions.socket.USERS,
|
||||
server: 'srv',
|
||||
network: 'srv',
|
||||
channel: 'chan1',
|
||||
users: ['user3', 'user2', '@user4', 'user1', '+user5']
|
||||
});
|
||||
@ -161,7 +222,7 @@ describe('channel reducer', () => {
|
||||
let state = reducer(undefined, socket_join('srv', 'chan1', 'nick1'));
|
||||
state = reducer(state, {
|
||||
type: actions.socket.TOPIC,
|
||||
server: 'srv',
|
||||
network: 'srv',
|
||||
channel: 'chan1',
|
||||
topic: 'the topic'
|
||||
});
|
||||
@ -215,31 +276,31 @@ describe('channel reducer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handles SOCKET_CHANNELS', () => {
|
||||
it('handles channels from INIT', () => {
|
||||
const state = reducer(undefined, {
|
||||
type: actions.socket.CHANNELS,
|
||||
data: [
|
||||
{ server: 'srv', name: 'chan1', topic: 'the topic' },
|
||||
{ server: 'srv', name: 'chan2' },
|
||||
{ server: 'srv2', name: 'chan1' }
|
||||
type: actions.INIT,
|
||||
channels: [
|
||||
{ network: 'srv', name: 'chan1', topic: 'the topic' },
|
||||
{ network: 'srv', name: 'chan2', joined: true },
|
||||
{ network: 'srv2', name: 'chan1' }
|
||||
]
|
||||
});
|
||||
|
||||
expect(state).toEqual({
|
||||
srv: {
|
||||
chan1: { name: 'chan1', joined: true, topic: 'the topic', users: [] },
|
||||
chan1: { name: 'chan1', topic: 'the topic', users: [] },
|
||||
chan2: { name: 'chan2', joined: true, users: [] }
|
||||
},
|
||||
srv2: {
|
||||
chan1: { name: 'chan1', joined: true, users: [] }
|
||||
chan1: { name: 'chan1', users: [] }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('handles SOCKET_SERVERS', () => {
|
||||
it('handles networks from INIT', () => {
|
||||
const state = reducer(undefined, {
|
||||
type: actions.socket.SERVERS,
|
||||
data: [{ host: '127.0.0.1' }, { host: 'thehost' }]
|
||||
type: actions.INIT,
|
||||
networks: [{ host: '127.0.0.1' }, { host: 'thehost' }]
|
||||
});
|
||||
|
||||
expect(state).toEqual({
|
||||
@ -248,7 +309,7 @@ describe('channel reducer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('optimistically adds the server on CONNECT', () => {
|
||||
it('optimistically adds the network on CONNECT', () => {
|
||||
const state = reducer(
|
||||
undefined,
|
||||
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 = {
|
||||
srv: {},
|
||||
srv2: {}
|
||||
@ -267,7 +328,7 @@ describe('channel reducer', () => {
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.DISCONNECT,
|
||||
server: 'srv2'
|
||||
network: 'srv2'
|
||||
});
|
||||
|
||||
expect(state).toEqual({
|
||||
@ -276,19 +337,19 @@ describe('channel reducer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
function socket_join(server, channel, user) {
|
||||
function socket_join(network, channel, user) {
|
||||
return {
|
||||
type: actions.socket.JOIN,
|
||||
server,
|
||||
network,
|
||||
user,
|
||||
channels: [channel]
|
||||
};
|
||||
}
|
||||
|
||||
function socket_mode(server, channel, user, add, remove) {
|
||||
function socket_mode(network, channel, user, add, remove) {
|
||||
return {
|
||||
type: actions.socket.MODE,
|
||||
server,
|
||||
network,
|
||||
channel,
|
||||
user,
|
||||
add,
|
||||
@ -323,7 +384,7 @@ describe('compareUsers()', () => {
|
||||
});
|
||||
|
||||
describe('getSortedChannels', () => {
|
||||
it('sorts servers and channels', () => {
|
||||
it('sorts networks and channels', () => {
|
||||
expect(
|
||||
getSortedChannels({
|
||||
channels: {
|
||||
|
@ -1,12 +1,13 @@
|
||||
import reducer, { broadcast, getMessageTab } from '../messages';
|
||||
import * as actions from '../actions';
|
||||
import appReducer from '../app';
|
||||
import { unix } from 'utils';
|
||||
|
||||
describe('message reducer', () => {
|
||||
it('adds the message on ADD_MESSAGE', () => {
|
||||
const state = reducer(undefined, {
|
||||
type: actions.ADD_MESSAGE,
|
||||
server: 'srv',
|
||||
network: 'srv',
|
||||
tab: '#chan1',
|
||||
message: {
|
||||
from: 'foo',
|
||||
@ -19,7 +20,7 @@ describe('message reducer', () => {
|
||||
'#chan1': [
|
||||
{
|
||||
from: 'foo',
|
||||
content: 'msg'
|
||||
content: [{ type: 'text', text: 'msg' }]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -29,7 +30,7 @@ describe('message reducer', () => {
|
||||
it('adds all the messages on ADD_MESSAGES', () => {
|
||||
const state = reducer(undefined, {
|
||||
type: actions.ADD_MESSAGES,
|
||||
server: 'srv',
|
||||
network: 'srv',
|
||||
tab: '#chan1',
|
||||
messages: [
|
||||
{
|
||||
@ -53,17 +54,17 @@ describe('message reducer', () => {
|
||||
'#chan1': [
|
||||
{
|
||||
from: 'foo',
|
||||
content: 'msg'
|
||||
content: [{ type: 'text', text: 'msg' }]
|
||||
},
|
||||
{
|
||||
from: 'bar',
|
||||
content: 'msg'
|
||||
content: [{ type: 'text', text: 'msg' }]
|
||||
}
|
||||
],
|
||||
'#chan2': [
|
||||
{
|
||||
from: 'foo',
|
||||
content: 'msg'
|
||||
content: [{ type: 'text', text: 'msg' }]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -79,7 +80,7 @@ describe('message reducer', () => {
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.ADD_MESSAGES,
|
||||
server: 'srv',
|
||||
network: 'srv',
|
||||
tab: '#chan1',
|
||||
prepend: true,
|
||||
messages: [
|
||||
@ -98,18 +99,18 @@ describe('message reducer', () => {
|
||||
it('adds date markers when prepending messages', () => {
|
||||
let state = {
|
||||
srv: {
|
||||
'#chan1': [{ id: 0, date: new Date(1999, 0, 1) }]
|
||||
'#chan1': [{ id: 0, date: new Date(1990, 0, 3) }]
|
||||
}
|
||||
};
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.ADD_MESSAGES,
|
||||
server: 'srv',
|
||||
network: 'srv',
|
||||
tab: '#chan1',
|
||||
prepend: true,
|
||||
messages: [
|
||||
{ id: 1, date: new Date(1990, 0, 2) },
|
||||
{ id: 2, date: new Date(1990, 0, 3) }
|
||||
{ id: 1, time: unix(new Date(1990, 0, 1)) },
|
||||
{ id: 2, time: unix(new Date(1990, 0, 2)) }
|
||||
]
|
||||
});
|
||||
|
||||
@ -135,7 +136,7 @@ describe('message reducer', () => {
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.ADD_MESSAGE,
|
||||
server: 'srv',
|
||||
network: 'srv',
|
||||
tab: '#chan1',
|
||||
message: { id: 1, date: new Date(1990, 0, 2) }
|
||||
});
|
||||
@ -150,18 +151,18 @@ describe('message reducer', () => {
|
||||
it('adds date markers when adding messages', () => {
|
||||
let state = {
|
||||
srv: {
|
||||
'#chan1': [{ id: 0, date: new Date(1999, 0, 1) }]
|
||||
'#chan1': [{ id: 0, date: new Date(1990, 0, 1) }]
|
||||
}
|
||||
};
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.ADD_MESSAGES,
|
||||
server: 'srv',
|
||||
network: 'srv',
|
||||
tab: '#chan1',
|
||||
messages: [
|
||||
{ id: 1, date: new Date(1990, 0, 2) },
|
||||
{ id: 2, date: new Date(1990, 0, 3) },
|
||||
{ id: 3, date: new Date(1990, 0, 3) }
|
||||
{ id: 1, time: unix(new Date(1990, 0, 2)) },
|
||||
{ id: 2, time: unix(new Date(1990, 0, 3)) },
|
||||
{ id: 3, time: unix(new Date(1990, 0, 3)) }
|
||||
]
|
||||
});
|
||||
|
||||
@ -196,12 +197,16 @@ describe('message reducer', () => {
|
||||
|
||||
expect(messages.srv).not.toHaveProperty('srv');
|
||||
expect(messages.srv['#chan1']).toHaveLength(1);
|
||||
expect(messages.srv['#chan1'][0].content).toBe('test');
|
||||
expect(messages.srv['#chan1'][0].content).toMatchObject([
|
||||
{ type: 'text', text: 'test' }
|
||||
]);
|
||||
expect(messages.srv['#chan3']).toHaveLength(1);
|
||||
expect(messages.srv['#chan3'][0].content).toBe('test');
|
||||
expect(messages.srv['#chan3'][0].content).toMatchObject([
|
||||
{ type: 'text', text: 'test' }
|
||||
]);
|
||||
});
|
||||
|
||||
it('deletes all messages related to server when disconnecting', () => {
|
||||
it('deletes all messages related to network when disconnecting', () => {
|
||||
let state = {
|
||||
srv: {
|
||||
'#chan1': [{ content: 'msg1' }, { content: 'msg2' }],
|
||||
@ -214,7 +219,7 @@ describe('message reducer', () => {
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.DISCONNECT,
|
||||
server: 'srv'
|
||||
network: 'srv'
|
||||
});
|
||||
|
||||
expect(state).toEqual({
|
||||
@ -237,7 +242,7 @@ describe('message reducer', () => {
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.PART,
|
||||
server: 'srv',
|
||||
network: 'srv',
|
||||
channels: ['#chan1']
|
||||
});
|
||||
|
||||
@ -250,6 +255,33 @@ describe('message reducer', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes direct messages when closing a direct message tab', () => {
|
||||
let state = {
|
||||
srv: {
|
||||
bob: [{ content: 'msg1' }, { content: 'msg2' }],
|
||||
'#chan2': [{ content: 'msg' }]
|
||||
},
|
||||
srv2: {
|
||||
'#chan1': [{ content: 'msg' }]
|
||||
}
|
||||
};
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.CLOSE_PRIVATE_CHAT,
|
||||
network: 'srv',
|
||||
nick: 'bob'
|
||||
});
|
||||
|
||||
expect(state).toEqual({
|
||||
srv: {
|
||||
'#chan2': [{ content: 'msg' }]
|
||||
},
|
||||
srv2: {
|
||||
'#chan1': [{ content: 'msg' }]
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMessageTab()', () => {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import reducer, { connect, setServerName } from '../servers';
|
||||
import reducer, { connect, setNetworkName } from '../networks';
|
||||
import * as actions from '../actions';
|
||||
|
||||
describe('server reducer', () => {
|
||||
it('adds the server on CONNECT', () => {
|
||||
describe('network reducer', () => {
|
||||
it('adds the network on CONNECT', () => {
|
||||
let state = reducer(
|
||||
undefined,
|
||||
connect({ host: '127.0.0.1', nick: 'nick' })
|
||||
@ -13,10 +13,8 @@ describe('server reducer', () => {
|
||||
name: '127.0.0.1',
|
||||
nick: 'nick',
|
||||
editedNick: null,
|
||||
status: {
|
||||
connected: false,
|
||||
error: null
|
||||
},
|
||||
connected: false,
|
||||
error: null,
|
||||
features: {}
|
||||
}
|
||||
});
|
||||
@ -28,10 +26,8 @@ describe('server reducer', () => {
|
||||
name: '127.0.0.1',
|
||||
nick: 'nick',
|
||||
editedNick: null,
|
||||
status: {
|
||||
connected: false,
|
||||
error: null
|
||||
},
|
||||
connected: false,
|
||||
error: null,
|
||||
features: {}
|
||||
}
|
||||
});
|
||||
@ -46,26 +42,22 @@ describe('server reducer', () => {
|
||||
name: '127.0.0.1',
|
||||
nick: 'nick',
|
||||
editedNick: null,
|
||||
status: {
|
||||
connected: false,
|
||||
error: null
|
||||
},
|
||||
connected: false,
|
||||
error: null,
|
||||
features: {}
|
||||
},
|
||||
'127.0.0.2': {
|
||||
name: 'srv',
|
||||
nick: 'nick',
|
||||
editedNick: null,
|
||||
status: {
|
||||
connected: false,
|
||||
error: null
|
||||
},
|
||||
connected: false,
|
||||
error: null,
|
||||
features: {}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('removes the server on DISCONNECT', () => {
|
||||
it('removes the network on DISCONNECT', () => {
|
||||
let state = {
|
||||
srv: {},
|
||||
srv2: {}
|
||||
@ -73,7 +65,7 @@ describe('server reducer', () => {
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.DISCONNECT,
|
||||
server: 'srv2'
|
||||
network: 'srv2'
|
||||
});
|
||||
|
||||
expect(state).toEqual({
|
||||
@ -81,14 +73,14 @@ describe('server reducer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handles SET_SERVER_NAME', () => {
|
||||
it('handles SET_NETWORK_NAME', () => {
|
||||
let state = {
|
||||
srv: {
|
||||
name: 'cake'
|
||||
}
|
||||
};
|
||||
|
||||
state = reducer(state, setServerName('pie', 'srv'));
|
||||
state = reducer(state, setNetworkName('pie', 'srv'));
|
||||
|
||||
expect(state).toEqual({
|
||||
srv: {
|
||||
@ -104,7 +96,7 @@ describe('server reducer', () => {
|
||||
);
|
||||
state = reducer(state, {
|
||||
type: actions.SET_NICK,
|
||||
server: '127.0.0.1',
|
||||
network: '127.0.0.1',
|
||||
nick: 'nick2',
|
||||
editing: true
|
||||
});
|
||||
@ -125,13 +117,13 @@ describe('server reducer', () => {
|
||||
);
|
||||
state = reducer(state, {
|
||||
type: actions.SET_NICK,
|
||||
server: '127.0.0.1',
|
||||
network: '127.0.0.1',
|
||||
nick: 'nick2',
|
||||
editing: true
|
||||
});
|
||||
state = reducer(state, {
|
||||
type: actions.SET_NICK,
|
||||
server: '127.0.0.1',
|
||||
network: '127.0.0.1',
|
||||
nick: ''
|
||||
});
|
||||
|
||||
@ -151,7 +143,7 @@ describe('server reducer', () => {
|
||||
);
|
||||
state = reducer(state, {
|
||||
type: actions.socket.NICK,
|
||||
server: '127.0.0.1',
|
||||
network: '127.0.0.1',
|
||||
oldNick: 'nick',
|
||||
newNick: 'nick2'
|
||||
});
|
||||
@ -172,13 +164,13 @@ describe('server reducer', () => {
|
||||
);
|
||||
state = reducer(state, {
|
||||
type: actions.SET_NICK,
|
||||
server: '127.0.0.1',
|
||||
network: '127.0.0.1',
|
||||
nick: 'nick2',
|
||||
editing: true
|
||||
});
|
||||
state = reducer(state, {
|
||||
type: actions.socket.NICK_FAIL,
|
||||
server: '127.0.0.1'
|
||||
network: '127.0.0.1'
|
||||
});
|
||||
|
||||
expect(state).toMatchObject({
|
||||
@ -190,25 +182,21 @@ describe('server reducer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('adds the servers on SOCKET_SERVERS', () => {
|
||||
it('adds the networks on INIT', () => {
|
||||
let state = reducer(undefined, {
|
||||
type: actions.socket.SERVERS,
|
||||
data: [
|
||||
type: actions.INIT,
|
||||
networks: [
|
||||
{
|
||||
host: '127.0.0.1',
|
||||
name: 'stuff',
|
||||
nick: 'nick',
|
||||
status: {
|
||||
connected: true
|
||||
}
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
host: '127.0.0.2',
|
||||
name: 'stuffz',
|
||||
nick: 'nick2',
|
||||
status: {
|
||||
connected: false
|
||||
}
|
||||
connected: false
|
||||
}
|
||||
]
|
||||
});
|
||||
@ -218,18 +206,14 @@ describe('server reducer', () => {
|
||||
name: 'stuff',
|
||||
nick: 'nick',
|
||||
editedNick: null,
|
||||
status: {
|
||||
connected: true
|
||||
},
|
||||
connected: true,
|
||||
features: {}
|
||||
},
|
||||
'127.0.0.2': {
|
||||
name: 'stuffz',
|
||||
nick: 'nick2',
|
||||
editedNick: null,
|
||||
status: {
|
||||
connected: false
|
||||
},
|
||||
connected: false,
|
||||
features: {}
|
||||
}
|
||||
});
|
||||
@ -242,7 +226,7 @@ describe('server reducer', () => {
|
||||
);
|
||||
state = reducer(state, {
|
||||
type: actions.socket.CONNECTION_UPDATE,
|
||||
server: '127.0.0.1',
|
||||
network: '127.0.0.1',
|
||||
connected: true
|
||||
});
|
||||
|
||||
@ -251,16 +235,14 @@ describe('server reducer', () => {
|
||||
name: '127.0.0.1',
|
||||
nick: 'nick',
|
||||
editedNick: null,
|
||||
status: {
|
||||
connected: true
|
||||
},
|
||||
connected: true,
|
||||
features: {}
|
||||
}
|
||||
});
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.socket.CONNECTION_UPDATE,
|
||||
server: '127.0.0.1',
|
||||
network: '127.0.0.1',
|
||||
connected: false,
|
||||
error: 'Bad stuff happened'
|
||||
});
|
||||
@ -270,10 +252,8 @@ describe('server reducer', () => {
|
||||
name: '127.0.0.1',
|
||||
nick: 'nick',
|
||||
editedNick: null,
|
||||
status: {
|
||||
connected: false,
|
||||
error: 'Bad stuff happened'
|
||||
},
|
||||
connected: false,
|
||||
error: 'Bad stuff happened',
|
||||
features: {}
|
||||
}
|
||||
});
|
@ -7,17 +7,17 @@ describe('tab reducer', () => {
|
||||
let state = reducer(undefined, setSelectedTab('srv', '#chan'));
|
||||
|
||||
expect(state).toEqual({
|
||||
selected: { server: 'srv', name: '#chan' },
|
||||
history: [{ server: 'srv', name: '#chan' }]
|
||||
selected: { network: 'srv', name: '#chan' },
|
||||
history: [{ network: 'srv', name: '#chan' }]
|
||||
});
|
||||
|
||||
state = reducer(state, setSelectedTab('srv', 'user1'));
|
||||
|
||||
expect(state).toEqual({
|
||||
selected: { server: 'srv', name: 'user1' },
|
||||
selected: { network: 'srv', name: 'user1' },
|
||||
history: [
|
||||
{ server: 'srv', name: '#chan' },
|
||||
{ server: 'srv', name: 'user1' }
|
||||
{ network: 'srv', name: '#chan' },
|
||||
{ network: 'srv', name: 'user1' }
|
||||
]
|
||||
});
|
||||
});
|
||||
@ -30,15 +30,15 @@ describe('tab reducer', () => {
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.PART,
|
||||
server: 'srv',
|
||||
network: 'srv',
|
||||
channels: ['#chan']
|
||||
});
|
||||
|
||||
expect(state).toEqual({
|
||||
selected: { server: 'srv', name: '#chan3' },
|
||||
selected: { network: 'srv', name: '#chan3' },
|
||||
history: [
|
||||
{ server: 'srv1', name: 'bob' },
|
||||
{ server: 'srv', name: '#chan3' }
|
||||
{ network: 'srv1', name: 'bob' },
|
||||
{ network: 'srv', name: '#chan3' }
|
||||
]
|
||||
});
|
||||
});
|
||||
@ -51,21 +51,21 @@ describe('tab reducer', () => {
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.CLOSE_PRIVATE_CHAT,
|
||||
server: 'srv1',
|
||||
network: 'srv1',
|
||||
nick: 'bob'
|
||||
});
|
||||
|
||||
expect(state).toEqual({
|
||||
selected: { server: 'srv', name: '#chan3' },
|
||||
selected: { network: 'srv', name: '#chan3' },
|
||||
history: [
|
||||
{ server: 'srv', name: '#chan' },
|
||||
{ server: 'srv', name: '#chan' },
|
||||
{ server: 'srv', name: '#chan3' }
|
||||
{ network: 'srv', name: '#chan' },
|
||||
{ network: 'srv', name: '#chan' },
|
||||
{ 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'));
|
||||
state = reducer(state, setSelectedTab('srv1', 'bob'));
|
||||
state = reducer(state, setSelectedTab('srv', '#chan'));
|
||||
@ -73,38 +73,42 @@ describe('tab reducer', () => {
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.DISCONNECT,
|
||||
server: 'srv'
|
||||
network: 'srv'
|
||||
});
|
||||
|
||||
expect(state).toEqual({
|
||||
selected: { server: 'srv', name: '#chan3' },
|
||||
history: [{ server: 'srv1', name: 'bob' }]
|
||||
selected: { network: 'srv', name: '#chan3' },
|
||||
history: [{ network: 'srv1', name: 'bob' }]
|
||||
});
|
||||
});
|
||||
|
||||
it('clears the tab when navigating to a non-tab page', () => {
|
||||
let state = reducer(undefined, setSelectedTab('srv', '#chan'));
|
||||
|
||||
state = reducer(state, locationChanged('settings'));
|
||||
state = reducer(state, locationChanged('settings', {}, {}));
|
||||
|
||||
expect(state).toEqual({
|
||||
selected: {},
|
||||
history: [{ server: 'srv', name: '#chan' }]
|
||||
history: [{ network: 'srv', name: '#chan' }]
|
||||
});
|
||||
});
|
||||
|
||||
it('selects the tab and adds it to history when navigating to a tab', () => {
|
||||
const state = reducer(
|
||||
undefined,
|
||||
locationChanged('chat', {
|
||||
server: 'srv',
|
||||
name: '#chan'
|
||||
})
|
||||
locationChanged(
|
||||
'chat',
|
||||
{
|
||||
network: 'srv',
|
||||
name: '#chan'
|
||||
},
|
||||
{}
|
||||
)
|
||||
);
|
||||
|
||||
expect(state).toEqual({
|
||||
selected: { server: 'srv', name: '#chan' },
|
||||
history: [{ server: 'srv', name: '#chan' }]
|
||||
selected: { network: 'srv', name: '#chan' },
|
||||
history: [{ network: 'srv', name: '#chan' }]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,8 +1,10 @@
|
||||
export const INIT = 'INIT';
|
||||
export const APP_SET = 'APP_SET';
|
||||
|
||||
export const INVITE = 'INVITE';
|
||||
export const JOIN = 'JOIN';
|
||||
export const KICK = 'KICK';
|
||||
export const KICKED = 'KICKED';
|
||||
export const PART = 'PART';
|
||||
export const SET_TOPIC = 'SET_TOPIC';
|
||||
|
||||
@ -35,7 +37,7 @@ export const CONNECT = 'CONNECT';
|
||||
export const DISCONNECT = 'DISCONNECT';
|
||||
export const RECONNECT = 'RECONNECT';
|
||||
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 SET_CERT = 'SET_CERT';
|
||||
@ -65,9 +67,9 @@ function createSocketActions(types) {
|
||||
export const socket = createSocketActions([
|
||||
'cert_fail',
|
||||
'cert_success',
|
||||
'channels',
|
||||
'channel_forward',
|
||||
'channel_search',
|
||||
'connected',
|
||||
'connection_update',
|
||||
'error',
|
||||
'features',
|
||||
@ -77,10 +79,10 @@ export const socket = createSocketActions([
|
||||
'nick_fail',
|
||||
'nick',
|
||||
'part',
|
||||
'kick',
|
||||
'pm',
|
||||
'quit',
|
||||
'search',
|
||||
'servers',
|
||||
'topic',
|
||||
'users'
|
||||
]);
|
||||
|
@ -15,7 +15,8 @@ const initialState = {
|
||||
windowWidth: 0,
|
||||
connectDefaults: {
|
||||
name: '',
|
||||
address: '',
|
||||
host: '',
|
||||
port: '',
|
||||
channels: [],
|
||||
ssl: false,
|
||||
password: false,
|
||||
@ -36,10 +37,18 @@ export default createReducer(initialState, {
|
||||
}
|
||||
},
|
||||
|
||||
[actions.socket.CONNECTED](state, { connected }) {
|
||||
state.connected = connected;
|
||||
},
|
||||
|
||||
[actions.UPDATE_MESSAGE_HEIGHT](state, action) {
|
||||
state.wrapWidth = action.wrapWidth;
|
||||
state.charWidth = action.charWidth;
|
||||
state.windowWidth = action.windowWidth;
|
||||
},
|
||||
|
||||
[actions.INIT](state, { app }) {
|
||||
Object.assign(state, app);
|
||||
}
|
||||
});
|
||||
|
||||
@ -51,10 +60,6 @@ export function appSet(key, value) {
|
||||
};
|
||||
}
|
||||
|
||||
export function setConnected(connected) {
|
||||
return appSet('connected', connected);
|
||||
}
|
||||
|
||||
export function setCharWidth(width) {
|
||||
return appSet('charWidth', width);
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ const 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) {
|
||||
state.end = false;
|
||||
|
||||
@ -18,7 +18,7 @@ export default createReducer(initialState, {
|
||||
state.results = results;
|
||||
|
||||
if (!q) {
|
||||
state.topCache[server] = results;
|
||||
state.topCache[network] = results;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -34,14 +34,14 @@ export default createReducer(initialState, {
|
||||
}
|
||||
});
|
||||
|
||||
export function searchChannels(server, q, start) {
|
||||
export function searchChannels(network, q, start) {
|
||||
return {
|
||||
type: actions.CHANNEL_SEARCH,
|
||||
server,
|
||||
network,
|
||||
q,
|
||||
socket: {
|
||||
type: 'channel_search',
|
||||
data: { server, q, start }
|
||||
data: { network, q, start }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { createSelector } from 'reselect';
|
||||
import get from 'lodash/get';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import createReducer from 'utils/createReducer';
|
||||
import { find, findIndex } from 'utils';
|
||||
import { trimPrefixChar, find, findIndex } from 'utils';
|
||||
import { getSelectedTab, updateSelection } from './tab';
|
||||
import * as actions from './actions';
|
||||
|
||||
@ -56,13 +56,18 @@ function removeUser(users, nick) {
|
||||
}
|
||||
}
|
||||
|
||||
function init(state, server, channel) {
|
||||
if (!state[server]) {
|
||||
state[server] = {};
|
||||
function init(state, network, channel) {
|
||||
if (!state[network]) {
|
||||
state[network] = {};
|
||||
}
|
||||
if (channel && !state[server][channel]) {
|
||||
state[server][channel] = { name: channel, users: [], joined: false };
|
||||
if (channel && !state[network][channel]) {
|
||||
state[network][channel] = {
|
||||
name: channel,
|
||||
users: [],
|
||||
joined: false
|
||||
};
|
||||
}
|
||||
return state[network][channel];
|
||||
}
|
||||
|
||||
export function compareUsers(a, b) {
|
||||
@ -93,18 +98,20 @@ export const getChannels = state => state.channels;
|
||||
|
||||
export const getSortedChannels = createSelector(getChannels, channels =>
|
||||
sortBy(
|
||||
Object.keys(channels).map(server => ({
|
||||
address: server,
|
||||
channels: sortBy(channels[server], channel => channel.name.toLowerCase())
|
||||
Object.keys(channels).map(network => ({
|
||||
address: network,
|
||||
channels: sortBy(channels[network], channel =>
|
||||
trimPrefixChar(channel.name, '#').toLowerCase()
|
||||
)
|
||||
})),
|
||||
server => server.address.toLowerCase()
|
||||
network => network.address.toLowerCase()
|
||||
)
|
||||
);
|
||||
|
||||
export const getSelectedChannel = createSelector(
|
||||
getSelectedTab,
|
||||
getChannels,
|
||||
(tab, channels) => get(channels, [tab.server, tab.name])
|
||||
(tab, channels) => get(channels, [tab.network, tab.name])
|
||||
);
|
||||
|
||||
export const getSelectedChannelUsers = createSelector(
|
||||
@ -120,43 +127,53 @@ export const getSelectedChannelUsers = createSelector(
|
||||
export default createReducer(
|
||||
{},
|
||||
{
|
||||
[actions.JOIN](state, { server, channels }) {
|
||||
channels.forEach(channel => init(state, server, channel));
|
||||
[actions.JOIN](state, { network, channels }) {
|
||||
channels.forEach(channel => init(state, network, channel));
|
||||
},
|
||||
|
||||
[actions.PART](state, { server, channels }) {
|
||||
channels.forEach(channel => delete state[server][channel]);
|
||||
[actions.PART](state, { network, channels }) {
|
||||
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];
|
||||
init(state, server, channel);
|
||||
state[server][channel].name = channel;
|
||||
state[server][channel].joined = true;
|
||||
state[server][channel].users.push(createUser(user));
|
||||
const chan = init(state, network, channel);
|
||||
chan.name = channel;
|
||||
chan.joined = true;
|
||||
chan.users.push(createUser(user));
|
||||
},
|
||||
|
||||
[actions.socket.CHANNEL_FORWARD](state, action) {
|
||||
init(state, action.server, action.new);
|
||||
delete state[action.server][action.old];
|
||||
init(state, action.network, action.new);
|
||||
delete state[action.network][action.old];
|
||||
},
|
||||
|
||||
[actions.socket.PART](state, { server, channel, user }) {
|
||||
if (state[server][channel]) {
|
||||
removeUser(state[server][channel].users, user);
|
||||
[actions.socket.PART](state, { network, channel, user }) {
|
||||
if (state[network][channel]) {
|
||||
removeUser(state[network][channel].users, user);
|
||||
}
|
||||
},
|
||||
|
||||
[actions.socket.QUIT](state, { server, user }) {
|
||||
Object.keys(state[server]).forEach(channel => {
|
||||
removeUser(state[server][channel].users, user);
|
||||
[actions.socket.QUIT](state, { network, user }) {
|
||||
Object.keys(state[network]).forEach(channel => {
|
||||
removeUser(state[network][channel].users, user);
|
||||
});
|
||||
},
|
||||
|
||||
[actions.socket.NICK](state, { server, oldNick, newNick }) {
|
||||
Object.keys(state[server]).forEach(channel => {
|
||||
[actions.KICKED](state, { network, channel, user, self }) {
|
||||
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(
|
||||
state[server][channel].users,
|
||||
state[network][channel].users,
|
||||
u => u.nick === oldNick
|
||||
);
|
||||
if (user) {
|
||||
@ -166,16 +183,8 @@ export default createReducer(
|
||||
});
|
||||
},
|
||||
|
||||
[actions.socket.USERS](state, { server, channel, users }) {
|
||||
state[server][channel].users = users.map(nick => loadUser(nick));
|
||||
},
|
||||
|
||||
[actions.socket.TOPIC](state, { server, channel, topic }) {
|
||||
state[server][channel].topic = topic;
|
||||
},
|
||||
|
||||
[actions.socket.MODE](state, { server, channel, user, remove, add }) {
|
||||
const u = find(state[server][channel].users, v => v.nick === user);
|
||||
[actions.socket.MODE](state, { network, channel, user, remove, add }) {
|
||||
const u = find(state[network][channel].users, v => v.nick === user);
|
||||
if (u) {
|
||||
if (remove) {
|
||||
let j = remove.length;
|
||||
@ -192,19 +201,31 @@ export default createReducer(
|
||||
}
|
||||
},
|
||||
|
||||
[actions.socket.CHANNELS](state, { data }) {
|
||||
if (data) {
|
||||
data.forEach(({ server, name, topic }) => {
|
||||
init(state, server, name);
|
||||
state[server][name].joined = true;
|
||||
state[server][name].topic = topic;
|
||||
});
|
||||
}
|
||||
[actions.socket.TOPIC](state, { network, channel, topic }) {
|
||||
state[network][channel].topic = topic;
|
||||
},
|
||||
|
||||
[actions.socket.SERVERS](state, { data }) {
|
||||
if (data) {
|
||||
data.forEach(({ host }) => init(state, host));
|
||||
[actions.socket.USERS](state, { network, channel, users }) {
|
||||
state[network][channel].users = users.map(nick => loadUser(nick));
|
||||
},
|
||||
|
||||
[actions.INIT](state, { networks, channels, users }) {
|
||||
if (networks) {
|
||||
networks.forEach(({ host }) => init(state, host));
|
||||
}
|
||||
|
||||
if (channels) {
|
||||
channels.forEach(({ network, name, topic, joined }) => {
|
||||
const chan = init(state, network, name);
|
||||
chan.joined = joined;
|
||||
chan.topic = topic;
|
||||
});
|
||||
}
|
||||
|
||||
if (users) {
|
||||
state[users.network][users.channel].users = users.users.map(nick =>
|
||||
loadUser(nick)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@ -212,33 +233,34 @@ export default createReducer(
|
||||
init(state, host);
|
||||
},
|
||||
|
||||
[actions.DISCONNECT](state, { server }) {
|
||||
delete state[server];
|
||||
[actions.DISCONNECT](state, { network }) {
|
||||
delete state[network];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export function join(channels, server) {
|
||||
export function join(channels, network, selectFirst = true) {
|
||||
return {
|
||||
type: actions.JOIN,
|
||||
channels,
|
||||
server,
|
||||
network,
|
||||
selectFirst,
|
||||
socket: {
|
||||
type: 'join',
|
||||
data: { channels, server }
|
||||
data: { channels, network }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function part(channels, server) {
|
||||
export function part(channels, network) {
|
||||
return (dispatch, getState) => {
|
||||
const action = {
|
||||
type: actions.PART,
|
||||
channels,
|
||||
server
|
||||
network
|
||||
};
|
||||
|
||||
const state = getState().channels[server];
|
||||
const state = getState().channels[network];
|
||||
const joined = channels.filter(c => state[c] && state[c].joined);
|
||||
|
||||
if (joined.length > 0) {
|
||||
@ -246,7 +268,7 @@ export function part(channels, server) {
|
||||
type: 'part',
|
||||
data: {
|
||||
channels: joined,
|
||||
server
|
||||
network
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -256,41 +278,55 @@ export function part(channels, server) {
|
||||
};
|
||||
}
|
||||
|
||||
export function invite(user, channel, server) {
|
||||
export function invite(user, channel, network) {
|
||||
return {
|
||||
type: actions.INVITE,
|
||||
user,
|
||||
channel,
|
||||
server,
|
||||
network,
|
||||
socket: {
|
||||
type: 'invite',
|
||||
data: { user, channel, server }
|
||||
data: { user, channel, network }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function kick(user, channel, server) {
|
||||
export function kick(user, channel, network) {
|
||||
return {
|
||||
type: actions.KICK,
|
||||
user,
|
||||
channel,
|
||||
server,
|
||||
network,
|
||||
socket: {
|
||||
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 {
|
||||
type: actions.SET_TOPIC,
|
||||
topic,
|
||||
channel,
|
||||
server,
|
||||
network,
|
||||
socket: {
|
||||
type: 'topic',
|
||||
data: { topic, channel, server }
|
||||
data: { topic, channel, network }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -5,9 +5,9 @@ import channelSearch from './channelSearch';
|
||||
import input from './input';
|
||||
import messages from './messages';
|
||||
import modals from './modals';
|
||||
import networks from './networks';
|
||||
import privateChats from './privateChats';
|
||||
import search from './search';
|
||||
import servers from './servers';
|
||||
import settings from './settings';
|
||||
import tab from './tab';
|
||||
import ui from './ui';
|
||||
@ -24,9 +24,9 @@ export default function createReducer(router) {
|
||||
input,
|
||||
messages,
|
||||
modals,
|
||||
networks,
|
||||
privateChats,
|
||||
search,
|
||||
servers,
|
||||
settings,
|
||||
tab,
|
||||
ui
|
||||
|
@ -6,8 +6,10 @@ import {
|
||||
linkify,
|
||||
timestamp,
|
||||
isChannel,
|
||||
formatDate
|
||||
formatDate,
|
||||
unix
|
||||
} from 'utils';
|
||||
import colorify from 'utils/colorify';
|
||||
import createReducer from 'utils/createReducer';
|
||||
import { getApp } from './app';
|
||||
import { getSelectedTab } from './tab';
|
||||
@ -19,9 +21,9 @@ export const getSelectedMessages = createSelector(
|
||||
getSelectedTab,
|
||||
getMessages,
|
||||
(tab, messages) => {
|
||||
const target = tab.name || tab.server;
|
||||
if (has(messages, [tab.server, target])) {
|
||||
return messages[tab.server][target];
|
||||
const target = tab.name || tab.network;
|
||||
if (has(messages, [tab.network, target])) {
|
||||
return messages[tab.network][target];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@ -35,17 +37,224 @@ export const getHasMoreMessages = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
function init(state, server, tab) {
|
||||
if (!state[server]) {
|
||||
state[server] = {};
|
||||
function init(state, network, tab) {
|
||||
if (!state[network]) {
|
||||
state[network] = {};
|
||||
}
|
||||
if (!state[server][tab]) {
|
||||
state[server][tab] = [];
|
||||
if (!state[network][tab]) {
|
||||
state[network][tab] = [];
|
||||
}
|
||||
}
|
||||
|
||||
function initNetworks(state, networks = []) {
|
||||
networks.forEach(({ host }) => {
|
||||
state[host] = {};
|
||||
});
|
||||
}
|
||||
|
||||
const collapsedEvents = ['join', 'part', 'quit', 'nick'];
|
||||
|
||||
function shouldCollapse(msg1, msg2) {
|
||||
return (
|
||||
msg1.events &&
|
||||
msg2.events &&
|
||||
collapsedEvents.indexOf(msg1.events[0].type) !== -1 &&
|
||||
collapsedEvents.indexOf(msg2.events[0].type) !== -1
|
||||
);
|
||||
}
|
||||
|
||||
const blocks = {
|
||||
nick: nick => ({ type: 'nick', text: nick }),
|
||||
text: text => ({ type: 'text', text }),
|
||||
events: count => ({ type: 'events', text: `${count} more` })
|
||||
};
|
||||
|
||||
const eventVerbs = {
|
||||
join: 'joined',
|
||||
part: 'left',
|
||||
quit: 'quit'
|
||||
};
|
||||
|
||||
function renderEvent(result, type, events) {
|
||||
const ending = eventVerbs[type];
|
||||
|
||||
if (result.length > 1) {
|
||||
result[result.length - 1].text += ', ';
|
||||
}
|
||||
|
||||
if (events.length === 1) {
|
||||
result.push(blocks.nick(events[0][0]));
|
||||
result.push(blocks.text(` ${ending}`));
|
||||
} else if (events.length === 2) {
|
||||
result.push(blocks.nick(events[0][0]));
|
||||
result.push(blocks.text(' and '));
|
||||
result.push(blocks.nick(events[1][0]));
|
||||
result.push(blocks.text(` ${ending}`));
|
||||
} else if (events.length > 2) {
|
||||
result.push(blocks.nick(events[0][0]));
|
||||
result.push(blocks.text(', '));
|
||||
result.push(blocks.nick(events[1][0]));
|
||||
result.push(blocks.text(' and '));
|
||||
result.push(blocks.events(events.length - 2));
|
||||
result.push(blocks.text(` ${ending}`));
|
||||
}
|
||||
}
|
||||
|
||||
function renderEvents(events) {
|
||||
const first = events[0];
|
||||
|
||||
if (first.type === 'kick') {
|
||||
const [kicked, by] = first.params;
|
||||
|
||||
return [blocks.nick(by), blocks.text(' kicked '), blocks.nick(kicked)];
|
||||
}
|
||||
|
||||
if (first.type === 'topic') {
|
||||
const [nick, topic] = first.params;
|
||||
|
||||
if (!topic) {
|
||||
return [blocks.nick(nick), blocks.text(' cleared the topic')];
|
||||
}
|
||||
|
||||
return [
|
||||
blocks.nick(nick),
|
||||
blocks.text(' changed the topic to: '),
|
||||
...colorify(linkify(topic))
|
||||
];
|
||||
}
|
||||
|
||||
const byType = {};
|
||||
for (let i = events.length - 1; i >= 0; i--) {
|
||||
const event = events[i];
|
||||
const [nick] = event.params;
|
||||
|
||||
if (!byType[event.type]) {
|
||||
byType[event.type] = [event.params];
|
||||
} else if (byType[event.type].indexOf(nick) === -1) {
|
||||
byType[event.type].push(event.params);
|
||||
}
|
||||
}
|
||||
|
||||
const result = [];
|
||||
|
||||
if (byType.join) {
|
||||
renderEvent(result, 'join', byType.join);
|
||||
}
|
||||
|
||||
if (byType.part) {
|
||||
renderEvent(result, 'part', byType.part);
|
||||
}
|
||||
|
||||
if (byType.quit) {
|
||||
renderEvent(result, 'quit', byType.quit);
|
||||
}
|
||||
|
||||
if (byType.nick) {
|
||||
if (result.length > 1) {
|
||||
result[result.length - 1].text += ', ';
|
||||
}
|
||||
|
||||
const [oldNick, newNick] = byType.nick[0];
|
||||
|
||||
result.push(blocks.nick(oldNick));
|
||||
result.push(blocks.text(' changed nick to '));
|
||||
result.push(blocks.nick(newNick));
|
||||
|
||||
if (byType.nick.length > 1) {
|
||||
result.push(blocks.text(' and '));
|
||||
result.push(blocks.events(byType.nick.length - 1));
|
||||
result.push(blocks.text(' changed nick'));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
let nextID = 0;
|
||||
|
||||
function initMessage(
|
||||
state,
|
||||
message,
|
||||
network,
|
||||
tab,
|
||||
wrapWidth,
|
||||
charWidth,
|
||||
windowWidth,
|
||||
prepend
|
||||
) {
|
||||
const messages = state[network][tab];
|
||||
|
||||
if (messages.length > 0 && !prepend) {
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
if (shouldCollapse(lastMessage, message)) {
|
||||
lastMessage.events.push(message.events[0]);
|
||||
lastMessage.content = renderEvents(lastMessage.events);
|
||||
|
||||
[lastMessage.breakpoints, lastMessage.length] = findBreakpoints(
|
||||
lastMessage.content
|
||||
);
|
||||
lastMessage.height = messageHeight(
|
||||
lastMessage,
|
||||
wrapWidth,
|
||||
charWidth,
|
||||
6 * charWidth,
|
||||
windowWidth
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.time) {
|
||||
message.date = new Date(message.time * 1000);
|
||||
} else {
|
||||
message.date = new Date();
|
||||
}
|
||||
|
||||
message.time = timestamp(message.date);
|
||||
|
||||
if (!message.id) {
|
||||
message.id = nextID;
|
||||
nextID++;
|
||||
}
|
||||
|
||||
if (tab.charAt(0) === '#') {
|
||||
message.channel = true;
|
||||
}
|
||||
|
||||
if (message.events) {
|
||||
message.type = 'info';
|
||||
message.content = renderEvents(message.events);
|
||||
} else {
|
||||
message.content = message.content || '';
|
||||
// Collapse multiple adjacent spaces into a single one
|
||||
message.content = message.content.replace(/\s\s+/g, ' ');
|
||||
|
||||
if (message.content.indexOf('\x01ACTION') === 0) {
|
||||
const { from } = message;
|
||||
message.from = null;
|
||||
message.type = 'action';
|
||||
message.content = from + message.content.slice(7, -1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!message.events) {
|
||||
message.content = colorify(linkify(message.content));
|
||||
}
|
||||
|
||||
[message.breakpoints, message.length] = findBreakpoints(message.content);
|
||||
message.height = messageHeight(
|
||||
message,
|
||||
wrapWidth,
|
||||
charWidth,
|
||||
6 * charWidth,
|
||||
windowWidth
|
||||
);
|
||||
message.indent = 6 * charWidth;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function createDateMessage(date) {
|
||||
const message = {
|
||||
id: nextID,
|
||||
@ -67,17 +276,37 @@ function isSameDay(d1, d2) {
|
||||
);
|
||||
}
|
||||
|
||||
function reducerPrependMessages(messages, server, tab, state) {
|
||||
function reducerPrependMessages(
|
||||
state,
|
||||
messages,
|
||||
network,
|
||||
tab,
|
||||
wrapWidth,
|
||||
charWidth,
|
||||
windowWidth
|
||||
) {
|
||||
const msgs = [];
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
if (i > 0 && !isSameDay(messages[i - 1].date, messages[i].date)) {
|
||||
msgs.push(createDateMessage(messages[i].date));
|
||||
const message = messages[i];
|
||||
initMessage(
|
||||
state,
|
||||
message,
|
||||
network,
|
||||
tab,
|
||||
wrapWidth,
|
||||
charWidth,
|
||||
windowWidth,
|
||||
true
|
||||
);
|
||||
|
||||
if (i > 0 && !isSameDay(messages[i - 1].date, message.date)) {
|
||||
msgs.push(createDateMessage(message.date));
|
||||
}
|
||||
msgs.push(messages[i]);
|
||||
msgs.push(message);
|
||||
}
|
||||
|
||||
const m = state[server][tab];
|
||||
const m = state[network][tab];
|
||||
|
||||
if (m.length > 0) {
|
||||
const lastNewMessage = msgs[msgs.length - 1];
|
||||
@ -93,8 +322,8 @@ function reducerPrependMessages(messages, server, tab, state) {
|
||||
m.unshift(...msgs);
|
||||
}
|
||||
|
||||
function reducerAddMessage(message, server, tab, state) {
|
||||
const messages = state[server][tab];
|
||||
function reducerAddMessage(message, network, tab, state) {
|
||||
const messages = state[network][tab];
|
||||
|
||||
if (messages.length > 0) {
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
@ -109,40 +338,82 @@ function reducerAddMessage(message, server, tab, state) {
|
||||
export default createReducer(
|
||||
{},
|
||||
{
|
||||
[actions.ADD_MESSAGE](state, { server, tab, message }) {
|
||||
init(state, server, tab);
|
||||
reducerAddMessage(message, server, tab, state);
|
||||
[actions.ADD_MESSAGE](
|
||||
state,
|
||||
{ network, tab, message, wrapWidth, charWidth, windowWidth }
|
||||
) {
|
||||
init(state, network, tab);
|
||||
|
||||
const shouldAdd = initMessage(
|
||||
state,
|
||||
message,
|
||||
network,
|
||||
tab,
|
||||
wrapWidth,
|
||||
charWidth,
|
||||
windowWidth
|
||||
);
|
||||
if (shouldAdd) {
|
||||
reducerAddMessage(message, network, tab, state);
|
||||
}
|
||||
},
|
||||
|
||||
[actions.ADD_MESSAGES](state, { server, tab, messages, prepend }) {
|
||||
[actions.ADD_MESSAGES](
|
||||
state,
|
||||
{ network, tab, messages, prepend, wrapWidth, charWidth, windowWidth }
|
||||
) {
|
||||
if (prepend) {
|
||||
init(state, server, tab);
|
||||
reducerPrependMessages(messages, server, tab, state);
|
||||
init(state, network, tab);
|
||||
reducerPrependMessages(
|
||||
state,
|
||||
messages,
|
||||
network,
|
||||
tab,
|
||||
wrapWidth,
|
||||
charWidth,
|
||||
windowWidth
|
||||
);
|
||||
} else {
|
||||
if (!messages[0].tab) {
|
||||
init(state, server, tab);
|
||||
init(state, network, tab);
|
||||
}
|
||||
|
||||
messages.forEach(message => {
|
||||
if (message.tab) {
|
||||
init(state, server, message.tab);
|
||||
init(state, network, message.tab);
|
||||
}
|
||||
|
||||
const shouldAdd = initMessage(
|
||||
state,
|
||||
message,
|
||||
network,
|
||||
message.tab || tab,
|
||||
wrapWidth,
|
||||
charWidth,
|
||||
windowWidth
|
||||
);
|
||||
if (shouldAdd) {
|
||||
reducerAddMessage(message, network, message.tab || tab, state);
|
||||
}
|
||||
reducerAddMessage(message, server, message.tab || tab, state);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
[actions.DISCONNECT](state, { server }) {
|
||||
delete state[server];
|
||||
[actions.DISCONNECT](state, { network }) {
|
||||
delete state[network];
|
||||
},
|
||||
|
||||
[actions.PART](state, { server, channels }) {
|
||||
channels.forEach(channel => delete state[server][channel]);
|
||||
[actions.PART](state, { network, channels }) {
|
||||
channels.forEach(channel => delete state[network][channel]);
|
||||
},
|
||||
|
||||
[actions.socket.CHANNEL_FORWARD](state, { server, old }) {
|
||||
if (state[server]) {
|
||||
delete state[server][old];
|
||||
[actions.CLOSE_PRIVATE_CHAT](state, { network, nick }) {
|
||||
delete state[network][nick];
|
||||
},
|
||||
|
||||
[actions.socket.CHANNEL_FORWARD](state, { network, old }) {
|
||||
if (state[network]) {
|
||||
delete state[network][old];
|
||||
}
|
||||
},
|
||||
|
||||
@ -150,9 +421,9 @@ export default createReducer(
|
||||
state,
|
||||
{ wrapWidth, charWidth, windowWidth }
|
||||
) {
|
||||
Object.keys(state).forEach(server =>
|
||||
Object.keys(state[server]).forEach(target =>
|
||||
state[server][target].forEach(message => {
|
||||
Object.keys(state).forEach(network =>
|
||||
Object.keys(state[network]).forEach(target =>
|
||||
state[network][target].forEach(message => {
|
||||
if (message.type === 'date') {
|
||||
return;
|
||||
}
|
||||
@ -169,65 +440,19 @@ export default createReducer(
|
||||
);
|
||||
},
|
||||
|
||||
[actions.socket.SERVERS](state, { data }) {
|
||||
if (data) {
|
||||
data.forEach(({ host }) => {
|
||||
state[host] = {};
|
||||
});
|
||||
}
|
||||
[actions.INIT](state, { networks }) {
|
||||
initNetworks(state, networks);
|
||||
},
|
||||
|
||||
[actions.socket.NETWORKS](state, { data }) {
|
||||
initNetworks(state, data);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function initMessage(message, tab, state) {
|
||||
if (message.time) {
|
||||
message.date = new Date(message.time * 1000);
|
||||
} else {
|
||||
message.date = new Date();
|
||||
}
|
||||
|
||||
message.time = timestamp(message.date);
|
||||
|
||||
if (!message.id) {
|
||||
message.id = nextID;
|
||||
nextID++;
|
||||
}
|
||||
|
||||
if (tab.charAt(0) === '#') {
|
||||
message.channel = true;
|
||||
}
|
||||
|
||||
message.content = message.content || '';
|
||||
|
||||
// Collapse multiple adjacent spaces into a single one
|
||||
message.content = message.content.replace(/\s\s+/g, ' ');
|
||||
|
||||
if (message.content.indexOf('\x01ACTION') === 0) {
|
||||
const { from } = message;
|
||||
message.from = null;
|
||||
message.type = 'action';
|
||||
message.content = from + message.content.slice(7, -1);
|
||||
}
|
||||
|
||||
const { wrapWidth, charWidth, windowWidth } = getApp(state);
|
||||
|
||||
message.length = message.content.length;
|
||||
message.breakpoints = findBreakpoints(message.content);
|
||||
message.height = messageHeight(
|
||||
message,
|
||||
wrapWidth,
|
||||
charWidth,
|
||||
6 * charWidth,
|
||||
windowWidth
|
||||
);
|
||||
message.content = linkify(message.content);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
export function getMessageTab(server, to) {
|
||||
export function getMessageTab(network, to) {
|
||||
if (!to || to === '*' || (!isChannel(to) && to.indexOf('.') !== -1)) {
|
||||
return server;
|
||||
return network;
|
||||
}
|
||||
return to;
|
||||
}
|
||||
@ -242,13 +467,13 @@ export function fetchMessages() {
|
||||
}
|
||||
|
||||
const tab = state.tab.selected;
|
||||
if (isChannel(tab)) {
|
||||
if (tab.name) {
|
||||
dispatch({
|
||||
type: actions.FETCH_MESSAGES,
|
||||
socket: {
|
||||
type: 'fetch_messages',
|
||||
data: {
|
||||
server: tab.server,
|
||||
network: tab.network,
|
||||
channel: tab.name,
|
||||
next: first.id
|
||||
}
|
||||
@ -258,10 +483,10 @@ export function fetchMessages() {
|
||||
};
|
||||
}
|
||||
|
||||
export function addFetchedMessages(server, tab) {
|
||||
export function addFetchedMessages(network, tab) {
|
||||
return {
|
||||
type: actions.ADD_FETCHED_MESSAGES,
|
||||
server,
|
||||
network,
|
||||
tab
|
||||
};
|
||||
}
|
||||
@ -275,44 +500,50 @@ export function updateMessageHeight(wrapWidth, charWidth, windowWidth) {
|
||||
};
|
||||
}
|
||||
|
||||
export function sendMessage(content, to, server) {
|
||||
export function sendMessage(content, to, network) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { wrapWidth, charWidth, windowWidth } = getApp(state);
|
||||
|
||||
dispatch({
|
||||
type: actions.ADD_MESSAGE,
|
||||
server,
|
||||
network,
|
||||
tab: to,
|
||||
message: initMessage(
|
||||
{
|
||||
from: state.servers[server].nick,
|
||||
content
|
||||
},
|
||||
to,
|
||||
state
|
||||
),
|
||||
message: {
|
||||
from: state.networks[network].nick,
|
||||
content
|
||||
},
|
||||
wrapWidth,
|
||||
charWidth,
|
||||
windowWidth,
|
||||
socket: {
|
||||
type: 'message',
|
||||
data: { content, to, server }
|
||||
data: { content, to, network }
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function addMessage(message, server, to) {
|
||||
const tab = getMessageTab(server, to);
|
||||
export function addMessage(message, network, to) {
|
||||
const tab = getMessageTab(network, to);
|
||||
|
||||
return (dispatch, getState) => {
|
||||
const { wrapWidth, charWidth, windowWidth } = getApp(getState());
|
||||
|
||||
return (dispatch, getState) =>
|
||||
dispatch({
|
||||
type: actions.ADD_MESSAGE,
|
||||
server,
|
||||
network,
|
||||
tab,
|
||||
message: initMessage(message, tab, getState())
|
||||
message,
|
||||
wrapWidth,
|
||||
charWidth,
|
||||
windowWidth
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function addMessages(messages, server, to, prepend, next) {
|
||||
const tab = getMessageTab(server, to);
|
||||
export function addMessages(messages, network, to, prepend, next) {
|
||||
const tab = getMessageTab(network, to);
|
||||
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
@ -322,39 +553,76 @@ export function addMessages(messages, server, to, prepend, next) {
|
||||
messages[0].next = true;
|
||||
}
|
||||
|
||||
messages.forEach(message =>
|
||||
initMessage(message, message.tab || tab, state)
|
||||
);
|
||||
const { wrapWidth, charWidth, windowWidth } = getApp(state);
|
||||
|
||||
dispatch({
|
||||
type: actions.ADD_MESSAGES,
|
||||
server,
|
||||
network,
|
||||
tab,
|
||||
messages,
|
||||
prepend
|
||||
prepend,
|
||||
wrapWidth,
|
||||
charWidth,
|
||||
windowWidth
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function broadcast(message, server, channels) {
|
||||
export function addEvent(network, tab, type, ...params) {
|
||||
return addMessage(
|
||||
{
|
||||
type: 'info',
|
||||
events: [
|
||||
{
|
||||
type,
|
||||
params,
|
||||
time: unix()
|
||||
}
|
||||
]
|
||||
},
|
||||
network,
|
||||
tab
|
||||
);
|
||||
}
|
||||
|
||||
export function broadcastEvent(network, channels, type, ...params) {
|
||||
const now = unix();
|
||||
|
||||
return addMessages(
|
||||
channels.map(channel => ({
|
||||
type: 'info',
|
||||
tab: channel,
|
||||
events: [
|
||||
{
|
||||
type,
|
||||
params,
|
||||
time: now
|
||||
}
|
||||
]
|
||||
})),
|
||||
network
|
||||
);
|
||||
}
|
||||
|
||||
export function broadcast(message, network, channels) {
|
||||
return addMessages(
|
||||
channels.map(channel => ({
|
||||
tab: channel,
|
||||
content: message,
|
||||
type: 'info'
|
||||
})),
|
||||
server
|
||||
network
|
||||
);
|
||||
}
|
||||
|
||||
export function print(message, server, channel, type) {
|
||||
export function print(message, network, channel, type) {
|
||||
if (Array.isArray(message)) {
|
||||
return addMessages(
|
||||
message.map(line => ({
|
||||
content: line,
|
||||
type
|
||||
})),
|
||||
server,
|
||||
network,
|
||||
channel
|
||||
);
|
||||
}
|
||||
@ -364,32 +632,32 @@ export function print(message, server, channel, type) {
|
||||
content: message,
|
||||
type
|
||||
},
|
||||
server,
|
||||
network,
|
||||
channel
|
||||
);
|
||||
}
|
||||
|
||||
export function inform(message, server, channel) {
|
||||
return print(message, server, channel, 'info');
|
||||
export function inform(message, network, channel) {
|
||||
return print(message, network, channel, 'info');
|
||||
}
|
||||
|
||||
export function runCommand(command, channel, server) {
|
||||
export function runCommand(command, channel, network) {
|
||||
return {
|
||||
type: actions.COMMAND,
|
||||
command,
|
||||
channel,
|
||||
server
|
||||
network
|
||||
};
|
||||
}
|
||||
|
||||
export function raw(message, server) {
|
||||
export function raw(message, network) {
|
||||
return {
|
||||
type: actions.RAW,
|
||||
message,
|
||||
server,
|
||||
network,
|
||||
socket: {
|
||||
type: 'raw',
|
||||
data: { message, server }
|
||||
data: { message, network }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
229
client/js/state/networks.js
Normal file
229
client/js/state/networks.js
Normal 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.INIT](state, { networks }) {
|
||||
if (networks) {
|
||||
networks.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;
|
||||
}
|
@ -1,18 +1,18 @@
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { findIndex } from 'utils';
|
||||
import { isDM } from 'utils';
|
||||
import createReducer from 'utils/createReducer';
|
||||
import { updateSelection } from './tab';
|
||||
import * as actions from './actions';
|
||||
|
||||
export const getPrivateChats = state => state.privateChats;
|
||||
|
||||
function open(state, server, nick) {
|
||||
if (!state[server]) {
|
||||
state[server] = [];
|
||||
function open(state, network, nick) {
|
||||
if (!state[network]) {
|
||||
state[network] = [];
|
||||
}
|
||||
if (findIndex(state[server], n => n === nick) === -1) {
|
||||
state[server].push(nick);
|
||||
state[server] = sortBy(state[server], v => v.toLowerCase());
|
||||
if (!state[network].includes(nick)) {
|
||||
state[network].push(nick);
|
||||
state[network] = sortBy(state[network], v => v.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,42 +20,66 @@ export default createReducer(
|
||||
{},
|
||||
{
|
||||
[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 }) {
|
||||
const i = findIndex(state[server], n => n === nick);
|
||||
[actions.CLOSE_PRIVATE_CHAT](state, { network, nick }) {
|
||||
const i = state[network]?.findIndex(n => n === nick);
|
||||
if (i !== -1) {
|
||||
state[server].splice(i, 1);
|
||||
state[network].splice(i, 1);
|
||||
}
|
||||
},
|
||||
|
||||
[actions.socket.PM](state, action) {
|
||||
if (action.from.indexOf('.') === -1) {
|
||||
open(state, action.server, action.from);
|
||||
[actions.INIT](state, { openDMs }) {
|
||||
if (openDMs) {
|
||||
openDMs.forEach(({ network, name }) => {
|
||||
if (!state[network]) {
|
||||
state[network] = [];
|
||||
}
|
||||
|
||||
state[network].push(name);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
[actions.DISCONNECT](state, { server }) {
|
||||
delete state[server];
|
||||
[actions.ADD_MESSAGE](state, { message }) {
|
||||
if (isDM(message)) {
|
||||
open(state, message.network, message.from);
|
||||
}
|
||||
},
|
||||
|
||||
[actions.DISCONNECT](state, { network }) {
|
||||
delete state[network];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export function openPrivateChat(server, nick) {
|
||||
return {
|
||||
type: actions.OPEN_PRIVATE_CHAT,
|
||||
server,
|
||||
nick
|
||||
export function openPrivateChat(network, nick) {
|
||||
return (dispatch, getState) => {
|
||||
if (!getState().privateChats[network]?.includes(nick)) {
|
||||
dispatch({
|
||||
type: actions.OPEN_PRIVATE_CHAT,
|
||||
network,
|
||||
nick,
|
||||
socket: {
|
||||
type: 'open_dm',
|
||||
data: { network, name: nick }
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function closePrivateChat(server, nick) {
|
||||
export function closePrivateChat(network, nick) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: actions.CLOSE_PRIVATE_CHAT,
|
||||
server,
|
||||
nick
|
||||
network,
|
||||
nick,
|
||||
socket: {
|
||||
type: 'close_dm',
|
||||
data: { network, name: nick }
|
||||
}
|
||||
});
|
||||
dispatch(updateSelection());
|
||||
};
|
||||
|
@ -18,15 +18,15 @@ export default createReducer(initialState, {
|
||||
}
|
||||
});
|
||||
|
||||
export function searchMessages(server, channel, phrase) {
|
||||
export function searchMessages(network, channel, phrase) {
|
||||
return {
|
||||
type: actions.SEARCH_MESSAGES,
|
||||
server,
|
||||
network,
|
||||
channel,
|
||||
phrase,
|
||||
socket: {
|
||||
type: 'search',
|
||||
data: { server, channel, phrase }
|
||||
data: { network, channel, phrase }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import get from 'lodash/get';
|
||||
import { getServers } from './servers';
|
||||
import { getNetworks } from './networks';
|
||||
import { getSelectedTab } from './tab';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const getSelectedTabTitle = createSelector(
|
||||
getSelectedTab,
|
||||
getServers,
|
||||
(tab, servers) => tab.name || get(servers, [tab.server, 'name'])
|
||||
getNetworks,
|
||||
(tab, networks) => tab.name || get(networks, [tab.network, 'name'])
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
@ -44,6 +44,10 @@ export default createReducer(
|
||||
} else {
|
||||
state[key] = value;
|
||||
}
|
||||
},
|
||||
|
||||
[actions.INIT](state, { settings }) {
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -12,7 +12,7 @@ const initialState = {
|
||||
|
||||
function selectTab(state, action) {
|
||||
state.selected = {
|
||||
server: action.server,
|
||||
network: action.network,
|
||||
name: action.name
|
||||
};
|
||||
state.history.push(state.selected);
|
||||
@ -23,20 +23,31 @@ export const getSelectedTab = state => state.tab.selected;
|
||||
export default createReducer(initialState, {
|
||||
[actions.SELECT_TAB]: selectTab,
|
||||
|
||||
[actions.JOIN](state, { network, channels, selectFirst }) {
|
||||
if (selectFirst) {
|
||||
state.selected = {
|
||||
network,
|
||||
name: channels[0]
|
||||
};
|
||||
state.history.push(state.selected);
|
||||
}
|
||||
},
|
||||
|
||||
[actions.PART](state, action) {
|
||||
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) {
|
||||
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) {
|
||||
state.history = state.history.filter(tab => tab.server !== action.server);
|
||||
state.history = state.history.filter(tab => tab.network !== action.network);
|
||||
},
|
||||
|
||||
[LOCATION_CHANGED](state, action) {
|
||||
@ -49,30 +60,30 @@ export default createReducer(initialState, {
|
||||
}
|
||||
});
|
||||
|
||||
export function select(server, name, doReplace) {
|
||||
export function select(network, name, doReplace) {
|
||||
const navigate = doReplace ? replace : push;
|
||||
if (name) {
|
||||
return navigate(`/${server}/${encodeURIComponent(name)}`);
|
||||
return navigate(`/${network}/${encodeURIComponent(name)}`);
|
||||
}
|
||||
return navigate(`/${server}`);
|
||||
return navigate(`/${network}`);
|
||||
}
|
||||
|
||||
export function tabExists(
|
||||
{ server, name },
|
||||
{ servers, channels, privateChats }
|
||||
{ network, name },
|
||||
{ networks, channels, privateChats }
|
||||
) {
|
||||
return (
|
||||
(name && get(channels, [server, name])) ||
|
||||
(!name && server && servers[server]) ||
|
||||
(name && find(privateChats[server], nick => nick === name))
|
||||
(name && get(channels, [network, name])) ||
|
||||
(!name && network && networks[network]) ||
|
||||
(name && find(privateChats[network], nick => nick === name))
|
||||
);
|
||||
}
|
||||
|
||||
function parseTabCookie() {
|
||||
const cookie = Cookie.get('tab');
|
||||
if (cookie) {
|
||||
const [server, name = null] = cookie.split(/;(.+)/);
|
||||
return { server, name };
|
||||
const [network, name = null] = cookie.split(/;(.+)/);
|
||||
return { network, name };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -88,35 +99,35 @@ export function updateSelection(tryCookie) {
|
||||
if (tryCookie) {
|
||||
const tab = parseTabCookie();
|
||||
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 { server } = state.tab.selected;
|
||||
const serverAddrs = Object.keys(servers);
|
||||
const { network } = state.tab.selected;
|
||||
const networkAddrs = Object.keys(networks);
|
||||
|
||||
if (serverAddrs.length === 0) {
|
||||
if (networkAddrs.length === 0) {
|
||||
dispatch(replace('/connect'));
|
||||
} else if (
|
||||
history.length > 0 &&
|
||||
tabExists(history[history.length - 1], state)
|
||||
) {
|
||||
const tab = history[history.length - 1];
|
||||
dispatch(select(tab.server, tab.name, true));
|
||||
} else if (servers[server]) {
|
||||
dispatch(select(server, null, true));
|
||||
dispatch(select(tab.network, tab.name, true));
|
||||
} else if (networks[network]) {
|
||||
dispatch(select(network, null, true));
|
||||
} 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 {
|
||||
type: actions.SELECT_TAB,
|
||||
server,
|
||||
network,
|
||||
name
|
||||
};
|
||||
}
|
||||
|
@ -10,4 +10,8 @@ precacheAndRoute(self.__WB_MANIFEST, {
|
||||
});
|
||||
|
||||
const handler = createHandlerBoundToURL('/');
|
||||
registerRoute(new NavigationRoute(handler));
|
||||
registerRoute(
|
||||
new NavigationRoute(handler, {
|
||||
denylist: [new RegExp('/downloads/')]
|
||||
})
|
||||
);
|
||||
|
@ -26,7 +26,7 @@ export default class Socket {
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.connected = true;
|
||||
this.emit('_connected', true);
|
||||
this.emit('connected', { connected: true });
|
||||
clearTimeout(this.timeoutConnect);
|
||||
this.backoff.reset();
|
||||
this.setTimeoutPing();
|
||||
@ -35,7 +35,7 @@ export default class Socket {
|
||||
this.ws.onclose = () => {
|
||||
if (this.connected) {
|
||||
this.connected = false;
|
||||
this.emit('_connected', false);
|
||||
this.emit('connected', { connected: false });
|
||||
}
|
||||
clearTimeout(this.timeoutConnect);
|
||||
clearTimeout(this.timeoutPing);
|
||||
|
@ -1,9 +1,19 @@
|
||||
import React from 'react';
|
||||
import TestRenderer from 'react-test-renderer';
|
||||
import { isChannel, isValidNick, isValidChannel, isValidUsername } from '..';
|
||||
import {
|
||||
trimPrefixChar,
|
||||
isChannel,
|
||||
isValidNick,
|
||||
isValidChannel,
|
||||
isValidUsername
|
||||
} from '..';
|
||||
import linkify from '../linkify';
|
||||
|
||||
const render = el => TestRenderer.create(el).toJSON();
|
||||
describe('trimPrefixChar()', () => {
|
||||
it('trims prefix characters', () => {
|
||||
expect(trimPrefixChar('##chan', '#')).toBe('chan');
|
||||
expect(trimPrefixChar('#chan', '#')).toBe('chan');
|
||||
expect(trimPrefixChar('chan', '#')).toBe('chan');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isChannel()', () => {
|
||||
it('it handles strings', () => {
|
||||
@ -81,21 +91,31 @@ describe('isValidUsername()', () => {
|
||||
|
||||
describe('linkify()', () => {
|
||||
const proto = href => (href.indexOf('http') !== 0 ? `http://${href}` : href);
|
||||
const linkTo = href =>
|
||||
render(
|
||||
<a href={proto(href)} rel="noopener noreferrer" target="_blank">
|
||||
{href}
|
||||
</a>
|
||||
);
|
||||
const linkTo = href => ({
|
||||
type: 'link',
|
||||
url: proto(href),
|
||||
text: href
|
||||
});
|
||||
const buildText = arr => {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (typeof arr[i] === 'string') {
|
||||
arr[i] = {
|
||||
type: 'text',
|
||||
text: arr[i]
|
||||
};
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
};
|
||||
|
||||
it('returns the arg when no matches are found', () =>
|
||||
[null, undefined, 10, false, true, 'just some text', ''].forEach(input =>
|
||||
expect(linkify(input)).toBe(input)
|
||||
it('returns a text block when no matches are found', () =>
|
||||
['just some text', ''].forEach(input =>
|
||||
expect(linkify(input)).toStrictEqual([{ type: 'text', text: input }])
|
||||
));
|
||||
|
||||
it('linkifies text', () =>
|
||||
Object.entries({
|
||||
'google.com': linkTo('google.com'),
|
||||
'google.com': [linkTo('google.com')],
|
||||
'google.com stuff': [linkTo('google.com'), ' stuff'],
|
||||
'cake google.com stuff': ['cake ', linkTo('google.com'), ' stuff'],
|
||||
'cake google.com stuff https://google.com': [
|
||||
@ -115,6 +135,6 @@ describe('linkify()', () => {
|
||||
'google.com ': [linkTo('google.com'), ' '],
|
||||
'/google.com?': ['/', linkTo('google.com'), '?']
|
||||
}).forEach(([input, expected]) =>
|
||||
expect(render(linkify(input))).toEqual(expected)
|
||||
expect(linkify(input)).toEqual(buildText(expected))
|
||||
));
|
||||
});
|
||||
|
@ -1,25 +1,5 @@
|
||||
/* eslint-disable no-bitwise */
|
||||
import { hsluvToHex } from 'hsluv';
|
||||
|
||||
//
|
||||
// github.com/sindresorhus/fnv1a
|
||||
//
|
||||
const OFFSET_BASIS_32 = 2166136261;
|
||||
|
||||
const fnv1a = string => {
|
||||
let hash = OFFSET_BASIS_32;
|
||||
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
hash ^= string.charCodeAt(i);
|
||||
|
||||
// 32-bit FNV prime: 2**24 + 2**8 + 0x93 = 16777619
|
||||
// Using bitshift for accuracy and performance. Numbers in JS suck.
|
||||
hash +=
|
||||
(hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
||||
}
|
||||
|
||||
return hash >>> 0;
|
||||
};
|
||||
import fnv1a from '@sindresorhus/fnv1a';
|
||||
|
||||
const colors = [];
|
||||
|
||||
|
356
client/js/utils/colorify.js
Normal file
356
client/js/utils/colorify.js
Normal file
@ -0,0 +1,356 @@
|
||||
export const formatChars = {
|
||||
bold: 0x02,
|
||||
italic: 0x1d,
|
||||
underline: 0x1f,
|
||||
strikethrough: 0x1e,
|
||||
color: 0x03,
|
||||
reverseColor: 0x16,
|
||||
reset: 0x0f
|
||||
};
|
||||
|
||||
export const colors = {
|
||||
0: 'white',
|
||||
1: 'black',
|
||||
2: 'blue',
|
||||
3: 'green',
|
||||
4: 'red',
|
||||
5: 'brown',
|
||||
6: 'magenta',
|
||||
7: 'orange',
|
||||
8: 'yellow',
|
||||
9: 'lightgreen',
|
||||
10: 'cyan',
|
||||
11: 'lightcyan',
|
||||
12: 'lightblue',
|
||||
13: 'pink',
|
||||
14: 'gray',
|
||||
15: 'lightgray',
|
||||
16: '#470000',
|
||||
17: '#472100',
|
||||
18: '#474700',
|
||||
19: '#324700',
|
||||
20: '#004700',
|
||||
21: '#00472c',
|
||||
22: '#004747',
|
||||
23: '#002747',
|
||||
24: '#000047',
|
||||
25: '#2e0047',
|
||||
26: '#470047',
|
||||
27: '#47002a',
|
||||
28: '#740000',
|
||||
29: '#743a00',
|
||||
30: '#747400',
|
||||
31: '#517400',
|
||||
32: '#007400',
|
||||
33: '#007449',
|
||||
34: '#007474',
|
||||
35: '#004074',
|
||||
36: '#000074',
|
||||
37: '#4b0074',
|
||||
38: '#740074',
|
||||
39: '#740045',
|
||||
40: '#b50000',
|
||||
41: '#b56300',
|
||||
42: '#b5b500',
|
||||
43: '#7db500',
|
||||
44: '#00b500',
|
||||
45: '#00b571',
|
||||
46: '#00b5b5',
|
||||
47: '#0063b5',
|
||||
48: '#0000b5',
|
||||
49: '#7500b5',
|
||||
50: '#b500b5',
|
||||
51: '#b5006b',
|
||||
52: '#ff0000',
|
||||
53: '#ff8c00',
|
||||
54: '#ffff00',
|
||||
55: '#b2ff00',
|
||||
56: '#00ff00',
|
||||
57: '#00ffa0',
|
||||
58: '#00ffff',
|
||||
59: '#008cff',
|
||||
60: '#0000ff',
|
||||
61: '#a500ff',
|
||||
62: '#ff00ff',
|
||||
63: '#ff0098',
|
||||
64: '#ff5959',
|
||||
65: '#ffb459',
|
||||
66: '#ffff71',
|
||||
67: '#cfff60',
|
||||
68: '#6fff6f',
|
||||
69: '#65ffc9',
|
||||
70: '#6dffff',
|
||||
71: '#59b4ff',
|
||||
72: '#5959ff',
|
||||
73: '#c459ff',
|
||||
74: '#ff66ff',
|
||||
75: '#ff59bc',
|
||||
76: '#ff9c9c',
|
||||
77: '#ffd39c',
|
||||
78: '#ffff9c',
|
||||
79: '#e2ff9c',
|
||||
80: '#9cff9c',
|
||||
81: '#9cffdb',
|
||||
82: '#9cffff',
|
||||
83: '#9cd3ff',
|
||||
84: '#9c9cff',
|
||||
85: '#dc9cff',
|
||||
86: '#ff9cff',
|
||||
87: '#ff94d3',
|
||||
88: '#000000',
|
||||
89: '#131313',
|
||||
90: '#282828',
|
||||
91: '#363636',
|
||||
92: '#4d4d4d',
|
||||
93: '#656565',
|
||||
94: '#818181',
|
||||
95: '#9f9f9f',
|
||||
96: '#bcbcbc',
|
||||
97: '#e2e2e2',
|
||||
98: '#ffffff'
|
||||
};
|
||||
|
||||
function tokenize(str) {
|
||||
const tokens = [];
|
||||
|
||||
let colorBuffer = '';
|
||||
let color = false;
|
||||
let background = false;
|
||||
let colorToken;
|
||||
|
||||
let start = 0;
|
||||
let end = 0;
|
||||
|
||||
const pushText = () => {
|
||||
if (end > start) {
|
||||
tokens.push({
|
||||
type: 'text',
|
||||
content: str.slice(start, end)
|
||||
});
|
||||
start = end;
|
||||
}
|
||||
};
|
||||
|
||||
const pushToken = token => {
|
||||
pushText();
|
||||
tokens.push(token);
|
||||
};
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const charCode = str.charCodeAt(i);
|
||||
|
||||
if (color) {
|
||||
if (charCode >= 48 && charCode <= 57 && colorBuffer.length < 2) {
|
||||
colorBuffer += str[i];
|
||||
} else if (charCode === 44 && !background) {
|
||||
colorToken.color = colors[parseInt(colorBuffer, 10)];
|
||||
colorBuffer = '';
|
||||
background = true;
|
||||
} else {
|
||||
if (background) {
|
||||
if (colorBuffer.length > 0) {
|
||||
colorToken.background = colors[parseInt(colorBuffer, 10)];
|
||||
} else {
|
||||
// Trailing comma
|
||||
start--;
|
||||
}
|
||||
} else {
|
||||
colorToken.color = colors[parseInt(colorBuffer, 10)];
|
||||
}
|
||||
|
||||
start--;
|
||||
colorBuffer = '';
|
||||
color = false;
|
||||
tokens.push(colorToken);
|
||||
}
|
||||
} else {
|
||||
switch (charCode) {
|
||||
case formatChars.bold:
|
||||
pushToken({
|
||||
type: 'bold'
|
||||
});
|
||||
break;
|
||||
|
||||
case formatChars.italic:
|
||||
pushToken({
|
||||
type: 'italic'
|
||||
});
|
||||
break;
|
||||
|
||||
case formatChars.underline:
|
||||
pushToken({
|
||||
type: 'underline'
|
||||
});
|
||||
break;
|
||||
|
||||
case formatChars.strikethrough:
|
||||
pushToken({
|
||||
type: 'strikethrough'
|
||||
});
|
||||
break;
|
||||
|
||||
case formatChars.color:
|
||||
pushText();
|
||||
|
||||
colorToken = {
|
||||
type: 'color'
|
||||
};
|
||||
color = true;
|
||||
background = false;
|
||||
break;
|
||||
|
||||
case formatChars.reverseColor:
|
||||
pushToken({
|
||||
type: 'reverse'
|
||||
});
|
||||
break;
|
||||
|
||||
case formatChars.reset:
|
||||
pushToken({
|
||||
type: 'reset'
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
start--;
|
||||
}
|
||||
}
|
||||
|
||||
start++;
|
||||
end++;
|
||||
}
|
||||
|
||||
if (start === 0) {
|
||||
return str;
|
||||
}
|
||||
|
||||
pushText();
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function colorifyString(str, state = {}) {
|
||||
const tokens = tokenize(str);
|
||||
|
||||
if (tokens === str) {
|
||||
return [tokens, state];
|
||||
}
|
||||
|
||||
const result = [];
|
||||
let style = state.style || {};
|
||||
let reverse = state.reverse || false;
|
||||
|
||||
const toggle = (prop, value, multiple) => {
|
||||
if (style[prop]) {
|
||||
if (multiple) {
|
||||
const props = style[prop].split(' ');
|
||||
const i = props.indexOf(value);
|
||||
if (i !== -1) {
|
||||
props.splice(i, 1);
|
||||
} else {
|
||||
props.push(value);
|
||||
}
|
||||
style[prop] = props.join(' ');
|
||||
} else {
|
||||
delete style[prop];
|
||||
}
|
||||
} else {
|
||||
style[prop] = value;
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i];
|
||||
|
||||
switch (token.type) {
|
||||
case 'bold':
|
||||
toggle('fontWeight', 700);
|
||||
break;
|
||||
|
||||
case 'italic':
|
||||
toggle('fontStyle', 'italic');
|
||||
break;
|
||||
|
||||
case 'underline':
|
||||
toggle('textDecoration', 'underline', true);
|
||||
break;
|
||||
|
||||
case 'strikethrough':
|
||||
toggle('textDecoration', 'line-through', true);
|
||||
break;
|
||||
|
||||
case 'color':
|
||||
if (!token.color) {
|
||||
delete style.color;
|
||||
delete style.background;
|
||||
} else if (reverse) {
|
||||
style.color = token.background;
|
||||
style.background = token.color;
|
||||
} else {
|
||||
style.color = token.color;
|
||||
style.background = token.background;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'reverse':
|
||||
reverse = !reverse;
|
||||
if (style.color) {
|
||||
const bg = style.background;
|
||||
style.background = style.color;
|
||||
style.color = bg;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'reset':
|
||||
style = {};
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
if (Object.keys(style).length > 0) {
|
||||
result.push({
|
||||
type: 'format',
|
||||
style,
|
||||
text: token.content
|
||||
});
|
||||
style = { ...style };
|
||||
} else {
|
||||
result.push({
|
||||
type: 'text',
|
||||
text: token.content
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return [result, { style, reverse }];
|
||||
}
|
||||
|
||||
export default function colorify(blocks) {
|
||||
if (!blocks) {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
const result = [];
|
||||
let colored;
|
||||
let state;
|
||||
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
const block = blocks[i];
|
||||
|
||||
if (block.type === 'text') {
|
||||
[colored, state] = colorifyString(block.text, state);
|
||||
if (colored !== block.text) {
|
||||
result.push(...colored);
|
||||
} else {
|
||||
result.push(block);
|
||||
}
|
||||
} else {
|
||||
result.push(block);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
@ -3,14 +3,6 @@ import padStart from 'lodash/padStart';
|
||||
export { findBreakpoints, messageHeight } from './messageHeight';
|
||||
export { default as linkify } from './linkify';
|
||||
|
||||
export function normalizeChannel(channel) {
|
||||
if (channel.indexOf('#') !== 0) {
|
||||
return channel;
|
||||
}
|
||||
|
||||
return channel.split('#').join('').toLowerCase();
|
||||
}
|
||||
|
||||
export function isChannel(name) {
|
||||
// TODO: Handle other channel types
|
||||
if (typeof name === 'object') {
|
||||
@ -19,17 +11,17 @@ export function isChannel(name) {
|
||||
return typeof name === 'string' && name[0] === '#';
|
||||
}
|
||||
|
||||
export function stringifyTab(server, name) {
|
||||
if (typeof server === 'object') {
|
||||
if (server.name) {
|
||||
return `${server.server};${server.name}`;
|
||||
export function stringifyTab(network, name) {
|
||||
if (typeof network === 'object') {
|
||||
if (network.name) {
|
||||
return `${network.network};${network.name}`;
|
||||
}
|
||||
return server.server;
|
||||
return network.network;
|
||||
}
|
||||
if (name) {
|
||||
return `${server};${name}`;
|
||||
return `${network};${name}`;
|
||||
}
|
||||
return server;
|
||||
return network;
|
||||
}
|
||||
|
||||
function isString(s, maxLength) {
|
||||
@ -42,6 +34,26 @@ function isString(s, maxLength) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isDM({ from, to }) {
|
||||
return !to && from?.indexOf('.') === -1 && !isChannel(from);
|
||||
}
|
||||
|
||||
export function trimPrefixChar(str, char) {
|
||||
if (!isString(str)) {
|
||||
return str;
|
||||
}
|
||||
|
||||
let start = 0;
|
||||
while (str[start] === char) {
|
||||
start++;
|
||||
}
|
||||
|
||||
if (start > 0) {
|
||||
return str.slice(start);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
// RFC 2812
|
||||
// nickname = ( letter / special ) *( letter / digit / special / "-" )
|
||||
// letter = A-Z / a-z
|
||||
@ -121,6 +133,10 @@ export function isValidUsername(username) {
|
||||
}
|
||||
|
||||
export function isInt(i, min, max) {
|
||||
if (typeof i === 'string') {
|
||||
i = parseInt(i, 10);
|
||||
}
|
||||
|
||||
if (i < min || i > max || Math.floor(i) !== i) {
|
||||
return false;
|
||||
}
|
||||
@ -137,6 +153,13 @@ export function timestamp(date = new Date()) {
|
||||
const dateFmt = new Intl.DateTimeFormat(window.navigator.language);
|
||||
export const formatDate = dateFmt.format;
|
||||
|
||||
export function unix(date) {
|
||||
if (date) {
|
||||
return Math.floor(date.getTime() / 1000);
|
||||
}
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
|
@ -1,20 +1,44 @@
|
||||
import Autolinker from 'autolinker';
|
||||
import React from 'react';
|
||||
|
||||
const autolinker = new Autolinker({
|
||||
stripPrefix: false,
|
||||
stripTrailingSlash: false
|
||||
});
|
||||
|
||||
function pushText(arr, text) {
|
||||
const last = arr[arr.length - 1];
|
||||
if (last?.type === 'text') {
|
||||
last.text += text;
|
||||
} else {
|
||||
arr.push({
|
||||
type: 'text',
|
||||
text
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function pushLink(arr, url, text) {
|
||||
arr.push({
|
||||
type: 'link',
|
||||
url,
|
||||
text
|
||||
});
|
||||
}
|
||||
|
||||
export default function linkify(text) {
|
||||
if (!text) {
|
||||
if (typeof text !== 'string') {
|
||||
return text;
|
||||
}
|
||||
|
||||
let matches = autolinker.parseText(text);
|
||||
|
||||
if (matches.length === 0) {
|
||||
return text;
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
text
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const result = [];
|
||||
@ -26,46 +50,27 @@ export default function linkify(text) {
|
||||
|
||||
if (match.getType() === 'url') {
|
||||
if (match.offset > pos) {
|
||||
if (typeof result[result.length - 1] === 'string') {
|
||||
result[result.length - 1] += text.slice(pos, match.offset);
|
||||
} else {
|
||||
result.push(text.slice(pos, match.offset));
|
||||
}
|
||||
pushText(result, text.slice(pos, match.offset));
|
||||
}
|
||||
|
||||
result.push(
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={match.getAnchorHref()}
|
||||
key={i}
|
||||
>
|
||||
{match.matchedText}
|
||||
</a>
|
||||
);
|
||||
} else if (typeof result[result.length - 1] === 'string') {
|
||||
result[result.length - 1] += text.slice(
|
||||
pos,
|
||||
match.offset + match.matchedText.length
|
||||
);
|
||||
pushLink(result, match.getAnchorHref(), match.matchedText);
|
||||
} else {
|
||||
result.push(text.slice(pos, match.offset + match.matchedText.length));
|
||||
pushText(
|
||||
result,
|
||||
text.slice(pos, match.offset + match.matchedText.length)
|
||||
);
|
||||
}
|
||||
|
||||
pos = match.offset + match.matchedText.length;
|
||||
}
|
||||
|
||||
if (pos < text.length) {
|
||||
if (typeof result[result.length - 1] === 'string') {
|
||||
result[result.length - 1] += text.slice(pos);
|
||||
if (result[result.length - 1]?.type === 'text') {
|
||||
result[result.length - 1].text += text.slice(pos);
|
||||
} else {
|
||||
result.push(text.slice(pos));
|
||||
pushText(result, text.slice(pos));
|
||||
}
|
||||
}
|
||||
|
||||
if (result.length === 1) {
|
||||
return result[0];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -2,20 +2,27 @@ const lineHeight = 24;
|
||||
const userListWidth = 200;
|
||||
const smallScreen = 600;
|
||||
|
||||
export function findBreakpoints(text) {
|
||||
export function findBreakpoints(blocks) {
|
||||
const breakpoints = [];
|
||||
let length = 0;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text.charAt(i);
|
||||
for (let j = 0; j < blocks.length; j++) {
|
||||
const {text} = blocks[j];
|
||||
|
||||
if (char === ' ') {
|
||||
breakpoints.push({ end: i, next: i + 1 });
|
||||
} else if (char === '-' && i !== text.length - 1) {
|
||||
breakpoints.push({ end: i + 1, next: i + 1 });
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text.charAt(i);
|
||||
|
||||
if (char === ' ') {
|
||||
breakpoints.push({ end: length + i, next: length + i + 1 });
|
||||
} else if (i !== text.length - 1 && (char === '-' || char === '?')) {
|
||||
breakpoints.push({ end: length + i + 1, next: length + i + 1 });
|
||||
}
|
||||
}
|
||||
|
||||
length += text.length;
|
||||
}
|
||||
|
||||
return breakpoints;
|
||||
return [breakpoints, length];
|
||||
}
|
||||
|
||||
export function messageHeight(
|
||||
|
@ -6,11 +6,21 @@ export const PUSH = 'ROUTER_PUSH';
|
||||
export const REPLACE = 'ROUTER_REPLACE';
|
||||
|
||||
export function locationChanged(route, params, location) {
|
||||
Object.keys(params).forEach(key => {
|
||||
params[key] = decodeURIComponent(params[key]);
|
||||
});
|
||||
|
||||
const query = {};
|
||||
new URLSearchParams(location.search).forEach((value, key) => {
|
||||
query[key] = value;
|
||||
});
|
||||
|
||||
return {
|
||||
type: LOCATION_CHANGED,
|
||||
route,
|
||||
params,
|
||||
location
|
||||
query,
|
||||
path: decodeURIComponent(location.pathname)
|
||||
};
|
||||
}
|
||||
|
||||
@ -28,13 +38,9 @@ export function replace(path) {
|
||||
};
|
||||
}
|
||||
|
||||
export function routeReducer(state = {}, action) {
|
||||
if (action.type === LOCATION_CHANGED) {
|
||||
return {
|
||||
route: action.route,
|
||||
params: action.params,
|
||||
location: action.location
|
||||
};
|
||||
export function routeReducer(state = {}, { type, ...action }) {
|
||||
if (type === LOCATION_CHANGED) {
|
||||
return action;
|
||||
}
|
||||
|
||||
return state;
|
||||
@ -44,7 +50,7 @@ export function routeMiddleware() {
|
||||
return next => action => {
|
||||
switch (action.type) {
|
||||
case PUSH:
|
||||
history.push(action.path);
|
||||
history.push(`${action.path}`);
|
||||
break;
|
||||
case REPLACE:
|
||||
history.replace(action.path);
|
||||
@ -55,24 +61,13 @@ export function routeMiddleware() {
|
||||
};
|
||||
}
|
||||
|
||||
function decode(location) {
|
||||
location.pathname = decodeURIComponent(location.pathname);
|
||||
return location;
|
||||
}
|
||||
|
||||
function match(routes, location) {
|
||||
let params;
|
||||
for (let i = 0; i < routes.length; i++) {
|
||||
params = routes[i].pattern.match(location.pathname);
|
||||
const params = routes[i].pattern.match(location.pathname);
|
||||
if (params !== null) {
|
||||
const keys = Object.keys(params);
|
||||
for (let j = 0; j < keys.length; j++) {
|
||||
params[keys[j]] = decodeURIComponent(params[keys[j]]);
|
||||
}
|
||||
return locationChanged(routes[i].name, params, decode({ ...location }));
|
||||
return locationChanged(routes[i].name, params, location);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function initRouter(routes, store) {
|
||||
@ -91,16 +86,11 @@ export default function initRouter(routes, store) {
|
||||
let matched = match(patterns, history.location);
|
||||
if (matched) {
|
||||
store.dispatch(matched);
|
||||
} else {
|
||||
matched = { location: {} };
|
||||
}
|
||||
|
||||
history.listen(({ location }) => {
|
||||
const nextMatch = match(patterns, location);
|
||||
if (
|
||||
nextMatch &&
|
||||
nextMatch.location.pathname !== matched.location.pathname
|
||||
) {
|
||||
if (nextMatch && nextMatch.path !== matched?.path) {
|
||||
matched = nextMatch;
|
||||
store.dispatch(matched);
|
||||
}
|
||||
|
6
client/jsconfig.json
Normal file
6
client/jsconfig.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./js"
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
@ -5,44 +5,47 @@
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"browserslist": [
|
||||
"Edge >= 16",
|
||||
"Edge >= 79",
|
||||
"Firefox >= 60",
|
||||
"Chrome >= 61",
|
||||
"Safari >= 10.1",
|
||||
"iOS >= 10.3"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.8.3",
|
||||
"@babel/plugin-proposal-export-namespace-from": "^7.8.3",
|
||||
"@babel/core": "^7.10.2",
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.1",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.10.1",
|
||||
"@babel/plugin-proposal-export-namespace-from": "^7.10.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-react-constant-elements": "^7.9.0",
|
||||
"@babel/plugin-transform-react-inline-elements": "^7.9.0",
|
||||
"@babel/preset-env": "^7.9.5",
|
||||
"@babel/preset-react": "^7.9.4",
|
||||
"@babel/plugin-transform-react-constant-elements": "^7.10.1",
|
||||
"@babel/plugin-transform-react-inline-elements": "^7.10.1",
|
||||
"@babel/preset-env": "^7.10.2",
|
||||
"@babel/preset-react": "^7.10.1",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^25.5.0",
|
||||
"babel-jest": "^26.0.1",
|
||||
"babel-loader": "^8.1.0",
|
||||
"brotli": "^1.3.1",
|
||||
"copy-webpack-plugin": "^5.1.1",
|
||||
"canvas": "^2.6.1",
|
||||
"copy-webpack-plugin": "^6.0.2",
|
||||
"cross-env": "^7.0.2",
|
||||
"css-loader": "^3.5.3",
|
||||
"cssnano": "^4.1.10",
|
||||
"del": "^5.1.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-airbnb": "^18.1.0",
|
||||
"eslint": "^7.2.0",
|
||||
"eslint-config-airbnb": "^18.2.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-import-resolver-webpack": "^0.12.1",
|
||||
"eslint-import-resolver-webpack": "^0.12.2",
|
||||
"eslint-loader": "^4.0.2",
|
||||
"eslint-plugin-babel": "^5.3.0",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"eslint-plugin-react-hooks": "^3.0.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.3.0",
|
||||
"eslint-plugin-react": "^7.20.0",
|
||||
"eslint-plugin-react-hooks": "^4.0.4",
|
||||
"express": "^4.17.1",
|
||||
"express-http-proxy": "^1.6.0",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-util": "^3.0.8",
|
||||
"jest": "^25.5.0",
|
||||
"jest": "^26.0.1",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"postcss-flexbugs-fixes": "^4.2.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
@ -50,7 +53,7 @@
|
||||
"prettier": "2.0.5",
|
||||
"react-test-renderer": "16.13.1",
|
||||
"style-loader": "^1.2.1",
|
||||
"terser-webpack-plugin": "^2.3.6",
|
||||
"terser-webpack-plugin": "^3.0.6",
|
||||
"through2": "^3.0.1",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-dev-middleware": "^3.7.2",
|
||||
@ -59,19 +62,19 @@
|
||||
"workbox-webpack-plugin": "^5.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sindresorhus/fnv1a": "^2.0.1",
|
||||
"autolinker": "^3.14.1",
|
||||
"backo": "^1.1.0",
|
||||
"classnames": "^2.2.6",
|
||||
"fontfaceobserver": "^2.0.9",
|
||||
"formik": "^2.1.4",
|
||||
"history": "^5.0.0-beta.8",
|
||||
"hsluv": "^0.1.0",
|
||||
"immer": "^6.0.3",
|
||||
"immer": "^7.0.1",
|
||||
"js-cookie": "^2.2.1",
|
||||
"lodash": "^4.17.15",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-hot-loader": "^4.12.20",
|
||||
"react-hot-loader": "^4.12.21",
|
||||
"react-icons": "^3.7.0",
|
||||
"react-modal": "^3.11.2",
|
||||
"react-redux": "^7.2.0",
|
||||
@ -91,9 +94,9 @@
|
||||
"test": "jest",
|
||||
"test:verbose": "jest --verbose",
|
||||
"test:watch": "jest --watch",
|
||||
"gen:install": "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:json": "easyjson -all -lower_camel_case -omit_empty ../server/json.go ../server/index_data.go && 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": {
|
||||
"moduleNameMapper": {
|
||||
|
@ -81,7 +81,9 @@ module.exports = {
|
||||
chunkFilename: '[name].[contenthash].css'
|
||||
}),
|
||||
new HashOutputPlugin(),
|
||||
new CopyPlugin(['public']),
|
||||
new CopyPlugin({
|
||||
patterns: ['public']
|
||||
}),
|
||||
new InjectManifest({
|
||||
swSrc: './js/sw.js',
|
||||
additionalManifestEntries: [
|
||||
@ -90,13 +92,7 @@ module.exports = {
|
||||
revision: '__INDEX_REVISON__'
|
||||
}
|
||||
],
|
||||
exclude: [
|
||||
/\.map$/,
|
||||
/^manifest.*\.js(?:on)?$/,
|
||||
/^boot.*\.js$/,
|
||||
/^runtime.*\.js$/,
|
||||
/\.txt$/
|
||||
]
|
||||
exclude: [/\.map$/, /^boot.*\.js$/, /^runtime.*\.js$/, /\.txt$/]
|
||||
})
|
||||
],
|
||||
optimization: {
|
||||
|
4305
client/yarn.lock
4305
client/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -48,7 +48,10 @@ var rootCmd = &cobra.Command{
|
||||
|
||||
storage.Initialize(viper.GetString("dir"), viper.GetString("data"), viper.GetString("conf"))
|
||||
|
||||
initConfig(storage.Path.Config(), viper.GetBool("reset-config"))
|
||||
err := initConfig(storage.Path.Config(), viper.GetBool("reset-config"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
@ -63,6 +66,14 @@ var rootCmd = &cobra.Command{
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
storage.GetMessageStore = func(user *storage.User) (storage.MessageStore, error) {
|
||||
return boltdb.New(storage.Path.Log(user.Username))
|
||||
}
|
||||
|
||||
storage.GetMessageSearchProvider = func(user *storage.User) (storage.MessageSearchProvider, error) {
|
||||
return bleve.New(storage.Path.Index(user.Username))
|
||||
}
|
||||
|
||||
cfg, cfgUpdated := config.LoadConfig()
|
||||
dispatch := server.New(cfg)
|
||||
|
||||
@ -76,14 +87,6 @@ var rootCmd = &cobra.Command{
|
||||
dispatch.Store = db
|
||||
dispatch.SessionStore = db
|
||||
|
||||
dispatch.GetMessageStore = func(user *storage.User) (storage.MessageStore, error) {
|
||||
return boltdb.New(storage.Path.Log(user.Username))
|
||||
}
|
||||
|
||||
dispatch.GetMessageSearchProvider = func(user *storage.User) (storage.MessageSearchProvider, error) {
|
||||
return bleve.New(storage.Path.Index(user.Username))
|
||||
}
|
||||
|
||||
dispatch.Run()
|
||||
},
|
||||
}
|
||||
@ -109,23 +112,35 @@ func init() {
|
||||
viper.BindPFlags(rootCmd.PersistentFlags())
|
||||
viper.BindPFlags(rootCmd.Flags())
|
||||
|
||||
viper.SetDefault("hexIP", false)
|
||||
viper.SetDefault("identd", true)
|
||||
viper.SetDefault("auto_ctcp", true)
|
||||
viper.SetDefault("verify_certificates", true)
|
||||
|
||||
viper.SetDefault("https.enabled", true)
|
||||
viper.SetDefault("https.port", 443)
|
||||
|
||||
viper.SetDefault("auth.anonymous", true)
|
||||
viper.SetDefault("auth.login", true)
|
||||
viper.SetDefault("auth.registration", true)
|
||||
|
||||
viper.SetDefault("dcc.enabled", true)
|
||||
viper.SetDefault("dcc.autoget.delete", true)
|
||||
|
||||
viper.SetDefault("proxy.protocol", "socks5")
|
||||
viper.SetDefault("proxy.host", "127.0.0.1")
|
||||
viper.SetDefault("proxy.port", 1080)
|
||||
}
|
||||
|
||||
func initConfig(configPath string, overwrite bool) {
|
||||
func initConfig(configPath string, overwrite bool) error {
|
||||
if _, err := os.Stat(configPath); overwrite || os.IsNotExist(err) {
|
||||
config, err := assets.Asset("config.default.toml")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("Writing default config to", configPath)
|
||||
|
||||
err = ioutil.WriteFile(configPath, config, 0600)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
return ioutil.WriteFile(configPath, config, 0600)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -1,20 +1,25 @@
|
||||
# IP address to listen on, leave empty to listen on anything
|
||||
address = ""
|
||||
port = 80
|
||||
# Run ident daemon on port 113
|
||||
identd = true
|
||||
# Hex encode the users IP and use it as the ident
|
||||
hexIP = false
|
||||
# Automatically reply to common CTCP messages
|
||||
auto_ctcp = true
|
||||
# Verify the certificate chain presented by the IRC server, if this check fails
|
||||
# the user will be able to choose to still connect
|
||||
verify_certificates = true
|
||||
|
||||
# Defaults for the client connect form
|
||||
[defaults]
|
||||
name = "Freenode"
|
||||
name = "freenode"
|
||||
host = "chat.freenode.net"
|
||||
port = 6697
|
||||
channels = [
|
||||
"#dispatch",
|
||||
"#go-nuts"
|
||||
"#dispatch"
|
||||
]
|
||||
password = ""
|
||||
server_password = ""
|
||||
ssl = true
|
||||
# Only allow a nick to be filled in
|
||||
readonly = false
|
||||
@ -62,7 +67,30 @@ secret = ""
|
||||
key = ""
|
||||
secret = ""
|
||||
|
||||
# Strict-Transport-Security
|
||||
[dcc]
|
||||
# Receive files through DCC, the user gets to choose if they want to accept the file,
|
||||
# the file download then gets proxied to the user
|
||||
enabled = true
|
||||
|
||||
[dcc.autoget]
|
||||
# Instead of proxying the file download directly to the user, dispatch automatically downloads
|
||||
# DCC files and sends a download link to the user once its done
|
||||
enabled = false
|
||||
# Delete the file after the user has downloaded it once
|
||||
delete = true
|
||||
# Delete the file after a certain time period of inactivity, not implemented yet
|
||||
delete_after = "30m"
|
||||
|
||||
[proxy]
|
||||
# Dispatch will make all outgoing connections through the specified proxy when enabled
|
||||
enabled = false
|
||||
protocol = "socks5"
|
||||
host = "127.0.0.1"
|
||||
port = 1080
|
||||
username = ""
|
||||
password = ""
|
||||
|
||||
# HTTP Strict-Transport-Security
|
||||
[https.hsts]
|
||||
enabled = false
|
||||
max_age = 31536000
|
||||
|
@ -12,24 +12,28 @@ type Config struct {
|
||||
Address string
|
||||
Port string
|
||||
Dev bool
|
||||
Identd bool
|
||||
HexIP bool
|
||||
AutoCTCP bool `mapstructure:"auto_ctcp"`
|
||||
VerifyCertificates bool `mapstructure:"verify_certificates"`
|
||||
Headers map[string]string
|
||||
Defaults Defaults
|
||||
HTTPS HTTPS
|
||||
LetsEncrypt LetsEncrypt
|
||||
Auth Auth
|
||||
DCC DCC
|
||||
Proxy Proxy
|
||||
}
|
||||
|
||||
type Defaults struct {
|
||||
Name string
|
||||
Host string
|
||||
Port int
|
||||
Channels []string
|
||||
Password string
|
||||
SSL bool
|
||||
ReadOnly bool
|
||||
ShowDetails bool `mapstructure:"show_details"`
|
||||
Name string
|
||||
Host string
|
||||
Port string
|
||||
Channels []string
|
||||
ServerPassword string `mapstructure:"server_password"`
|
||||
SSL bool
|
||||
ReadOnly bool
|
||||
ShowDetails bool `mapstructure:"show_details"`
|
||||
}
|
||||
|
||||
type HTTPS struct {
|
||||
@ -64,6 +68,26 @@ type Provider struct {
|
||||
Secret string
|
||||
}
|
||||
|
||||
type DCC struct {
|
||||
Enabled bool
|
||||
Autoget Autoget
|
||||
}
|
||||
|
||||
type Autoget struct {
|
||||
Enabled bool
|
||||
Delete bool
|
||||
DeleteAfter time.Duration `mapstructure:"delete_after"`
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
Enabled bool
|
||||
Protocol string
|
||||
Host string
|
||||
Port string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func LoadConfig() (*Config, chan *Config) {
|
||||
viper.SetConfigName("config")
|
||||
viper.AddConfigPath(storage.Path.ConfigRoot())
|
||||
|
33
go.mod
33
go.mod
@ -4,47 +4,52 @@ go 1.14
|
||||
|
||||
require (
|
||||
github.com/RoaringBitmap/roaring v0.4.23 // indirect
|
||||
github.com/blevesearch/bleve v1.0.7
|
||||
github.com/caddyserver/certmagic v0.10.12
|
||||
github.com/blevesearch/bleve v1.0.9
|
||||
github.com/caddyserver/certmagic v0.11.2
|
||||
github.com/cenkalti/backoff/v4 v4.0.2 // indirect
|
||||
github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d // indirect
|
||||
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect
|
||||
github.com/cznic/strutil v0.0.0-20181122101858-275e90344537 // indirect
|
||||
github.com/dsnet/compress v0.0.1
|
||||
github.com/eyedeekay/goSam v0.32.22
|
||||
github.com/eyedeekay/sam3 v0.32.3 // indirect
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.9
|
||||
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a // indirect
|
||||
github.com/go-acme/lego/v3 v3.6.0 // indirect
|
||||
github.com/golang/protobuf v1.4.0 // indirect
|
||||
github.com/golang/protobuf v1.4.2 // indirect
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/jmhodges/levigo v1.0.0 // indirect
|
||||
github.com/jpillora/backoff v1.0.0
|
||||
github.com/kjk/betterguid v0.0.0-20170621091430-c442874ba63a
|
||||
github.com/klauspost/cpuid v1.2.3
|
||||
github.com/klauspost/cpuid v1.3.0
|
||||
github.com/mailru/easyjson v0.7.2-0.20200424172602-f0a000e7a8e0
|
||||
github.com/miekg/dns v1.1.29 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/mitchellh/mapstructure v1.3.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.3.2 // indirect
|
||||
github.com/onsi/ginkgo v1.8.0 // indirect
|
||||
github.com/onsi/gomega v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml v1.7.0 // indirect
|
||||
github.com/pelletier/go-toml v1.8.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20190512091148-babf20351dd7 // indirect
|
||||
github.com/spf13/afero v1.2.2 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/cobra v1.0.0
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.6.3
|
||||
github.com/spf13/viper v1.7.0
|
||||
github.com/stretchr/testify v1.5.1
|
||||
github.com/tdewolff/minify/v2 v2.7.4
|
||||
github.com/tdewolff/minify/v2 v2.7.6
|
||||
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect
|
||||
github.com/tinylib/msgp v1.1.2 // indirect
|
||||
github.com/xdg-go/scram v0.0.0-20180814205039-7eeb5667e42c
|
||||
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c // indirect
|
||||
github.com/xdg/stringprep v1.0.0 // indirect
|
||||
go.etcd.io/bbolt v1.3.4
|
||||
golang.org/x/crypto v0.0.0-20200427165652-729f1e841bcc // indirect
|
||||
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0
|
||||
golang.org/x/sys v0.0.0-20200428200454-593003d681fa // indirect
|
||||
gopkg.in/ini.v1 v1.55.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.5.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 // indirect
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9
|
||||
golang.org/x/sys v0.0.0-20200610111108-226ff32320da // indirect
|
||||
google.golang.org/protobuf v1.24.0 // indirect
|
||||
gopkg.in/ini.v1 v1.57.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
||||
)
|
||||
|
160
go.sum
160
go.sum
@ -14,6 +14,7 @@ cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNF
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
@ -45,22 +46,23 @@ github.com/RoaringBitmap/roaring v0.4.23 h1:gpyfd12QohbqhFO4NVDUdoPOCXsyahYRQhIN
|
||||
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.0/go.mod h1:zpDJeKyp9ScW4NNrbdr+Eyxvry3ilGPewKoXw3XGN1k=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.8/go.mod h1:aVvklgKsPENRkl29bNwrHISa1F+YLGTHArMxZMBqWM8=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190808125512-07798873deee/go.mod h1:myCDvQSzCW+wB1WAlocEru4wMGJxy+vlxHdhegi1CDQ=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.112/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/aws/aws-sdk-go v1.23.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/aws/aws-sdk-go v1.30.20/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/blevesearch/bleve v1.0.7 h1:4PspZE7XABMSKcVpzAKp0E05Yer1PIYmTWk+1ngNr/c=
|
||||
github.com/blevesearch/bleve v1.0.7/go.mod h1:3xvmBtaw12Y4C9iA1RTzwWCof5j5HjydjCTiDE2TeE0=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/blevesearch/bleve v1.0.9 h1:kqw/Ank/61UV9/Bx9kCcnfH6qWPgmS8O5LNfpsgzASg=
|
||||
github.com/blevesearch/bleve v1.0.9/go.mod h1:tb04/rbU29clbtNgorgFd8XdJea4x3ybYaOjWKr+UBU=
|
||||
github.com/blevesearch/blevex v0.0.0-20190916190636-152f0fe5c040 h1:SjYVcfJVZoCfBlg+fkaq2eoZHTf5HaJfaTeTkOtyfHQ=
|
||||
github.com/blevesearch/blevex v0.0.0-20190916190636-152f0fe5c040/go.mod h1:WH+MU2F4T0VmSdaPX+Wu5GYoZBrYWdOZWSjzvYcDmqQ=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
|
||||
@ -71,12 +73,16 @@ github.com/blevesearch/segment v0.9.0 h1:5lG7yBCx98or7gK2cHMKPukPZ/31Kag7nONpoBt
|
||||
github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ=
|
||||
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
|
||||
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
|
||||
github.com/blevesearch/zap/v11 v11.0.7 h1:nnmAOP6eXBkqEa1Srq1eqA5Wmn4w+BZjLdjynNxvd+M=
|
||||
github.com/blevesearch/zap/v11 v11.0.7/go.mod h1:bJoY56fdU2m/IP4LLz/1h4jY2thBoREvoqbuJ8zhm9k=
|
||||
github.com/blevesearch/zap/v12 v12.0.7 h1:y8FWSAYkdc4p1dn4YLxNNr1dxXlSUsakJh2Fc/r6cj4=
|
||||
github.com/blevesearch/zap/v12 v12.0.7/go.mod h1:70DNK4ZN4tb42LubeDbfpp6xnm8g3ROYVvvZ6pEoXD8=
|
||||
github.com/caddyserver/certmagic v0.10.12 h1:aZtgzcIssiMSlP0jDdpDBbBzQ5INf5eKL9T6Nf3YzKM=
|
||||
github.com/caddyserver/certmagic v0.10.12/go.mod h1:Y8jcUBctgk/IhpAzlHKfimZNyXCkfGgRTC0orl8gROQ=
|
||||
github.com/blevesearch/zap/v11 v11.0.9 h1:wlSrDBeGN1G4M51NQHIXca23ttwUfQpWaK7uhO5lRSo=
|
||||
github.com/blevesearch/zap/v11 v11.0.9/go.mod h1:47hzinvmY2EvvJruzsSCJpro7so8L1neseaGjrtXHOY=
|
||||
github.com/blevesearch/zap/v12 v12.0.9 h1:PpatkY+BLVFZf0Ok3/fwgI/I4RU0z5blXFGuQANmqXk=
|
||||
github.com/blevesearch/zap/v12 v12.0.9/go.mod h1:paQuvxy7yXor+0Mx8p2KNmJgygQbQNN+W6HRfL5Hvwc=
|
||||
github.com/blevesearch/zap/v13 v13.0.1 h1:NSCM6uKu77Vn/x9nlPp4pE1o/bftqcOWZEHSyZVpGBQ=
|
||||
github.com/blevesearch/zap/v13 v13.0.1/go.mod h1:XmyNLMvMf8Z5FjLANXwUeDW3e1+o77TTGUWrth7T9WI=
|
||||
github.com/blevesearch/zap/v14 v14.0.0 h1:HF8Ysjm13qxB0jTGaKLlatNXmJbQD8bY+PrPxm5v4hE=
|
||||
github.com/blevesearch/zap/v14 v14.0.0/go.mod h1:sUc/gPGJlFbSQ2ZUh/wGRYwkKx+Dg/5p+dd+eq6QMXk=
|
||||
github.com/caddyserver/certmagic v0.11.2 h1:nPBqyuFNHJEf2FwC1ixJjArtTKWyPqpaH6k4jl7gxYI=
|
||||
github.com/caddyserver/certmagic v0.11.2/go.mod h1:fqY1IZk5iqhsj5FU3Vw20Sjq66tEKaanTFYNZ74soMY=
|
||||
github.com/cenkalti/backoff/v4 v4.0.0 h1:6VeaLF9aI+MAUQ95106HwWzYZgJJpZ4stumjj6RFYAU=
|
||||
github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg=
|
||||
github.com/cenkalti/backoff/v4 v4.0.2 h1:JIufpQLbh4DkbQoii76ItQIUFzevQSqOLZca4eamEDs=
|
||||
@ -95,6 +101,7 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/couchbase/ghistogram v0.1.0 h1:b95QcQTCzjTUocDXp/uMgSNQi8oj1tGwnJ4bODWZnps=
|
||||
@ -103,7 +110,6 @@ github.com/couchbase/moss v0.1.0 h1:HCL+xxHUwmOaL44kMM/gU08OW6QGCui1WVFO58bjhNI=
|
||||
github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs=
|
||||
github.com/couchbase/vellum v1.0.1 h1:qrj9ohvZedvc51S5KzPfJ6P6z0Vqzv7Lx7k3mVc2WOk=
|
||||
github.com/couchbase/vellum v1.0.1/go.mod h1:FcwrEivFpNi24R3jLOs3n+fs5RnuQnQqCLBJ1uAg1W4=
|
||||
github.com/cpu/goacmedns v0.0.1/go.mod h1:sesf/pNnCYwUevQEQfEwY0Y3DydlQWSGZbaMElOWxok=
|
||||
github.com/cpu/goacmedns v0.0.2/go.mod h1:4MipLkI+qScwqtVxcNO6okBhbgRrr7/tKXUSgSL0teQ=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
@ -121,7 +127,7 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
|
||||
github.com/dnaeon/go-vcr v0.0.0-20180814043457-aafff18a5cc2/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||
github.com/dnsimple/dnsimple-go v0.30.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg=
|
||||
github.com/dnsimple/dnsimple-go v0.60.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg=
|
||||
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
|
||||
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
@ -132,12 +138,18 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/exoscale/egoscale v0.18.1/go.mod h1:Z7OOdzzTOz1Q1PjQXumlz9Wn/CddH0zSYdCF3rnBKXE=
|
||||
github.com/eyedeekay/goSam v0.32.22 h1:SD0GEQ8MGJ1wZ4MepSa0mpZjiZAGMSTUhSwAZVrm2kg=
|
||||
github.com/eyedeekay/goSam v0.32.22/go.mod h1:YIklxqKiJ3I5JNRgb5pM7VCQOSNDGnVulHnrKBbbECM=
|
||||
github.com/eyedeekay/ramp v0.0.0-20190429201811-305b382042ab/go.mod h1:h7mvUAMgZ/rtRDUOkvKTK+8LnDMeUhJSoa5EPdB51fc=
|
||||
github.com/eyedeekay/sam3 v0.32.2/go.mod h1:Y3igFVzN4ybqkkpfUWULGhw7WRp8lieq0ORXbLBbcZM=
|
||||
github.com/eyedeekay/sam3 v0.32.3/go.mod h1:qRA9KIIVxbrHlkj+ZB+OoxFGFgdKeGp1vSgPw26eOVU=
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
@ -150,19 +162,17 @@ github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a h1:FQqo
|
||||
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31 h1:gclg6gY70GLy3PbkQ1AERPfmLMMagS60DKF78eWwLn8=
|
||||
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
|
||||
github.com/go-acme/lego/v3 v3.4.0 h1:deB9NkelA+TfjGHVw8J7iKl/rMtffcGMWSMmptvMv0A=
|
||||
github.com/go-acme/lego/v3 v3.4.0/go.mod h1:xYbLDuxq3Hy4bMUT1t9JIuz6GWIWb3m5X+TeTHYaT7M=
|
||||
github.com/go-acme/lego/v3 v3.6.0 h1:Rv0MrX3DpVp9Xg77yR7x+PCksLLph3Ut/69/9Kim8ac=
|
||||
github.com/go-acme/lego/v3 v3.6.0/go.mod h1:sB/T7hfyz0HYIBvPmz/C8jIaxF6scbbiGKTzbQ22V6A=
|
||||
github.com/go-acme/lego/v3 v3.7.0 h1:qC5/8/CbltyAE8fGLE6bGlqucj7pXc/vBxiLwLOsmAQ=
|
||||
github.com/go-acme/lego/v3 v3.7.0/go.mod h1:4eDjjYkAsDXyNcwN8IhhZAwxz9Ltiks1Zmpv0q20J7A=
|
||||
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-ini/ini v1.44.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
@ -192,6 +202,9 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
@ -231,11 +244,27 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
@ -243,6 +272,7 @@ github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhK
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
||||
github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U=
|
||||
github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
@ -253,7 +283,6 @@ github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
@ -268,6 +297,8 @@ github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid v1.2.3 h1:CCtW0xUnWGVINKvE/WWOYKdsPV6mawAtvQuSl8guwQs=
|
||||
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid v1.3.0 h1:2JqaNE1hGdABW2YbA3TenkO7RiPFRvSWnEnGqWh9sHE=
|
||||
github.com/klauspost/cpuid v1.3.0/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
|
||||
github.com/kljensen/snowball v0.6.0 h1:6DZLCcZeL0cLfodx+Md4/OLC6b/bfurWUOUGs1ydfOU=
|
||||
github.com/kljensen/snowball v0.6.0/go.mod h1:27N7E8fVU5H68RlUmnWwZCfxgt4POBJfENGMvNRhldw=
|
||||
github.com/kolo/xmlrpc v0.0.0-20190717152603-07c4ee3fd181/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ=
|
||||
@ -287,28 +318,32 @@ github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDe
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailru/easyjson v0.7.1 h1:mdxE1MF9o53iCb2Ghj1VfWvh7ZOwHpnVG/xwXrV90U8=
|
||||
github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||
github.com/mailru/easyjson v0.7.2-0.20200424172602-f0a000e7a8e0 h1:kBQYXw1PdcnwYP5hntk8LEDPdq++fubPN76BlfGLdIM=
|
||||
github.com/mailru/easyjson v0.7.2-0.20200424172602-f0a000e7a8e0/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.1.15 h1:CSSIDtllwGLMoA6zjdKnaE6Tx6eVUxQ29LUgGetiDCI=
|
||||
github.com/miekg/dns v1.1.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
|
||||
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed/go.mod h1:3rdaFaCv4AyBgu5ALFM0+tSuHrBh6v692nyQe3ikrq0=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.3.0 h1:iDwIio/3gk2QtLLEsqU5lInaMzos0hDTz8a6lazSFVw=
|
||||
github.com/mitchellh/mapstructure v1.3.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg=
|
||||
github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
@ -320,7 +355,6 @@ github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOl
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/nrdcg/auroradns v1.0.0/go.mod h1:6JPXKzIRzZzMqtTDgueIhTi6rFf1QvYE/HzqidhOhjw=
|
||||
github.com/nrdcg/auroradns v1.0.1/go.mod h1:y4pc0i9QXYlFCWrhWrUSIETnZgrf4KuwjDIWmmXo3JI=
|
||||
github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ=
|
||||
github.com/nrdcg/goinwx v0.6.1/go.mod h1:XPiut7enlbEdntAqalBIqcYcTEVhpv/dKWgDCX2SwKQ=
|
||||
@ -340,17 +374,20 @@ github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
|
||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/oracle/oci-go-sdk v7.0.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
|
||||
github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014/go.mod h1:joRatxRJaZBsY3JAOEMcoOp05CnZzsx4scTxi95DHyQ=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=
|
||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||
github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw=
|
||||
github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs=
|
||||
github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ=
|
||||
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
@ -380,8 +417,9 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sacloud/libsacloud v1.26.1/go.mod h1:79ZwATmHLIFZIMd7sxA3LwzVy/B77uj3LDoToVTxDoQ=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
@ -416,8 +454,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs=
|
||||
github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw=
|
||||
github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/steveyen/gtreap v0.1.0 h1:CjhzTa274PyJLJuMZwIzCO1PfC00oRa8d1Kc78bFXJM=
|
||||
github.com/steveyen/gtreap v0.1.0/go.mod h1:kl/5J7XbrOmlIbYIXdRHDDE5QxHqpk0cmkT7Z4dM9/Y=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@ -434,10 +472,10 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
github.com/tdewolff/minify/v2 v2.7.4 h1:r0OZQ3QzWeDS5cXq53Bk4IFIBDZ7fiXIkw1a4bHONsw=
|
||||
github.com/tdewolff/minify/v2 v2.7.4/go.mod h1:BkDSm8aMMT0ALGmpt7j3Ra7nLUgZL0qhyrAHXwxcy5w=
|
||||
github.com/tdewolff/parse/v2 v2.4.2 h1:Bu2Qv6wepkc+Ou7iB/qHjAhEImlAP5vedzlQRUdj3BI=
|
||||
github.com/tdewolff/parse/v2 v2.4.2/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
|
||||
github.com/tdewolff/minify/v2 v2.7.6 h1:b6UzNphZeDm3AVmk0a69orkNLPJzJx3k/AQ/W2xoMs8=
|
||||
github.com/tdewolff/minify/v2 v2.7.6/go.mod h1:Mt3hGbK/ETDplEP9EMNZo1lPkM3TZq0rDIVV76nFgY0=
|
||||
github.com/tdewolff/parse/v2 v2.4.3 h1:k24zHgTRGm7LkvbTEreuavyZTf0k8a/lIenggv62OiU=
|
||||
github.com/tdewolff/parse/v2 v2.4.3/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
|
||||
github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4=
|
||||
github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok=
|
||||
@ -448,7 +486,6 @@ github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDW
|
||||
github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ=
|
||||
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/transip/gotransip v0.0.0-20190812104329-6d8d9179b66f/go.mod h1:i0f4R4o2HM0m3DZYQWsj6/MEowD57VzoH0v3d7igeFY=
|
||||
github.com/transip/gotransip/v6 v6.0.2/go.mod h1:pQZ36hWWRahCUXkFWlx9Hs711gLd8J4qdgLdRzmtY+g=
|
||||
github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
@ -458,9 +495,14 @@ github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX
|
||||
github.com/vultr/govultr v0.1.4/go.mod h1:9H008Uxr/C4vFNGLqKx232C206GL0PBHzOP0809bGNA=
|
||||
github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc=
|
||||
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
github.com/xdg-go/scram v0.0.0-20180814205039-7eeb5667e42c h1:Wm21TPasVdeOUTg1m/uNkRdMuvI+jIeYfTIwq98Z2V0=
|
||||
github.com/xdg-go/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:FV1RpvYFmF8wnKtr3ArzkC0b+tAySCbw8eP7QSIvLKM=
|
||||
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk=
|
||||
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
|
||||
github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0=
|
||||
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
@ -481,6 +523,7 @@ go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277/go.mod h1:2X8KaoNd1J0lZ
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@ -492,8 +535,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200427165652-729f1e841bcc h1:ZGI/fILM2+ueot/UixBSoj9188jCAxVHEZEGhqq67I4=
|
||||
golang.org/x/crypto v0.0.0-20200427165652-729f1e841bcc/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -527,7 +570,9 @@ golang.org/x/net v0.0.0-20180611182652-db08ff08e862/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY=
|
||||
@ -544,16 +589,14 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191027093000-83d349e8ac1a h1:Yu34BogBivvmu7SAzHHaB9nZWH5D1C+z3F1jyIaYZSQ=
|
||||
golang.org/x/net v0.0.0-20191027093000-83d349e8ac1a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U=
|
||||
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -570,9 +613,11 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -604,8 +649,8 @@ golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200428200454-593003d681fa h1:yMbJOvnfYkO1dSAviTu/ZguZWLBTXx4xE3LYrxUCCiA=
|
||||
golang.org/x/sys v0.0.0-20200428200454-593003d681fa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200610111108-226ff32320da h1:bGb80FudwxpeucJUjPYJXuJ8Hk91vNtfvrymzwiei38=
|
||||
golang.org/x/sys v0.0.0-20200610111108-226ff32320da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -634,6 +679,7 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
@ -688,6 +734,7 @@ google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
@ -704,6 +751,12 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@ -714,19 +767,18 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
|
||||
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.51.1 h1:GyboHr4UqMiLUybYjd22ZjQIKEJEpgtLXtuGbR21Oho=
|
||||
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ=
|
||||
gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190730140822-b51389932cbc/go.mod h1:VV+3haRsgDiVLxyifmMBrBIuCWFBPYKbRssXB9z67Hw=
|
||||
gopkg.in/resty.v1 v1.9.1/go.mod h1:vo52Hzryw9PnPHcJfPsBiFW62XhNx5OczbV9y+IMpgc=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/square/go-jose.v2 v2.5.0 h1:OZ4sdq+Y+SHfYB7vfthi1Ei8b0vkP8ZPQgUfUwdUSqo=
|
||||
gopkg.in/square/go-jose.v2 v2.5.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
|
||||
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
@ -737,6 +789,8 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
@ -6,4 +6,4 @@ Tag=$(git describe --tags)
|
||||
Commit=$(git rev-parse --short HEAD)
|
||||
Date=$(date +'%Y-%m-%dT%TZ')
|
||||
|
||||
go install -ldflags "-s -w -X $Import.Tag=$Tag -X $Import.Commit=$Commit -X $Import.Date=$Date"
|
||||
CGO_ENABLED=0 go install -ldflags "-s -w -X $Import.Tag=$Tag -X $Import.Commit=$Commit -X $Import.Date=$Date"
|
||||
|
35
pkg/cookie/cookie.go
Normal file
35
pkg/cookie/cookie.go
Normal file
@ -0,0 +1,35 @@
|
||||
package cookie
|
||||
|
||||
import "net/http"
|
||||
|
||||
const HostPrefix = "__Host-"
|
||||
|
||||
func Harden(r *http.Request, cookie *http.Cookie) *http.Cookie {
|
||||
cookie.HttpOnly = true
|
||||
cookie.Secure = r.TLS != nil
|
||||
|
||||
if cookie.Path == "" {
|
||||
cookie.Path = "/"
|
||||
}
|
||||
|
||||
if cookie.Path == "/" && cookie.Secure {
|
||||
cookie.Name = HostPrefix + cookie.Name
|
||||
}
|
||||
|
||||
if cookie.SameSite == 0 {
|
||||
cookie.SameSite = http.SameSiteLaxMode
|
||||
}
|
||||
|
||||
return cookie
|
||||
}
|
||||
|
||||
func Set(w http.ResponseWriter, r *http.Request, cookie *http.Cookie) {
|
||||
http.SetCookie(w, Harden(r, cookie))
|
||||
}
|
||||
|
||||
func Name(r *http.Request, name string) string {
|
||||
if r.TLS != nil {
|
||||
return HostPrefix + name
|
||||
}
|
||||
return name
|
||||
}
|
33
pkg/cryptoutil/tls.go
Normal file
33
pkg/cryptoutil/tls.go
Normal file
@ -0,0 +1,33 @@
|
||||
package cryptoutil
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func DescribeTLS(version, cipherSuite uint16) string {
|
||||
cs := tls.CipherSuiteName(cipherSuite)
|
||||
cs = cs[:strings.LastIndexByte(cs, '_')]
|
||||
cs = cipherSuiteReplacer.Replace(cs)
|
||||
|
||||
return TLSVersionName(version) + " and " + cs
|
||||
}
|
||||
|
||||
func TLSVersionName(version uint16) string {
|
||||
return tlsVersionNames[version]
|
||||
}
|
||||
|
||||
var (
|
||||
tlsVersionNames = map[uint16]string{
|
||||
tls.VersionTLS10: "TLS 1.0",
|
||||
tls.VersionTLS11: "TLS 1.1",
|
||||
tls.VersionTLS12: "TLS 1.2",
|
||||
tls.VersionTLS13: "TLS 1.3",
|
||||
}
|
||||
|
||||
cipherSuiteReplacer = strings.NewReplacer(
|
||||
"_WITH_", " with ",
|
||||
"TLS_", "",
|
||||
"_", "-",
|
||||
)
|
||||
)
|
@ -39,11 +39,10 @@ func Serve(handler http.Handler, cfg Config) error {
|
||||
httpSrv.WriteTimeout = 5 * time.Second
|
||||
|
||||
httpsSrv := &http.Server{
|
||||
Addr: net.JoinHostPort(cfg.Addr, cfg.PortHTTPS),
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
Handler: handler,
|
||||
Addr: net.JoinHostPort(cfg.Addr, cfg.PortHTTPS),
|
||||
ReadTimeout: 5 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
Handler: handler,
|
||||
}
|
||||
|
||||
redirect := HTTPSRedirect(cfg.PortHTTPS, handler)
|
||||
@ -101,7 +100,6 @@ func Serve(handler http.Handler, cfg Config) error {
|
||||
}
|
||||
} else {
|
||||
httpSrv.ReadTimeout = 5 * time.Second
|
||||
httpSrv.WriteTimeout = 10 * time.Second
|
||||
httpSrv.IdleTimeout = 120 * time.Second
|
||||
httpSrv.Handler = handler
|
||||
|
||||
|
144
pkg/ident/server.go
Normal file
144
pkg/ident/server.go
Normal file
@ -0,0 +1,144 @@
|
||||
package ident
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultAddr is the address a Server listens on when no Addr is specified
|
||||
DefaultAddr = ":113"
|
||||
// DefaultTimeout is the the time a Server will wait before failing
|
||||
// reads and writes if no Timeout is specified
|
||||
DefaultTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// Server implements the server-side of the Ident protocol
|
||||
type Server struct {
|
||||
// Addr is the host:port address to listen on
|
||||
Addr string
|
||||
// Timeout is the time to wait before failing reads and writes
|
||||
Timeout time.Duration
|
||||
|
||||
entries map[string]entry
|
||||
listener net.Listener
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
remoteHost string
|
||||
ident string
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
return &Server{
|
||||
entries: map[string]entry{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Listen() error {
|
||||
var err error
|
||||
|
||||
addr := s.Addr
|
||||
if addr == "" {
|
||||
addr = DefaultAddr
|
||||
}
|
||||
|
||||
s.listener, err = net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.listener.Close()
|
||||
|
||||
for {
|
||||
conn, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go s.handle(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Stop() error {
|
||||
return s.listener.Close()
|
||||
}
|
||||
|
||||
func (s *Server) Add(local, remote net.Addr, ident string) {
|
||||
if local == nil || remote == nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, localPort, err := net.SplitHostPort(local.String())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
remoteHost, remotePort, err := net.SplitHostPort(remote.String())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.lock.Lock()
|
||||
s.entries[localPort+","+remotePort] = entry{
|
||||
remoteHost: remoteHost,
|
||||
ident: ident,
|
||||
}
|
||||
s.lock.Unlock()
|
||||
}
|
||||
|
||||
func (s *Server) Remove(local, remote net.Addr) {
|
||||
if local == nil || remote == nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, localPort, err := net.SplitHostPort(local.String())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, remotePort, err := net.SplitHostPort(remote.String())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.lock.Lock()
|
||||
delete(s.entries, localPort+","+remotePort)
|
||||
s.lock.Unlock()
|
||||
}
|
||||
|
||||
func (s *Server) handle(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
timeout := s.Timeout
|
||||
if timeout == 0 {
|
||||
timeout = DefaultTimeout
|
||||
}
|
||||
|
||||
scan := bufio.NewScanner(conn)
|
||||
scan.Buffer(make([]byte, 32), 32)
|
||||
|
||||
conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
if !scan.Scan() {
|
||||
return
|
||||
}
|
||||
query := scan.Text()
|
||||
|
||||
s.lock.Lock()
|
||||
entry, ok := s.entries[strings.ReplaceAll(query, " ", "")]
|
||||
s.lock.Unlock()
|
||||
|
||||
if ok {
|
||||
remoteHost, _, err := net.SplitHostPort(conn.RemoteAddr().String())
|
||||
if err != nil || remoteHost != entry.remoteHost {
|
||||
return
|
||||
}
|
||||
|
||||
conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
conn.Write([]byte(fmt.Sprintf("%s : USERID : Dispatch : %s\r\n", query, entry.ident)))
|
||||
}
|
||||
}
|
155
pkg/irc/cap.go
Normal file
155
pkg/irc/cap.go
Normal file
@ -0,0 +1,155 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var clientWantedCaps = []string{"cap-notify"}
|
||||
|
||||
func (c *Client) GetCapability(name string) ([]string, bool) {
|
||||
c.lock.Lock()
|
||||
values, ok := c.enabledCapabilities[name]
|
||||
c.lock.Unlock()
|
||||
return values, ok
|
||||
}
|
||||
|
||||
func (c *Client) HasCapability(name string, values ...string) bool {
|
||||
if capValues, ok := c.GetCapability(name); ok {
|
||||
if len(values) == 0 || capValues == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, v := range values {
|
||||
for _, vCap := range capValues {
|
||||
if v == vCap {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Client) beginCAP() {
|
||||
c.write("CAP LS 302")
|
||||
}
|
||||
|
||||
func (c *Client) beginSASL() bool {
|
||||
if c.negotiating {
|
||||
if mechs, ok := c.GetCapability("sasl"); ok {
|
||||
if mechs != nil {
|
||||
c.filterSASLMechanisms(mechs)
|
||||
}
|
||||
c.tryNextSASL()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Client) finishCAP() {
|
||||
if c.negotiating {
|
||||
c.negotiating = false
|
||||
c.write("CAP END")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) handleCAP(msg *Message) {
|
||||
if len(msg.Params) < 3 {
|
||||
c.write("CAP END")
|
||||
return
|
||||
}
|
||||
|
||||
caps := parseCaps(msg.LastParam())
|
||||
|
||||
switch msg.Params[1] {
|
||||
case "LS":
|
||||
for cap, values := range caps {
|
||||
for _, wanted := range c.wantedCapabilities {
|
||||
if cap == wanted {
|
||||
c.requestedCapabilities[cap] = values
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(msg.Params) == 3 {
|
||||
if len(c.requestedCapabilities) == 0 {
|
||||
c.write("CAP END")
|
||||
return
|
||||
}
|
||||
|
||||
c.negotiating = true
|
||||
|
||||
reqCaps := []string{}
|
||||
for cap := range c.requestedCapabilities {
|
||||
reqCaps = append(reqCaps, cap)
|
||||
}
|
||||
|
||||
c.write("CAP REQ :" + strings.Join(reqCaps, " "))
|
||||
}
|
||||
|
||||
case "ACK":
|
||||
c.lock.Lock()
|
||||
for cap := range caps {
|
||||
if v, ok := c.requestedCapabilities[cap]; ok {
|
||||
c.enabledCapabilities[cap] = v
|
||||
delete(c.requestedCapabilities, cap)
|
||||
}
|
||||
}
|
||||
c.lock.Unlock()
|
||||
|
||||
if len(c.requestedCapabilities) == 0 && !c.beginSASL() {
|
||||
c.finishCAP()
|
||||
}
|
||||
|
||||
case "NAK":
|
||||
for cap := range caps {
|
||||
delete(c.requestedCapabilities, cap)
|
||||
}
|
||||
|
||||
if len(c.requestedCapabilities) == 0 && !c.beginSASL() {
|
||||
c.finishCAP()
|
||||
}
|
||||
|
||||
case "NEW":
|
||||
reqCaps := []string{}
|
||||
for cap, values := range caps {
|
||||
for _, wanted := range c.wantedCapabilities {
|
||||
if cap == wanted && !c.HasCapability(cap) {
|
||||
c.requestedCapabilities[cap] = values
|
||||
reqCaps = append(reqCaps, cap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(reqCaps) > 0 {
|
||||
c.write("CAP REQ :" + strings.Join(reqCaps, " "))
|
||||
}
|
||||
|
||||
case "DEL":
|
||||
c.lock.Lock()
|
||||
for cap := range caps {
|
||||
delete(c.enabledCapabilities, cap)
|
||||
}
|
||||
c.lock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func parseCaps(caps string) map[string][]string {
|
||||
result := map[string][]string{}
|
||||
|
||||
parts := strings.Split(caps, " ")
|
||||
for _, part := range parts {
|
||||
capParts := strings.Split(part, "=")
|
||||
name := capParts[0]
|
||||
|
||||
if len(capParts) > 1 {
|
||||
result[name] = strings.Split(capParts[1], ",")
|
||||
} else {
|
||||
result[name] = nil
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
58
pkg/irc/cap_test.go
Normal file
58
pkg/irc/cap_test.go
Normal file
@ -0,0 +1,58 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseCaps(t *testing.T) {
|
||||
cases := []struct {
|
||||
input string
|
||||
expected map[string][]string
|
||||
}{
|
||||
{
|
||||
"sasl",
|
||||
map[string][]string{
|
||||
"sasl": nil,
|
||||
},
|
||||
}, {
|
||||
"sasl=PLAIN",
|
||||
map[string][]string{
|
||||
"sasl": {"PLAIN"},
|
||||
},
|
||||
}, {
|
||||
"cake sasl=PLAIN",
|
||||
map[string][]string{
|
||||
"cake": nil,
|
||||
"sasl": {"PLAIN"},
|
||||
},
|
||||
}, {
|
||||
"cake sasl=PLAIN pie",
|
||||
map[string][]string{
|
||||
"cake": nil,
|
||||
"sasl": {"PLAIN"},
|
||||
"pie": nil,
|
||||
},
|
||||
}, {
|
||||
"cake sasl=PLAIN pie=BLUEBERRY,RASPBERRY",
|
||||
map[string][]string{
|
||||
"cake": nil,
|
||||
"sasl": {"PLAIN"},
|
||||
"pie": {"BLUEBERRY", "RASPBERRY"},
|
||||
},
|
||||
}, {
|
||||
"cake sasl=PLAIN pie=BLUEBERRY,RASPBERRY cheesecake",
|
||||
map[string][]string{
|
||||
"cake": nil,
|
||||
"sasl": {"PLAIN"},
|
||||
"pie": {"BLUEBERRY", "RASPBERRY"},
|
||||
"cheesecake": nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
assert.Equal(t, tc.expected, parseCaps(tc.input))
|
||||
}
|
||||
}
|
@ -11,26 +11,54 @@ import (
|
||||
"github.com/jpillora/backoff"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Server string
|
||||
Host string
|
||||
TLS bool
|
||||
TLSConfig *tls.Config
|
||||
Password string
|
||||
Username string
|
||||
Realname string
|
||||
type Config struct {
|
||||
Host string
|
||||
Port string
|
||||
TLS bool
|
||||
TLSConfig *tls.Config
|
||||
ServerPassword string
|
||||
Nick string
|
||||
Username string
|
||||
Realname string
|
||||
|
||||
SASLMechanisms []string
|
||||
Account string
|
||||
Password string
|
||||
|
||||
// Automatically reply to common CTCP messages
|
||||
AutoCTCP bool
|
||||
// Version is the reply to VERSION and FINGER CTCP messages
|
||||
Version string
|
||||
// Source is the reply to SOURCE CTCP messages
|
||||
Source string
|
||||
|
||||
HandleNickInUse func(string) string
|
||||
|
||||
Dialer Dialer
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
Config *Config
|
||||
|
||||
Messages chan *Message
|
||||
ConnectionChanged chan ConnectionState
|
||||
Features *Features
|
||||
nick string
|
||||
channels []string
|
||||
|
||||
state *state
|
||||
nick string
|
||||
channels []string
|
||||
|
||||
wantedCapabilities []string
|
||||
requestedCapabilities map[string][]string
|
||||
enabledCapabilities map[string][]string
|
||||
negotiating bool
|
||||
saslMechanisms []SASL
|
||||
currentSASL SASL
|
||||
|
||||
conn net.Conn
|
||||
connected bool
|
||||
registered bool
|
||||
dialer *net.Dialer
|
||||
dialer Dialer
|
||||
recvBuf []byte
|
||||
scan *bufio.Scanner
|
||||
backoff *backoff.Backoff
|
||||
@ -42,23 +70,87 @@ type Client struct {
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func NewClient(nick, username string) *Client {
|
||||
return &Client{
|
||||
nick: nick,
|
||||
Features: NewFeatures(),
|
||||
Username: username,
|
||||
Realname: nick,
|
||||
Messages: make(chan *Message, 32),
|
||||
ConnectionChanged: make(chan ConnectionState, 16),
|
||||
out: make(chan string, 32),
|
||||
quit: make(chan struct{}),
|
||||
reconnect: make(chan struct{}),
|
||||
recvBuf: make([]byte, 0, 4096),
|
||||
func NewClient(config *Config) *Client {
|
||||
if config.Port == "" {
|
||||
if config.TLS {
|
||||
config.Port = "6697"
|
||||
} else {
|
||||
config.Port = "6667"
|
||||
}
|
||||
}
|
||||
|
||||
if config.Username == "" {
|
||||
config.Username = config.Nick
|
||||
}
|
||||
|
||||
if config.Realname == "" {
|
||||
config.Realname = config.Nick
|
||||
}
|
||||
|
||||
if config.SASLMechanisms == nil {
|
||||
config.SASLMechanisms = DefaultSASLMechanisms
|
||||
}
|
||||
|
||||
if config.Dialer == nil {
|
||||
config.Dialer = DefaultDialer
|
||||
}
|
||||
|
||||
client := &Client{
|
||||
Config: config,
|
||||
Messages: make(chan *Message, 32),
|
||||
ConnectionChanged: make(chan ConnectionState, 4),
|
||||
Features: NewFeatures(),
|
||||
nick: config.Nick,
|
||||
requestedCapabilities: map[string][]string{},
|
||||
enabledCapabilities: map[string][]string{},
|
||||
dialer: config.Dialer,
|
||||
recvBuf: make([]byte, 0, 4096),
|
||||
backoff: &backoff.Backoff{
|
||||
Min: 500 * time.Millisecond,
|
||||
Max: 30 * time.Second,
|
||||
Jitter: true,
|
||||
},
|
||||
out: make(chan string, 32),
|
||||
quit: make(chan struct{}),
|
||||
reconnect: make(chan struct{}),
|
||||
}
|
||||
client.state = newState(client)
|
||||
client.initSASL()
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *Client) initSASL() {
|
||||
saslMechanisms := []SASL{}
|
||||
|
||||
for _, mech := range c.Config.SASLMechanisms {
|
||||
if mech == "EXTERNAL" {
|
||||
if c.Config.TLSConfig != nil && len(c.Config.TLSConfig.Certificates) > 0 {
|
||||
saslMechanisms = append(saslMechanisms, &SASLExternal{})
|
||||
}
|
||||
} else if c.Config.Account != "" && c.Config.Password != "" {
|
||||
if mech == "PLAIN" {
|
||||
saslMechanisms = append(saslMechanisms, &SASLPlain{
|
||||
Username: c.Config.Account,
|
||||
Password: c.Config.Password,
|
||||
})
|
||||
} else if strings.HasPrefix(mech, "SCRAM-") {
|
||||
saslMechanisms = append(saslMechanisms, &SASLScram{
|
||||
Username: c.Config.Account,
|
||||
Password: c.Config.Password,
|
||||
Hash: mech[6:],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.wantedCapabilities = append([]string{}, clientWantedCaps...)
|
||||
c.negotiating = false
|
||||
c.currentSASL = nil
|
||||
|
||||
if len(saslMechanisms) > 0 {
|
||||
c.wantedCapabilities = append(c.wantedCapabilities, "sasl")
|
||||
c.saslMechanisms = saslMechanisms
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,6 +161,10 @@ func (c *Client) GetNick() string {
|
||||
return nick
|
||||
}
|
||||
|
||||
func (c *Client) Is(nick string) bool {
|
||||
return c.EqualFold(nick, c.GetNick())
|
||||
}
|
||||
|
||||
func (c *Client) setNick(nick string) {
|
||||
c.lock.Lock()
|
||||
c.nick = nick
|
||||
@ -95,6 +191,42 @@ func (c *Client) setRegistered(reg bool) {
|
||||
c.lock.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) Host() string {
|
||||
return c.Config.Host
|
||||
}
|
||||
|
||||
func (c *Client) LocalAddr() net.Addr {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
if c.conn != nil {
|
||||
return c.conn.LocalAddr()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) RemoteAddr() net.Addr {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
if c.conn != nil {
|
||||
return c.conn.RemoteAddr()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) MOTD() []string {
|
||||
return c.state.getMOTD()
|
||||
}
|
||||
|
||||
func (c *Client) ChannelUsers(channel string) []string {
|
||||
return c.state.getUsers(channel)
|
||||
}
|
||||
|
||||
func (c *Client) ChannelTopic(channel string) string {
|
||||
return c.state.getTopic(channel)
|
||||
}
|
||||
|
||||
func (c *Client) Nick(nick string) {
|
||||
c.Write("NICK " + nick)
|
||||
}
|
||||
@ -149,6 +281,13 @@ func (c *Client) Notice(target, msg string) {
|
||||
c.Writef("NOTICE %s :%s", target, msg)
|
||||
}
|
||||
|
||||
func (c *Client) ReplyCTCP(target, command, params string) {
|
||||
c.Notice(target, EncodeCTCP(&CTCP{
|
||||
Command: command,
|
||||
Params: params,
|
||||
}))
|
||||
}
|
||||
|
||||
func (c *Client) Whois(nick string) {
|
||||
c.Write("WHOIS " + nick)
|
||||
}
|
||||
@ -173,12 +312,17 @@ func (c *Client) writeUser(username, realname string) {
|
||||
c.writef("USER %s 0 * :%s", username, realname)
|
||||
}
|
||||
|
||||
func (c *Client) authenticate(response string) {
|
||||
c.write("AUTHENTICATE " + response)
|
||||
}
|
||||
|
||||
func (c *Client) register() {
|
||||
if c.Password != "" {
|
||||
c.writePass(c.Password)
|
||||
c.beginCAP()
|
||||
if c.Config.ServerPassword != "" {
|
||||
c.writePass(c.Config.ServerPassword)
|
||||
}
|
||||
c.writeNick(c.nick)
|
||||
c.writeUser(c.Username, c.Realname)
|
||||
c.writeNick(c.Config.Nick)
|
||||
c.writeUser(c.Config.Username, c.Config.Realname)
|
||||
}
|
||||
|
||||
func (c *Client) addChannel(channel string) {
|
||||
@ -193,6 +337,7 @@ func (c *Client) removeChannels(channels ...string) {
|
||||
for i, ch := range c.channels {
|
||||
if c.EqualFold(removeCh, ch) {
|
||||
c.channels = append(c.channels[:i], c.channels[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,12 +7,8 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func testClient() *Client {
|
||||
return NewClient("test", "testing")
|
||||
}
|
||||
|
||||
func testClientSend() (*Client, chan string) {
|
||||
c := testClient()
|
||||
c := NewClient(&Config{})
|
||||
conn := &mockConn{hook: make(chan string, 16)}
|
||||
c.conn = conn
|
||||
c.sendRecv.Add(1)
|
||||
@ -128,6 +124,12 @@ func TestNotice(t *testing.T) {
|
||||
assert.Equal(t, "NOTICE user :the message\r\n", <-out)
|
||||
}
|
||||
|
||||
func TestReplyCTCP(t *testing.T) {
|
||||
c, out := testClientSend()
|
||||
c.ReplyCTCP("user", "PING", "PONG")
|
||||
assert.Equal(t, "NOTICE user :\x01PING PONG\x01\r\n", <-out)
|
||||
}
|
||||
|
||||
func TestWhois(t *testing.T) {
|
||||
c, out := testClientSend()
|
||||
c.Whois("user")
|
||||
@ -142,15 +144,17 @@ func TestAway(t *testing.T) {
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
c, out := testClientSend()
|
||||
c.nick = "nick"
|
||||
c.Username = "user"
|
||||
c.Realname = "rn"
|
||||
c.Config.Nick = "nick"
|
||||
c.Config.Username = "user"
|
||||
c.Config.Realname = "rn"
|
||||
c.register()
|
||||
assert.Equal(t, "CAP LS 302\r\n", <-out)
|
||||
assert.Equal(t, "NICK nick\r\n", <-out)
|
||||
assert.Equal(t, "USER user 0 * :rn\r\n", <-out)
|
||||
|
||||
c.Password = "pass"
|
||||
c.Config.ServerPassword = "pass"
|
||||
c.register()
|
||||
assert.Equal(t, "CAP LS 302\r\n", <-out)
|
||||
assert.Equal(t, "PASS pass\r\n", <-out)
|
||||
assert.Equal(t, "NICK nick\r\n", <-out)
|
||||
assert.Equal(t, "USER user 0 * :rn\r\n", <-out)
|
||||
|
@ -8,35 +8,25 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultDialer = &net.Dialer{Timeout: 10 * time.Second}
|
||||
|
||||
ErrBadProtocol = errors.New("This server does not speak IRC")
|
||||
)
|
||||
|
||||
func (c *Client) Connect(address string) {
|
||||
if idx := strings.Index(address, ":"); idx < 0 {
|
||||
c.Host = address
|
||||
type Dialer interface {
|
||||
Dial(network, address string) (net.Conn, error)
|
||||
}
|
||||
|
||||
if c.TLS {
|
||||
address += ":6697"
|
||||
} else {
|
||||
address += ":6667"
|
||||
}
|
||||
} else {
|
||||
c.Host = address[:idx]
|
||||
}
|
||||
c.Server = address
|
||||
c.dialer = &net.Dialer{Timeout: 10 * time.Second}
|
||||
|
||||
c.connChange(false, nil)
|
||||
func (c *Client) Connect() {
|
||||
go c.run()
|
||||
}
|
||||
|
||||
func (c *Client) Reconnect() {
|
||||
close(c.reconnect)
|
||||
c.tryConnect()
|
||||
}
|
||||
|
||||
func (c *Client) Write(data string) {
|
||||
@ -78,6 +68,8 @@ func (c *Client) run() {
|
||||
|
||||
c.sendRecv.Wait()
|
||||
c.reconnect = make(chan struct{})
|
||||
c.state.reset()
|
||||
c.initSASL()
|
||||
|
||||
time.Sleep(c.backoff.Duration())
|
||||
c.tryConnect()
|
||||
@ -132,28 +124,29 @@ func (c *Client) connect() error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
if c.TLS {
|
||||
conn, err := tls.DialWithDialer(c.dialer, "tcp", c.Server, c.TLSConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.conn = conn
|
||||
} else {
|
||||
conn, err := c.dialer.Dial("tcp", c.Server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.conn = conn
|
||||
conn, err := c.dialer.Dial("tcp", net.JoinHostPort(c.Config.Host, c.Config.Port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Config.TLS {
|
||||
c.Config.TLSConfig.ServerName = c.Config.Host
|
||||
|
||||
tlsConn := tls.Client(conn, c.Config.TLSConfig)
|
||||
err = tlsConn.Handshake()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn = tlsConn
|
||||
}
|
||||
|
||||
c.conn = conn
|
||||
c.connected = true
|
||||
c.connChange(true, nil)
|
||||
c.scan = bufio.NewScanner(c.conn)
|
||||
c.scan.Buffer(c.recvBuf, cap(c.recvBuf))
|
||||
|
||||
c.register()
|
||||
go c.register()
|
||||
|
||||
c.sendRecv.Add(1)
|
||||
go c.recv()
|
||||
@ -191,8 +184,8 @@ func (c *Client) recv() {
|
||||
return
|
||||
|
||||
default:
|
||||
c.connChange(false, nil)
|
||||
c.Reconnect()
|
||||
c.connChange(false, c.scan.Err())
|
||||
close(c.reconnect)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -204,42 +197,12 @@ func (c *Client) recv() {
|
||||
|
||||
msg := ParseMessage(string(b))
|
||||
if msg == nil {
|
||||
close(c.quit)
|
||||
c.connChange(false, ErrBadProtocol)
|
||||
close(c.quit)
|
||||
return
|
||||
}
|
||||
|
||||
switch msg.Command {
|
||||
case Ping:
|
||||
go c.write("PONG :" + msg.LastParam())
|
||||
|
||||
case Join:
|
||||
if c.EqualFold(msg.Nick, c.GetNick()) {
|
||||
c.addChannel(msg.Params[0])
|
||||
}
|
||||
|
||||
case Nick:
|
||||
if c.EqualFold(msg.Nick, c.GetNick()) {
|
||||
c.setNick(msg.LastParam())
|
||||
}
|
||||
|
||||
case ReplyWelcome:
|
||||
c.setNick(msg.Params[0])
|
||||
c.setRegistered(true)
|
||||
c.flushChannels()
|
||||
|
||||
c.backoff.Reset()
|
||||
c.sendRecv.Add(1)
|
||||
go c.send()
|
||||
|
||||
case ReplyISupport:
|
||||
c.Features.Parse(msg.Params)
|
||||
|
||||
case ErrNicknameInUse:
|
||||
if c.HandleNickInUse != nil {
|
||||
go c.writeNick(c.HandleNickInUse(msg.Params[1]))
|
||||
}
|
||||
}
|
||||
c.handleMessage(msg)
|
||||
|
||||
c.Messages <- msg
|
||||
}
|
||||
|
@ -78,34 +78,38 @@ func (i *mockIrcd) handle(conn net.Conn) {
|
||||
}
|
||||
|
||||
func TestConnect(t *testing.T) {
|
||||
c := testClient()
|
||||
c.Connect("127.0.0.1:45678")
|
||||
assert.Equal(t, c.Host, "127.0.0.1")
|
||||
assert.Equal(t, c.Server, "127.0.0.1:45678")
|
||||
c := NewClient(&Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: "45678",
|
||||
})
|
||||
c.Connect()
|
||||
waitConnAndClose(t, c)
|
||||
}
|
||||
|
||||
func TestConnectTLS(t *testing.T) {
|
||||
c := testClient()
|
||||
c.TLS = true
|
||||
c.TLSConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
c.Connect("127.0.0.1:45679")
|
||||
assert.Equal(t, c.Host, "127.0.0.1")
|
||||
assert.Equal(t, c.Server, "127.0.0.1:45679")
|
||||
c := NewClient(&Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: "45679",
|
||||
TLS: true,
|
||||
TLSConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
})
|
||||
c.Connect()
|
||||
waitConnAndClose(t, c)
|
||||
}
|
||||
|
||||
func TestConnectDefaultPorts(t *testing.T) {
|
||||
c := testClient()
|
||||
c.Connect("127.0.0.1")
|
||||
assert.Equal(t, "127.0.0.1:6667", c.Server)
|
||||
c := NewClient(&Config{
|
||||
Host: "127.0.0.1",
|
||||
})
|
||||
assert.Equal(t, "6667", c.Config.Port)
|
||||
|
||||
c = testClient()
|
||||
c.TLS = true
|
||||
c.Connect("127.0.0.1")
|
||||
assert.Equal(t, "127.0.0.1:6697", c.Server)
|
||||
c = NewClient(&Config{
|
||||
Host: "127.0.0.1",
|
||||
TLS: true,
|
||||
})
|
||||
assert.Equal(t, "6697", c.Config.Port)
|
||||
}
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
@ -121,7 +125,7 @@ func TestWrite(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRecv(t *testing.T) {
|
||||
c := testClient()
|
||||
c := NewClient(&Config{})
|
||||
conn := &mockConn{hook: make(chan string, 16)}
|
||||
c.conn = conn
|
||||
|
||||
@ -136,14 +140,14 @@ func TestRecv(t *testing.T) {
|
||||
|
||||
assert.Equal(t, "PONG :test\r\n", <-conn.hook)
|
||||
assert.Equal(t, &Message{Command: "CMD"}, <-c.Messages)
|
||||
assert.Equal(t, &Message{Command: Ping, Params: []string{"test"}}, <-c.Messages)
|
||||
assert.Equal(t, &Message{Command: ReplyWelcome, Params: []string{"foo"}}, <-c.Messages)
|
||||
assert.Equal(t, &Message{Command: PING, Params: []string{"test"}}, <-c.Messages)
|
||||
assert.Equal(t, &Message{Command: RPL_WELCOME, Params: []string{"foo"}}, <-c.Messages)
|
||||
}
|
||||
|
||||
func TestRecvTriggersReconnect(t *testing.T) {
|
||||
c := testClient()
|
||||
c := NewClient(&Config{})
|
||||
c.conn = &mockConn{}
|
||||
c.scan = bufio.NewScanner(bytes.NewBufferString("001 bob\r\n"))
|
||||
c.scan = bufio.NewScanner(bytes.NewBufferString(""))
|
||||
done := make(chan struct{})
|
||||
ok := false
|
||||
go func() {
|
||||
@ -164,7 +168,7 @@ func TestRecvTriggersReconnect(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
c := testClient()
|
||||
c := NewClient(&Config{})
|
||||
close(c.quit)
|
||||
ok := false
|
||||
done := make(chan struct{})
|
||||
|
167
pkg/irc/const.go
167
pkg/irc/const.go
@ -1,43 +1,134 @@
|
||||
package irc
|
||||
|
||||
const (
|
||||
Error = "ERROR"
|
||||
Join = "JOIN"
|
||||
Mode = "MODE"
|
||||
Nick = "NICK"
|
||||
Notice = "NOTICE"
|
||||
Part = "PART"
|
||||
Ping = "PING"
|
||||
Privmsg = "PRIVMSG"
|
||||
Quit = "QUIT"
|
||||
Topic = "TOPIC"
|
||||
CAP = "CAP"
|
||||
AUTHENTICATE = "AUTHENTICATE"
|
||||
PASS = "PASS"
|
||||
NICK = "NICK"
|
||||
USER = "USER"
|
||||
OPER = "OPER"
|
||||
QUIT = "QUIT"
|
||||
JOIN = "JOIN"
|
||||
PART = "PART"
|
||||
KICK = "KICK"
|
||||
TOPIC = "TOPIC"
|
||||
NAMES = "NAMES"
|
||||
LIST = "LIST"
|
||||
MOTD = "MOTD"
|
||||
VERSION = "VERSION"
|
||||
ADMIN = "ADMIN"
|
||||
CONNECT = "CONNECT"
|
||||
TIME = "TIME"
|
||||
STATS = "STATS"
|
||||
INFO = "INFO"
|
||||
MODE = "MODE"
|
||||
PRIVMSG = "PRIVMSG"
|
||||
NOTICE = "NOTICE"
|
||||
USERHOST = "USERHOST"
|
||||
KILL = "KILL"
|
||||
ERROR = "ERROR"
|
||||
PING = "PING"
|
||||
PONG = "PONG"
|
||||
|
||||
ReplyWelcome = "001"
|
||||
ReplyYourHost = "002"
|
||||
ReplyCreated = "003"
|
||||
ReplyISupport = "005"
|
||||
ReplyLUserClient = "251"
|
||||
ReplyLUserOp = "252"
|
||||
ReplyLUserUnknown = "253"
|
||||
ReplyLUserChannels = "254"
|
||||
ReplyLUserMe = "255"
|
||||
ReplyAway = "301"
|
||||
ReplyWhoisUser = "311"
|
||||
ReplyWhoisServer = "312"
|
||||
ReplyWhoisOperator = "313"
|
||||
ReplyWhoisIdle = "317"
|
||||
ReplyEndOfWhois = "318"
|
||||
ReplyWhoisChannels = "319"
|
||||
ReplyList = "322"
|
||||
ReplyListEnd = "323"
|
||||
ReplyNoTopic = "331"
|
||||
ReplyTopic = "332"
|
||||
ReplyNamReply = "353"
|
||||
ReplyEndOfNames = "366"
|
||||
ReplyMotd = "372"
|
||||
ReplyMotdStart = "375"
|
||||
ReplyEndOfMotd = "376"
|
||||
ErrErroneousNickname = "432"
|
||||
ErrNicknameInUse = "433"
|
||||
ErrForward = "470"
|
||||
RPL_WELCOME = "001"
|
||||
RPL_YOURHOST = "002"
|
||||
RPL_CREATED = "003"
|
||||
RPL_MYINFO = "004"
|
||||
RPL_ISUPPORT = "005"
|
||||
RPL_BOUNCE = "010"
|
||||
RPL_UMODEIS = "221"
|
||||
RPL_LUSERCLIENT = "251"
|
||||
RPL_LUSEROP = "252"
|
||||
RPL_LUSERUNKNOWN = "253"
|
||||
RPL_LUSERCHANNELS = "254"
|
||||
RPL_LUSERME = "255"
|
||||
RPL_ADMINME = "256"
|
||||
RPL_ADMINLOC1 = "257"
|
||||
RPL_ADMINLOC2 = "258"
|
||||
RPL_ADMINEMAIL = "259"
|
||||
RPL_TRYAGAIN = "263"
|
||||
RPL_LOCALUSERS = "265"
|
||||
RPL_GLOBALUSERS = "266"
|
||||
RPL_WHOISCERTFP = "276"
|
||||
RPL_NONE = "300"
|
||||
RPL_AWAY = "301"
|
||||
RPL_USERHOST = "302"
|
||||
RPL_ISON = "303"
|
||||
RPL_UNAWAY = "305"
|
||||
RPL_NOWAWAY = "306"
|
||||
RPL_WHOISUSER = "311"
|
||||
RPL_WHOISSERVER = "312"
|
||||
RPL_WHOISOPERATOR = "313"
|
||||
RPL_WHOWASUSER = "314"
|
||||
RPL_WHOISIDLE = "317"
|
||||
RPL_ENDOFWHOIS = "318"
|
||||
RPL_WHOISCHANNELS = "319"
|
||||
RPL_LISTSTART = "321"
|
||||
RPL_LIST = "322"
|
||||
RPL_LISTEND = "323"
|
||||
RPL_CHANNELMODEIS = "324"
|
||||
RPL_CREATIONTIME = "329"
|
||||
RPL_NOTOPIC = "331"
|
||||
RPL_TOPIC = "332"
|
||||
RPL_TOPICWHOTIME = "333"
|
||||
RPL_INVITING = "341"
|
||||
RPL_INVITELIST = "346"
|
||||
RPL_ENDOFINVITELIST = "347"
|
||||
RPL_EXCEPTLIST = "348"
|
||||
RPL_ENDOFEXCEPTLIST = "349"
|
||||
RPL_VERSION = "351"
|
||||
RPL_NAMREPLY = "353"
|
||||
RPL_ENDOFNAMES = "366"
|
||||
RPL_BANLIST = "367"
|
||||
RPL_ENDOFBANLIST = "368"
|
||||
RPL_ENDOFWHOWAS = "369"
|
||||
RPL_MOTDSTART = "375"
|
||||
RPL_MOTD = "372"
|
||||
RPL_ENDOFMOTD = "376"
|
||||
RPL_YOUREOPER = "381"
|
||||
RPL_REHASHING = "382"
|
||||
ERR_UNKNOWNERROR = "400"
|
||||
ERR_NOSUCHNICK = "401"
|
||||
ERR_NOSUCHSERVER = "402"
|
||||
ERR_NOSUCHCHANNEL = "403"
|
||||
ERR_CANNOTSENDTOCHAN = "404"
|
||||
ERR_TOOMANYCHANNELS = "405"
|
||||
ERR_UNKNOWNCOMMAND = "421"
|
||||
ERR_NOMOTD = "422"
|
||||
ERR_ERRONEUSNICKNAME = "432"
|
||||
ERR_NICKNAMEINUSE = "433"
|
||||
ERR_NICKCOLLISION = "436"
|
||||
ERR_UNAVAILRESOURCE = "437"
|
||||
ERR_USERNOTINCHANNEL = "441"
|
||||
ERR_NOTONCHANNEL = "442"
|
||||
ERR_USERONCHANNEL = "443"
|
||||
ERR_NOTREGISTERED = "451"
|
||||
ERR_NEEDMOREPARAMS = "461"
|
||||
ERR_ALREADYREGISTERED = "462"
|
||||
ERR_PASSWDMISMATCH = "464"
|
||||
ERR_YOUREBANNEDCREEP = "465"
|
||||
ERR_FORWARD = "470"
|
||||
ERR_CHANNELISFULL = "471"
|
||||
ERR_UNKNOWNMODE = "472"
|
||||
ERR_INVITEONLYCHAN = "473"
|
||||
ERR_BANNEDFROMCHAN = "474"
|
||||
ERR_BADCHANNELKEY = "475"
|
||||
ERR_NOPRIVILEGES = "481"
|
||||
ERR_CHANOPRIVSNEEDED = "482"
|
||||
ERR_CANTKILLSERVER = "483"
|
||||
ERR_NOOPERHOST = "491"
|
||||
ERR_UMODEUNKNOWNFLAG = "501"
|
||||
ERR_USERSDONTMATCH = "502"
|
||||
RPL_STARTTLS = "670"
|
||||
ERR_STARTTLS = "691"
|
||||
ERR_NOPRIVS = "723"
|
||||
RPL_LOGGEDIN = "900"
|
||||
RPL_LOGGEDOUT = "901"
|
||||
ERR_NICKLOCKED = "902"
|
||||
RPL_SASLSUCCESS = "903"
|
||||
ERR_SASLFAIL = "904"
|
||||
ERR_SASLTOOLONG = "905"
|
||||
ERR_SASLABORTED = "906"
|
||||
ERR_SASLALREADY = "907"
|
||||
RPL_SASLMECHS = "908"
|
||||
)
|
||||
|
69
pkg/irc/ctcp.go
Normal file
69
pkg/irc/ctcp.go
Normal file
@ -0,0 +1,69 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ClientInfo is the CTCP messages this client implements
|
||||
const ClientInfo = "ACTION CLIENTINFO DCC FINGER PING SOURCE TIME VERSION USERINFO"
|
||||
|
||||
type CTCP struct {
|
||||
Command string
|
||||
Params string
|
||||
}
|
||||
|
||||
func DecodeCTCP(str string) *CTCP {
|
||||
if len(str) > 1 && str[0] == 0x01 {
|
||||
parts := strings.SplitN(strings.Trim(str, "\x01"), " ", 2)
|
||||
ctcp := CTCP{}
|
||||
|
||||
if parts[0] != "" {
|
||||
ctcp.Command = parts[0]
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(parts) == 2 {
|
||||
ctcp.Params = parts[1]
|
||||
}
|
||||
|
||||
return &ctcp
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func EncodeCTCP(ctcp *CTCP) string {
|
||||
if ctcp == nil || ctcp.Command == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("\x01%s %s\x01", ctcp.Command, ctcp.Params)
|
||||
}
|
||||
|
||||
func (c *Client) handleCTCP(ctcp *CTCP, msg *Message) {
|
||||
switch ctcp.Command {
|
||||
case "CLIENTINFO":
|
||||
c.ReplyCTCP(msg.Sender, ctcp.Command, ClientInfo)
|
||||
|
||||
case "FINGER", "VERSION":
|
||||
if c.Config.Version != "" {
|
||||
c.ReplyCTCP(msg.Sender, ctcp.Command, c.Config.Version)
|
||||
}
|
||||
|
||||
case "PING":
|
||||
c.ReplyCTCP(msg.Sender, ctcp.Command, ctcp.Params)
|
||||
|
||||
case "SOURCE":
|
||||
if c.Config.Source != "" {
|
||||
c.ReplyCTCP(msg.Sender, ctcp.Command, c.Config.Source)
|
||||
}
|
||||
|
||||
case "TIME":
|
||||
c.ReplyCTCP(msg.Sender, ctcp.Command, time.Now().UTC().Format(time.RFC3339))
|
||||
|
||||
case "USERINFO":
|
||||
c.ReplyCTCP(msg.Sender, ctcp.Command, fmt.Sprintf("%s (%s)", c.GetNick(), c.Config.Realname))
|
||||
}
|
||||
}
|
208
pkg/irc/dcc.go
Normal file
208
pkg/irc/dcc.go
Normal file
@ -0,0 +1,208 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DCCSend struct {
|
||||
File string `json:"file"`
|
||||
IP string `json:"ip"`
|
||||
Port string `json:"port"`
|
||||
Length uint64 `json:"length"`
|
||||
|
||||
dialer Dialer
|
||||
}
|
||||
|
||||
func (c *Client) ParseDCCSend(ctcp *CTCP) *DCCSend {
|
||||
params := strings.Split(ctcp.Params, " ")
|
||||
|
||||
if len(params) > 4 {
|
||||
ip, err := strconv.Atoi(params[2])
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
length, err := strconv.ParseUint(params[4], 10, 64)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
filename := path.Base(params[1])
|
||||
if filename == "/" || filename == "." {
|
||||
filename = ""
|
||||
}
|
||||
|
||||
return &DCCSend{
|
||||
File: filename,
|
||||
IP: intToIP(ip),
|
||||
Port: params[3],
|
||||
Length: length,
|
||||
dialer: c.dialer,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pack *DCCSend) Download(w io.Writer, progress chan DownloadProgress) error {
|
||||
if progress != nil {
|
||||
progress <- DownloadProgress{
|
||||
File: pack.File,
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := pack.dialer.Dial("tcp", net.JoinHostPort(pack.IP, pack.Port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
totalBytes := uint64(0)
|
||||
accBytes := uint64(0)
|
||||
averageSpeed := float64(0)
|
||||
buf := make([]byte, 4*1024)
|
||||
start := time.Now()
|
||||
prevUpdate := start
|
||||
|
||||
for {
|
||||
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := w.Write(buf[:n]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
accBytes += uint64(n)
|
||||
totalBytes += uint64(n)
|
||||
|
||||
_, err = conn.Write(uint64Bytes(totalBytes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if progress != nil {
|
||||
if dt := time.Since(prevUpdate); dt >= time.Second {
|
||||
prevUpdate = time.Now()
|
||||
|
||||
speed := float64(accBytes) / dt.Seconds()
|
||||
if averageSpeed == 0 {
|
||||
averageSpeed = speed
|
||||
} else {
|
||||
averageSpeed = 0.2*speed + 0.8*averageSpeed
|
||||
}
|
||||
accBytes = 0
|
||||
|
||||
bytesRemaining := float64(pack.Length - totalBytes)
|
||||
percentage := 100 * (float64(totalBytes) / float64(pack.Length))
|
||||
|
||||
progress <- DownloadProgress{
|
||||
Speed: formatByteCount(averageSpeed, true),
|
||||
PercCompletion: percentage,
|
||||
BytesRemaining: formatByteCount(bytesRemaining, false),
|
||||
BytesCompleted: formatByteCount(float64(totalBytes), false),
|
||||
SecondsElapsed: secondsSince(start),
|
||||
SecondsToGo: bytesRemaining / averageSpeed,
|
||||
File: pack.File,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if progress != nil {
|
||||
progress <- DownloadProgress{
|
||||
PercCompletion: 100,
|
||||
BytesCompleted: formatByteCount(float64(totalBytes), false),
|
||||
SecondsElapsed: secondsSince(start),
|
||||
File: pack.File,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pack *DCCSend) Size() string {
|
||||
return formatByteCount(float64(pack.Length), false)
|
||||
}
|
||||
|
||||
type DownloadProgress struct {
|
||||
File string `json:"file"`
|
||||
Error error `json:"error"`
|
||||
BytesCompleted string `json:"bytes_completed"`
|
||||
BytesRemaining string `json:"bytes_remaining"`
|
||||
PercCompletion float64 `json:"perc_completion"`
|
||||
Speed string `json:"speed"`
|
||||
SecondsElapsed int64 `json:"elapsed"`
|
||||
SecondsToGo float64 `json:"eta"`
|
||||
}
|
||||
|
||||
func intToIP(n int) string {
|
||||
var byte1 = n & 255
|
||||
var byte2 = ((n >> 8) & 255)
|
||||
var byte3 = ((n >> 16) & 255)
|
||||
var byte4 = ((n >> 24) & 255)
|
||||
return fmt.Sprintf("%d.%d.%d.%d", byte4, byte3, byte2, byte1)
|
||||
}
|
||||
|
||||
func uint64Bytes(i uint64) []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(b, i)
|
||||
return b
|
||||
}
|
||||
|
||||
func secondsSince(t time.Time) int64 {
|
||||
return int64(math.Round(time.Since(t).Seconds()))
|
||||
}
|
||||
|
||||
const (
|
||||
_ = 1.0 << (10 * iota)
|
||||
kibibyte
|
||||
mebibyte
|
||||
gibibyte
|
||||
)
|
||||
|
||||
func formatByteCount(b float64, speed bool) string {
|
||||
unit := ""
|
||||
value := b
|
||||
|
||||
switch {
|
||||
case b >= gibibyte:
|
||||
unit = "GiB"
|
||||
value = value / gibibyte
|
||||
case b >= mebibyte:
|
||||
unit = "MiB"
|
||||
value = value / mebibyte
|
||||
case b >= kibibyte:
|
||||
unit = "KiB"
|
||||
value = value / kibibyte
|
||||
case b > 1 || b == 0:
|
||||
unit = "bytes"
|
||||
case b == 1:
|
||||
unit = "byte"
|
||||
}
|
||||
|
||||
if speed {
|
||||
unit = unit + "/s"
|
||||
}
|
||||
|
||||
stringValue := strings.TrimSuffix(
|
||||
fmt.Sprintf("%.2f", value), ".00",
|
||||
)
|
||||
|
||||
return fmt.Sprintf("%s %s", stringValue, unit)
|
||||
}
|
177
pkg/irc/internal.go
Normal file
177
pkg/irc/internal.go
Normal file
@ -0,0 +1,177 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (c *Client) handleMessage(msg *Message) {
|
||||
switch msg.Command {
|
||||
case CAP:
|
||||
c.handleCAP(msg)
|
||||
|
||||
case PING:
|
||||
go c.write("PONG :" + msg.LastParam())
|
||||
|
||||
case JOIN:
|
||||
if len(msg.Params) > 0 {
|
||||
channel := msg.Params[0]
|
||||
|
||||
if c.Is(msg.Sender) {
|
||||
c.addChannel(channel)
|
||||
}
|
||||
|
||||
c.state.addUser(msg.Sender, channel)
|
||||
}
|
||||
|
||||
case PART:
|
||||
if len(msg.Params) > 0 {
|
||||
channel := msg.Params[0]
|
||||
|
||||
if c.Is(msg.Sender) {
|
||||
c.state.removeChannel(channel)
|
||||
} else {
|
||||
c.state.removeUser(msg.Sender, channel)
|
||||
}
|
||||
}
|
||||
|
||||
case QUIT:
|
||||
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:
|
||||
if c.Is(msg.Sender) {
|
||||
c.setNick(msg.LastParam())
|
||||
}
|
||||
|
||||
msg.meta = c.state.renameUser(msg.Sender, msg.LastParam())
|
||||
|
||||
case PRIVMSG:
|
||||
if c.Config.AutoCTCP {
|
||||
if ctcp := msg.ToCTCP(); ctcp != nil {
|
||||
c.handleCTCP(ctcp, msg)
|
||||
}
|
||||
}
|
||||
|
||||
case MODE:
|
||||
if len(msg.Params) > 1 {
|
||||
target := msg.Params[0]
|
||||
if len(msg.Params) > 2 && isChannel(target) {
|
||||
mode := ParseMode(msg.Params[1])
|
||||
mode.Network = c.Host()
|
||||
mode.Channel = target
|
||||
mode.User = msg.Params[2]
|
||||
|
||||
c.state.setMode(target, msg.Params[2], mode.Add, mode.Remove)
|
||||
|
||||
msg.meta = mode
|
||||
}
|
||||
}
|
||||
|
||||
case TOPIC, RPL_TOPIC:
|
||||
chIndex := 0
|
||||
if msg.Command == RPL_TOPIC {
|
||||
chIndex = 1
|
||||
}
|
||||
|
||||
if len(msg.Params) > chIndex {
|
||||
c.state.setTopic(msg.LastParam(), msg.Params[chIndex])
|
||||
}
|
||||
|
||||
case RPL_NOTOPIC:
|
||||
if len(msg.Params) > 1 {
|
||||
channel := msg.Params[1]
|
||||
c.state.setTopic("", channel)
|
||||
}
|
||||
|
||||
case RPL_WELCOME:
|
||||
if len(msg.Params) > 0 {
|
||||
c.setNick(msg.Params[0])
|
||||
}
|
||||
c.negotiating = false
|
||||
c.setRegistered(true)
|
||||
c.flushChannels()
|
||||
|
||||
c.backoff.Reset()
|
||||
c.sendRecv.Add(1)
|
||||
go c.send()
|
||||
|
||||
case RPL_ISUPPORT:
|
||||
c.Features.Parse(msg.Params)
|
||||
|
||||
case ERR_NICKNAMEINUSE, ERR_NICKCOLLISION, ERR_UNAVAILRESOURCE:
|
||||
if c.Config.HandleNickInUse != nil && len(msg.Params) > 1 {
|
||||
go c.writeNick(c.Config.HandleNickInUse(msg.Params[1]))
|
||||
}
|
||||
|
||||
case RPL_NAMREPLY:
|
||||
if len(msg.Params) > 2 {
|
||||
channel := msg.Params[2]
|
||||
users := strings.Split(strings.TrimSuffix(msg.LastParam(), " "), " ")
|
||||
|
||||
userBuffer := c.state.userBuffers[channel]
|
||||
c.state.userBuffers[channel] = append(userBuffer, users...)
|
||||
}
|
||||
|
||||
case RPL_ENDOFNAMES:
|
||||
if len(msg.Params) > 1 {
|
||||
channel := msg.Params[1]
|
||||
users := c.state.userBuffers[channel]
|
||||
|
||||
c.state.setUsers(users, channel)
|
||||
delete(c.state.userBuffers, channel)
|
||||
msg.meta = users
|
||||
}
|
||||
|
||||
case ERROR:
|
||||
c.Messages <- msg
|
||||
c.connChange(false, nil)
|
||||
time.Sleep(5 * time.Second)
|
||||
close(c.quit)
|
||||
return
|
||||
}
|
||||
|
||||
c.handleSASL(msg)
|
||||
}
|
||||
|
||||
type Mode struct {
|
||||
Network string
|
||||
Channel string
|
||||
User string
|
||||
Add string
|
||||
Remove string
|
||||
}
|
||||
|
||||
func ParseMode(mode string) *Mode {
|
||||
m := Mode{}
|
||||
add := false
|
||||
|
||||
for _, c := range mode {
|
||||
if c == '+' {
|
||||
add = true
|
||||
} else if c == '-' {
|
||||
add = false
|
||||
} else if add {
|
||||
m.Add += string(c)
|
||||
} else {
|
||||
m.Remove += string(c)
|
||||
}
|
||||
}
|
||||
|
||||
return &m
|
||||
}
|
||||
|
||||
func isChannel(s string) bool {
|
||||
return strings.IndexAny(s, "&#+!") == 0
|
||||
}
|
37
pkg/irc/internal_test.go
Normal file
37
pkg/irc/internal_test.go
Normal file
@ -0,0 +1,37 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHandlePing(t *testing.T) {
|
||||
c, out := testClientSend()
|
||||
c.handleMessage(&Message{
|
||||
Command: "PING",
|
||||
Params: []string{"voi voi"},
|
||||
})
|
||||
assert.Equal(t, "PONG :voi voi\r\n", <-out)
|
||||
}
|
||||
|
||||
func TestHandleNamreply(t *testing.T) {
|
||||
c, _ := testClientSend()
|
||||
|
||||
c.handleMessage(&Message{
|
||||
Command: RPL_NAMREPLY,
|
||||
Params: []string{"", "", "#chan", "a b c"},
|
||||
})
|
||||
c.handleMessage(&Message{
|
||||
Command: RPL_NAMREPLY,
|
||||
Params: []string{"", "", "#chan", "d"},
|
||||
})
|
||||
|
||||
endMsg := &Message{
|
||||
Command: RPL_ENDOFNAMES,
|
||||
Params: []string{"", "#chan"},
|
||||
}
|
||||
c.handleMessage(endMsg)
|
||||
|
||||
assert.Equal(t, []string{"a", "b", "c", "d"}, endMsg.meta)
|
||||
}
|
@ -6,10 +6,13 @@ import (
|
||||
|
||||
type Message struct {
|
||||
Tags map[string]string
|
||||
Prefix string
|
||||
Nick string
|
||||
Sender string
|
||||
Ident string
|
||||
Host string
|
||||
Command string
|
||||
Params []string
|
||||
|
||||
meta interface{}
|
||||
}
|
||||
|
||||
func (m *Message) LastParam() string {
|
||||
@ -19,6 +22,14 @@ func (m *Message) LastParam() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Message) IsFromServer() bool {
|
||||
return m.Sender == "" || strings.Contains(m.Sender, ".")
|
||||
}
|
||||
|
||||
func (m *Message) ToCTCP() *CTCP {
|
||||
return DecodeCTCP(m.LastParam())
|
||||
}
|
||||
|
||||
func ParseMessage(line string) *Message {
|
||||
msg := Message{}
|
||||
|
||||
@ -57,14 +68,23 @@ func ParseMessage(line string) *Message {
|
||||
if next == -1 {
|
||||
return nil
|
||||
}
|
||||
msg.Prefix = line[1:next]
|
||||
prefix := line[1:next]
|
||||
|
||||
if i := strings.Index(msg.Prefix, "!"); i > 0 {
|
||||
msg.Nick = msg.Prefix[:i]
|
||||
} else if i := strings.Index(msg.Prefix, "@"); i > 0 {
|
||||
msg.Nick = msg.Prefix[:i]
|
||||
if i := strings.Index(prefix, "!"); i > 0 {
|
||||
msg.Sender = prefix[:i]
|
||||
prefix = prefix[i+1:]
|
||||
|
||||
if i = strings.Index(prefix, "@"); i > 0 {
|
||||
msg.Ident = prefix[:i]
|
||||
msg.Host = prefix[i+1:]
|
||||
} else {
|
||||
msg.Ident = prefix
|
||||
}
|
||||
} else if i = strings.Index(prefix, "@"); i > 0 {
|
||||
msg.Sender = prefix[:i]
|
||||
msg.Host = prefix[i+1:]
|
||||
} else {
|
||||
msg.Nick = msg.Prefix
|
||||
msg.Sender = prefix
|
||||
}
|
||||
|
||||
line = line[next+1:]
|
||||
|
@ -14,16 +14,16 @@ func TestParseMessage(t *testing.T) {
|
||||
{
|
||||
":user CMD #chan :some message",
|
||||
&Message{
|
||||
Prefix: "user",
|
||||
Nick: "user",
|
||||
Sender: "user",
|
||||
Command: "CMD",
|
||||
Params: []string{"#chan", "some message"},
|
||||
},
|
||||
}, {
|
||||
":nick!user@host.com CMD a b",
|
||||
&Message{
|
||||
Prefix: "nick!user@host.com",
|
||||
Nick: "nick",
|
||||
Sender: "nick",
|
||||
Ident: "user",
|
||||
Host: "host.com",
|
||||
Command: "CMD",
|
||||
Params: []string{"a", "b"},
|
||||
},
|
||||
@ -53,15 +53,16 @@ func TestParseMessage(t *testing.T) {
|
||||
}, {
|
||||
":nick@host.com CMD",
|
||||
&Message{
|
||||
Prefix: "nick@host.com",
|
||||
Nick: "nick",
|
||||
Sender: "nick",
|
||||
Host: "host.com",
|
||||
Command: "CMD",
|
||||
},
|
||||
}, {
|
||||
":ni@ck!user!name@host!.com CMD",
|
||||
&Message{
|
||||
Prefix: "ni@ck!user!name@host!.com",
|
||||
Nick: "ni@ck",
|
||||
Sender: "ni@ck",
|
||||
Ident: "user!name",
|
||||
Host: "host!.com",
|
||||
Command: "CMD",
|
||||
},
|
||||
}, {
|
||||
@ -114,18 +115,20 @@ func TestParseMessage(t *testing.T) {
|
||||
Tags: map[string]string{
|
||||
"x": "y",
|
||||
},
|
||||
Prefix: "nick!user@host.com",
|
||||
Nick: "nick",
|
||||
Sender: "nick",
|
||||
Ident: "user",
|
||||
Host: "host.com",
|
||||
Command: "CMD",
|
||||
},
|
||||
}, {
|
||||
"@x=y :nick!user@host.com CMD :pie and cake",
|
||||
"@x=y :nick!user@host.com CMD :pie and cake",
|
||||
&Message{
|
||||
Tags: map[string]string{
|
||||
"x": "y",
|
||||
},
|
||||
Prefix: "nick!user@host.com",
|
||||
Nick: "nick",
|
||||
Sender: "nick",
|
||||
Ident: "user",
|
||||
Host: "host.com",
|
||||
Command: "CMD",
|
||||
Params: []string{"pie and cake"},
|
||||
},
|
||||
@ -135,8 +138,9 @@ func TestParseMessage(t *testing.T) {
|
||||
Tags: map[string]string{
|
||||
"x": "y",
|
||||
},
|
||||
Prefix: "nick!user@host.com",
|
||||
Nick: "nick",
|
||||
Sender: "nick",
|
||||
Ident: "user",
|
||||
Host: "host.com",
|
||||
Command: "CMD",
|
||||
Params: []string{"beans", "rainbows", "pie and cake"},
|
||||
},
|
||||
|
33
pkg/irc/meta.go
Normal file
33
pkg/irc/meta.go
Normal file
@ -0,0 +1,33 @@
|
||||
package irc
|
||||
|
||||
// GetNickChannels returns the channels the client has in common with
|
||||
// the user that changed nick
|
||||
func GetNickChannels(msg *Message) []string {
|
||||
return stringListMeta(msg)
|
||||
}
|
||||
|
||||
// GetQuitChannels returns the channels the client has in common with
|
||||
// the user that quit
|
||||
func GetQuitChannels(msg *Message) []string {
|
||||
return stringListMeta(msg)
|
||||
}
|
||||
|
||||
func GetMode(msg *Message) *Mode {
|
||||
if mode, ok := msg.meta.(*Mode); ok {
|
||||
return mode
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNamreplyUsers returns all RPL_NAMREPLY users
|
||||
// when passed a RPL_ENDOFNAMES message
|
||||
func GetNamreplyUsers(msg *Message) []string {
|
||||
return stringListMeta(msg)
|
||||
}
|
||||
|
||||
func stringListMeta(msg *Message) []string {
|
||||
if list, ok := msg.meta.([]string); ok {
|
||||
return list
|
||||
}
|
||||
return nil
|
||||
}
|
179
pkg/irc/sasl.go
Normal file
179
pkg/irc/sasl.go
Normal file
@ -0,0 +1,179 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"hash"
|
||||
"strings"
|
||||
|
||||
"github.com/xdg-go/scram"
|
||||
)
|
||||
|
||||
var DefaultSASLMechanisms = []string{
|
||||
"EXTERNAL",
|
||||
//"SCRAM-SHA-512",
|
||||
"SCRAM-SHA-256",
|
||||
//"SCRAM-SHA-1",
|
||||
"PLAIN",
|
||||
}
|
||||
|
||||
type SASL interface {
|
||||
Name() string
|
||||
Step(response string) (string, error)
|
||||
}
|
||||
|
||||
type SASLPlain struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (s *SASLPlain) Name() string {
|
||||
return "PLAIN"
|
||||
}
|
||||
|
||||
func (s *SASLPlain) Step(string) (string, error) {
|
||||
buf := bytes.Buffer{}
|
||||
buf.WriteString(s.Username)
|
||||
buf.WriteByte(0x0)
|
||||
buf.WriteString(s.Username)
|
||||
buf.WriteByte(0x0)
|
||||
buf.WriteString(s.Password)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
|
||||
}
|
||||
|
||||
type SASLExternal struct{}
|
||||
|
||||
func (s *SASLExternal) Name() string {
|
||||
return "EXTERNAL"
|
||||
}
|
||||
|
||||
func (s *SASLExternal) Step(string) (string, error) {
|
||||
return "+", nil
|
||||
}
|
||||
|
||||
var (
|
||||
scramHashes = map[string]scram.HashGeneratorFcn{
|
||||
"SHA-512": func() hash.Hash { return sha512.New() },
|
||||
"SHA-256": func() hash.Hash { return sha256.New() },
|
||||
"SHA-1": func() hash.Hash { return sha1.New() },
|
||||
}
|
||||
|
||||
ErrUnsupportedHash = errors.New("unsupported hash algorithm")
|
||||
)
|
||||
|
||||
type SASLScram struct {
|
||||
Username string
|
||||
Password string
|
||||
Hash string
|
||||
conv *scram.ClientConversation
|
||||
}
|
||||
|
||||
func (s *SASLScram) Name() string {
|
||||
return "SCRAM-" + s.Hash
|
||||
}
|
||||
|
||||
func (s *SASLScram) Step(response string) (string, error) {
|
||||
if s.conv == nil {
|
||||
if hash, ok := scramHashes[s.Hash]; ok {
|
||||
client, err := hash.NewClient(s.Username, s.Password, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s.conv = client.NewConversation()
|
||||
} else {
|
||||
return "", ErrUnsupportedHash
|
||||
}
|
||||
}
|
||||
|
||||
challenge := ""
|
||||
if response != "+" {
|
||||
b, err := base64.StdEncoding.DecodeString(response)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
challenge = string(b)
|
||||
}
|
||||
|
||||
res, err := s.conv.Step(challenge)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if s.conv.Done() {
|
||||
s.conv = nil
|
||||
return "+", nil
|
||||
} else {
|
||||
return base64.StdEncoding.EncodeToString([]byte(res)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) tryNextSASL() {
|
||||
if len(c.saslMechanisms) > 0 {
|
||||
c.currentSASL, c.saslMechanisms = c.saslMechanisms[0], c.saslMechanisms[1:]
|
||||
c.authenticate(c.currentSASL.Name())
|
||||
} else {
|
||||
c.finishCAP()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) filterSASLMechanisms(supportedMechs []string) {
|
||||
saslMechanisms := []SASL{}
|
||||
|
||||
for _, mech := range c.saslMechanisms {
|
||||
for _, supported := range supportedMechs {
|
||||
if mech.Name() == supported {
|
||||
saslMechanisms = append(saslMechanisms, mech)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.saslMechanisms = saslMechanisms
|
||||
}
|
||||
|
||||
func (c *Client) handleSASL(msg *Message) {
|
||||
switch msg.Command {
|
||||
case AUTHENTICATE:
|
||||
if c.currentSASL == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: handle 400 chunking on incoming messages
|
||||
auth, err := c.currentSASL.Step(msg.LastParam())
|
||||
if err != nil {
|
||||
c.tryNextSASL()
|
||||
return
|
||||
}
|
||||
|
||||
for len(auth) >= 400 {
|
||||
c.authenticate(auth)
|
||||
auth = auth[400:]
|
||||
}
|
||||
if len(auth) > 0 {
|
||||
c.authenticate(auth)
|
||||
} else {
|
||||
c.authenticate("+")
|
||||
}
|
||||
|
||||
case ERR_SASLFAIL, ERR_SASLTOOLONG, ERR_SASLABORTED:
|
||||
c.tryNextSASL()
|
||||
|
||||
case RPL_SASLMECHS:
|
||||
if len(msg.Params) > 1 {
|
||||
supportedMechs := strings.Split(msg.Params[1], ",")
|
||||
c.filterSASLMechanisms(supportedMechs)
|
||||
}
|
||||
|
||||
if len(c.saslMechanisms) == 0 {
|
||||
c.finishCAP()
|
||||
}
|
||||
|
||||
case RPL_SASLSUCCESS, RPL_LOGGEDIN, ERR_NICKLOCKED:
|
||||
c.finishCAP()
|
||||
}
|
||||
}
|
236
pkg/irc/state.go
Normal file
236
pkg/irc/state.go
Normal file
@ -0,0 +1,236 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type state struct {
|
||||
client *Client
|
||||
|
||||
users map[string][]*User
|
||||
topic map[string]string
|
||||
|
||||
userBuffers map[string][]string
|
||||
|
||||
motd []string
|
||||
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
const userModePrefixes = "~&@%+"
|
||||
const userModeChars = "qaohv"
|
||||
|
||||
type User struct {
|
||||
nick string
|
||||
modes string
|
||||
prefix string
|
||||
}
|
||||
|
||||
func NewUser(nick string) *User {
|
||||
user := &User{nick: nick}
|
||||
|
||||
if i := strings.IndexAny(nick, userModePrefixes); i == 0 {
|
||||
i = strings.Index(userModePrefixes, string(nick[0]))
|
||||
user.modes = string(userModeChars[i])
|
||||
user.nick = nick[1:]
|
||||
user.updatePrefix()
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
func (u *User) String() string {
|
||||
return u.prefix + u.nick
|
||||
}
|
||||
|
||||
func (u *User) AddModes(modes string) {
|
||||
for _, mode := range modes {
|
||||
if strings.Contains(u.modes, string(mode)) {
|
||||
continue
|
||||
}
|
||||
u.modes += string(mode)
|
||||
}
|
||||
u.updatePrefix()
|
||||
}
|
||||
|
||||
func (u *User) RemoveModes(modes string) {
|
||||
for _, mode := range modes {
|
||||
u.modes = strings.Replace(u.modes, string(mode), "", 1)
|
||||
}
|
||||
u.updatePrefix()
|
||||
}
|
||||
|
||||
func (u *User) updatePrefix() {
|
||||
for i, mode := range userModeChars {
|
||||
if strings.Contains(u.modes, string(mode)) {
|
||||
u.prefix = string(userModePrefixes[i])
|
||||
return
|
||||
}
|
||||
}
|
||||
u.prefix = ""
|
||||
}
|
||||
|
||||
func newState(client *Client) *state {
|
||||
return &state{
|
||||
client: client,
|
||||
users: make(map[string][]*User),
|
||||
topic: make(map[string]string),
|
||||
userBuffers: make(map[string][]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) reset() {
|
||||
s.lock.Lock()
|
||||
s.users = make(map[string][]*User)
|
||||
s.topic = make(map[string]string)
|
||||
s.userBuffers = make(map[string][]string)
|
||||
s.motd = []string{}
|
||||
s.lock.Unlock()
|
||||
}
|
||||
|
||||
func (s *state) removeChannel(channel string) {
|
||||
s.lock.Lock()
|
||||
delete(s.users, channel)
|
||||
delete(s.topic, channel)
|
||||
s.lock.Unlock()
|
||||
}
|
||||
|
||||
func (s *state) getUsers(channel string) []string {
|
||||
s.lock.Lock()
|
||||
|
||||
users := make([]string, len(s.users[channel]))
|
||||
for i, user := range s.users[channel] {
|
||||
users[i] = user.String()
|
||||
}
|
||||
|
||||
s.lock.Unlock()
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
func (s *state) setUsers(users []string, channel string) {
|
||||
s.lock.Lock()
|
||||
|
||||
s.users[channel] = make([]*User, len(users))
|
||||
for i, nick := range users {
|
||||
s.users[channel][i] = NewUser(nick)
|
||||
}
|
||||
|
||||
s.lock.Unlock()
|
||||
}
|
||||
|
||||
func (s *state) addUser(user, channel string) {
|
||||
s.lock.Lock()
|
||||
|
||||
if users, ok := s.users[channel]; ok {
|
||||
for _, u := range users {
|
||||
if u.nick == user {
|
||||
s.lock.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s.users[channel] = append(users, NewUser(user))
|
||||
} else {
|
||||
s.users[channel] = []*User{NewUser(user)}
|
||||
}
|
||||
|
||||
s.lock.Unlock()
|
||||
}
|
||||
|
||||
func (s *state) removeUser(user, channel string) {
|
||||
s.lock.Lock()
|
||||
s.internalRemoveUser(user, channel)
|
||||
s.lock.Unlock()
|
||||
}
|
||||
|
||||
func (s *state) removeUserAll(user string) []string {
|
||||
channels := []string{}
|
||||
s.lock.Lock()
|
||||
|
||||
for channel := range s.users {
|
||||
if s.internalRemoveUser(user, channel) {
|
||||
channels = append(channels, channel)
|
||||
}
|
||||
}
|
||||
|
||||
s.lock.Unlock()
|
||||
return channels
|
||||
}
|
||||
|
||||
func (s *state) renameUser(oldNick, newNick string) []string {
|
||||
s.lock.Lock()
|
||||
channels := s.renameAll(oldNick, newNick)
|
||||
s.lock.Unlock()
|
||||
return channels
|
||||
}
|
||||
|
||||
func (s *state) setMode(channel, user, add, remove string) {
|
||||
s.lock.Lock()
|
||||
|
||||
for _, u := range s.users[channel] {
|
||||
if u.nick == user {
|
||||
u.AddModes(add)
|
||||
u.RemoveModes(remove)
|
||||
|
||||
s.lock.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s.lock.Unlock()
|
||||
}
|
||||
|
||||
func (s *state) getTopic(channel string) string {
|
||||
s.lock.Lock()
|
||||
topic := s.topic[channel]
|
||||
s.lock.Unlock()
|
||||
return topic
|
||||
}
|
||||
|
||||
func (s *state) setTopic(topic, channel string) {
|
||||
s.lock.Lock()
|
||||
s.topic[channel] = topic
|
||||
s.lock.Unlock()
|
||||
}
|
||||
|
||||
func (s *state) getMOTD() []string {
|
||||
s.lock.Lock()
|
||||
motd := s.motd
|
||||
s.lock.Unlock()
|
||||
return motd
|
||||
}
|
||||
|
||||
func (s *state) rename(channel, oldNick, newNick string) bool {
|
||||
for _, user := range s.users[channel] {
|
||||
if user.nick == oldNick {
|
||||
user.nick = newNick
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *state) renameAll(oldNick, newNick string) []string {
|
||||
channels := []string{}
|
||||
|
||||
for channel := range s.users {
|
||||
if s.rename(channel, oldNick, newNick) {
|
||||
channels = append(channels, channel)
|
||||
}
|
||||
}
|
||||
|
||||
return channels
|
||||
}
|
||||
|
||||
func (s *state) internalRemoveUser(user, channel string) bool {
|
||||
for i, u := range s.users[channel] {
|
||||
if u.nick == user {
|
||||
users := s.users[channel]
|
||||
s.users[channel] = append(users[:i], users[i+1:]...)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
89
pkg/irc/state_test.go
Normal file
89
pkg/irc/state_test.go
Normal file
@ -0,0 +1,89 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStateGetSetUsers(t *testing.T) {
|
||||
state := newState(NewClient(&Config{}))
|
||||
users := []string{"a", "b"}
|
||||
state.setUsers(users, "#chan")
|
||||
assert.Equal(t, users, state.getUsers("#chan"))
|
||||
state.setUsers(users, "#chan")
|
||||
assert.Equal(t, users, state.getUsers("#chan"))
|
||||
}
|
||||
|
||||
func TestStateAddRemoveUser(t *testing.T) {
|
||||
state := newState(NewClient(&Config{}))
|
||||
state.addUser("user", "#chan")
|
||||
state.addUser("user", "#chan")
|
||||
assert.Len(t, state.getUsers("#chan"), 1)
|
||||
state.addUser("user2", "#chan")
|
||||
assert.Equal(t, []string{"user", "user2"}, state.getUsers("#chan"))
|
||||
state.removeUser("user", "#chan")
|
||||
assert.Equal(t, []string{"user2"}, state.getUsers("#chan"))
|
||||
}
|
||||
|
||||
func TestStateRemoveUserAll(t *testing.T) {
|
||||
state := newState(NewClient(&Config{}))
|
||||
state.addUser("user", "#chan1")
|
||||
state.addUser("user", "#chan2")
|
||||
state.removeUserAll("user")
|
||||
assert.Empty(t, state.getUsers("#chan1"))
|
||||
assert.Empty(t, state.getUsers("#chan2"))
|
||||
}
|
||||
|
||||
func TestStateRenameUser(t *testing.T) {
|
||||
state := newState(NewClient(&Config{}))
|
||||
state.addUser("user", "#chan1")
|
||||
state.addUser("user", "#chan2")
|
||||
state.renameUser("user", "new")
|
||||
assert.Equal(t, []string{"new"}, state.getUsers("#chan1"))
|
||||
assert.Equal(t, []string{"new"}, state.getUsers("#chan2"))
|
||||
|
||||
state.addUser("@gotop", "#chan3")
|
||||
state.renameUser("gotop", "stillgotit")
|
||||
assert.Equal(t, []string{"@stillgotit"}, state.getUsers("#chan3"))
|
||||
}
|
||||
|
||||
func TestStateMode(t *testing.T) {
|
||||
state := newState(NewClient(&Config{}))
|
||||
state.addUser("+user", "#chan")
|
||||
state.setMode("#chan", "user", "o", "v")
|
||||
assert.Equal(t, []string{"@user"}, state.getUsers("#chan"))
|
||||
state.setMode("#chan", "user", "v", "")
|
||||
assert.Equal(t, []string{"@user"}, state.getUsers("#chan"))
|
||||
state.setMode("#chan", "user", "", "o")
|
||||
assert.Equal(t, []string{"+user"}, state.getUsers("#chan"))
|
||||
state.setMode("#chan", "user", "q", "")
|
||||
assert.Equal(t, []string{"~user"}, state.getUsers("#chan"))
|
||||
}
|
||||
|
||||
func TestStateTopic(t *testing.T) {
|
||||
state := newState(NewClient(&Config{}))
|
||||
assert.Equal(t, "", state.getTopic("#chan"))
|
||||
state.setTopic("the topic", "#chan")
|
||||
assert.Equal(t, "the topic", state.getTopic("#chan"))
|
||||
}
|
||||
|
||||
func TestStateChannelUserMode(t *testing.T) {
|
||||
user := NewUser("&test")
|
||||
assert.Equal(t, "test", user.nick)
|
||||
assert.Equal(t, "a", string(user.modes[0]))
|
||||
assert.Equal(t, "&test", user.String())
|
||||
|
||||
user.RemoveModes("a")
|
||||
assert.Equal(t, "test", user.String())
|
||||
user.AddModes("o")
|
||||
assert.Equal(t, "@test", user.String())
|
||||
user.AddModes("q")
|
||||
assert.Equal(t, "~test", user.String())
|
||||
user.AddModes("v")
|
||||
assert.Equal(t, "~test", user.String())
|
||||
user.RemoveModes("qo")
|
||||
assert.Equal(t, "+test", user.String())
|
||||
user.RemoveModes("v")
|
||||
assert.Equal(t, "test", user.String())
|
||||
}
|
@ -6,6 +6,8 @@ import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/khlieng/dispatch/pkg/cookie"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -48,18 +50,11 @@ func (s *Session) SetCookie(w http.ResponseWriter, r *http.Request) {
|
||||
created := time.Unix(s.createdAt, 0)
|
||||
s.lock.Unlock()
|
||||
|
||||
cookie := &http.Cookie{
|
||||
Name: CookieName,
|
||||
Value: s.Key(),
|
||||
Path: "/",
|
||||
Expires: created.Add(Expiration),
|
||||
HttpOnly: true,
|
||||
Secure: r.TLS != nil,
|
||||
}
|
||||
|
||||
if v := cookie.String(); v != "" {
|
||||
w.Header().Add("Set-Cookie", v+"; SameSite=Lax")
|
||||
}
|
||||
cookie.Set(w, r, &http.Cookie{
|
||||
Name: CookieName,
|
||||
Value: s.Key(),
|
||||
Expires: created.Add(Expiration),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Session) Expired() bool {
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/khlieng/dispatch/pkg/cookie"
|
||||
"github.com/khlieng/dispatch/pkg/session"
|
||||
"github.com/khlieng/dispatch/storage"
|
||||
)
|
||||
@ -11,7 +12,7 @@ import (
|
||||
func (d *Dispatch) handleAuth(w http.ResponseWriter, r *http.Request, createUser, refresh bool) *State {
|
||||
var state *State
|
||||
|
||||
cookie, err := r.Cookie(session.CookieName)
|
||||
cookie, err := r.Cookie(cookie.Name(r, session.CookieName))
|
||||
if err != nil {
|
||||
if createUser {
|
||||
state, err = d.newUser(w, r)
|
||||
@ -63,18 +64,6 @@ func (d *Dispatch) newUser(w http.ResponseWriter, r *http.Request) (*State, erro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
messageStore, err := d.GetMessageStore(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.SetMessageStore(messageStore)
|
||||
|
||||
search, err := d.GetMessageSearchProvider(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.SetMessageSearchProvider(search)
|
||||
|
||||
log.Println(r.RemoteAddr, "[Auth] New anonymous user | ID:", user.ID)
|
||||
|
||||
session, err := session.New(user.ID)
|
||||
|
@ -24,25 +24,26 @@ const indexTemplate = `
|
||||
<script>{{.InlineScript}}</script>
|
||||
{{end}}
|
||||
|
||||
{{range .Scripts}}
|
||||
<script src="{{.}}" defer></script>
|
||||
{{end}}
|
||||
|
||||
<link rel="preload" href="/font/RobotoMono-Regular.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/font/Montserrat-Regular.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/font/Montserrat-Bold.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/font/RobotoMono-Bold.woff2" as="font" type="font/woff2" crossorigin>
|
||||
|
||||
{{if .Stylesheet}}
|
||||
<link href="{{.Stylesheet}}" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{{.Stylesheet}}">
|
||||
{{end}}
|
||||
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="apple-touch-icon" href="/icon_192.png">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>This page needs JavaScript enabled to function.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
{{range .Scripts}}
|
||||
<script src="{{.}}"></script>
|
||||
{{end}}
|
||||
</body>
|
||||
|
||||
</html>`
|
||||
|
@ -11,14 +11,8 @@ import (
|
||||
)
|
||||
|
||||
type connectDefaults struct {
|
||||
Name string
|
||||
Host string
|
||||
Port int
|
||||
Channels []string
|
||||
Password bool
|
||||
SSL bool
|
||||
ReadOnly bool
|
||||
ShowDetails bool
|
||||
*config.Defaults
|
||||
ServerPassword bool
|
||||
}
|
||||
|
||||
type dispatchVersion struct {
|
||||
@ -28,9 +22,10 @@ type dispatchVersion struct {
|
||||
}
|
||||
|
||||
type indexData struct {
|
||||
Defaults *config.Defaults
|
||||
Servers []Server
|
||||
Defaults connectDefaults
|
||||
Networks []*storage.Network
|
||||
Channels []*storage.Channel
|
||||
OpenDMs []storage.Tab
|
||||
HexIP bool
|
||||
Version dispatchVersion
|
||||
|
||||
@ -43,12 +38,15 @@ type indexData struct {
|
||||
Messages *Messages
|
||||
}
|
||||
|
||||
func (d *Dispatch) getIndexData(r *http.Request, path string, state *State) *indexData {
|
||||
func (d *Dispatch) getIndexData(r *http.Request, state *State) *indexData {
|
||||
cfg := d.Config()
|
||||
|
||||
data := indexData{
|
||||
Defaults: &cfg.Defaults,
|
||||
HexIP: cfg.HexIP,
|
||||
Defaults: connectDefaults{
|
||||
Defaults: &cfg.Defaults,
|
||||
ServerPassword: cfg.Defaults.ServerPassword != "",
|
||||
},
|
||||
HexIP: cfg.HexIP,
|
||||
Version: dispatchVersion{
|
||||
Tag: version.Tag,
|
||||
Commit: version.Commit,
|
||||
@ -56,77 +54,54 @@ func (d *Dispatch) getIndexData(r *http.Request, path string, state *State) *ind
|
||||
},
|
||||
}
|
||||
|
||||
if data.Defaults.Password != "" {
|
||||
data.Defaults.Password = "******"
|
||||
}
|
||||
|
||||
if state == nil {
|
||||
data.Settings = storage.DefaultClientSettings()
|
||||
return &data
|
||||
}
|
||||
|
||||
data.Settings = state.user.GetClientSettings()
|
||||
data.Settings = state.user.ClientSettings()
|
||||
|
||||
servers, err := state.user.GetServers()
|
||||
if err != nil {
|
||||
return nil
|
||||
state.lock.Lock()
|
||||
for _, network := range state.networks {
|
||||
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()
|
||||
for _, server := range servers {
|
||||
server.Password = ""
|
||||
server.Username = ""
|
||||
server.Realname = ""
|
||||
state.lock.Unlock()
|
||||
|
||||
s := Server{
|
||||
Server: server,
|
||||
Status: newConnectionUpdate(server.Host, connections[server.Host]),
|
||||
}
|
||||
|
||||
if i, ok := state.irc[server.Host]; ok {
|
||||
s.Features = i.Features.Map()
|
||||
}
|
||||
|
||||
data.Servers = append(data.Servers, s)
|
||||
openDMs, err := state.user.OpenDMs()
|
||||
if err == nil {
|
||||
data.OpenDMs = openDMs
|
||||
}
|
||||
|
||||
channels, err := state.user.GetChannels()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for i, channel := range channels {
|
||||
channels[i].Topic = channelStore.GetTopic(channel.Server, channel.Name)
|
||||
}
|
||||
data.Channels = channels
|
||||
|
||||
server, channel := getTabFromPath(path)
|
||||
if isInChannel(channels, server, channel) {
|
||||
data.addUsersAndMessages(server, channel, state)
|
||||
return &data
|
||||
}
|
||||
|
||||
server, channel = parseTabCookie(r, path)
|
||||
if isInChannel(channels, server, channel) {
|
||||
data.addUsersAndMessages(server, channel, state)
|
||||
tab, err := tabFromRequest(r)
|
||||
if err == nil && hasTab(data.Channels, openDMs, tab.Network, tab.Name) {
|
||||
data.addUsersAndMessages(tab.Network, tab.Name, state)
|
||||
}
|
||||
|
||||
return &data
|
||||
}
|
||||
|
||||
func (d *indexData) addUsersAndMessages(server, channel string, state *State) {
|
||||
users := channelStore.GetUsers(server, channel)
|
||||
if len(users) > 0 {
|
||||
d.Users = &Userlist{
|
||||
Server: server,
|
||||
Channel: channel,
|
||||
Users: users,
|
||||
func (d *indexData) addUsersAndMessages(network, name string, state *State) {
|
||||
if i, ok := state.client(network); ok && isChannel(name) {
|
||||
if users := i.ChannelUsers(name); len(users) > 0 {
|
||||
d.Users = &Userlist{
|
||||
Network: network,
|
||||
Channel: name,
|
||||
Users: users,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messages, hasMore, err := state.user.GetLastMessages(server, channel, 50)
|
||||
messages, hasMore, err := state.user.LastMessages(network, name, 50)
|
||||
if err == nil && len(messages) > 0 {
|
||||
m := Messages{
|
||||
Server: server,
|
||||
To: channel,
|
||||
Network: network,
|
||||
To: name,
|
||||
Messages: messages,
|
||||
}
|
||||
|
||||
@ -138,10 +113,16 @@ func (d *indexData) addUsersAndMessages(server, channel string, state *State) {
|
||||
}
|
||||
}
|
||||
|
||||
func isInChannel(channels []*storage.Channel, server, channel string) bool {
|
||||
if channel != "" {
|
||||
func hasTab(channels []*storage.Channel, openDMs []storage.Tab, network, name string) bool {
|
||||
if name != "" {
|
||||
for _, ch := range channels {
|
||||
if server == ch.Server && channel == ch.Name {
|
||||
if network == ch.Network && name == ch.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for _, tab := range openDMs {
|
||||
if network == tab.Network && name == tab.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -149,30 +130,52 @@ func isInChannel(channels []*storage.Channel, server, channel string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func getTabFromPath(rawPath string) (string, string) {
|
||||
path := strings.Split(strings.Trim(rawPath, "/"), "/")
|
||||
if len(path) >= 2 {
|
||||
name, err := url.PathUnescape(path[len(path)-1])
|
||||
if err == nil && isChannel(name) {
|
||||
return path[len(path)-2], name
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
func tabFromRequest(r *http.Request) (Tab, error) {
|
||||
tab := Tab{}
|
||||
|
||||
var path string
|
||||
if strings.HasPrefix(r.URL.Path, "/ws") {
|
||||
path = r.URL.EscapedPath()[3:]
|
||||
} else {
|
||||
referer, err := url.Parse(r.Referer())
|
||||
if err != nil {
|
||||
return tab, err
|
||||
}
|
||||
|
||||
path = referer.EscapedPath()
|
||||
}
|
||||
|
||||
func parseTabCookie(r *http.Request, path string) (string, string) {
|
||||
if path == "/" {
|
||||
cookie, err := r.Cookie("tab")
|
||||
if err == nil {
|
||||
v, err := url.PathUnescape(cookie.Value)
|
||||
if err == nil {
|
||||
tab := strings.SplitN(v, ";", 2)
|
||||
if err != nil {
|
||||
return tab, err
|
||||
}
|
||||
|
||||
if len(tab) == 2 && isChannel(tab[1]) {
|
||||
return tab[0], tab[1]
|
||||
v, err := url.PathUnescape(cookie.Value)
|
||||
if err != nil {
|
||||
return tab, err
|
||||
}
|
||||
|
||||
parts := strings.SplitN(v, ";", 2)
|
||||
if len(parts) == 2 {
|
||||
tab.Network = parts[0]
|
||||
tab.Name = parts[1]
|
||||
}
|
||||
} else {
|
||||
parts := strings.Split(strings.Trim(path, "/"), "/")
|
||||
if len(parts) > 0 && len(parts) < 3 {
|
||||
if len(parts) == 2 {
|
||||
name, err := url.PathUnescape(parts[1])
|
||||
if err != nil {
|
||||
return tab, err
|
||||
}
|
||||
|
||||
tab.Name = name
|
||||
}
|
||||
|
||||
tab.Network = parts[0]
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
|
||||
return tab, nil
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user