70 Commits

Author SHA1 Message Date
b002eef285 Fix navicon 2020-04-30 14:36:30 +02:00
c566d5d61d Copy location object 2020-04-30 13:44:36 +02:00
13a31c30d9 Update .goreleaser.yml 2020-04-30 08:16:06 +02:00
508a41ee45 Update client dependencies 2020-04-30 07:54:30 +02:00
1794e2680a Update server dependencies 2020-04-29 04:23:32 +02:00
c704ebb042 Use react-icons 2020-04-29 03:13:35 +02:00
bb66740fd1 Update gulp instructions in README 2020-04-24 03:04:46 +02:00
4010132884 Linkify topics in channel joining modal 2020-04-24 02:37:56 +02:00
77543e3aed Switch to bbolt 2020-04-23 01:06:36 +02:00
360bed00f9 Implement old storage.Path API 2020-04-20 03:02:15 +02:00
164e071e7f Update README 2020-04-20 02:15:57 +02:00
01914f070d Turn modules off when installing go-bindata on travis 2020-04-20 02:07:12 +02:00
00e40dc153 Update go.mod and modules.txt format 2020-04-20 01:38:06 +02:00
47efab2e56 Update .travis.yml 2020-04-20 01:27:25 +02:00
c171a620e0 Merge pull request #58 from daftaupe/configpath
Allow dispatch to store data and configuration separately
2020-04-19 20:30:34 +02:00
ca81475fa5 Add option --config and --data to specify where to store the configuration and the data 2020-03-20 18:24:17 +01:00
52b2b6677f Merge pull request #50 from khlieng/dependabot/npm_and_yarn/client/eslint-utils-1.4.3
Bump eslint-utils from 1.3.1 to 1.4.3 in /client
2019-10-31 22:37:03 +01:00
5013ab6db1 Bump eslint-utils from 1.3.1 to 1.4.3 in /client
Bumps [eslint-utils](https://github.com/mysticatea/eslint-utils) from 1.3.1 to 1.4.3.
- [Release notes](https://github.com/mysticatea/eslint-utils/releases)
- [Commits](https://github.com/mysticatea/eslint-utils/compare/v1.3.1...v1.4.3)

Signed-off-by: dependabot[bot] <support@github.com>
2019-10-31 10:55:29 +00:00
855f4d3e64 Stop using pointers for nested config structs, closes #41 2019-06-09 02:29:35 +02:00
540efa03c4 Update dependencies 2019-06-09 02:01:48 +02:00
7ad76273c0 Fix MessageBox scroll stutter when state updates while close to the bottom 2019-02-08 09:28:55 +01:00
c1e1f2c327 Update dependencies 2019-02-08 09:10:06 +01:00
4eda7ef396 Fix getSortedChannels test 2019-02-08 08:56:21 +01:00
fad2e030d4 Fix h2 push hash check 2019-02-01 06:39:38 +01:00
3e90e6c86d Only count joined channels 2019-01-30 04:48:37 +01:00
71bfe92dae Update dependencies 2019-01-30 03:51:13 +01:00
613d9fca6e Send irc features to the client 2019-01-27 08:53:07 +01:00
9267c661dc Timeout channel list updates 2019-01-25 11:24:57 +01:00
f8e12f5938 Handle channel forwarding and errors better 2019-01-25 11:02:31 +01:00
497934888c Fix channel reducer tests 2019-01-25 08:36:21 +01:00
24960f23b9 Add topic modal 2019-01-25 03:57:58 +01:00
aab1ad3e99 Fix h2 push 2019-01-25 02:32:22 +01:00
815b518c2c Update dependencies 2019-01-23 08:52:17 +01:00
5e674254f0 Use pointer receiver in stateData 2019-01-23 08:20:16 +01:00
24b26aa85f Add channel joining UI, closes #37 2019-01-23 07:34:39 +01:00
f25594e962 Add casefolding to irc lib 2019-01-13 05:10:11 +01:00
075e404079 Reset backoff on RPL_WELCOME 2019-01-11 05:00:15 +01:00
eee260f154 Read lines with a bufio.Scanner, reuse input buffer, ignore empty messages, handle multiple spaces between tags and prefix 2019-01-11 04:53:50 +01:00
a3618b97ae Add list command 2019-01-11 02:46:46 +01:00
e4d5d2737b Use strings.Replacer to unescape tags 2019-01-11 02:19:57 +01:00
0085cea5a1 Add react-modal, replace confirm usage with it 2019-01-05 07:08:34 +01:00
63cf65100d Pull https handling out into a new package 2018-12-31 03:33:05 +01:00
67e32661f1 Update dependencies 2018-12-31 02:20:22 +01:00
95eff71e2e Add go 1.12beta1 travis build 2018-12-21 01:53:35 +01:00
8526805c2f Add headers config, closes #25 2018-12-20 11:51:31 +01:00
6aaa2b521d Avoid sending join when the channels input is empty 2018-12-19 03:09:13 +01:00
0d9290d037 Update client dependencies 2018-12-19 02:55:50 +01:00
6fedb23363 Prerender index page 2018-12-17 14:44:46 +01:00
fc643483be Dont redirect private IPs and localhost 2018-12-17 12:45:33 +01:00
6c3a5777c4 Use certmagic, simplify config, set HTTP timeouts and a modern TLSConfig 2018-12-16 12:32:03 +01:00
c5a9a5b1c1 Print go version 2018-12-15 11:30:29 +01:00
6a816fbff6 Add date marker tests 2018-12-15 11:20:49 +01:00
3c105c493b Handle messages with no content, improve prepend perf 2018-12-15 11:09:57 +01:00
50d735aaa3 Add date markers 2018-12-14 14:24:23 +01:00
34d89c75b2 Use correct gulp version on travis 2018-12-11 11:17:05 +01:00
71f79fd84e Pass in config struct 2018-12-11 10:54:05 +01:00
8f1105bc59 Only attempt to maintain scroll position when prepending messages if there was previously any messages 2018-12-08 11:56:02 +01:00
0e46fbcc82 Clean up initialState module 2018-12-08 11:25:08 +01:00
c1ca29511e Simplify routing logic 2018-12-08 11:08:01 +01:00
35c2d682e3 Improve routing 2018-12-07 08:48:40 +01:00
aca380629f Update dotfiles 2018-12-06 11:05:10 +01:00
007fc80e72 Update dependencies 2018-12-06 11:03:03 +01:00
cca3f5bc93 Use stable readme download links 2018-12-02 11:31:35 +01:00
f05e405fb1 Embed version info in docker build 2018-12-02 07:39:23 +01:00
73205b64f6 v0.5.4 2018-11-29 13:07:27 +01:00
5861a54dfc Improve 404 handling 2018-11-29 13:06:37 +01:00
869713d236 Fix initial scroll position sometimes being off 2018-11-29 12:05:42 +01:00
0438a099cf Fix dev mode, turn off react concurrent mode, update dependencies 2018-11-29 11:54:05 +01:00
df71c54d37 Use a map to serve files 2018-11-27 12:07:48 +01:00
d24d33d94c Fix push cookie hash check 2018-11-27 11:34:02 +01:00
1382 changed files with 200272 additions and 119456 deletions

View File

@ -1,3 +1,5 @@
.git
dist
dispatch
client/dist
client/node_modules
client/node_modules
client/yarn-error.log

1
.gitignore vendored
View File

@ -3,4 +3,3 @@ dispatch
client/dist
client/node_modules
client/yarn-error.log
ca-certificates.crt

View File

@ -19,17 +19,17 @@ builds:
- 6
- 7
archive:
files:
- none*
archives:
- files:
- none*
format_overrides:
- goos: windows
format: zip
format_overrides:
- goos: windows
format: zip
replacements:
amd64: x64
darwin: mac
replacements:
amd64: x64
darwin: mac
checksum:
name_template: "checksums.txt"
@ -37,7 +37,6 @@ checksum:
changelog:
filters:
exclude:
- ^v.*
- "(?i)^update.*dep"
- Merge pull request
- Merge branch

View File

@ -13,12 +13,12 @@ matrix:
- go: tip
install:
- go get github.com/jteeuwen/go-bindata/...
- GO111MODULE=off go get github.com/jteeuwen/go-bindata/...
- cd client
- nvm install 11.2.0
- nvm use 11.2.0
- nvm install 12.4.0
- nvm use 12.4.0
- npm install -g yarn
- yarn global add gulp@next
- yarn global add gulp
- yarn
script:

View File

@ -6,7 +6,7 @@ RUN apk add --update git make build-base && \
WORKDIR /go/src/github.com/khlieng/dispatch
COPY . /go/src/github.com/khlieng/dispatch
RUN go build .
RUN chmod +x install.sh && ./install.sh
# Runtime
FROM alpine
@ -14,7 +14,7 @@ FROM alpine
RUN apk add --update ca-certificates && \
rm -rf /var/cache/apk/*
COPY --from=build /go/src/github.com/khlieng/dispatch/dispatch /dispatch
COPY --from=build /go/bin/dispatch /dispatch
EXPOSE 80/tcp

View File

@ -18,9 +18,9 @@ There is a few different ways of getting it:
### 1. Binary
- **[Windows (x64)](https://github.com/khlieng/dispatch/releases/download/v0.5.3/dispatch_0.5.3_windows_x64.zip)**
- **[macOS (x64)](https://github.com/khlieng/dispatch/releases/download/v0.5.3/dispatch_0.5.3_mac_x64.tar.gz)**
- **[Linux (x64)](https://github.com/khlieng/dispatch/releases/download/v0.5.3/dispatch_0.5.3_linux_x64.tar.gz)**
- **[Windows (x64)](https://release.khlieng.com/khlieng/dispatch/windows_x64)**
- **[macOS (x64)](https://release.khlieng.com/khlieng/dispatch/mac_x64)**
- **[Linux (x64)](https://release.khlieng.com/khlieng/dispatch/linux_x64)**
- [Other versions](https://github.com/khlieng/dispatch/releases)
### 2. Go
@ -51,7 +51,6 @@ docker run -p <http port>:80 -p <https port>:443 -v <path>:/data khlieng/dispatc
### Server
```bash
cd $GOPATH/src/github.com/khlieng/dispatch
go install
```
@ -62,9 +61,9 @@ This requires [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com).
Fetch the dependencies:
```bash
go get github.com/jteeuwen/go-bindata/...
yarn global add gulp@next
cd $GOPATH/src/github.com/khlieng/dispatch/client
GO111MODULE=off go get github.com/jteeuwen/go-bindata/...
yarn global add gulp-cli
cd client
yarn
```
@ -94,11 +93,11 @@ The libraries this project is built with.
### Server
- [Bolt](https://github.com/boltdb/bolt)
- [Bolt](https://github.com/etcd-io/bbolt)
- [Bleve](https://github.com/blevesearch/bleve)
- [Cobra](https://github.com/spf13/cobra)
- [Viper](https://github.com/spf13/viper)
- [Lego](https://github.com/xenolf/lego)
- [CertMagic](https://github.com/mholt/certmagic)
### Client

File diff suppressed because one or more lines are too long

View File

@ -13,9 +13,13 @@
"no-param-reassign": 0,
"no-plusplus": 0,
"no-restricted-globals": 1,
"no-underscore-dangle": 1,
"react/destructuring-assignment": 0,
"react/jsx-filename-extension": 0,
"react/prop-types": 0
"react/jsx-props-no-spreading": 0,
"react/prop-types": 0,
"react/state-in-constructor": 0,
"react/static-property-placement": 0
},
"settings": {
"import/resolver": {

View File

@ -1,3 +1,5 @@
{
"singleQuote": true
"singleQuote": true,
"trailingComma": "none",
"arrowParens": "avoid"
}

View File

@ -13,12 +13,10 @@ module.exports = {
['@babel/plugin-proposal-class-properties', { loose: true }],
'@babel/plugin-proposal-export-default-from',
'@babel/plugin-proposal-export-namespace-from',
'@babel/plugin-syntax-dynamic-import'
'@babel/plugin-syntax-dynamic-import',
'react-hot-loader/babel'
],
env: {
development: {
plugins: ['react-hot-loader/babel']
},
test: {
plugins: ['@babel/plugin-transform-modules-commonjs']
},

View File

@ -1,62 +0,0 @@
@font-face {
font-family: 'fontello';
src: url('/font/fontello.woff2?48901973') format('woff2'),
url('/font/fontello.woff?48901973') format('woff');
font-weight: normal;
font-style: normal;
}
[class^='icon-']:before,
[class*=' icon-']:before {
font-family: 'fontello';
font-style: normal;
font-weight: normal;
speak: none;
display: inline-block;
text-decoration: inherit;
width: 1em;
margin-right: 0.2em;
text-align: center;
/* opacity: .8; */
/* For safety - reset parent styles, that can break glyph codes*/
font-variant: normal;
text-transform: none;
/* fix buttons height, for twitter bootstrap */
line-height: 1em;
/* Animation center compensation - margins should be symmetric */
/* remove if not needed */
margin-left: 0.2em;
/* you can be more comfortable with increased icons size */
/* font-size: 120%; */
/* Font smoothing. That was taken from TWBS */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Uncomment for 3D effect */
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}
.icon-cancel:before {
content: '\e800';
} /* '' */
.icon-menu:before {
content: '\e801';
} /* '' */
.icon-cog:before {
content: '\e802';
} /* '' */
.icon-search:before {
content: '\e803';
} /* '' */
.icon-user:before {
content: '\f061';
} /* '' */
.icon-ellipsis:before {
content: '\f141';
} /* '' */

View File

@ -19,6 +19,15 @@ h6 {
font-family: Montserrat, sans-serif;
}
a {
text-decoration: none;
color: #0066ff;
}
a:hover {
text-decoration: underline;
}
input {
font: 16px Roboto Mono, monospace;
border: none;
@ -38,6 +47,9 @@ textarea {
}
button {
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
height: 50px;
background: #6bb758;
@ -57,6 +69,33 @@ button:active {
background: #6bb758;
}
.button-normal {
background: #222;
}
.button-normal:hover {
background: #111;
}
.button-normal:active {
background: #222;
}
.icon-button {
background: none;
width: 40px;
color: #222;
font-size: 20px;
}
.icon-button:hover {
background: none;
}
.icon-button:active {
background: none;
}
label {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
@ -128,6 +167,10 @@ i[class*=' icon-']:before {
color: #f6546a !important;
}
.disabled {
color: #999 !important;
}
.textinput {
display: block;
position: relative;
@ -225,7 +268,8 @@ i[class*=' icon-']:before {
top: 0;
bottom: 50px;
width: 100%;
overflow: auto;
overflow-x: hidden;
overflow-y: auto;
}
.tablist p {
@ -233,6 +277,9 @@ i[class*=' icon-']:before {
padding: 3px 15px;
padding-right: 10px;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tablist p:last-child {
@ -248,12 +295,6 @@ i[class*=' icon-']:before {
border-left: 5px solid #6bb758;
}
.tab-content {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tab-server {
display: flex;
align-items: center;
@ -267,11 +308,29 @@ i[class*=' icon-']:before {
}
.tab-label {
margin-top: 10px;
margin: 5px;
margin-left: 15px;
margin-bottom: 5px;
font-size: 12px;
color: #999;
display: flex;
align-items: center;
height: 25px;
}
.tab-label span {
flex: 1;
}
.tab-label button {
width: 24px;
height: 100%;
font-size: 20px;
background: none;
color: #999;
}
.tab-label button:hover {
color: #ccc;
}
.side-buttons {
@ -284,20 +343,13 @@ i[class*=' icon-']:before {
border-top: 1px solid #1d1d1d;
}
.side-buttons i {
flex: 100%;
color: #999;
line-height: 50px;
cursor: pointer;
font-size: 18px;
border-left: 1px solid #1d1d1d;
}
.side-buttons button {
font-size: 24px;
background: #222;
color: #999;
flex: 1;
}
.side-buttons i:hover {
.side-buttons button:hover {
color: #ccc;
background: #1d1d1d;
}
@ -331,7 +383,7 @@ i[class*=' icon-']:before {
.connect-form {
margin: auto 20px;
padding-top: 20px;
padding: 20px 0;
width: 350px;
text-align: center;
}
@ -368,11 +420,6 @@ input::-webkit-inner-spin-button {
.connect-form label {
user-select: none;
cursor: default;
}
.connect-form button {
margin-bottom: 20px;
}
.connect-form-address {
@ -401,16 +448,14 @@ input::-webkit-inner-spin-button {
color: #777;
}
.connect-form i {
display: block;
cursor: pointer;
color: #999;
text-align: center;
.connect-form-button-optionals {
font-size: 24px;
padding: 5px 0;
color: #999;
height: 40px;
width: 100%;
}
.connect-form i:hover {
.connect-form-button-optionals:hover {
color: #666;
}
@ -425,6 +470,7 @@ input::-webkit-inner-spin-button {
border-bottom: 1px solid #ddd;
display: flex;
font-size: 20px;
padding-right: 5px;
}
.chat-channel .chat-title-bar {
@ -433,21 +479,10 @@ input::-webkit-inner-spin-button {
.navicon {
display: none;
padding: 0 15px;
line-height: 50px;
font-size: 20px;
width: 50px;
cursor: pointer;
}
.chat-title-bar i {
padding: 0 15px;
cursor: pointer;
}
.chat-server .icon-search {
display: none;
}
.chat-server .userlist,
.chat-private .userlist {
display: none;
@ -458,17 +493,16 @@ input::-webkit-inner-spin-button {
display: none;
}
.button-leave {
border-left: 1px solid #ddd;
.chat-title-bar .icon-button:not(.navicon) {
color: #999;
}
.button-leave:hover {
background: #ddd;
.chat-title-bar .icon-button:hover {
color: #222;
}
.button-userlist {
display: none;
border-left: 1px solid #ddd;
}
.chat-server .button-userlist,
@ -476,20 +510,24 @@ input::-webkit-inner-spin-button {
display: none;
}
.editable-wrap {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.editable-wrap-editable {
cursor: pointer;
}
.chat-title {
margin-left: 10px;
padding: 0 5px;
margin-left: 15px;
font: 24px Montserrat, sans-serif;
font-weight: 700;
color: #222;
white-space: nowrap;
line-height: 50px;
}
.chat-server .chat-title {
cursor: pointer;
}
input.chat-title {
background: none;
cursor: text !important;
@ -498,7 +536,8 @@ input.chat-title {
.chat-topic-wrap {
flex: 1;
position: relative;
margin: 0 15px;
margin-left: 15px;
margin-right: 5px;
}
.chat-topic {
@ -507,21 +546,16 @@ input.chat-title {
top: 3px;
font-size: 16px;
color: #999;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-topic a {
color: #999;
text-decoration: none;
}
.chat-topic a:hover {
text-decoration: underline;
}
.userlist-bar {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
right: 0;
@ -529,14 +563,11 @@ input.chat-title {
height: 50px;
border-left: 1px solid #ddd;
border-bottom: 1px solid #ddd;
line-height: 50px;
text-align: center;
padding: 0 15px;
font-family: Montserrat, sans-serif;
}
.userlist-bar i {
margin-right: 3px;
.userlist-bar svg {
margin-right: 5px;
}
.search {
@ -565,17 +596,19 @@ input.chat-title {
border-bottom: 1px solid #ddd;
}
.search i {
padding: 15px;
color: #ddd;
}
.search-input {
flex: 1;
padding: 15px;
padding-left: 0;
}
.search-input-icon {
font-size: 20px;
align-self: center;
margin: 0 15px;
color: #ddd;
}
.search-results {
position: absolute;
top: 50px;
@ -595,8 +628,6 @@ input.chat-title {
top: 50px;
bottom: 50px;
right: 0;
z-index: 1;
overflow: hidden;
}
.chat-channel .messagebox {
@ -615,6 +646,24 @@ input.chat-title {
overflow-y: scroll !important;
}
.messagebox-topdate-container {
position: absolute;
text-align: center;
left: 0;
height: 0;
}
.messagebox-topdate {
position: relative;
top: -12px;
background: #f0f0f0;
color: #999;
border-radius: 50vh;
padding: 0 5px;
font-size: 12px;
z-index: 2;
}
.message {
padding: 4px 15px;
}
@ -637,6 +686,18 @@ input.chat-title {
color: #ff6698;
}
.message-date {
text-align: center;
color: #999;
font-size: 12px;
margin-top: 12px;
}
.message-date hr {
border: none;
border-bottom: 1px solid #ddd;
}
.message-time {
font-style: normal;
font-weight: 400;
@ -649,15 +710,6 @@ input.chat-title {
cursor: pointer;
}
.message a {
text-decoration: none;
color: #0066ff;
}
.message a:hover {
text-decoration: underline;
}
.message-input-wrap {
position: absolute;
left: 0;
@ -695,7 +747,7 @@ input.message-input-nick.invalid {
flex: 1;
width: 100%;
height: 100%;
padding: 0 15px;
padding: 0 10px;
}
.userlist {
@ -706,7 +758,7 @@ input.message-input-nick.invalid {
width: 200px;
border-left: 1px solid #ddd;
background: #f0f0f0;
z-index: 2;
z-index: 1;
transition: transform 0.2s;
}
@ -754,7 +806,7 @@ input.message-input-nick.invalid {
color: #222;
}
.settings button {
.settings-button {
width: 200px;
}
@ -798,8 +850,8 @@ input.message-input-nick.invalid {
margin-right: 0;
}
.settings-button {
margin-top: 10px;
.settings-file:last-of-type {
margin-bottom: 10px;
}
}
@ -819,14 +871,180 @@ input.message-input-nick.invalid {
}
.suspense-fallback {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
font: 700 64px 'Montserrat', sans-serif;
height: 100%;
color: #ddd;
}
.suspense-modal-fallback {
position: fixed;
right: 15px;
bottom: 3px;
z-index: 1;
font: 700 64px 'Montserrat', sans-serif;
color: #ddd;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.33);
opacity: 0;
transition: opacity 0.2s;
}
.modal-overlay-opening {
opacity: 1;
}
.modal-overlay-closing {
opacity: 0;
}
.modal {
width: 600px;
min-width: 0;
padding: 15px;
background: #f0f0f0;
border: 1px solid #ddd;
outline: none;
margin: 15px;
text-align: center;
font-family: 'Montserrat', sans-serif;
transform: translateY(-20px);
transition: transform 0.2s;
}
.modal-opening {
transform: translateY(0);
}
.modal-closing {
transform: translateY(-20px);
}
.modal p {
margin-bottom: 5px;
}
.modal button {
width: 120px;
}
.modal button {
margin: 0 5px;
margin-top: 10px;
}
.modal-header {
display: flex;
align-items: center;
}
.modal-header h2 {
flex: 1;
}
.modal-close {
color: #999;
cursor: pointer;
width: auto !important;
height: auto;
margin: 0 !important;
}
.modal-close:hover {
color: #222;
}
.modal-content {
margin-top: 15px;
}
.modal-channel {
display: flex;
flex-direction: column;
height: calc(100vh - 120px);
padding: 0;
}
.modal-channel input {
flex: 1;
padding: 15px;
}
.modal-channel-button-join {
margin: 0 !important;
width: 60px !important;
height: 30px;
}
.modal-channel-input-wrap {
display: flex;
}
.modal-channel-close {
background: #fff;
width: 40px !important;
margin: 0 !important;
}
.modal-channel-close:hover {
background: #fff;
}
.modal-channel-result {
margin: 15px;
text-align: left;
}
.modal-channel-result-header {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.modal-channel-topic {
font-size: 12px;
font-family: Roboto Mono, monospace;
color: #444;
}
.modal-channel-name {
cursor: pointer;
margin-right: 15px;
}
.modal-channel-users {
font-size: 16px;
flex: 1;
margin-left: 5px;
}
.modal-channel-results {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
.modal-channel-button-more {
margin-bottom: 15px !important;
}
@media (max-width: 600px) {
.tablist {
width: 200px;
@ -839,7 +1057,7 @@ input.message-input-nick.invalid {
}
.navicon {
display: inline-block;
display: block;
}
.main-container.off-canvas {
@ -854,8 +1072,12 @@ input.message-input-nick.invalid {
margin-left: 0;
}
.chat-topic {
font-size: 12px;
.chat-title-bar .editable-wrap {
flex: 1;
}
.chat-topic-wrap {
display: none;
}
.userlist-bar {
@ -896,4 +1118,15 @@ input.message-input-nick.invalid {
.button-install {
margin-left: 50px;
}
.modal-channel {
margin: 0;
width: auto;
height: auto;
position: fixed;
top: 5px;
left: 5px;
right: 5px;
bottom: 5px;
}
}

View File

@ -12,7 +12,7 @@ var br = require('brotli');
var del = require('del');
function brotli(opts) {
return through.obj(function(file, enc, callback) {
return through.obj(function (file, enc, callback) {
if (file.isNull()) {
return callback(null, file);
}
@ -40,7 +40,7 @@ function js(cb) {
process.env['NODE_ENV'] = 'production';
compiler.run(function(err, stats) {
compiler.run(function (err, stats) {
if (err) throw new gutil.PluginError('webpack', err);
gutil.log(
@ -104,13 +104,13 @@ function serve() {
app.use(
'*',
proxy('localhost:1337', {
proxyReqPathResolver: function(req) {
proxyReqPathResolver: function (req) {
return req.originalUrl;
}
})
);
app.listen(3000, function(err) {
app.listen(3000, function (err) {
if (err) {
console.log(err);
return;
@ -120,9 +120,13 @@ function serve() {
});
}
const assets = gulp.parallel(js, config, public);
const build = gulp.series(clean, assets, compress, cleanup, bindata);
const build = gulp.series(
clean,
gulp.parallel(js, config),
compress,
cleanup,
bindata
);
const dev = gulp.series(
clean,

View File

@ -79,7 +79,9 @@ export default createCommandMiddleware(COMMAND, {
topic({ dispatch, getState, server, channel }, ...newTopic) {
if (newTopic.length > 0) {
dispatch(setTopic(newTopic.join(' '), channel, server));
} else if (channel) {
return;
}
if (channel) {
const { topic } = getState().channels[server][channel];
if (topic) {
return text(topic);

View File

@ -1,9 +1,10 @@
import React, { Suspense, lazy } from 'react';
import React, { Suspense, lazy, useState } from 'react';
import Route from 'containers/Route';
import AppInfo from 'components/AppInfo';
import TabList from 'components/TabList';
import cn from 'classnames';
const Modals = lazy(() => import('components/modals'));
const Chat = lazy(() => import('containers/Chat'));
const Connect = lazy(() => import('containers/Connect'));
const Settings = lazy(() => import('containers/Settings'));
@ -18,8 +19,15 @@ const App = ({
select,
push,
hideMenu,
newVersionAvailable
openModal,
newVersionAvailable,
hasOpenModals
}) => {
const [renderModals, setRenderModals] = useState(false);
if (!renderModals && hasOpenModals) {
setRenderModals(true);
}
const mainClass = cn('main-container', {
'off-canvas': showTabList
});
@ -52,12 +60,10 @@ const App = ({
showTabList={showTabList}
select={select}
push={push}
openModal={openModal}
/>
<div className={mainClass}>
<Suspense
maxDuration={1000}
fallback={<div className="suspense-fallback">...</div>}
>
<Suspense fallback={<div className="suspense-fallback">...</div>}>
<Route name="chat">
<Chat />
</Route>
@ -68,6 +74,11 @@ const App = ({
<Settings />
</Route>
</Suspense>
<Suspense
fallback={<div className="suspense-modal-fallback">...</div>}
>
{renderModals && <Modals />}
</Suspense>
</div>
</div>
</div>

View File

@ -1,12 +1,8 @@
import React from 'react';
import { Provider } from 'react-redux';
import { hot, setConfig } from 'react-hot-loader';
import { hot } from 'react-hot-loader';
import App from 'containers/App';
setConfig({
pureSFC: true
});
const Root = ({ store }) => (
<Provider store={store}>
<App />

View File

@ -1,7 +1,10 @@
import React, { PureComponent } from 'react';
import classnames from 'classnames';
import get from 'lodash/get';
import { FiPlus, FiUser, FiSettings } from 'react-icons/fi';
import Button from 'components/ui/Button';
import TabListItem from './TabListItem';
import TabListItem from 'containers/TabListItem';
import { count } from 'utils';
export default class TabList extends PureComponent {
handleTabClick = (server, target) => this.props.select(server, target);
@ -11,7 +14,14 @@ export default class TabList extends PureComponent {
handleSettingsClick = () => this.props.push('/settings');
render() {
const { tab, channels, servers, privateChats, showTabList } = this.props;
const {
tab,
channels,
servers,
privateChats,
showTabList,
openModal
} = this.props;
const tabs = [];
const className = classnames('tablist', {
@ -32,13 +42,40 @@ export default class TabList extends PureComponent {
/>
);
server.channels.forEach(name =>
const chanCount = count(server.channels, c => c.joined);
const chanLimit =
get(srv.features, ['CHANLIMIT', '#'], 0) || srv.features.MAXCHANNELS;
let chanLabel;
if (chanLimit > 0) {
chanLabel = (
<span>
<span className="success">{chanCount}</span>/{chanLimit}
</span>
);
} else if (chanCount > 0) {
chanLabel = <span className="success">{chanCount}</span>;
}
tabs.push(
<div
key={`${address}-chans}`}
className="tab-label"
onClick={() => openModal('channel', { server: address })}
>
<span>CHANNELS {chanLabel}</span>
<Button title="Join Channel">+</Button>
</div>
);
server.channels.forEach(({ name, joined }) =>
tabs.push(
<TabListItem
key={address + name}
server={address}
target={name}
content={name}
joined={joined}
selected={tab.server === address && tab.name === name}
onClick={this.handleTabClick}
/>
@ -48,7 +85,13 @@ export default class TabList extends PureComponent {
if (privateChats[address] && privateChats[address].length > 0) {
tabs.push(
<div key={`${address}-pm}`} className="tab-label">
Private messages
<span>
DIRECT MESSAGES{' '}
<span style={{ color: '#6bb758' }}>
{privateChats[address].length}
</span>
</span>
{/* <Button>+</Button> */}
</div>
);
@ -71,9 +114,9 @@ export default class TabList extends PureComponent {
<div className={className}>
<div className="tab-container">{tabs}</div>
<div className="side-buttons">
<Button onClick={this.handleConnectClick}>+</Button>
<i className="icon-user" />
<i className="icon-cog" onClick={this.handleSettingsClick} />
<Button icon={FiPlus} onClick={this.handleConnectClick} />
<Button icon={FiUser} />
<Button icon={FiSettings} onClick={this.handleSettingsClick} />
</div>
</div>
);

View File

@ -1,4 +1,4 @@
import React, { memo } from 'react';
import React from 'react';
import classnames from 'classnames';
const TabListItem = ({
@ -7,12 +7,15 @@ const TabListItem = ({
server,
selected,
connected,
joined,
error,
onClick
}) => {
const className = classnames({
'tab-server': !target,
success: !target && connected,
error: !target && !connected,
error: (!target && !connected) || (!joined && error),
disabled: !!target && !error && joined === false,
selected
});
@ -23,4 +26,4 @@ const TabListItem = ({
);
};
export default memo(TabListItem);
export default TabListItem;

View File

@ -0,0 +1,157 @@
import React, { memo, useState, useEffect, useCallback, useRef } from 'react';
import get from 'lodash/get';
import { FiUsers, FiX } from 'react-icons/fi';
import withModal from 'components/modals/withModal';
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';
const Channel = memo(({ server, name, topic, userCount, joined, ...props }) => {
const handleJoinClick = useCallback(() => props.join([name], server), []);
return (
<div className="modal-channel-result">
<div className="modal-channel-result-header">
<h2 className="modal-channel-name" onClick={handleJoinClick}>
{name}
</h2>
<FiUsers />
<span className="modal-channel-users">{userCount}</span>
{joined ? (
<span style={{ color: '#6bb758' }}>Joined</span>
) : (
<Button
className="modal-channel-button-join"
category="normal"
onClick={handleJoinClick}
>
Join
</Button>
)}
</div>
<p className="modal-channel-topic">{linkify(topic)}</p>
</div>
);
});
const AddChannel = ({ search, payload: { server }, onClose, ...props }) => {
const [q, setQ] = useState('');
const inputEl = useRef();
const resultsEl = useRef();
const prevSearch = useRef('');
useEffect(() => {
inputEl.current.focus();
props.searchChannels(server, '');
}, []);
const handleSearch = useCallback(
e => {
let nextQ = e.target.value.trim().toLowerCase();
setQ(nextQ);
if (nextQ !== q) {
resultsEl.current.scrollTop = 0;
while (nextQ.charAt(0) === '#') {
nextQ = nextQ.slice(1);
}
if (nextQ !== prevSearch.current) {
prevSearch.current = nextQ;
props.searchChannels(server, nextQ);
}
}
},
[q]
);
const handleKey = useCallback(e => {
if (e.key === 'Enter') {
let channel = e.target.value.trim();
if (channel !== '') {
onClose(false);
if (channel.charAt(0) !== '#') {
channel = `#${channel}`;
}
props.join([channel], server);
props.select(server, channel);
}
}
}, []);
const handleLoadMore = useCallback(
() => props.searchChannels(server, q, search.results.length),
[q, search.results.length]
);
let hasMore = !search.end;
if (hasMore) {
if (search.results.length < 10) {
hasMore = false;
} else if (
search.results.length > 10 &&
(search.results.length - 10) % 50 !== 0
) {
hasMore = false;
}
}
return (
<>
<div className="modal-channel-input-wrap">
<input
ref={inputEl}
type="text"
value={q}
placeholder="Enter channel name"
onKeyDown={handleKey}
onChange={handleSearch}
/>
<Button
icon={FiX}
className="modal-close modal-channel-close"
onClick={onClose}
/>
</div>
<div ref={resultsEl} className="modal-channel-results">
{search.results.map(channel => (
<Channel
key={`${server} ${channel.name}`}
server={server}
join={props.join}
joined={get(
props.channels,
[server, channel.name, 'joined'],
false
)}
{...channel}
/>
))}
{hasMore && (
<Button
className="modal-channel-button-more"
onClick={handleLoadMore}
>
Load more
</Button>
)}
</div>
</>
);
};
export default withModal({
name: 'channel',
state: {
channels: state => state.channels,
search: state => state.channelSearch
},
actions: { searchChannels, join, select }
})(AddChannel);

View File

@ -0,0 +1,27 @@
import React, { useCallback } from 'react';
import withModal from 'components/modals/withModal';
import Button from 'components/ui/Button';
const Confirm = ({
payload: { question, confirmation, onConfirm },
onClose
}) => {
const handleConfirm = useCallback(() => {
onClose(false);
onConfirm();
}, []);
return (
<>
<p>{question}</p>
<Button onClick={handleConfirm}>{confirmation || 'OK'}</Button>
<Button category="normal" onClick={onClose}>
Cancel
</Button>
</>
);
};
export default withModal({
name: 'confirm'
})(Confirm);

View File

@ -0,0 +1,21 @@
import React from 'react';
import { FiX } from 'react-icons/fi';
import Button from 'components/ui/Button';
import withModal from 'components/modals/withModal';
import { linkify } from 'utils';
const Topic = ({ payload: { topic, channel }, onClose }) => {
return (
<>
<div className="modal-header">
<h2>Topic in {channel}</h2>
<Button icon={FiX} className="modal-close" onClick={onClose} />
</div>
<p className="modal-content">{linkify(topic)}</p>
</>
);
};
export default withModal({
name: 'topic'
})(Topic);

View File

@ -0,0 +1,14 @@
import React, { memo } from 'react';
import AddChannel from 'components/modals/AddChannel';
import Confirm from 'components/modals/Confirm';
import Topic from 'components/modals/Topic';
const Modals = () => (
<>
<AddChannel />
<Confirm />
<Topic />
</>
);
export default memo(Modals, () => true);

View File

@ -0,0 +1,71 @@
import React, { useCallback } from 'react';
import Modal from 'react-modal';
import { createStructuredSelector } from 'reselect';
import get from 'lodash/get';
import { getModals, closeModal } from 'state/modals';
import connect from 'utils/connect';
import { bindActionCreators } from 'redux';
Modal.setAppElement('#root');
export default function withModal({ name, ...modalProps }) {
modalProps = {
className: {
base: `modal modal-${name}`,
afterOpen: 'modal-opening',
beforeClose: 'modal-closing'
},
overlayClassName: {
base: 'modal-overlay',
afterOpen: 'modal-overlay-opening',
beforeClose: 'modal-overlay-closing'
},
closeTimeoutMS: 200,
...modalProps
};
return WrappedComponent => {
const ReduxModal = ({ onRequestClose, ...props }) => {
const handleRequestClose = useCallback(
(dismissed = true) => {
onRequestClose();
if (dismissed && props.payload.onDismiss) {
props.payload.onDismiss();
}
},
[props.payload.onDismiss]
);
return (
<Modal
contentLabel={name}
onRequestClose={handleRequestClose}
{...modalProps}
{...props}
>
<WrappedComponent onClose={handleRequestClose} {...props} />
</Modal>
);
};
const mapState = createStructuredSelector({
isOpen: state => get(getModals(state), [name, 'isOpen'], false),
payload: state => get(getModals(state), [name, 'payload'], {}),
...modalProps.state
});
const mapDispatch = dispatch => {
const actions = { onRequestClose: () => dispatch(closeModal(name)) };
if (modalProps.actions) {
return {
...actions,
...bindActionCreators(modalProps.actions, dispatch)
};
}
return actions;
};
return connect(mapState, mapDispatch)(ReduxModal);
};
}

View File

@ -65,6 +65,7 @@ export default class Chat extends Component {
addFetchedMessages,
fetchMessages,
inputActions,
openModal,
runCommand,
sendMessage,
toggleSearch,
@ -86,6 +87,7 @@ export default class Chat extends Component {
status={status}
tab={tab}
title={title}
openModal={openModal}
onCloseClick={this.handleCloseClick}
onTitleChange={this.handleTitleChange}
onToggleSearch={toggleSearch}
@ -97,6 +99,7 @@ export default class Chat extends Component {
hasMoreMessages={hasMoreMessages}
messages={messages}
tab={tab}
hideTopDate={search.show}
onAddMore={addFetchedMessages}
onFetchMore={fetchMessages}
onNickClick={this.handleNickClick}

View File

@ -1,14 +1,17 @@
import React, { memo } from 'react';
import Navicon from 'containers/Navicon';
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 { isChannel, linkify } from 'utils';
import { isChannel } from 'utils';
const ChatTitle = ({
status,
title,
tab,
channel,
openModal,
onTitleChange,
onToggleSearch,
onToggleUserList,
@ -26,10 +29,7 @@ const ChatTitle = ({
let serverError = null;
if (!tab.name && status.error) {
serverError = (
<span className="chat-topic error">
Error:
{status.error}
</span>
<span className="chat-topic error">Error: {status.error}</span>
);
}
@ -47,24 +47,34 @@ const ChatTitle = ({
<span className="chat-title">{title}</span>
</Editable>
<div className="chat-topic-wrap">
<span className="chat-topic">
{channel && linkify(channel.topic)}
</span>
{channel && channel.topic && (
<span
className="chat-topic"
onClick={() =>
openModal('topic', {
topic: channel.topic,
channel: channel.name
})
}
>
{channel.topic}
</span>
)}
{serverError}
</div>
<i className="icon-search" title="Search" onClick={onToggleSearch} />
<i
className="icon-cancel button-leave"
title={closeTitle}
onClick={onCloseClick}
{tab.name && (
<Button icon={FiSearch} title="Search" onClick={onToggleSearch} />
)}
<Button icon={FiX} title={closeTitle} onClick={onCloseClick} />
<Button
icon={FiUsers}
className="button-userlist"
onClick={onToggleUserList}
/>
<i className="icon-user button-userlist" onClick={onToggleUserList} />
</div>
<div className="userlist-bar">
<i className="icon-user" />
<span className="chat-usercount">
{channel && channel.users.length}
</span>
<FiUsers />
{channel && channel.users.length}
</div>
</div>
);

View File

@ -7,6 +7,15 @@ const Message = ({ message, coloredNick, style, onNickClick }) => {
[`message-${message.type}`]: message.type
});
if (message.type === 'date') {
return (
<div className={className} style={style}>
{message.content}
<hr />
</div>
);
}
style = {
...style,
paddingLeft: `${window.messageIndent + 15}px`,

View File

@ -2,6 +2,7 @@ import React, { PureComponent, createRef } from 'react';
import { VariableSizeList as List } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
import debounce from 'lodash/debounce';
import { formatDate, measureScrollBarWidth } from 'utils';
import { getScrollPos, saveScrollPos } from 'utils/scrollPosition';
import { windowHeight } from 'utils/size';
import Message from './Message';
@ -12,8 +13,24 @@ const fetchThreshold = 600;
// this is done to prevent the scroll from jumping all over the place
const scrollbackDebounce = 150;
const scrollBarWidth = `${measureScrollBarWidth()}px`;
const hasSameLastMessage = (m1, m2) => {
if (m1.length === 0 || m2.length === 0) {
if (m1.length === 0 && m2.length === 0) {
return true;
}
return false;
}
return m1[m1.length - 1].id === m2[m2.length - 1].id;
};
export default class MessageBox extends PureComponent {
state = { topDate: '' };
list = createRef();
outer = createRef();
addMore = debounce(() => {
@ -29,6 +46,8 @@ export default class MessageBox extends PureComponent {
}
componentDidUpdate(prevProps) {
const { messages } = this.props;
if (prevProps.tab !== this.props.tab) {
this.loadScrollPos(true);
}
@ -36,8 +55,11 @@ export default class MessageBox extends PureComponent {
if (this.nextScrollTop > 0) {
this.list.current.scrollTo(this.nextScrollTop);
this.nextScrollTop = 0;
} else if (this.bottom) {
this.list.current.scrollToItem(this.props.messages.length + 1);
} else if (
this.bottom &&
!hasSameLastMessage(messages, prevProps.messages)
) {
this.list.current.scrollToItem(messages.length + 1);
}
}
@ -58,7 +80,7 @@ export default class MessageBox extends PureComponent {
if (prevProps.messages[0] !== this.props.messages[0]) {
const { messages, hasMoreMessages } = this.props;
if (prevProps.tab === this.props.tab) {
if (prevProps.tab === this.props.tab && prevProps.messages.length > 0) {
const addedMessages = messages.length - prevProps.messages.length;
let addedHeight = 0;
for (let i = 0; i < addedMessages; i++) {
@ -87,7 +109,8 @@ export default class MessageBox extends PureComponent {
return 100;
}
return 7;
} else if (index === messages.length + 1) {
}
if (index === messages.length + 1) {
return 7;
}
return messages[index - 1].height;
@ -98,7 +121,8 @@ export default class MessageBox extends PureComponent {
if (index === 0) {
return 'top';
} else if (index === messages.length + 1) {
}
if (index === messages.length + 1) {
return 'bottom';
}
return messages[index - 1].id;
@ -133,7 +157,7 @@ export default class MessageBox extends PureComponent {
const messageBoxHeight = windowHeight() - 100;
if (totalHeight > messageBoxHeight) {
this.initialScrollTop = totalHeight - messageBoxHeight;
this.initialScrollTop = totalHeight;
}
}
}
@ -177,6 +201,17 @@ export default class MessageBox extends PureComponent {
this.bottom = scrollOffset + clientHeight >= scrollHeight - 20;
};
handleItemsRendered = ({ visibleStartIndex }) => {
const startIndex = visibleStartIndex === 0 ? 0 : visibleStartIndex - 1;
const firstVisibleMessage = this.props.messages[startIndex];
if (firstVisibleMessage && firstVisibleMessage.date) {
this.setState({ topDate: formatDate(firstVisibleMessage.date) });
} else {
this.setState({ topDate: '' });
}
};
handleMouseDown = () => {
this.mouseDown = true;
};
@ -203,7 +238,8 @@ export default class MessageBox extends PureComponent {
);
}
return null;
} else if (index === messages.length + 1) {
}
if (index === messages.length + 1) {
return null;
}
@ -221,12 +257,27 @@ export default class MessageBox extends PureComponent {
};
render() {
const { messages, hideTopDate } = this.props;
const { topDate } = this.state;
const dateContainerStyle = {
right: scrollBarWidth
};
return (
<div
className="messagebox"
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
>
<div
className="messagebox-topdate-container"
style={dateContainerStyle}
>
{!hideTopDate && topDate && (
<span className="messagebox-topdate">{topDate}</span>
)}
</div>
<AutoSizer>
{({ width, height }) => (
<List
@ -234,12 +285,13 @@ export default class MessageBox extends PureComponent {
outerRef={this.outer}
width={width}
height={height}
itemCount={this.props.messages.length + 2}
itemCount={messages.length + 2}
itemKey={this.getItemKey}
itemSize={this.getRowHeight}
estimatedItemSize={32}
initialScrollOffset={this.initialScrollTop}
onScroll={this.handleScroll}
onItemsRendered={this.handleItemsRendered}
className="messagebox-window"
overscanCount={5}
>

View File

@ -1,17 +1,15 @@
import React, { memo, useRef, useEffect } from 'react';
import { FiSearch } from 'react-icons/fi';
import SearchResult from './SearchResult';
const Search = ({ search, onSearch }) => {
const inputEl = useRef();
useEffect(
() => {
if (search.show) {
inputEl.current.focus();
}
},
[search.show]
);
useEffect(() => {
if (search.show) {
inputEl.current.focus();
}
}, [search.show]);
const style = {
display: search.show ? 'block' : 'none'
@ -25,7 +23,7 @@ const Search = ({ search, onSearch }) => {
return (
<div className="search" style={style}>
<div className="search-input-wrap">
<i className="icon-search" />
<FiSearch className="search-input-icon" />
<input
ref={inputEl}
className="search-input"

View File

@ -28,7 +28,8 @@ export default class UserList extends PureComponent {
if (index === 0) {
return 12;
} else if (index === users.length + 1) {
}
if (index === users.length + 1) {
return 10;
}
return 24;
@ -39,7 +40,8 @@ export default class UserList extends PureComponent {
if (index === 0) {
return 'top';
} else if (index === users.length + 1) {
}
if (index === users.length + 1) {
return 'bottom';
}
return index;

View File

@ -1,12 +1,13 @@
import React, { Component } from 'react';
import { createSelector } from 'reselect';
import { Form, withFormik } from 'formik';
import Navicon from 'containers/Navicon';
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,
@ -37,12 +38,39 @@ class Connect extends Component {
return (
<div>
{!hexIP && <TextInput name="username" />}
<TextInput name="password" type="password" />
<TextInput name="realname" />
<TextInput name="password" type="password" noTrim />
<TextInput name="realname" noTrim />
</div>
);
};
transformPort = port => {
if (!port) {
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;
@ -70,10 +98,15 @@ class Connect extends Component {
form = (
<Form className="connect-form">
<h1>Connect</h1>
<TextInput name="name" autoCapitalize="words" />
<TextInput name="name" autoCapitalize="words" noTrim />
<div className="connect-form-address">
<TextInput name="host" noError />
<TextInput name="port" type="number" noError />
<TextInput
name="port"
type="number"
blurTransform={this.transformPort}
noError
/>
<Checkbox
name="tls"
label="SSL"
@ -84,9 +117,13 @@ class Connect extends Component {
<Error name="host" />
<Error name="port" />
<TextInput name="nick" />
<TextInput name="channels" />
<TextInput name="channels" transform={this.transformChannels} />
{this.state.showOptionals && this.renderOptionals()}
<i className="icon-ellipsis" onClick={this.handleShowClick} />
<Button
className="connect-form-button-optionals"
icon={FiMoreHorizontal}
onClick={this.handleShowClick}
/>
<Button type="submit">Connect</Button>
</Form>
);
@ -120,16 +157,10 @@ export default withFormik({
username: '',
password: defaults.password ? ' ' : '',
realname: '',
tls: defaults.ssl
tls: defaults.ssl || false
};
},
validate: values => {
Object.keys(values).forEach(k => {
if (typeof values[k] === 'string') {
values[k] = values[k].trim();
}
});
const errors = {};
if (!values.host) {
@ -138,9 +169,7 @@ export default withFormik({
errors.host = 'Invalid host';
}
if (!values.port) {
values.port = values.tls ? 6697 : 6667;
} else if (!isInt(values.port, 1, 65535)) {
if (!isInt(values.port, 1, 65535)) {
errors.port = 'Invalid port';
}
@ -154,29 +183,24 @@ export default withFormik({
errors.username = 'Invalid username';
}
values.channels = values.channels
.split(',')
.map(channel => {
channel = channel.trim();
if (channel) {
if (isValidChannel(channel, false)) {
if (channel[0] !== '#') {
channel = `#${channel}`;
}
} else {
errors.channels = 'Invalid channel(s)';
}
}
return channel;
})
.filter(s => s)
.join(',');
const channels = values.channels.split(',');
for (let i = channels.length - 1; i >= 0; i--) {
if (i === channels.length - 1 && channels[i] === '') {
/* eslint-disable-next-line no-continue */
continue;
}
if (!isValidChannel(channels[i])) {
errors.channels = 'Invalid channel(s)';
break;
}
}
return errors;
},
handleSubmit: (values, { props }) => {
const { connect, select, join } = props;
const channels = values.channels.split(',');
const channels = values.channels ? values.channels.split(',') : [];
delete values.channels;
values.port = `${values.port}`;

View File

@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import Navicon from 'containers/Navicon';
import Navicon from 'components/ui/Navicon';
import Button from 'components/ui/Button';
import Checkbox from 'components/ui/Checkbox';
import FileInput from 'components/ui/FileInput';
@ -17,14 +17,11 @@ const Settings = ({
const status = settings.uploadingCert ? 'Uploading...' : 'Upload';
const error = settings.certError;
const handleInstallClick = useCallback(
async () => {
installable.prompt();
await installable.userChoice;
onInstall();
},
[installable]
);
const handleInstallClick = useCallback(async () => {
installable.prompt();
await installable.userChoice;
onInstall();
}, [installable]);
return (
<div className="settings-container">
@ -32,7 +29,10 @@ const Settings = ({
<Navicon />
<h1>Settings</h1>
{installable && (
<Button className="button-install" onClick={handleInstallClick}>
<Button
className="settings-button button-install"
onClick={handleInstallClick}
>
<h2>Install</h2>
</Button>
)}

View File

@ -1,7 +1,19 @@
import React from 'react';
import cn from 'classnames';
const Button = ({ children, ...props }) => (
<button type="button" {...props}>
const Button = ({ children, category, className, icon: Icon, ...props }) => (
<button
className={cn(
{
[`button-${category}`]: category,
'icon-button': Icon && !children
},
className
)}
type="button"
{...props}
>
{Icon && <Icon />}
{children}
</button>
);

View File

@ -1,4 +1,5 @@
import React, { PureComponent, createRef } from 'react';
import cn from 'classnames';
import { stringWidth } from 'utils';
export default class Editable extends PureComponent {
@ -17,11 +18,7 @@ export default class Editable extends PureComponent {
// eslint-disable-next-line react/no-did-update-set-state
this.updateInputWidth(this.props.value);
this.inputEl.current.focus();
}
}
getSnapshotBeforeUpdate(prevProps) {
if (this.state.editing && prevProps.value !== this.props.value) {
} else if (this.state.editing && prevProps.value !== this.props.value) {
this.updateInputWidth(this.props.value);
}
}
@ -79,7 +76,7 @@ export default class Editable extends PureComponent {
};
render() {
const { children, className, value } = this.props;
const { children, className, editable, value } = this.props;
const style = {
width: this.state.width,
@ -90,7 +87,7 @@ export default class Editable extends PureComponent {
return this.state.editing ? (
<input
ref={this.inputEl}
className={className}
className={`editable-wrap ${className}`}
type="text"
value={value}
onBlur={this.handleBlur}
@ -101,7 +98,14 @@ export default class Editable extends PureComponent {
spellCheck={false}
/>
) : (
<div onClick={this.startEditing}>{children}</div>
<div
className={cn('editable-wrap', {
'editable-wrap-editable': editable
})}
onClick={this.startEditing}
>
{children}
</div>
);
}
}

View File

@ -1,7 +1,19 @@
import React from 'react';
import { FiMenu } from 'react-icons/fi';
import { useDispatch } from 'react-redux';
import Button from 'components/ui/Button';
import { toggleMenu } from 'state/ui';
const Navicon = ({ onClick }) => (
<i className="icon-menu navicon" onClick={onClick} />
);
const Navicon = () => {
const dispatch = useDispatch();
return (
<Button
className="navicon"
icon={FiMenu}
onClick={() => dispatch(toggleMenu())}
/>
);
};
export default Navicon;

View File

@ -4,6 +4,24 @@ 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);
@ -38,49 +56,83 @@ export default class TextInput extends PureComponent {
};
render() {
const { name, label = capitalize(name), noError, ...props } = this.props;
const {
name,
label = capitalize(name),
noError,
noTrim,
transform,
blurTransform,
...props
} = this.props;
return (
<FastField
name={name}
render={({ field, form }) => {
return (
<>
<div className="textinput">
<input
className={field.value && 'value'}
type="text"
name={name}
autoCapitalize="off"
autoCorrect="off"
autoComplete="off"
spellCheck="false"
ref={this.input}
onFocus={this.handleFocus}
{...field}
{...props}
/>
<span
className={classnames('textinput-1', {
value: field.value,
error: form.touched[name] && form.errors[name]
})}
>
{label}
</span>
<span
className={classnames('textinput-2', {
value: field.value,
error: form.touched[name] && form.errors[name]
})}
>
{label}
</span>
</div>
{!noError && <Error name={name} />}
</>
);
}}
render={({ field, form }) => (
<>
<div className="textinput">
<input
className={field.value && 'value'}
type="text"
name={name}
autoCapitalize="off"
autoCorrect="off"
autoComplete="off"
spellCheck="false"
ref={this.input}
onFocus={this.handleFocus}
{...field}
{...props}
onChange={e => {
let v = getValue(e, !noTrim);
if (transform) {
v = transform(v);
}
if (v !== field.value) {
form.setFieldValue(name, v);
if (props.onChange) {
props.onChange(e);
}
}
}}
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);
}
}}
/>
<span
className={classnames('textinput-1', {
value: field.value,
error: form.touched[name] && form.errors[name]
})}
>
{label}
</span>
<span
className={classnames('textinput-2', {
value: field.value,
error: form.touched[name] && form.errors[name]
})}
>
{label}
</span>
</div>
{!noError && <Error name={name} />}
</>
)}
/>
);
}

View File

@ -5,22 +5,19 @@ import Checkbox from 'components/ui/Checkbox';
const FormikCheckbox = ({ name, onChange, ...props }) => (
<FastField
name={name}
render={({ field, form }) => {
return (
<Checkbox
name={name}
checked={field.value}
onChange={e => {
form.setFieldTouched(name, true);
field.onChange(e);
if (onChange) {
onChange(e);
}
}}
{...props}
/>
);
}}
render={({ field }) => (
<Checkbox
name={name}
checked={field.value}
onChange={e => {
field.onChange(e);
if (onChange) {
onChange(e);
}
}}
{...props}
/>
)}
/>
);

View File

@ -2,6 +2,7 @@ import { createStructuredSelector } from 'reselect';
import App from 'components/App';
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 { getSelectedTab, select } from 'state/tab';
@ -16,12 +17,10 @@ const mapState = createStructuredSelector({
servers: getServers,
showTabList: getShowTabList,
tab: getSelectedTab,
newVersionAvailable: state => state.app.newVersionAvailable
newVersionAvailable: state => state.app.newVersionAvailable,
hasOpenModals: getHasOpenModals
});
const mapDispatch = { push, select, hideMenu };
const mapDispatch = { push, select, hideMenu, openModal };
export default connect(
mapState,
mapDispatch
)(App);
export default connect(mapState, mapDispatch)(App);

View File

@ -22,6 +22,7 @@ import {
fetchMessages,
addFetchedMessages
} from 'state/messages';
import { openModal } from 'state/modals';
import { openPrivateChat, closePrivateChat } from 'state/privateChats';
import { getSearch, searchMessages, toggleSearch } from 'state/search';
import {
@ -58,6 +59,7 @@ const mapDispatch = dispatch => ({
closePrivateChat,
disconnect,
fetchMessages,
openModal,
openPrivateChat,
part,
runCommand,
@ -83,7 +85,4 @@ const mapDispatch = dispatch => ({
)
});
export default connect(
mapState,
mapDispatch
)(Chat);
export default connect(mapState, mapDispatch)(Chat);

View File

@ -17,7 +17,4 @@ const mapDispatch = {
select
};
export default connect(
mapState,
mapDispatch
)(Connect);
export default connect(mapState, mapDispatch)(Connect);

View File

@ -1,12 +0,0 @@
import Navicon from 'components/ui/Navicon';
import { toggleMenu } from 'state/ui';
import connect from 'utils/connect';
const mapDispatch = {
onClick: toggleMenu
};
export default connect(
null,
mapDispatch
)(Navicon);

View File

@ -24,7 +24,4 @@ const mapDispatch = {
onInstall: () => appSet('installable', null)
};
export default connect(
mapState,
mapDispatch
)(Settings);
export default connect(mapState, mapDispatch)(Settings);

View File

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

8
client/js/hot.js Normal file
View File

@ -0,0 +1,8 @@
import { setConfig } from 'react-hot-loader';
import ReactDOM from 'react-dom';
setConfig({
ignoreSFC: !!ReactDOM.setHotElementComparator,
pureSFC: true,
pureRender: true
});

View File

@ -1,5 +1,6 @@
import './hot';
import React from 'react';
import { createRoot } from 'react-dom';
import { render } from 'react-dom';
import Root from 'components/Root';
import { appSet } from 'state/app';
@ -10,7 +11,6 @@ import routes from './routes';
import runModules from './modules';
import { register } from './serviceWorker';
import '../css/fonts.css';
import '../css/fontello.css';
import '../css/style.css';
const production = process.env.NODE_ENV === 'production';
@ -23,7 +23,7 @@ const store = configureStore(socket);
initRouter(routes, store);
runModules({ store, socket });
createRoot(document.getElementById('root')).render(<Root store={store} />);
render(<Root store={store} />, document.getElementById('root'));
window.addEventListener('beforeinstallprompt', e => {
e.preventDefault();

View File

@ -1,6 +1,7 @@
import documentTitle from './documentTitle';
import fonts from './fonts';
import initialState from './initialState';
import route from './route';
import socket from './socket';
import storage from './storage';
import widthUpdates from './widthUpdates';
@ -8,6 +9,7 @@ import widthUpdates from './widthUpdates';
export default function runModules(ctx) {
fonts(ctx);
initialState(ctx);
route(ctx);
documentTitle(ctx);
socket(ctx);

View File

@ -1,22 +1,10 @@
/* eslint-disable no-underscore-dangle */
import Cookie from 'js-cookie';
import { socket as socketActions } from 'state/actions';
import { getWrapWidth, setConnectDefaults, appSet } from 'state/app';
import { getWrapWidth, appSet } from 'state/app';
import { addMessages } from 'state/messages';
import { setSettings } from 'state/settings';
import { select, updateSelection } from 'state/tab';
import { find } from 'utils';
import { when } from 'utils/observe';
import { replace } from 'utils/router';
function loadState({ store }, env) {
store.dispatch(setConnectDefaults(env.defaults));
store.dispatch(
appSet({
hexIP: env.hexIP,
version: env.version
})
);
store.dispatch(setSettings(env.settings, true));
if (env.servers) {
@ -24,31 +12,6 @@ function loadState({ store }, env) {
type: socketActions.SERVERS,
data: env.servers
});
if (!store.getState().router.route) {
const tab = Cookie.get('tab');
if (tab) {
const [server, name = null] = tab.split(/;(.+)/);
if (
name &&
find(
env.channels,
chan => chan.server === server && chan.name === name
)
) {
store.dispatch(select(server, name, true));
} else if (find(env.servers, srv => srv.host === server)) {
store.dispatch(select(server, null, true));
} else {
store.dispatch(updateSelection());
}
} else {
store.dispatch(updateSelection());
}
}
} else {
store.dispatch(replace('/connect'));
}
if (env.channels) {
@ -65,16 +28,26 @@ function loadState({ store }, env) {
});
}
// Wait until wrapWidth gets initialized so that height calculations
// only happen once for these messages
when(store, getWrapWidth, () => {
if (env.messages) {
store.dispatch(
appSet({
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));
}
});
});
}
}
/* eslint-disable no-underscore-dangle */
export default async function initialState(ctx) {
const env = await window.__init__;
ctx.socket.connect();

View File

@ -0,0 +1,22 @@
import { updateSelection } from 'state/tab';
import { observe, when } from 'utils/observe';
export default function route({ store }) {
let first = true;
when(
store,
state => state.app.initialized,
() =>
observe(
store,
state => state.router,
router => {
if (!router.route || router.route === 'chat') {
store.dispatch(updateSelection(first));
first = false;
}
}
)
);
}

View File

@ -7,9 +7,10 @@ import {
addMessage,
addMessages
} from 'state/messages';
import { openModal } from 'state/modals';
import { reconnect } from 'state/servers';
import { select } from 'state/tab';
import { find, normalizeChannel } from 'utils';
import { find } from 'utils';
function withReason(message, reason) {
return message + (reason ? ` (${reason})` : '');
@ -45,22 +46,7 @@ export default function handleSocket({
},
join({ user, server, channels }) {
const state = getState();
const tab = state.tab.selected;
const [joinedChannel] = channels;
if (tab.server && tab.name) {
const { nick } = state.servers[tab.server];
if (
tab.server === server &&
nick === user &&
tab.name !== joinedChannel &&
normalizeChannel(tab.name) === normalizeChannel(joinedChannel)
) {
dispatch(select(server, joinedChannel));
}
}
dispatch(inform(`${user} joined the channel`, server, joinedChannel));
dispatch(inform(`${user} joined the channel`, server, channels[0]));
},
part({ user, server, channel, reason }) {
@ -75,10 +61,12 @@ export default function handleSocket({
},
nick({ server, oldNick, newNick }) {
const channels = findChannels(getState(), server, oldNick);
dispatch(
broadcast(`${oldNick} changed nick to ${newNick}`, server, channels)
);
if (oldNick) {
const channels = findChannels(getState(), server, oldNick);
dispatch(
broadcast(`${oldNick} changed nick to ${newNick}`, server, channels)
);
}
},
topic({ server, channel, topic, nick }) {
@ -93,7 +81,12 @@ export default function handleSocket({
},
motd({ content, server }) {
dispatch(addMessages(content.map(line => ({ content: line })), server));
dispatch(
addMessages(
content.map(line => ({ content: line })),
server
)
);
},
whois(data) {
@ -120,16 +113,22 @@ export default function handleSocket({
dispatch(addMessage(message, tab.server, tab.name));
},
error({ server, target, message }) {
dispatch(addMessage({ content: message, type: 'error' }, server, target));
},
connection_update({ server, errorType }) {
if (
errorType === 'verify' &&
window.confirm(
'The server is using a self-signed certificate, continue anyway?'
)
) {
if (errorType === 'verify') {
dispatch(
reconnect(server, {
skipVerify: true
openModal('confirm', {
question:
'The server is using a self-signed certificate, continue anyway?',
onConfirm: () =>
dispatch(
reconnect(server, {
skipVerify: true
})
)
})
);
}
@ -140,6 +139,16 @@ export default function handleSocket({
}
};
const afterHandlers = {
channel_forward(forward) {
const { selected } = getState().tab;
if (selected.server === forward.server && selected.name === forward.old) {
dispatch(select(forward.server, forward.new, true));
}
}
};
socket.onMessage((type, data) => {
let action;
if (Array.isArray(data)) {
@ -157,5 +166,9 @@ export default function handleSocket({
}
dispatch(action);
if (type in afterHandlers) {
afterHandlers[type](data);
}
});
}

View File

@ -23,10 +23,8 @@ function registerValidSW(swUrl, config) {
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
if (config && config.onSuccess) {
config.onSuccess(registration);
}
} else if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
};

View File

@ -46,9 +46,13 @@ describe('channel reducer', () => {
expect(state).toEqual({
srv: {
chan1: {
name: 'chan1',
joined: true,
users: [{ mode: '', nick: 'nick1', renderName: 'nick1' }]
},
chan2: {
name: 'chan2',
joined: true,
users: [{ mode: '', nick: 'nick2', renderName: 'nick2' }]
}
}
@ -61,6 +65,8 @@ describe('channel reducer', () => {
expect(state).toEqual({
srv: {
chan1: {
name: 'chan1',
joined: true,
users: [{ mode: '', nick: 'nick1', renderName: 'nick1' }]
}
}
@ -81,9 +87,13 @@ describe('channel reducer', () => {
expect(state).toEqual({
srv: {
chan1: {
name: 'chan1',
joined: true,
users: [{ mode: '', nick: 'nick1', renderName: 'nick1' }]
},
chan2: {
name: 'chan2',
joined: true,
users: []
}
}
@ -105,12 +115,16 @@ describe('channel reducer', () => {
expect(state).toEqual({
srv: {
chan1: {
name: 'chan1',
joined: true,
users: [
{ mode: '', nick: 'nick3', renderName: 'nick3' },
{ mode: '', nick: 'nick2', renderName: 'nick2' }
]
},
chan2: {
name: 'chan2',
joined: true,
users: [{ mode: '', nick: 'nick2', renderName: 'nick2' }]
}
}
@ -118,7 +132,8 @@ describe('channel reducer', () => {
});
it('handles SOCKET_USERS', () => {
const state = reducer(undefined, {
let state = reducer(undefined, socket_join('srv', 'chan1', 'nick1'));
state = reducer(state, {
type: actions.socket.USERS,
server: 'srv',
channel: 'chan1',
@ -128,6 +143,8 @@ describe('channel reducer', () => {
expect(state).toEqual({
srv: {
chan1: {
name: 'chan1',
joined: true,
users: [
{ mode: '', nick: 'user3', renderName: 'user3' },
{ mode: '', nick: 'user2', renderName: 'user2' },
@ -141,18 +158,18 @@ describe('channel reducer', () => {
});
it('handles SOCKET_TOPIC', () => {
const state = reducer(undefined, {
let state = reducer(undefined, socket_join('srv', 'chan1', 'nick1'));
state = reducer(state, {
type: actions.socket.TOPIC,
server: 'srv',
channel: 'chan1',
topic: 'the topic'
});
expect(state).toEqual({
expect(state).toMatchObject({
srv: {
chan1: {
topic: 'the topic',
users: []
topic: 'the topic'
}
}
});
@ -165,7 +182,7 @@ describe('channel reducer', () => {
state = reducer(state, socket_mode('srv', 'chan1', 'nick1', 'o', ''));
expect(state).toEqual({
expect(state).toMatchObject({
srv: {
chan1: {
users: [
@ -183,7 +200,7 @@ describe('channel reducer', () => {
state = reducer(state, socket_mode('srv', 'chan1', 'nick2', 'o', ''));
state = reducer(state, socket_mode('srv', 'chan2', 'not_there', 'x', ''));
expect(state).toEqual({
expect(state).toMatchObject({
srv: {
chan1: {
users: [
@ -210,11 +227,11 @@ describe('channel reducer', () => {
expect(state).toEqual({
srv: {
chan1: { topic: 'the topic', users: [] },
chan2: { users: [] }
chan1: { name: 'chan1', joined: true, topic: 'the topic', users: [] },
chan2: { name: 'chan2', joined: true, users: [] }
},
srv2: {
chan1: { users: [] }
chan1: { name: 'chan1', joined: true, users: [] }
}
});
});
@ -313,18 +330,35 @@ describe('getSortedChannels', () => {
'bob.com': {},
'127.0.0.1': {
'#chan1': {
name: '#chan1',
users: [],
topic: 'cake'
},
'#pie': {},
'##apples': {}
'#pie': {
name: '#pie'
},
'##apples': {
name: '##apples'
}
}
}
})
).toEqual([
{
address: '127.0.0.1',
channels: ['##apples', '#chan1', '#pie']
channels: [
{
name: '##apples'
},
{
name: '#chan1',
users: [],
topic: 'cake'
},
{
name: '#pie'
}
]
},
{
address: 'bob.com',

View File

@ -82,7 +82,10 @@ describe('message reducer', () => {
server: 'srv',
tab: '#chan1',
prepend: true,
messages: [{ id: 1 }, { id: 2 }]
messages: [
{ id: 1, date: new Date() },
{ id: 2, date: new Date() }
]
});
expect(state).toMatchObject({
@ -92,6 +95,90 @@ describe('message reducer', () => {
});
});
it('adds date markers when prepending messages', () => {
let state = {
srv: {
'#chan1': [{ id: 0, date: new Date(1999, 0, 1) }]
}
};
state = reducer(state, {
type: actions.ADD_MESSAGES,
server: 'srv',
tab: '#chan1',
prepend: true,
messages: [
{ id: 1, date: new Date(1990, 0, 2) },
{ id: 2, date: new Date(1990, 0, 3) }
]
});
expect(state).toMatchObject({
srv: {
'#chan1': [
{ id: 1 },
{ type: 'date' },
{ id: 2 },
{ type: 'date' },
{ id: 0 }
]
}
});
});
it('adds a date marker when adding a message', () => {
let state = {
srv: {
'#chan1': [{ id: 0, date: new Date(1999, 0, 1) }]
}
};
state = reducer(state, {
type: actions.ADD_MESSAGE,
server: 'srv',
tab: '#chan1',
message: { id: 1, date: new Date(1990, 0, 2) }
});
expect(state).toMatchObject({
srv: {
'#chan1': [{ id: 0 }, { type: 'date' }, { id: 1 }]
}
});
});
it('adds date markers when adding messages', () => {
let state = {
srv: {
'#chan1': [{ id: 0, date: new Date(1999, 0, 1) }]
}
};
state = reducer(state, {
type: actions.ADD_MESSAGES,
server: '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) }
]
});
expect(state).toMatchObject({
srv: {
'#chan1': [
{ id: 0 },
{ type: 'date' },
{ id: 1 },
{ type: 'date' },
{ id: 2 },
{ id: 3 }
]
}
});
});
it('adds messages to the correct tabs when broadcasting', () => {
let state = {
app: appReducer(undefined, { type: '' })

View File

@ -16,7 +16,8 @@ describe('server reducer', () => {
status: {
connected: false,
error: null
}
},
features: {}
}
});
@ -30,7 +31,8 @@ describe('server reducer', () => {
status: {
connected: false,
error: null
}
},
features: {}
}
});
@ -47,7 +49,8 @@ describe('server reducer', () => {
status: {
connected: false,
error: null
}
},
features: {}
},
'127.0.0.2': {
name: 'srv',
@ -56,7 +59,8 @@ describe('server reducer', () => {
status: {
connected: false,
error: null
}
},
features: {}
}
});
});
@ -216,7 +220,8 @@ describe('server reducer', () => {
editedNick: null,
status: {
connected: true
}
},
features: {}
},
'127.0.0.2': {
name: 'stuffz',
@ -224,7 +229,8 @@ describe('server reducer', () => {
editedNick: null,
status: {
connected: false
}
},
features: {}
}
});
});
@ -247,7 +253,8 @@ describe('server reducer', () => {
editedNick: null,
status: {
connected: true
}
},
features: {}
}
});
@ -266,7 +273,8 @@ describe('server reducer', () => {
status: {
connected: false,
error: 'Bad stuff happened'
}
},
features: {}
}
});
});

View File

@ -6,6 +6,8 @@ export const KICK = 'KICK';
export const PART = 'PART';
export const SET_TOPIC = 'SET_TOPIC';
export const CHANNEL_SEARCH = 'CHANNEL_SEARCH';
export const INPUT_HISTORY_ADD = 'INPUT_HISTORY_ADD';
export const INPUT_HISTORY_DECREMENT = 'INPUT_HISTORY_DECREMENT';
export const INPUT_HISTORY_INCREMENT = 'INPUT_HISTORY_INCREMENT';
@ -19,6 +21,9 @@ export const FETCH_MESSAGES = 'FETCH_MESSAGES';
export const RAW = 'RAW';
export const UPDATE_MESSAGE_HEIGHT = 'UPDATE_MESSAGE_HEIGHT';
export const OPEN_MODAL = 'OPEN_MODAL';
export const CLOSE_MODAL = 'CLOSE_MODAL';
export const CLOSE_PRIVATE_CHAT = 'CLOSE_PRIVATE_CHAT';
export const OPEN_PRIVATE_CHAT = 'OPEN_PRIVATE_CHAT';
@ -61,7 +66,11 @@ export const socket = createSocketActions([
'cert_fail',
'cert_success',
'channels',
'channel_forward',
'channel_search',
'connection_update',
'error',
'features',
'join',
'message',
'mode',

View File

@ -1,4 +1,3 @@
import assign from 'lodash/assign';
import createReducer from 'utils/createReducer';
import * as actions from './actions';
@ -31,7 +30,7 @@ const initialState = {
export default createReducer(initialState, {
[actions.APP_SET](state, { key, value }) {
if (typeof key === 'object') {
assign(state, key);
Object.assign(state, key);
} else {
state[key] = value;
}
@ -59,7 +58,3 @@ export function setConnected(connected) {
export function setCharWidth(width) {
return appSet('charWidth', width);
}
export function setConnectDefaults(defaults) {
return appSet('connectDefaults', defaults);
}

View File

@ -0,0 +1,41 @@
import createReducer from 'utils/createReducer';
import * as actions from 'state/actions';
const initialState = {
results: [],
end: false
};
export default createReducer(initialState, {
[actions.socket.CHANNEL_SEARCH](state, { results, start }) {
if (results) {
state.end = false;
if (start > 0) {
state.results.push(...results);
} else {
state.results = results;
}
} else {
state.end = true;
}
},
[actions.OPEN_MODAL](state, { name }) {
if (name === 'channel') {
return initialState;
}
}
});
export function searchChannels(server, q, start) {
return {
type: actions.CHANNEL_SEARCH,
server,
q,
socket: {
type: 'channel_search',
data: { server, q, start }
}
};
}

View File

@ -61,7 +61,7 @@ function init(state, server, channel) {
state[server] = {};
}
if (channel && !state[server][channel]) {
state[server][channel] = { users: [] };
state[server][channel] = { name: channel, users: [], joined: false };
}
}
@ -91,18 +91,14 @@ export function compareUsers(a, b) {
export const getChannels = state => state.channels;
export const getSortedChannels = createSelector(
getChannels,
channels =>
sortBy(
Object.keys(channels).map(server => ({
address: server,
channels: sortBy(Object.keys(channels[server]), channel =>
channel.toLowerCase()
)
})),
server => server.address.toLowerCase()
)
export const getSortedChannels = createSelector(getChannels, channels =>
sortBy(
Object.keys(channels).map(server => ({
address: server,
channels: sortBy(channels[server], channel => channel.name.toLowerCase())
})),
server => server.address.toLowerCase()
)
);
export const getSelectedChannel = createSelector(
@ -124,6 +120,10 @@ export const getSelectedChannelUsers = createSelector(
export default createReducer(
{},
{
[actions.JOIN](state, { server, channels }) {
channels.forEach(channel => init(state, server, channel));
},
[actions.PART](state, { server, channels }) {
channels.forEach(channel => delete state[server][channel]);
},
@ -131,9 +131,16 @@ export default createReducer(
[actions.socket.JOIN](state, { server, 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));
},
[actions.socket.CHANNEL_FORWARD](state, action) {
init(state, action.server, action.new);
delete state[action.server][action.old];
},
[actions.socket.PART](state, { server, channel, user }) {
if (state[server][channel]) {
removeUser(state[server][channel].users, user);
@ -160,12 +167,10 @@ export default createReducer(
},
[actions.socket.USERS](state, { server, channel, users }) {
init(state, server, channel);
state[server][channel].users = users.map(nick => loadUser(nick));
},
[actions.socket.TOPIC](state, { server, channel, topic }) {
init(state, server, channel);
state[server][channel].topic = topic;
},
@ -191,6 +196,7 @@ export default createReducer(
if (data) {
data.forEach(({ server, name, topic }) => {
init(state, server, name);
state[server][name].joined = true;
state[server][name].topic = topic;
});
}
@ -225,16 +231,27 @@ export function join(channels, server) {
}
export function part(channels, server) {
return dispatch => {
dispatch({
return (dispatch, getState) => {
const action = {
type: actions.PART,
channels,
server,
socket: {
server
};
const state = getState().channels[server];
const joined = channels.filter(c => state[c] && state[c].joined);
if (joined.length > 0) {
action.socket = {
type: 'part',
data: { channels, server }
}
});
data: {
channels: joined,
server
}
};
}
dispatch(action);
dispatch(updateSelection());
};
}

View File

@ -1,8 +1,10 @@
import { combineReducers } from 'redux';
import app from './app';
import channels from './channels';
import channelSearch from './channelSearch';
import input from './input';
import messages from './messages';
import modals from './modals';
import privateChats from './privateChats';
import search from './search';
import servers from './servers';
@ -18,8 +20,10 @@ export default function createReducer(router) {
router,
app,
channels,
channelSearch,
input,
messages,
modals,
privateChats,
search,
servers,

View File

@ -5,7 +5,8 @@ import {
messageHeight,
linkify,
timestamp,
isChannel
isChannel,
formatDate
} from 'utils';
import createReducer from 'utils/createReducer';
import { getApp } from './app';
@ -43,22 +44,90 @@ function init(state, server, tab) {
}
}
let nextID = 0;
function createDateMessage(date) {
const message = {
id: nextID,
type: 'date',
content: formatDate(date),
height: 40
};
nextID++;
return message;
}
function isSameDay(d1, d2) {
return (
d1.getDate() === d2.getDate() &&
d1.getMonth() === d2.getMonth() &&
d1.getFullYear() === d2.getFullYear()
);
}
function reducerPrependMessages(messages, server, tab, state) {
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));
}
msgs.push(messages[i]);
}
const m = state[server][tab];
if (m.length > 0) {
const lastNewMessage = msgs[msgs.length - 1];
const firstMessage = m[0];
if (
firstMessage.date &&
!isSameDay(firstMessage.date, lastNewMessage.date)
) {
msgs.push(createDateMessage(firstMessage.date));
}
}
m.unshift(...msgs);
}
function reducerAddMessage(message, server, tab, state) {
const messages = state[server][tab];
if (messages.length > 0) {
const lastMessage = messages[messages.length - 1];
if (lastMessage.date && !isSameDay(lastMessage.date, message.date)) {
messages.push(createDateMessage(message.date));
}
}
messages.push(message);
}
export default createReducer(
{},
{
[actions.ADD_MESSAGE](state, { server, tab, message }) {
init(state, server, tab);
state[server][tab].push(message);
reducerAddMessage(message, server, tab, state);
},
[actions.ADD_MESSAGES](state, { server, tab, messages, prepend }) {
if (prepend) {
init(state, server, tab);
state[server][tab].unshift(...messages);
reducerPrependMessages(messages, server, tab, state);
} else {
if (!messages[0].tab) {
init(state, server, tab);
}
messages.forEach(message => {
init(state, server, message.tab || tab);
state[server][message.tab || tab].push(message);
if (message.tab) {
init(state, server, message.tab);
}
reducerAddMessage(message, server, message.tab || tab, state);
});
}
},
@ -71,6 +140,12 @@ export default createReducer(
channels.forEach(channel => delete state[server][channel]);
},
[actions.socket.CHANNEL_FORWARD](state, { server, old }) {
if (state[server]) {
delete state[server][old];
}
},
[actions.UPDATE_MESSAGE_HEIGHT](
state,
{ wrapWidth, charWidth, windowWidth }
@ -78,6 +153,10 @@ export default createReducer(
Object.keys(state).forEach(server =>
Object.keys(state[server]).forEach(target =>
state[server][target].forEach(message => {
if (message.type === 'date') {
return;
}
message.height = messageHeight(
message,
wrapWidth,
@ -100,15 +179,15 @@ export default createReducer(
}
);
let nextID = 0;
function initMessage(message, tab, state) {
if (message.time) {
message.time = timestamp(new Date(message.time * 1000));
message.date = new Date(message.time * 1000);
} else {
message.time = timestamp();
message.date = new Date();
}
message.time = timestamp(message.date);
if (!message.id) {
message.id = nextID;
nextID++;
@ -118,6 +197,8 @@ function initMessage(message, tab, state) {
message.channel = true;
}
message.content = message.content || '';
// Collapse multiple adjacent spaces into a single one
message.content = message.content.replace(/\s\s+/g, ' ');

47
client/js/state/modals.js Normal file
View File

@ -0,0 +1,47 @@
import { createSelector } from 'reselect';
import createReducer from 'utils/createReducer';
import * as actions from './actions';
export const getModals = state => state.modals;
export const getHasOpenModals = createSelector(getModals, modals => {
const keys = Object.keys(modals);
for (let i = 0; i < keys.length; i++) {
if (modals[keys[i]].isOpen) {
return true;
}
}
return false;
});
export default createReducer(
{},
{
[actions.OPEN_MODAL](state, { name, payload = {} }) {
state[name] = {
isOpen: true,
payload
};
},
[actions.CLOSE_MODAL](state, { name }) {
state[name].isOpen = false;
}
}
);
export function openModal(name, payload) {
return {
type: actions.OPEN_MODAL,
name,
payload
};
}
export function closeModal(name) {
return {
type: actions.CLOSE_MODAL,
name
};
}

View File

@ -45,7 +45,8 @@ export default createReducer(
status: {
connected: false,
error: null
}
},
features: {}
};
}
},
@ -79,8 +80,8 @@ export default createReducer(
[actions.socket.SERVERS](state, { data }) {
if (data) {
data.forEach(({ host, name, nick, status }) => {
state[host] = { name, nick, status, editedNick: null };
data.forEach(({ host, name = host, nick, status, features = {} }) => {
state[host] = { name, nick, status, features, editedNick: null };
});
}
},
@ -90,6 +91,17 @@ export default createReducer(
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;
}
}
}
}
);

View File

@ -1,4 +1,3 @@
import assign from 'lodash/assign';
import createReducer from 'utils/createReducer';
import * as actions from './actions';
@ -41,7 +40,7 @@ export default createReducer(
[actions.SETTINGS_SET](state, { key, value, settings }) {
if (settings) {
assign(state, settings);
Object.assign(state, settings);
} else {
state[key] = value;
}
@ -80,7 +79,7 @@ export function setCert(fileName, cert) {
return {
type: actions.SET_CERT,
fileName,
cert: cert
cert
};
}
@ -88,7 +87,7 @@ export function setKey(fileName, key) {
return {
type: actions.SET_KEY,
fileName,
key: key
key
};
}

View File

@ -1,6 +1,9 @@
import get from 'lodash/get';
import Cookie from 'js-cookie';
import createReducer from 'utils/createReducer';
import { push, replace, LOCATION_CHANGED } from 'utils/router';
import * as actions from './actions';
import { find } from '../utils';
const initialState = {
selected: {},
@ -54,17 +57,52 @@ export function select(server, name, doReplace) {
return navigate(`/${server}`);
}
export function updateSelection() {
export function tabExists(
{ server, name },
{ servers, channels, privateChats }
) {
return (
(name && get(channels, [server, name])) ||
(!name && server && servers[server]) ||
(name && find(privateChats[server], nick => nick === name))
);
}
function parseTabCookie() {
const cookie = Cookie.get('tab');
if (cookie) {
const [server, name = null] = cookie.split(/;(.+)/);
return { server, name };
}
return null;
}
export function updateSelection(tryCookie) {
return (dispatch, getState) => {
const state = getState();
const { history } = state.tab;
if (tabExists(state.tab.selected, state)) {
return;
}
if (tryCookie) {
const tab = parseTabCookie();
if (tab && tabExists(tab, state)) {
return dispatch(select(tab.server, tab.name, true));
}
}
const { servers } = state;
const { history } = state.tab;
const { server } = state.tab.selected;
const serverAddrs = Object.keys(servers);
if (serverAddrs.length === 0) {
dispatch(replace('/connect'));
} else if (history.length > 0) {
} 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]) {

View File

@ -1,6 +1,13 @@
workbox.skipWaiting();
workbox.clientsClaim();
import { skipWaiting, clientsClaim } from 'workbox-core';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { NavigationRoute, registerRoute } from 'workbox-routing';
workbox.precaching.precacheAndRoute(self.__precacheManifest, {
skipWaiting();
clientsClaim();
precacheAndRoute(self.__WB_MANIFEST, {
ignoreUrlParametersMatching: [/.*/]
});
const handler = createHandlerBoundToURL('/');
registerRoute(new NavigationRoute(handler));

View File

@ -3,11 +3,6 @@ import { connect } from 'react-redux';
const strictEqual = (a, b) => a === b;
export default (mapState, mapDispatch) =>
connect(
mapState,
mapDispatch,
null,
{
areStatePropsEqual: strictEqual
}
);
connect(mapState, mapDispatch, null, {
areStatePropsEqual: strictEqual
});

View File

@ -8,10 +8,7 @@ export function normalizeChannel(channel) {
return channel;
}
return channel
.split('#')
.join('')
.toLowerCase();
return channel.split('#').join('').toLowerCase();
}
export function isChannel(name) {
@ -137,6 +134,9 @@ export function timestamp(date = new Date()) {
return `${h}:${m}`;
}
const dateFmt = new Intl.DateTimeFormat(window.navigator.language);
export const formatDate = dateFmt.format;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
@ -167,7 +167,7 @@ export function measureScrollBarWidth() {
}
export function findIndex(arr, pred) {
if (!arr) {
if (!Array.isArray(arr) || typeof pred !== 'function') {
return -1;
}
@ -187,3 +187,17 @@ export function find(arr, pred) {
}
return null;
}
export function count(arr, pred) {
if (!Array.isArray(arr) || typeof pred !== 'function') {
return 0;
}
let c = 0;
for (let i = 0; i < arr.length; i++) {
if (pred(arr[i])) {
c++;
}
}
return c;
}

View File

@ -7,6 +7,10 @@ const autolinker = new Autolinker({
});
export default function linkify(text) {
if (!text) {
return text;
}
let matches = autolinker.parseText(text);
if (matches.length === 0) {

View File

@ -1,8 +1,6 @@
import createHistory from 'history/createBrowserHistory';
import history from 'history/browser';
import UrlPattern from 'url-pattern';
const history = createHistory();
export const LOCATION_CHANGED = 'ROUTER_LOCATION_CHANGED';
export const PUSH = 'ROUTER_PUSH';
export const REPLACE = 'ROUTER_REPLACE';
@ -71,7 +69,7 @@ function match(routes, location) {
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, decode({ ...location }));
}
}
return null;
@ -97,7 +95,7 @@ export default function initRouter(routes, store) {
matched = { location: {} };
}
history.listen(location => {
history.listen(({ location }) => {
const nextMatch = match(patterns, location);
if (
nextMatch &&

View File

@ -1,4 +1,5 @@
let width, height;
let width;
let height;
const listeners = [];
function update() {

View File

@ -12,72 +12,78 @@
"iOS >= 10.3"
],
"devDependencies": {
"@babel/core": "^7.1.5",
"@babel/plugin-proposal-class-properties": "^7.1.0",
"@babel/plugin-proposal-export-default-from": "^7.0.0",
"@babel/plugin-proposal-export-namespace-from": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-transform-react-constant-elements": "^7.0.0",
"@babel/plugin-transform-react-inline-elements": "^7.0.0",
"@babel/preset-env": "^7.1.5",
"@babel/preset-react": "^7.0.0",
"babel-core": "^7.0.0-0",
"babel-eslint": "^10.0.1",
"babel-jest": "^23.6.0",
"babel-loader": "^8.0.4",
"@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/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-eslint": "^10.1.0",
"babel-jest": "^25.5.0",
"babel-loader": "^8.1.0",
"brotli": "^1.3.1",
"css-loader": "^1.0.1",
"cssnano": "^4.1.7",
"del": "^3.0.0",
"eslint": "^5.8.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-config-prettier": "^3.1.0",
"eslint-import-resolver-webpack": "^0.10.1",
"eslint-loader": "^2.1.1",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jsx-a11y": "^6.1.2",
"eslint-plugin-react": "^7.11.1",
"express": "^4.16.4",
"express-http-proxy": "^1.5.0",
"gulp": "4.0.0",
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^3.5.3",
"cssnano": "^4.1.10",
"del": "^5.1.0",
"eslint": "^6.8.0",
"eslint-config-airbnb": "^18.1.0",
"eslint-config-prettier": "^6.11.0",
"eslint-import-resolver-webpack": "^0.12.1",
"eslint-loader": "^4.0.2",
"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",
"express": "^4.17.1",
"express-http-proxy": "^1.6.0",
"gulp": "4.0.2",
"gulp-util": "^3.0.8",
"jest": "^23.6.0",
"mini-css-extract-plugin": "^0.4.4",
"postcss-flexbugs-fixes": "^4.1.0",
"jest": "^25.5.0",
"mini-css-extract-plugin": "^0.9.0",
"postcss-flexbugs-fixes": "^4.2.1",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.4.0",
"prettier": "1.15.2",
"react-test-renderer": "^16.7.0-alpha.0",
"style-loader": "^0.23.1",
"terser-webpack-plugin": "^1.1.0",
"through2": "^3.0.0",
"webpack": "^4.25.1",
"webpack-dev-middleware": "^3.4.0",
"webpack-hot-middleware": "^2.24.3",
"webpack-plugin-hash-output": "^3.1.0",
"workbox-webpack-plugin": "^3.6.3"
"postcss-preset-env": "^6.7.0",
"prettier": "2.0.5",
"react-test-renderer": "16.13.1",
"style-loader": "^1.2.1",
"terser-webpack-plugin": "^2.3.6",
"through2": "^3.0.1",
"webpack": "^4.43.0",
"webpack-dev-middleware": "^3.7.2",
"webpack-hot-middleware": "^2.25.0",
"webpack-plugin-hash-output": "^3.2.1",
"workbox-webpack-plugin": "^5.1.3"
},
"dependencies": {
"autolinker": "^1.7.1",
"autolinker": "^3.14.1",
"backo": "^1.1.0",
"classnames": "^2.2.6",
"fontfaceobserver": "^2.0.9",
"formik": "^1.3.1",
"history": "4.5.1",
"hsluv": "^0.0.3",
"immer": "^1.7.3",
"js-cookie": "^2.1.4",
"lodash": "^4.17.11",
"react": "^16.7.0-alpha.0",
"react-dom": "^16.7.0-alpha.0",
"react-hot-loader": "^4.4.0",
"react-redux": "^6.0.0-beta.2",
"formik": "^2.1.4",
"history": "^5.0.0-beta.8",
"hsluv": "^0.1.0",
"immer": "^6.0.3",
"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-icons": "^3.7.0",
"react-modal": "^3.11.2",
"react-redux": "^7.2.0",
"react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.2.2",
"redux": "^4.0.1",
"react-window": "^1.8.5",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"url-pattern": "^1.0.3"
"url-pattern": "^1.0.3",
"workbox-core": "^5.1.3",
"workbox-precaching": "^5.1.3",
"workbox-routing": "^5.1.3"
},
"scripts": {
"prettier": "prettier --write {.*,*.js,css/*.css,**/*.test.js}",
@ -85,10 +91,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/... github.com/SlinSo/egon/cmd/egon",
"gen:install": "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:template": "egon -s -m ../server"
"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"
},
"jest": {
"moduleNameMapper": {
@ -96,6 +101,9 @@
"^containers(.*)$": "<rootDir>/js/containers$1",
"^state(.*)$": "<rootDir>/js/state$1",
"^utils(.*)$": "<rootDir>/js/utils$1"
}
},
"transformIgnorePatterns": [
"node_modules/?!(history)"
]
}
}

Binary file not shown.

Binary file not shown.

View File

@ -17,5 +17,5 @@
"background_color": "#f0f0f0",
"display": "standalone",
"scope": "/",
"theme_color": "#f0f0f0"
"theme_color": "#222"
}

View File

@ -4,9 +4,12 @@ var postcssPresetEnv = require('postcss-preset-env');
module.exports = {
mode: 'development',
entry: ['webpack-hot-middleware/client', './js/index'],
entry: {
main: ['webpack-hot-middleware/client', './js/index'],
boot: './js/boot'
},
output: {
filename: 'bundle.js',
filename: '[name].js',
publicPath: '/'
},
resolve: {
@ -28,7 +31,11 @@ module.exports = {
fix: true
}
},
{ test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ },
{
test: /\.js$/,
use: ['babel-loader', 'react-hot-loader/webpack'],
exclude: /node_modules/
},
{
test: /\.css$/,
use: [

View File

@ -5,6 +5,7 @@ var cssnano = require('cssnano');
var TerserPlugin = require('terser-webpack-plugin');
var { InjectManifest } = require('workbox-webpack-plugin');
var HashOutputPlugin = require('webpack-plugin-hash-output');
var CopyPlugin = require('copy-webpack-plugin');
module.exports = {
mode: 'production',
@ -79,28 +80,31 @@ module.exports = {
filename: '[name].[contenthash].css',
chunkFilename: '[name].[contenthash].css'
}),
new HashOutputPlugin(),
new CopyPlugin(['public']),
new InjectManifest({
swSrc: './js/sw.js',
importWorkboxFrom: 'local',
globDirectory: './public',
globPatterns: ['*', 'font/*.woff2'],
additionalManifestEntries: [
{
url: '/',
revision: '__INDEX_REVISON__'
}
],
exclude: [
/\.map$/,
/^manifest.*\.js(?:on)?$/,
/^boot.*\.js$/,
/^runtime.*\.js$/
/^runtime.*\.js$/,
/\.txt$/
]
}),
new HashOutputPlugin()
})
],
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
safari10: true
},
cache: true,
parallel: true
}
})
],
splitChunks: {

File diff suppressed because it is too large Load Diff

View File

@ -5,10 +5,10 @@ import (
"io/ioutil"
"log"
"os"
"time"
"runtime"
"github.com/fsnotify/fsnotify"
"github.com/khlieng/dispatch/assets"
"github.com/khlieng/dispatch/config"
"github.com/khlieng/dispatch/server"
"github.com/khlieng/dispatch/storage"
"github.com/khlieng/dispatch/storage/bleve"
@ -29,6 +29,7 @@ const logo = `
%s
Commit: %s
Build Date: %s
Runtime: %s
`
@ -36,41 +37,25 @@ var rootCmd = &cobra.Command{
Use: "dispatch",
Short: "Web-based IRC client in Go.",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if v, _ := cmd.Flags().GetBool("version"); v {
if viper.GetBool("version") {
printVersion()
os.Exit(0)
}
if cmd.Use == "dispatch" {
fmt.Printf(logo, version.Tag, version.Commit, version.Date)
if cmd == cmd.Root() {
fmt.Printf(logo, version.Tag, version.Commit, version.Date, runtime.Version())
}
storage.Initialize(viper.GetString("dir"))
storage.Initialize(viper.GetString("dir"), viper.GetString("data"), viper.GetString("conf"))
initConfig(storage.Path.Config(), viper.GetBool("reset_config"))
viper.SetConfigName("config")
viper.AddConfigPath(storage.Path.Root())
viper.ReadInConfig()
viper.WatchConfig()
prev := time.Now()
viper.OnConfigChange(func(e fsnotify.Event) {
now := time.Now()
// fsnotify sometimes fires twice
if now.Sub(prev) > time.Second {
log.Println("New config loaded")
prev = now
}
})
initConfig(storage.Path.Config(), viper.GetBool("reset-config"))
},
Run: func(cmd *cobra.Command, args []string) {
if viper.GetBool("dev") {
log.Println("Running in development mode, access client at http://localhost:3000")
}
log.Println("Storing data at", storage.Path.Root())
log.Println("Storing data at", storage.Path.DataRoot())
db, err := boltdb.New(storage.Path.Database())
if err != nil {
@ -78,19 +63,28 @@ var rootCmd = &cobra.Command{
}
defer db.Close()
srv := server.Dispatch{
Store: db,
SessionStore: db,
cfg, cfgUpdated := config.LoadConfig()
dispatch := server.New(cfg)
GetMessageStore: func(user *storage.User) (storage.MessageStore, error) {
return boltdb.New(storage.Path.Log(user.Username))
},
GetMessageSearchProvider: func(user *storage.User) (storage.MessageSearchProvider, error) {
return bleve.New(storage.Path.Index(user.Username))
},
go func() {
for {
dispatch.SetConfig(<-cfgUpdated)
log.Println("New config loaded")
}
}()
dispatch.Store = db
dispatch.SessionStore = db
dispatch.GetMessageStore = func(user *storage.User) (storage.MessageStore, error) {
return boltdb.New(storage.Path.Log(user.Username))
}
srv.Run()
dispatch.GetMessageSearchProvider = func(user *storage.User) (storage.MessageSearchProvider, error) {
return bleve.New(storage.Path.Index(user.Username))
}
dispatch.Run()
},
}
@ -103,6 +97,8 @@ func init() {
rootCmd.AddCommand(configCmd)
rootCmd.AddCommand(versionCmd)
rootCmd.PersistentFlags().String("data", storage.DefaultDirectory(), "directory to store data in")
rootCmd.PersistentFlags().String("conf", storage.DefaultDirectory(), "directory to store configuration in")
rootCmd.PersistentFlags().String("dir", storage.DefaultDirectory(), "directory to store config and data in")
rootCmd.PersistentFlags().Bool("reset-config", false, "reset to the default configuration, overwriting the current one")
rootCmd.Flags().StringP("address", "a", "", "interface to which the server will bind")
@ -110,14 +106,11 @@ func init() {
rootCmd.Flags().Bool("dev", false, "development mode")
rootCmd.Flags().BoolP("version", "v", false, "show version")
viper.BindPFlag("dir", rootCmd.PersistentFlags().Lookup("dir"))
viper.BindPFlag("reset_config", rootCmd.PersistentFlags().Lookup("reset-config"))
viper.BindPFlag("address", rootCmd.Flags().Lookup("address"))
viper.BindPFlag("port", rootCmd.Flags().Lookup("port"))
viper.BindPFlag("dev", rootCmd.Flags().Lookup("dev"))
viper.BindPFlags(rootCmd.PersistentFlags())
viper.BindPFlags(rootCmd.Flags())
viper.SetDefault("hexIP", false)
viper.SetDefault("verify_client_certificates", true)
viper.SetDefault("verify_certificates", true)
}
func initConfig(configPath string, overwrite bool) {

View File

@ -2,6 +2,7 @@ package commands
import (
"fmt"
"runtime"
"github.com/khlieng/dispatch/version"
"github.com/spf13/cobra"
@ -17,5 +18,5 @@ var versionCmd = &cobra.Command{
}
func printVersion() {
fmt.Printf("%s\nCommit: %s\nBuild Date: %s\n", version.Tag, version.Commit, version.Date)
fmt.Printf("%s\nCommit: %s\nBuild Date: %s\nRuntime: %s\n", version.Tag, version.Commit, version.Date, runtime.Version())
}

View File

@ -22,25 +22,19 @@ readonly = false
show_details = false
[https]
enabled = false
enabled = true
port = 443
# Redirect all http traffic to https
redirect = true
# Path to your cert and private key if you are not using
# the Let's Encrypt integration
cert = ""
key = ""
[letsencrypt]
# Your domain or subdomain
# Your domain or subdomain, if not set a certificate will be
# fetched for whatever domain dispatch gets accessed through
domain = ""
# An email address lets you recover your accounts private key
email = ""
# The port Let's Encrypt listens on, comment this out to let it bind
# to port 80 as needed, doing so means dispatch itself cannot use port 80
port = 5001
# Have dispatch proxy traffic from port 80 to the Let's Encrypt port
proxy = true
# Not implemented
[auth]
@ -74,3 +68,7 @@ enabled = false
max_age = 31536000
include_subdomains = false
preload = false
# Add your own HTTP headers to the index page
[headers]
# X-Example = "Rainbows"

82
config/config.go Normal file
View File

@ -0,0 +1,82 @@
package config
import (
"time"
"github.com/khlieng/dispatch/storage"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
)
type Config struct {
Address string
Port string
Dev bool
HexIP bool
VerifyCertificates bool `mapstructure:"verify_certificates"`
Headers map[string]string
Defaults Defaults
HTTPS HTTPS
LetsEncrypt LetsEncrypt
}
type Defaults struct {
Name string
Host string
Port int
Channels []string
Password string
SSL bool
ReadOnly bool
ShowDetails bool `mapstructure:"show_details"`
}
type HTTPS struct {
Enabled bool
Port string
Cert string
Key string
HSTS HSTS
}
type HSTS struct {
Enabled bool
MaxAge string `mapstructure:"max_age"`
IncludeSubdomains bool `mapstructure:"include_subdomains"`
Preload bool
}
type LetsEncrypt struct {
Domain string
Email string
}
func LoadConfig() (*Config, chan *Config) {
viper.SetConfigName("config")
viper.AddConfigPath(storage.Path.ConfigRoot())
viper.ReadInConfig()
config := &Config{}
viper.Unmarshal(config)
viper.WatchConfig()
configCh := make(chan *Config, 1)
prev := time.Now()
viper.OnConfigChange(func(e fsnotify.Event) {
now := time.Now()
// fsnotify sometimes fires twice
if now.Sub(prev) > time.Second {
config := &Config{}
err := viper.Unmarshal(config)
if err == nil {
configCh <- config
}
prev = now
}
})
return config, configCh
}

93
go.mod
View File

@ -1,59 +1,50 @@
module github.com/khlieng/dispatch
go 1.14
require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/RoaringBitmap/roaring v0.4.16 // indirect
github.com/blevesearch/bleve v0.0.0-20180525174403-1d6d47ed3ad9
github.com/blevesearch/blevex v0.0.0-20180227211930-4b158bb555a3 // indirect
github.com/blevesearch/go-porterstemmer v0.0.0-20141230013033-23a2c8e5cf1f // indirect
github.com/blevesearch/segment v0.0.0-20160915185041-762005e7a34f // indirect
github.com/boltdb/bolt v0.0.0-20180302180052-fd01fc79c553
github.com/couchbase/vellum v0.0.0-20180910213445-01d5c56e6095 // indirect
github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07 // indirect
github.com/cznic/mathutil v0.0.0-20181021201202-eba54fb065b7 // indirect
github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186 // indirect
github.com/dsnet/compress v0.0.0-20171208185109-cc9eb1d7ad76
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 // indirect
github.com/RoaringBitmap/roaring v0.4.23 // indirect
github.com/blevesearch/bleve v1.0.7
github.com/caddyserver/certmagic v0.10.12
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/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.7
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd // indirect
github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493 // indirect
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
github.com/gorilla/websocket v1.4.0
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jmhodges/levigo v0.0.0-20161115193449-c42d9e0ca023 // indirect
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7
github.com/jtolds/gls v4.2.1+incompatible // 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/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/kr/pretty v0.1.0 // indirect
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329
github.com/miekg/dns v1.0.15 // indirect
github.com/mitchellh/go-homedir v1.0.0
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae // indirect
github.com/onsi/gomega v1.4.2 // indirect
github.com/philhofer/fwd v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446 // indirect
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
github.com/spf13/cast v1.3.0
github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.3 // indirect
github.com/spf13/viper v1.2.1
github.com/steveyen/gtreap v0.0.0-20150807155958-0abe01ef9be2 // indirect
github.com/stretchr/testify v1.2.2
github.com/syndtr/goleveldb v0.0.0-20181105012736-f9080354173f // indirect
github.com/tecbot/gorocksdb v0.0.0-20181010114359-8752a9433481 // indirect
github.com/tinylib/msgp v0.0.0-20180215042507-3b5c87ab5fb0 // indirect
github.com/willf/bitset v1.1.9 // indirect
github.com/xenolf/lego v1.2.1
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 // indirect
golang.org/x/net v0.0.0-20181113165502-88d92db4c548
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/square/go-jose.v2 v2.1.9 // indirect
github.com/klauspost/cpuid v1.2.3
github.com/mailru/easyjson v0.7.1
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/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/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/stretchr/testify v1.5.1
github.com/tdewolff/minify/v2 v2.7.4
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect
github.com/tinylib/msgp v1.1.2 // 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
)

749
go.sum
View File

@ -1,154 +1,745 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
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/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=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/go-autorest/autorest v0.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg=
github.com/Azure/go-autorest/autorest v0.5.0/go.mod h1:9HLKlQjVBH6U3oDfsXOeVc56THsLPw1L03yban4xThw=
github.com/Azure/go-autorest/autorest/adal v0.1.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E=
github.com/Azure/go-autorest/autorest/adal v0.2.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E=
github.com/Azure/go-autorest/autorest/azure/auth v0.1.0/go.mod h1:Gf7/i2FUpyb/sGBLIFxTBzrNzBo7aPXXE3ZVeDRwdpM=
github.com/Azure/go-autorest/autorest/azure/cli v0.1.0/go.mod h1:Dk8CUAt/b/PzkfeRsWzVG9Yj3ps8mS8ECztu43rdU8U=
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc=
github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8=
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/RoaringBitmap/roaring v0.4.16 h1:NholfewybRLOwACgfqfzn/N5xa6keKNs4fP00t0cwLo=
github.com/RoaringBitmap/roaring v0.4.16/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
github.com/blevesearch/bleve v0.0.0-20180525174403-1d6d47ed3ad9 h1:q25+axgzH1KX+j63v3yrkY1VHc6PkyTfpnzOmtAH154=
github.com/blevesearch/bleve v0.0.0-20180525174403-1d6d47ed3ad9/go.mod h1:Y2lmIkzV6mcNfAnAdOd+ZxHkHchhBfU/xroGIp61wfw=
github.com/blevesearch/blevex v0.0.0-20180227211930-4b158bb555a3 h1:U6vnxZrTfItfiUiYx0lf/LgHjRSfaKK5QHSom3lEbnA=
github.com/blevesearch/blevex v0.0.0-20180227211930-4b158bb555a3/go.mod h1:WH+MU2F4T0VmSdaPX+Wu5GYoZBrYWdOZWSjzvYcDmqQ=
github.com/blevesearch/go-porterstemmer v0.0.0-20141230013033-23a2c8e5cf1f h1:J9ZVHbB2X6JNxbKw/f3Y4E9Xq+Ro+zPiivzgmi3RTvg=
github.com/blevesearch/go-porterstemmer v0.0.0-20141230013033-23a2c8e5cf1f/go.mod h1:haWQqFT3RdOGz7PJuM3or/pWNJS1pKkoZJWCkWu0DVA=
github.com/blevesearch/segment v0.0.0-20160915185041-762005e7a34f h1:kqbi9lqXLLs+zfWlgo1PIiRQ86n33K1JKotjj4rSYOg=
github.com/blevesearch/segment v0.0.0-20160915185041-762005e7a34f/go.mod h1:IInt5XRvpiGE09KOk9mmCMLjHhydIhNPKPPFLFBB7L8=
github.com/boltdb/bolt v0.0.0-20180302180052-fd01fc79c553 h1:yvSJ8qbaWLeS7COhu2KJ0epn4mmc+aGeBP7Dpg7xQTY=
github.com/boltdb/bolt v0.0.0-20180302180052-fd01fc79c553/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/couchbase/vellum v0.0.0-20180910213445-01d5c56e6095 h1:dh7mqP7LS9voSd1Wx515giC2lPjPVduBpujISaftHrc=
github.com/couchbase/vellum v0.0.0-20180910213445-01d5c56e6095/go.mod h1:prYTC8EgTu3gwbqJihkud9zRXISvyulAplQ6exdCo1g=
github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07 h1:UHFGPvSxX4C4YBApSPvmUfL8tTvWLj2ryqvT9K4Jcuk=
github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
github.com/cznic/mathutil v0.0.0-20181021201202-eba54fb065b7 h1:y+DH9ARrWiiNBV+6waYP2IPcsRbxdU1qsnycPfShF4c=
github.com/cznic/mathutil v0.0.0-20181021201202-eba54fb065b7/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186 h1:0rkFMAbn5KBKNpJyHQ6Prb95vIKanmAe62KxsrN+sqA=
github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks=
github.com/RoaringBitmap/roaring v0.4.21/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
github.com/RoaringBitmap/roaring v0.4.23 h1:gpyfd12QohbqhFO4NVDUdoPOCXsyahYRQhINmlHxKeo=
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/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/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/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=
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
github.com/blevesearch/mmap-go v1.0.2 h1:JtMHb+FgQCTTYIhtMvimw15dJwu1Y5lrZDMOFXVWPk0=
github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+7LMvAB5IbSA=
github.com/blevesearch/segment v0.9.0 h1:5lG7yBCx98or7gK2cHMKPukPZ/31Kag7nONpoBt22Ac=
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/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=
github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg=
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cloudflare-go v0.10.2/go.mod h1:qhVI5MKwBGhdNU89ZRz2plgYutcJ5PCekLxXn56w6SY=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
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-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=
github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k=
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=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d h1:SwD98825d6bdB+pEuTxWOXiSjBrHdOl/UVp75eI7JT8=
github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 h1:iwZdTE0PVqJCos1vaoKsclOGD3ADKpshg3SRtYBbwso=
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
github.com/cznic/strutil v0.0.0-20181122101858-275e90344537 h1:MZRmHqDBd0vxNwenEbKSQqRVT24d3C05ft8kduSwlqM=
github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dsnet/compress v0.0.0-20171208185109-cc9eb1d7ad76 h1:eX+pdPPlD279OWgdx7f6KqIRSONuK7egk+jDx7OM3Ac=
github.com/dsnet/compress v0.0.0-20171208185109-cc9eb1d7ad76/go.mod h1:KjxHHirfLaw19iGT70HvVjHQsL1vq1SRQB4yOsAfy2s=
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 h1:aaQcKT9WumO6JEJcRyTqFVq4XUZiUcKR2/GI31TOcz8=
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
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/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=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
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/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/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=
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd h1:r04MMPyLHj/QwZuMJ5+7tJcBr1AQjpiAK/rZWRrQT7o=
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493 h1:OTanQnFt0bi5iLFSdbEVA/idR6Q2WhCm+deb7ir2CcM=
github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2 h1:Ujru1hufTHVb++eG6OuNDKMxZnGIvF6o/u8q/8h2+I4=
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a h1:FQqoVvjbiUioBBFUL5up+h+GdCa/AnJsL/1bIs/veSI=
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-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-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=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
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/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/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gophercloud/gophercloud v0.3.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 h1:twflg0XRTjwKpxb/jFExr4HGq6on2dEOmnL6FV+fgPw=
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
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/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/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=
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jmhodges/levigo v0.0.0-20161115193449-c42d9e0ca023 h1:y5P5G9cANJZt3MXlMrgELo5mNLZPXH8aGFFFG7IzPU0=
github.com/jmhodges/levigo v0.0.0-20161115193449-c42d9e0ca023/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 h1:K//n/AqR5HjG3qxbrBCL4vJPW0MVFSs9CPK1OOJdRME=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
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=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
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=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kjk/betterguid v0.0.0-20170621091430-c442874ba63a h1:b+Gt8sQs//Sl5Dcem5zP9Qc2FgEUAygREa2AAa2Vmcw=
github.com/kjk/betterguid v0.0.0-20170621091430-c442874ba63a/go.mod h1:uxRAhHE1nl34DpWgfe0CYbNYbCnYplaB6rZH9ReWtUk=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
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/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=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA=
github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w=
github.com/linode/linodego v0.10.0/go.mod h1:cziNP7pbvE3mXIPneHj0oRY8L1WtGEIKlZ8LANE4eXA=
github.com/liquidweb/liquidweb-go v1.6.0/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVLEIG/i5J9cyixzQ=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/miekg/dns v1.0.15 h1:9+UupePBQCG6zf1q/bGmTO1vumoG13jsrbWOSX1W6Tw=
github.com/miekg/dns v1.0.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KHeTRY+7I=
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
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/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
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.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/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-vnc v0.0.0-20150629162542-723ed9867aed/go.mod h1:3rdaFaCv4AyBgu5ALFM0+tSuHrBh6v692nyQe3ikrq0=
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/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=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae h1:VeRdUYdCw49yizlSbMEn2SZ+gT+3IUKx8BqxyQdz+BY=
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
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=
github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I=
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
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/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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446 h1:/NRJ5vAYoqz+7sG51ubIDHXeWO8DlTSrToPu6q11ziA=
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
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=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20190512091148-babf20351dd7 h1:FUL3b97ZY2EPqg2NbXKuMHs5pXJB9hjj1fDHnF2vl28=
github.com/remyoudompheng/bigfft v0.0.0-20190512091148-babf20351dd7/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
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/sacloud/libsacloud v1.26.1/go.mod h1:79ZwATmHLIFZIMd7sxA3LwzVy/B77uj3LDoToVTxDoQ=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
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=
github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg=
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.2.1 h1:bIcUwXqLseLF3BDAZduuNfekWG87ibtFxi59Bq+oI9M=
github.com/spf13/viper v1.2.1/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI=
github.com/steveyen/gtreap v0.0.0-20150807155958-0abe01ef9be2 h1:JNEGSiWg6D3lcBCMCBqN3ELniXujt+0QNHLhNnO0w3s=
github.com/steveyen/gtreap v0.0.0-20150807155958-0abe01ef9be2/go.mod h1:mjqs7N0Q6m5HpR7QfXVBZXZWSqTjQLeTujjA/xUp2uw=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
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/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=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/syndtr/goleveldb v0.0.0-20181105012736-f9080354173f h1:EEVjSRihF8NIbfyCcErpSpNHEKrY3s8EAwqiPENZZn8=
github.com/syndtr/goleveldb v0.0.0-20181105012736-f9080354173f/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
github.com/tecbot/gorocksdb v0.0.0-20181010114359-8752a9433481 h1:HOxvxvnntLiPn123Fk+twfUhCQdMDaqmb0cclArW0T0=
github.com/tecbot/gorocksdb v0.0.0-20181010114359-8752a9433481/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8=
github.com/tinylib/msgp v0.0.0-20180215042507-3b5c87ab5fb0 h1:uAwzi+JwkDdOtQZVqPYljFvJr7i43ZgUYXKypk9Eibk=
github.com/tinylib/msgp v0.0.0-20180215042507-3b5c87ab5fb0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/willf/bitset v1.1.9 h1:GBtFynGY9ZWZmEC9sWuu41/7VBXPFCOAbCbqTflOg9c=
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/xenolf/lego v1.2.1 h1:wAsBCIaTDlgYbR/yVuP0gzcnZrA94NVc84K6vfOIyyA=
github.com/xenolf/lego v1.2.1/go.mod h1:fwiGnfsIjG7OHPfOvgK7Y/Qo6+2Ox0iozjNTkZICKbY=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 h1:kkXA53yGe04D0adEYJwEVQjeBppL01Exg+fnMjfUraU=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
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/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=
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8=
github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7/go.mod h1:imsgLplxEC/etjIhdr3dNzV3JeT27LbVu5pYWm0JCBY=
github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
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=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
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/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=
go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y=
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-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=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
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/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=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180611182652-db08ff08e862/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
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-20181113165502-88d92db4c548 h1:lqFnrcY5rM6XXZ41MVa5mTlOBrBYultJDG1orIvlqPA=
golang.org/x/net v0.0.0-20181113165502-88d92db4c548/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-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=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
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/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=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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-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-20181107165924-66b7b1311ac8 h1:YoY1wS6JYVRpIfFngRf2HHo9R9dAne3xbkGOQ5rJXjU=
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=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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/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=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
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-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=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
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/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=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
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=
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=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/square/go-jose.v2 v2.1.9 h1:YCFbL5T2gbmC2sMG12s1x2PAlTK5TZNte3hjZEIcCAg=
gopkg.in/square/go-jose.v2 v2.1.9/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
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/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/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=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
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=
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=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/sh -
Import="github.com/khlieng/dispatch/version"

196
pkg/https/https.go Normal file
View File

@ -0,0 +1,196 @@
package https
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"net/url"
"time"
"github.com/caddyserver/certmagic"
"github.com/khlieng/dispatch/pkg/netutil"
"github.com/klauspost/cpuid"
)
type Config struct {
Addr string
PortHTTP string
PortHTTPS string
HTTPOnly bool
StoragePath string
Domain string
Email string
Cert string
Key string
}
func Serve(handler http.Handler, cfg Config) error {
errCh := make(chan error, 1)
httpSrv := &http.Server{
Addr: net.JoinHostPort(cfg.Addr, cfg.PortHTTP),
}
if !cfg.HTTPOnly {
httpSrv.ReadTimeout = 5 * time.Second
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,
}
redirect := HTTPSRedirect(cfg.PortHTTPS, handler)
if cfg.Cert != "" || cfg.Key != "" {
httpSrv.Handler = redirect
httpsSrv.TLSConfig = TLSConfig(nil)
go func() {
errCh <- httpSrv.ListenAndServe()
}()
go func() {
errCh <- httpsSrv.ListenAndServeTLS(cfg.Cert, cfg.Key)
}()
} else {
if cfg.StoragePath != "" {
certmagic.Default.Storage = &certmagic.FileStorage{
Path: cfg.StoragePath,
}
}
certmagic.Default.MustStaple = true
magic := certmagic.NewDefault()
acme := certmagic.NewACMEManager(magic, certmagic.ACMEManager{
Agreed: true,
Email: cfg.Email,
})
magic.Issuer = acme
domains := []string{cfg.Domain}
if cfg.Domain == "" {
domains = []string{}
magic.OnDemand = maxObtain(3)
}
err := magic.ManageSync(domains)
if err != nil {
return err
}
httpSrv.Handler = acme.HTTPChallengeHandler(redirect)
httpsSrv.TLSConfig = TLSConfig(magic.TLSConfig())
go func() {
errCh <- httpSrv.ListenAndServe()
}()
go func() {
errCh <- httpsSrv.ListenAndServeTLS("", "")
}()
}
} else {
httpSrv.ReadTimeout = 5 * time.Second
httpSrv.WriteTimeout = 10 * time.Second
httpSrv.IdleTimeout = 120 * time.Second
httpSrv.Handler = handler
return httpSrv.ListenAndServe()
}
return <-errCh
}
func HTTPSRedirect(portHTTPS string, fallback http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
host = r.Host
}
if fallback != nil && netutil.IsPrivate(host) {
fallback.ServeHTTP(w, r)
return
}
u := url.URL{
Scheme: "https",
Host: net.JoinHostPort(host, portHTTPS),
Path: r.RequestURI,
}
w.Header().Set("Connection", "close")
w.Header().Set("Location", u.String())
w.WriteHeader(http.StatusMovedPermanently)
}
}
func TLSConfig(tlsConfig *tls.Config) *tls.Config {
if tlsConfig == nil {
tlsConfig = &tls.Config{}
}
tlsConfig.MinVersion = tls.VersionTLS12
tlsConfig.CipherSuites = defaultCipherSuites()
tlsConfig.CurvePreferences = []tls.CurveID{
tls.X25519,
tls.CurveP256,
}
tlsConfig.PreferServerCipherSuites = true
return tlsConfig
}
func defaultCipherSuites() []uint16 {
if cpuid.CPU.AesNi() {
return []uint16{
tls.TLS_FALLBACK_SCSV,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
}
}
return []uint16{
tls.TLS_FALLBACK_SCSV,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}
}
func maxObtain(limit int) *certmagic.OnDemandConfig {
requested := []string{}
return &certmagic.OnDemandConfig{
DecisionFunc: func(name string) error {
for _, n := range requested {
if name == n {
return nil
}
}
if len(requested) == limit {
return fmt.Errorf("OnDemand cert limit reached")
}
requested = append(requested, name)
return nil
},
}
}

125
pkg/irc/case.go Normal file
View File

@ -0,0 +1,125 @@
package irc
import (
"unicode/utf8"
)
const (
// ASCII maps a-z as the lower case of A-Z
ASCII = "ascii"
// RFC1459 maps a-z and {, |, }, ~ as the lower case of A-Z and [, \, ], ^
RFC1459 = "rfc1459"
// RFC1459Strict maps a-z and {, |, } as the lower case of A-Z and [, \, ]
RFC1459Strict = "strict-rfc1459"
)
func (c *Client) Casefold(s string) string {
mapping := c.Features.String("CASEMAPPING")
if mapping == "" {
mapping = RFC1459
}
return Casefold(mapping, s)
}
func (c *Client) EqualFold(s1, s2 string) bool {
mapping := c.Features.String("CASEMAPPING")
if mapping == "" {
mapping = RFC1459
}
return EqualFold(mapping, s1, s2)
}
func Casefold(mapping, s string) string {
switch mapping {
case ASCII:
return toLower(s, 'Z')
case RFC1459:
return toLower(s, '^')
case RFC1459Strict:
return toLower(s, ']')
}
return s
}
func EqualFold(mapping, s1, s2 string) bool {
switch mapping {
case ASCII:
return equalFold(s1, s2, 'Z')
case RFC1459:
return equalFold(s1, s2, '^')
case RFC1459Strict:
return equalFold(s1, s2, ']')
}
return s1 == s2
}
func toLower(s string, end byte) string {
hasUpper := false
for i := 0; i < len(s); i++ {
c := s[i]
if hasUpper = 'A' <= c && c <= end; hasUpper {
break
}
}
if !hasUpper {
return s
}
b := make([]byte, len(s))
for i := 0; i < len(s); i++ {
c := s[i]
// Skip Unicode characters
if c >= utf8.RuneSelf {
_, size := utf8.DecodeRuneInString(s[i:])
for cEnd := i + size; i < cEnd; i++ {
b[i] = s[i]
}
i--
continue
}
if 'A' <= c && c <= end {
c += 32
}
b[i] = c
}
return string(b)
}
func equalFold(s1, s2 string, end rune) bool {
for s1 != "" && s2 != "" {
var r1, r2 rune
if s1[0] < utf8.RuneSelf {
r1, s1 = rune(s1[0]), s1[1:]
} else {
r, size := utf8.DecodeRuneInString(s1)
r1, s1 = r, s1[size:]
}
if s2[0] < utf8.RuneSelf {
r2, s2 = rune(s2[0]), s2[1:]
} else {
r, size := utf8.DecodeRuneInString(s2)
r2, s2 = r, s2[size:]
}
if r1 == r2 {
continue
}
if r2 < r1 {
r2, r1 = r1, r2
}
if 'A' <= r1 && r1 <= end && r2 == r1+32 {
continue
}
return false
}
return s1 == s2
}

27
pkg/irc/case_test.go Normal file
View File

@ -0,0 +1,27 @@
package irc
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCasefold(t *testing.T) {
assert.Equal(t, "caላke[^", Casefold(ASCII, "CaላkE[^"))
assert.Equal(t, "caላke{~", Casefold(RFC1459, "CaላkE[^"))
assert.Equal(t, "caላke{^", Casefold(RFC1459Strict, "CaላkE[^"))
}
func TestEqualFold(t *testing.T) {
assert.True(t, EqualFold(ASCII, "caላke[^", "CaላkE[^"))
assert.False(t, EqualFold(ASCII, "caላke{~", "CaላkE[^"))
assert.True(t, EqualFold(RFC1459, "caላke{~", "CaላkE[^"))
assert.False(t, EqualFold(RFC1459, "cላke[^", "CaላkE[^"))
assert.True(t, EqualFold(RFC1459Strict, "caላke{^", "CaላkE[^"))
assert.False(t, EqualFold(RFC1459Strict, "caላke[~", "CaላkE[^"))
assert.True(t, EqualFold(ASCII, "", ""))
assert.False(t, EqualFold(ASCII, "", " "))
}

View File

@ -11,26 +11,27 @@ import (
)
type Client struct {
Server string
Host string
TLS bool
TLSConfig *tls.Config
Password string
Username string
Realname string
Server string
Host string
TLS bool
TLSConfig *tls.Config
Password string
Username string
Realname string
HandleNickInUse func(string) string
Messages chan *Message
ConnectionChanged chan ConnectionState
HandleNickInUse func(string) string
nick string
channels []string
Support *iSupport
Features *Features
nick string
channels []string
conn net.Conn
connected bool
registered bool
dialer *net.Dialer
reader *bufio.Reader
recvBuf []byte
scan *bufio.Scanner
backoff *backoff.Backoff
out chan string
@ -43,7 +44,7 @@ type Client struct {
func NewClient(nick, username string) *Client {
return &Client{
nick: nick,
Support: newISupport(),
Features: NewFeatures(),
Username: username,
Realname: nick,
Messages: make(chan *Message, 32),
@ -51,6 +52,7 @@ func NewClient(nick, username string) *Client {
out: make(chan string, 32),
quit: make(chan struct{}),
reconnect: make(chan struct{}),
recvBuf: make([]byte, 0, 4096),
backoff: &backoff.Backoff{
Jitter: true,
},
@ -151,6 +153,10 @@ func (c *Client) Away(message string) {
c.Write("AWAY :" + message)
}
func (c *Client) List() {
c.Write("LIST")
}
func (c *Client) writePass(password string) {
c.write("PASS " + password)
}

View File

@ -2,6 +2,7 @@ package irc
import (
"bufio"
"bytes"
"crypto/tls"
"crypto/x509"
"errors"
@ -119,9 +120,6 @@ func (c *Client) tryConnect() {
return
}
} else {
c.backoff.Reset()
c.flushChannels()
return
}
@ -151,7 +149,8 @@ func (c *Client) connect() error {
c.connected = true
c.connChange(true, nil)
c.reader = bufio.NewReader(c.conn)
c.scan = bufio.NewScanner(c.conn)
c.scan.Buffer(c.recvBuf, cap(c.recvBuf))
c.register()
@ -185,25 +184,24 @@ func (c *Client) recv() {
defer c.sendRecv.Done()
for {
line, err := c.reader.ReadString('\n')
if err != nil {
if !c.scan.Scan() {
select {
case <-c.quit:
return
default:
c.connChange(false, nil)
if !c.Registered() {
time.Sleep(15 * time.Second)
}
c.Reconnect()
return
}
}
msg := parseMessage(line)
b := bytes.Trim(c.scan.Bytes(), " ")
if len(b) == 0 {
continue
}
msg := ParseMessage(string(b))
if msg == nil {
close(c.quit)
c.connChange(false, ErrBadProtocol)
@ -215,23 +213,26 @@ func (c *Client) recv() {
go c.write("PONG :" + msg.LastParam())
case Join:
if msg.Nick == c.GetNick() {
if c.EqualFold(msg.Nick, c.GetNick()) {
c.addChannel(msg.Params[0])
}
case Nick:
if msg.Nick == c.GetNick() {
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.Support.parse(msg.Params)
c.Features.Parse(msg.Params)
case ErrNicknameInUse:
if c.HandleNickInUse != nil {

View File

@ -129,19 +129,21 @@ func TestRecv(t *testing.T) {
buf.WriteString("CMD\r\n")
buf.WriteString("PING :test\r\n")
buf.WriteString("001 foo\r\n")
c.reader = bufio.NewReader(buf)
c.scan = bufio.NewScanner(buf)
c.sendRecv.Add(1)
go c.recv()
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)
}
func TestRecvTriggersReconnect(t *testing.T) {
c := testClient()
c.conn = &mockConn{}
c.reader = bufio.NewReader(bytes.NewBufferString("001 bob\r\n"))
c.scan = bufio.NewScanner(bytes.NewBufferString("001 bob\r\n"))
done := make(chan struct{})
ok := false
go func() {
@ -210,24 +212,32 @@ func waitConnAndClose(t *testing.T, c *Client) {
}
var testCert = []byte(`-----BEGIN CERTIFICATE-----
MIIB0zCCAX2gAwIBAgIJAI/M7BYjwB+uMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTIwOTEyMjE1MjAyWhcNMTUwOTEyMjE1MjAyWjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANLJ
hPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wok/4xIA+ui35/MmNa
rtNuC+BdZ1tMuVCPFZcCAwEAAaNQME4wHQYDVR0OBBYEFJvKs8RfJaXTH08W+SGv
zQyKn0H8MB8GA1UdIwQYMBaAFJvKs8RfJaXTH08W+SGvzQyKn0H8MAwGA1UdEwQF
MAMBAf8wDQYJKoZIhvcNAQEFBQADQQBJlffJHybjDGxRMqaRmDhX0+6v02TUKZsW
r5QuVbpQhH6u+0UgcW0jp9QwpxoPTLTWGXEWBBBurxFwiCBhkQ+V
MIICEzCCAXygAwIBAgIQMIMChMLGrR+QvmQvpwAU6zANBgkqhkiG9w0BAQsFADAS
MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
iQKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9SjY1bIw4
iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZBl2+XsDul
rKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQABo2gwZjAO
BgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUw
AwEB/zAuBgNVHREEJzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAA
AAAAATANBgkqhkiG9w0BAQsFAAOBgQCEcetwO59EWk7WiJsG4x8SY+UIAA+flUI9
tyC4lNhbcF2Idq9greZwbYCqTTTr2XiRNSMLCOjKyI7ukPoPjo16ocHj+P3vZGfs
h1fIw3cSS2OolhloGw/XM6RWPWtPAlGykKLciQrBru5NAPvCMsb/I1DAceTiotQM
fblo6RBxUQ==
-----END CERTIFICATE-----`)
var testKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBANLJhPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wo
k/4xIA+ui35/MmNartNuC+BdZ1tMuVCPFZcCAwEAAQJAEJ2N+zsR0Xn8/Q6twa4G
6OB1M1WO+k+ztnX/1SvNeWu8D6GImtupLTYgjZcHufykj09jiHmjHx8u8ZZB/o1N
MQIhAPW+eyZo7ay3lMz1V01WVjNKK9QSn1MJlb06h/LuYv9FAiEA25WPedKgVyCW
SmUwbPw8fnTcpqDWE3yTO3vKcebqMSsCIBF3UmVue8YU3jybC3NxuXq3wNm34R8T
xVLHwDXh/6NJAiEAl2oHGGLz64BuAfjKrqwz7qMYr9HCLIe/YsoWq/olzScCIQDi
D2lWusoe2/nEqfDVVWGWlyJ7yOmqaVm/iNUN9B2N2g==
MIICXgIBAAKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9
SjY1bIw4iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZB
l2+XsDulrKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQAB
AoGAGRzwwir7XvBOAy5tM/uV6e+Zf6anZzus1s1Y1ClbjbE6HXbnWWF/wbZGOpet
3Zm4vD6MXc7jpTLryzTQIvVdfQbRc6+MUVeLKwZatTXtdZrhu+Jk7hx0nTPy8Jcb
uJqFk541aEw+mMogY/xEcfbWd6IOkp+4xqjlFLBEDytgbIECQQDvH/E6nk+hgN4H
qzzVtxxr397vWrjrIgPbJpQvBsafG7b0dA4AFjwVbFLmQcj2PprIMmPcQrooz8vp
jy4SHEg1AkEA/v13/5M47K9vCxmb8QeD/asydfsgS5TeuNi8DoUBEmiSJwma7FXY
fFUtxuvL7XvjwjN5B30pNEbc6Iuyt7y4MQJBAIt21su4b3sjXNueLKH85Q+phy2U
fQtuUE9txblTu14q3N7gHRZB4ZMhFYyDy8CKrN2cPg/Fvyt0Xlp/DoCzjA0CQQDU
y2ptGsuSmgUtWj3NM9xuwYPm+Z/F84K6+ARYiZ6PYj013sovGKUFfYAqVXVlxtIX
qyUBnu3X9ps8ZfjLZO7BAkEAlT4R5Yl6cGhaJQYZHOde3JEMhNRcVFMO8dJDaFeo
f9Oeos0UUothgiDktdQHxdNEwLjQf7lJJBzV+5OtwswCWA==
-----END RSA PRIVATE KEY-----`)

View File

@ -28,6 +28,8 @@ const (
ReplyWhoisIdle = "317"
ReplyEndOfWhois = "318"
ReplyWhoisChannels = "319"
ReplyList = "322"
ReplyListEnd = "323"
ReplyNoTopic = "331"
ReplyTopic = "332"
ReplyNamReply = "353"
@ -37,4 +39,5 @@ const (
ReplyEndOfMotd = "376"
ErrErroneousNickname = "432"
ErrNicknameInUse = "433"
ErrForward = "470"
)

134
pkg/irc/feature.go Normal file
View File

@ -0,0 +1,134 @@
package irc
import (
"strconv"
"strings"
"sync"
)
type Features struct {
m map[string]interface{}
lock sync.Mutex
}
func NewFeatures() *Features {
return &Features{
m: map[string]interface{}{},
}
}
func (f *Features) Map() map[string]interface{} {
m := map[string]interface{}{}
f.lock.Lock()
for k, v := range f.m {
m[k] = v
}
f.lock.Unlock()
return m
}
func (f *Features) Parse(params []string) {
f.lock.Lock()
for _, param := range params[1 : len(params)-1] {
key, val := splitParam(param)
if key == "" {
continue
}
if key[0] == '-' {
delete(f.m, key[1:])
} else {
if t, ok := featureTransforms[key]; ok {
f.m[key] = t(val)
} else {
f.m[key] = val
}
}
}
f.lock.Unlock()
}
func (f *Features) Has(key string) bool {
f.lock.Lock()
_, has := f.m[key]
f.lock.Unlock()
return has
}
func (f *Features) Get(key string) interface{} {
f.lock.Lock()
v := f.m[key]
f.lock.Unlock()
return v
}
func (f *Features) String(key string) string {
if v, ok := f.Get(key).(string); ok {
return v
}
return ""
}
func (f *Features) Int(key string) int {
if v, ok := f.Get(key).(int); ok {
return v
}
return 0
}
type featureTransform func(interface{}) interface{}
func toInt(v interface{}) interface{} {
s := v.(string)
if s == "" {
return 0
}
i, _ := strconv.Atoi(s)
return i
}
func toCharList(v interface{}) interface{} {
s := v.(string)
list := make([]string, len(s))
for i := range s {
list[i] = s[i : i+1]
}
return list
}
func parseChanlimit(v interface{}) interface{} {
limits := map[string]int{}
pairs := strings.Split(v.(string), ",")
for _, p := range pairs {
pair := strings.Split(p, ":")
if len(pair) == 2 {
prefixes := pair[0]
limit, _ := strconv.Atoi(pair[1])
for i := range prefixes {
limits[prefixes[i:i+1]] = limit
}
}
}
return limits
}
var featureTransforms = map[string]featureTransform{
"AWAYLEN": toInt,
"CHANLIMIT": parseChanlimit,
"CHANNELLEN": toInt,
"CHANTYPES": toCharList,
"HOSTLEN": toInt,
"KICKLEN": toInt,
"MAXCHANNELS": toInt,
"MAXTARGETS": toInt,
"MODES": toInt,
"NICKLEN": toInt,
"TOPICLEN": toInt,
"USERLEN": toInt,
}

49
pkg/irc/feature_test.go Normal file
View File

@ -0,0 +1,49 @@
package irc
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseFeatures(t *testing.T) {
s := NewFeatures()
featureTransforms["CAKE"] = toInt
s.Parse([]string{"bob", "CAKE=31", "PIE", ":durr"})
assert.Equal(t, 31, s.Int("CAKE"))
assert.Equal(t, "", s.String("CAKE"))
assert.True(t, s.Has("CAKE"))
assert.True(t, s.Has("PIE"))
assert.False(t, s.Has("APPLES"))
assert.Equal(t, "", s.String("APPLES"))
assert.Equal(t, 0, s.Int("APPLES"))
s.Parse([]string{"bob", "-PIE", ":hurr"})
assert.False(t, s.Has("PIE"))
s.Parse([]string{"bob", "CAKE=1337", ":durr"})
assert.Equal(t, 1337, s.Int("CAKE"))
s.Parse([]string{"bob", "CAKE=", ":durr"})
assert.Equal(t, "", s.String("CAKE"))
assert.True(t, s.Has("CAKE"))
delete(featureTransforms, "CAKE")
s.Parse([]string{"bob", "CAKE===", ":durr"})
assert.Equal(t, "==", s.String("CAKE"))
s.Parse([]string{"bob", "-CAKE=31", ":durr"})
assert.False(t, s.Has("CAKE"))
s.Parse([]string{"bob", "CHANLIMIT=#&:50", ":durr"})
assert.Equal(t, map[string]int{"#": 50, "&": 50}, s.Get("CHANLIMIT"))
s.Parse([]string{"bob", "CHANLIMIT=#:50,&:25", ":durr"})
assert.Equal(t, map[string]int{"#": 50, "&": 25}, s.Get("CHANLIMIT"))
s.Parse([]string{"bob", "CHANLIMIT=&:50,#:", ":durr"})
assert.Equal(t, map[string]int{"#": 0, "&": 50}, s.Get("CHANLIMIT"))
s.Parse([]string{"bob", "CHANTYPES=#&", ":durr"})
assert.Equal(t, []string{"#", "&"}, s.Get("CHANTYPES"))
}

View File

@ -2,9 +2,6 @@ package irc
import (
"strings"
"sync"
"github.com/spf13/cast"
)
type Message struct {
@ -22,8 +19,7 @@ func (m *Message) LastParam() string {
return ""
}
func parseMessage(line string) *Message {
line = strings.Trim(line, "\r\n ")
func ParseMessage(line string) *Message {
msg := Message{}
if strings.HasPrefix(line, "@") {
@ -35,21 +31,24 @@ func parseMessage(line string) *Message {
if len(tags) > 0 {
msg.Tags = map[string]string{}
}
for _, tag := range tags {
key, val := splitParam(tag)
if key == "" {
continue
}
for _, tag := range tags {
key, val := splitParam(tag)
if key == "" {
continue
}
if val != "" {
msg.Tags[key] = unescapeTag(val)
} else {
msg.Tags[key] = ""
if val != "" {
msg.Tags[key] = unescapeTag(val)
} else {
msg.Tags[key] = ""
}
}
}
for line[next+1] == ' ' {
next++
}
line = line[next+1:]
}
@ -73,7 +72,7 @@ func parseMessage(line string) *Message {
cmdEnd := len(line)
trailing := ""
if i := strings.Index(line, " :"); i > 0 {
if i := strings.Index(line, " :"); i >= 0 {
cmdEnd = i
trailing = line[i+2:]
}
@ -94,55 +93,6 @@ func parseMessage(line string) *Message {
return &msg
}
type iSupport struct {
support map[string]string
lock sync.Mutex
}
func newISupport() *iSupport {
return &iSupport{
support: map[string]string{},
}
}
func (i *iSupport) parse(params []string) {
i.lock.Lock()
for _, param := range params[1 : len(params)-1] {
key, val := splitParam(param)
if key == "" {
continue
}
if key[0] == '-' {
delete(i.support, key[1:])
} else {
i.support[key] = val
}
}
i.lock.Unlock()
}
func (i *iSupport) Has(key string) bool {
i.lock.Lock()
_, has := i.support[key]
i.lock.Unlock()
return has
}
func (i *iSupport) Get(key string) string {
i.lock.Lock()
v := i.support[key]
i.lock.Unlock()
return v
}
func (i *iSupport) GetInt(key string) int {
i.lock.Lock()
v := cast.ToInt(i.support[key])
i.lock.Unlock()
return v
}
func splitParam(param string) (string, string) {
parts := strings.SplitN(param, "=", 2)
if len(parts) == 2 {
@ -151,11 +101,14 @@ func splitParam(param string) (string, string) {
return parts[0], ""
}
var unescapeTagReplacer = strings.NewReplacer(
"\\:", ";",
"\\s", " ",
"\\\\", "\\",
"\\r", "\r",
"\\n", "\n",
)
func unescapeTag(s string) string {
s = strings.Replace(s, "\\:", ";", -1)
s = strings.Replace(s, "\\s", " ", -1)
s = strings.Replace(s, "\\\\", "\\", -1)
s = strings.Replace(s, "\\r", "\r", -1)
s = strings.Replace(s, "\\n", "\n", -1)
return s
return unescapeTagReplacer.Replace(s)
}

View File

@ -12,7 +12,7 @@ func TestParseMessage(t *testing.T) {
expected *Message
}{
{
":user CMD #chan :some message\r\n",
":user CMD #chan :some message",
&Message{
Prefix: "user",
Nick: "user",
@ -20,7 +20,7 @@ func TestParseMessage(t *testing.T) {
Params: []string{"#chan", "some message"},
},
}, {
":nick!user@host.com CMD a b\r\n",
":nick!user@host.com CMD a b",
&Message{
Prefix: "nick!user@host.com",
Nick: "nick",
@ -28,80 +28,80 @@ func TestParseMessage(t *testing.T) {
Params: []string{"a", "b"},
},
}, {
"CMD a b :\r\n",
"CMD a b :",
&Message{
Command: "CMD",
Params: []string{"a", "b", ""},
},
}, {
"CMD a b\r\n",
"CMD a b",
&Message{
Command: "CMD",
Params: []string{"a", "b"},
},
}, {
"CMD\r\n",
"CMD",
&Message{
Command: "CMD",
},
}, {
"CMD :tests and stuff\r\n",
"CMD :tests and stuff",
&Message{
Command: "CMD",
Params: []string{"tests and stuff"},
},
}, {
":nick@host.com CMD\r\n",
":nick@host.com CMD",
&Message{
Prefix: "nick@host.com",
Nick: "nick",
Command: "CMD",
},
}, {
":ni@ck!user!name@host!.com CMD\r\n",
":ni@ck!user!name@host!.com CMD",
&Message{
Prefix: "ni@ck!user!name@host!.com",
Nick: "ni@ck",
Command: "CMD",
},
}, {
"CMD #cake pie \r\n",
"CMD #cake pie ",
&Message{
Command: "CMD",
Params: []string{"#cake", "pie"},
},
}, {
" CMD #cake pie\r\n",
" CMD #cake pie",
&Message{
Command: "CMD",
Params: []string{"#cake", "pie"},
},
}, {
"CMD #cake ::pie\r\n",
"CMD #cake ::pie",
&Message{
Command: "CMD",
Params: []string{"#cake", ":pie"},
},
}, {
"CMD #cake : pie\r\n",
"CMD #cake : pie",
&Message{
Command: "CMD",
Params: []string{"#cake", " pie"},
},
}, {
"CMD #cake :pie :P <3\r\n",
"CMD #cake :pie :P <3",
&Message{
Command: "CMD",
Params: []string{"#cake", "pie :P <3"},
},
}, {
"CMD #cake :pie!\r\n",
"CMD #cake :pie!",
&Message{
Command: "CMD",
Params: []string{"#cake", "pie!"},
},
}, {
"@x=y CMD\r\n",
"@x=y CMD",
&Message{
Tags: map[string]string{
"x": "y",
@ -109,7 +109,7 @@ func TestParseMessage(t *testing.T) {
Command: "CMD",
},
}, {
"@x=y :nick!user@host.com CMD\r\n",
"@x=y :nick!user@host.com CMD",
&Message{
Tags: map[string]string{
"x": "y",
@ -119,7 +119,7 @@ func TestParseMessage(t *testing.T) {
Command: "CMD",
},
}, {
"@x=y :nick!user@host.com CMD :pie and cake\r\n",
"@x=y :nick!user@host.com CMD :pie and cake",
&Message{
Tags: map[string]string{
"x": "y",
@ -130,7 +130,19 @@ func TestParseMessage(t *testing.T) {
Params: []string{"pie and cake"},
},
}, {
"@x=y;a=b CMD\r\n",
"@x=y :nick!user@host.com CMD beans rainbows :pie and cake",
&Message{
Tags: map[string]string{
"x": "y",
},
Prefix: "nick!user@host.com",
Nick: "nick",
Command: "CMD",
Params: []string{"beans", "rainbows", "pie and cake"},
},
},
{
"@x=y;a=b CMD",
&Message{
Tags: map[string]string{
"x": "y",
@ -139,7 +151,7 @@ func TestParseMessage(t *testing.T) {
Command: "CMD",
},
}, {
"@x=y;a=\\\\\\:\\s\\r\\n CMD\r\n",
"@x=y;a=\\\\\\:\\s\\r\\n CMD",
&Message{
Tags: map[string]string{
"x": "y",
@ -151,49 +163,27 @@ func TestParseMessage(t *testing.T) {
}
for _, tc := range cases {
assert.Equal(t, tc.expected, parseMessage(tc.input))
assert.Equal(t, tc.expected, ParseMessage(tc.input))
}
}
func BenchmarkParseMessage(b *testing.B) {
for i := 0; i < b.N; i++ {
ParseMessage("@x=y :nick!user@host.com CMD beans rainbows :pie and cake")
}
}
func TestLastParam(t *testing.T) {
assert.Equal(t, "some message", parseMessage(":user CMD #chan :some message\r\n").LastParam())
assert.Equal(t, "", parseMessage("NO_PARAMS").LastParam())
assert.Equal(t, "some message", ParseMessage(":user CMD #chan :some message").LastParam())
assert.Equal(t, "", ParseMessage("NO_PARAMS").LastParam())
}
func TestBadMessagePanic(t *testing.T) {
parseMessage("@\r\n")
parseMessage("@ :\r\n")
parseMessage("@ :\r\n")
parseMessage(":user\r\n")
parseMessage(":\r\n")
parseMessage(":")
parseMessage("")
}
func TestParseISupport(t *testing.T) {
s := newISupport()
s.parse([]string{"bob", "CAKE=31", "PIE", ":durr"})
assert.Equal(t, 31, s.GetInt("CAKE"))
assert.Equal(t, "31", s.Get("CAKE"))
assert.True(t, s.Has("CAKE"))
assert.True(t, s.Has("PIE"))
assert.False(t, s.Has("APPLES"))
assert.Equal(t, "", s.Get("APPLES"))
assert.Equal(t, 0, s.GetInt("APPLES"))
s.parse([]string{"bob", "-PIE", ":hurr"})
assert.False(t, s.Has("PIE"))
s.parse([]string{"bob", "CAKE=1337", ":durr"})
assert.Equal(t, 1337, s.GetInt("CAKE"))
s.parse([]string{"bob", "CAKE=", ":durr"})
assert.Equal(t, "", s.Get("CAKE"))
assert.True(t, s.Has("CAKE"))
s.parse([]string{"bob", "CAKE===", ":durr"})
assert.Equal(t, "==", s.Get("CAKE"))
s.parse([]string{"bob", "-CAKE=31", ":durr"})
assert.False(t, s.Has("CAKE"))
func TestBadMessage(t *testing.T) {
assert.Nil(t, ParseMessage("@"))
assert.Nil(t, ParseMessage("@ :"))
assert.Nil(t, ParseMessage("@ :"))
assert.Nil(t, ParseMessage("@ :"))
assert.Nil(t, ParseMessage(":user"))
assert.Nil(t, ParseMessage(":"))
assert.Nil(t, ParseMessage(""))
}

View File

@ -1,38 +0,0 @@
package letsencrypt
import (
"path/filepath"
)
type Directory string
func (d Directory) Domain(domain string) string {
return filepath.Join(string(d), "certs", domain)
}
func (d Directory) Cert(domain string) string {
return filepath.Join(d.Domain(domain), "cert.pem")
}
func (d Directory) Key(domain string) string {
return filepath.Join(d.Domain(domain), "key.pem")
}
func (d Directory) Meta(domain string) string {
return filepath.Join(d.Domain(domain), "metadata.json")
}
func (d Directory) User(email string) string {
if email == "" {
email = defaultUser
}
return filepath.Join(string(d), "users", email)
}
func (d Directory) UserRegistration(email string) string {
return filepath.Join(d.User(email), "registration.json")
}
func (d Directory) UserKey(email string) string {
return filepath.Join(d.User(email), "key.pem")
}

View File

@ -1,265 +0,0 @@
package letsencrypt
import (
"crypto/tls"
"encoding/json"
"io/ioutil"
"os"
"sync"
"time"
"github.com/xenolf/lego/acme"
)
const URL = "https://acme-v02.api.letsencrypt.org/directory"
const KeySize = 2048
var directory Directory
func Run(dir, domain, email, port string) (*state, error) {
directory = Directory(dir)
user, err := getUser(email)
if err != nil {
return nil, err
}
client, err := acme.NewClient(URL, &user, acme.RSA2048)
if err != nil {
return nil, err
}
client.SetHTTPAddress(port)
if user.Registration == nil {
user.Registration, err = client.Register(true)
if err != nil {
return nil, err
}
err = saveUser(user)
if err != nil {
return nil, err
}
}
s := &state{
client: client,
domain: domain,
}
if certExists(domain) {
if !s.renew() {
err = s.loadCert()
if err != nil {
return nil, err
}
}
s.refreshOCSP()
} else {
err = s.obtain()
if err != nil {
return nil, err
}
}
go s.maintain()
return s, nil
}
type state struct {
client *acme.Client
domain string
cert *tls.Certificate
certPEM []byte
lock sync.Mutex
}
func (s *state) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
s.lock.Lock()
cert := s.cert
s.lock.Unlock()
return cert, nil
}
func (s *state) getCertPEM() []byte {
s.lock.Lock()
certPEM := s.certPEM
s.lock.Unlock()
return certPEM
}
func (s *state) setCert(meta *acme.CertificateResource) {
cert, err := tls.X509KeyPair(meta.Certificate, meta.PrivateKey)
if err == nil {
s.lock.Lock()
if s.cert != nil {
cert.OCSPStaple = s.cert.OCSPStaple
}
s.cert = &cert
s.certPEM = meta.Certificate
s.lock.Unlock()
}
}
func (s *state) setOCSP(ocsp []byte) {
cert := tls.Certificate{
OCSPStaple: ocsp,
}
s.lock.Lock()
if s.cert != nil {
cert.Certificate = s.cert.Certificate
cert.PrivateKey = s.cert.PrivateKey
}
s.cert = &cert
s.lock.Unlock()
}
func (s *state) obtain() error {
cert, err := s.client.ObtainCertificate([]string{s.domain}, true, nil, false)
if err != nil {
return err
}
s.setCert(cert)
s.refreshOCSP()
err = saveCert(cert)
if err != nil {
return err
}
return nil
}
func (s *state) renew() bool {
cert, err := ioutil.ReadFile(directory.Cert(s.domain))
if err != nil {
return false
}
exp, err := acme.GetPEMCertExpiration(cert)
if err != nil {
return false
}
daysLeft := int(exp.Sub(time.Now().UTC()).Hours() / 24)
if daysLeft <= 30 {
metaBytes, err := ioutil.ReadFile(directory.Meta(s.domain))
if err != nil {
return false
}
key, err := ioutil.ReadFile(directory.Key(s.domain))
if err != nil {
return false
}
var meta acme.CertificateResource
err = json.Unmarshal(metaBytes, &meta)
if err != nil {
return false
}
meta.Certificate = cert
meta.PrivateKey = key
newMeta, err := s.client.RenewCertificate(meta, true, false)
if err != nil {
return false
}
s.setCert(newMeta)
err = saveCert(newMeta)
if err != nil {
return false
}
return true
}
return false
}
func (s *state) refreshOCSP() {
ocsp, resp, err := acme.GetOCSPForCert(s.getCertPEM())
if err == nil && resp.Status == acme.OCSPGood {
s.setOCSP(ocsp)
}
}
func (s *state) maintain() {
renew := time.Tick(24 * time.Hour)
ocsp := time.Tick(1 * time.Hour)
for {
select {
case <-renew:
s.renew()
case <-ocsp:
s.refreshOCSP()
}
}
}
func (s *state) loadCert() error {
cert, err := ioutil.ReadFile(directory.Cert(s.domain))
if err != nil {
return err
}
key, err := ioutil.ReadFile(directory.Key(s.domain))
if err != nil {
return err
}
s.setCert(&acme.CertificateResource{
Certificate: cert,
PrivateKey: key,
})
return nil
}
func certExists(domain string) bool {
if _, err := os.Stat(directory.Cert(domain)); err != nil {
return false
}
if _, err := os.Stat(directory.Key(domain)); err != nil {
return false
}
return true
}
func saveCert(cert *acme.CertificateResource) error {
err := os.MkdirAll(directory.Domain(cert.Domain), 0700)
if err != nil {
return err
}
err = ioutil.WriteFile(directory.Cert(cert.Domain), cert.Certificate, 0600)
if err != nil {
return err
}
err = ioutil.WriteFile(directory.Key(cert.Domain), cert.PrivateKey, 0600)
if err != nil {
return err
}
jsonBytes, err := json.MarshalIndent(&cert, "", " ")
if err != nil {
return err
}
err = ioutil.WriteFile(directory.Meta(cert.Domain), jsonBytes, 0600)
if err != nil {
return err
}
return nil
}

View File

@ -1,109 +0,0 @@
package letsencrypt
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"io/ioutil"
"os"
"github.com/xenolf/lego/acme"
)
const defaultUser = "default"
type User struct {
Email string
Registration *acme.RegistrationResource
key crypto.PrivateKey
}
func (u User) GetEmail() string {
return u.Email
}
func (u User) GetRegistration() *acme.RegistrationResource {
return u.Registration
}
func (u User) GetPrivateKey() crypto.PrivateKey {
return u.key
}
func newUser(email string) (User, error) {
var err error
user := User{Email: email}
user.key, err = rsa.GenerateKey(rand.Reader, KeySize)
if err != nil {
return user, err
}
return user, nil
}
func getUser(email string) (User, error) {
var user User
reg, err := os.Open(directory.UserRegistration(email))
if err != nil {
if os.IsNotExist(err) {
return newUser(email)
}
return user, err
}
defer reg.Close()
err = json.NewDecoder(reg).Decode(&user)
if err != nil {
return user, err
}
user.key, err = loadRSAPrivateKey(directory.UserKey(email))
if err != nil {
return user, err
}
return user, nil
}
func saveUser(user User) error {
err := os.MkdirAll(directory.User(user.Email), 0700)
if err != nil {
return err
}
err = saveRSAPrivateKey(user.key, directory.UserKey(user.Email))
if err != nil {
return err
}
jsonBytes, err := json.MarshalIndent(&user, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(directory.UserRegistration(user.Email), jsonBytes, 0600)
}
func loadRSAPrivateKey(file string) (crypto.PrivateKey, error) {
keyBytes, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
keyBlock, _ := pem.Decode(keyBytes)
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
}
func saveRSAPrivateKey(key crypto.PrivateKey, file string) error {
pemKey := pem.Block{
Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key.(*rsa.PrivateKey)),
}
keyOut, err := os.Create(file)
if err != nil {
return err
}
defer keyOut.Close()
return pem.Encode(keyOut, &pemKey)
}

View File

@ -1,35 +0,0 @@
package letsencrypt
import (
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
)
func tempdir() string {
f, _ := ioutil.TempDir("", "")
return f
}
func testUser(t *testing.T, email string) {
user, err := newUser(email)
assert.Nil(t, err)
key := user.GetPrivateKey()
assert.NotNil(t, key)
err = saveUser(user)
assert.Nil(t, err)
user, err = getUser(email)
assert.Nil(t, err)
assert.Equal(t, email, user.GetEmail())
assert.Equal(t, key, user.GetPrivateKey())
}
func TestUser(t *testing.T) {
directory = Directory(tempdir())
testUser(t, "test@test.com")
testUser(t, "")
}

40
pkg/netutil/netutil.go Normal file
View File

@ -0,0 +1,40 @@
package netutil
import "net"
var privateNets []*net.IPNet
func init() {
for _, cidr := range []string{
"127.0.0.0/8",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"::1/128",
"fe80::/10",
"fc00::/7",
} {
_, network, _ := net.ParseCIDR(cidr)
privateNets = append(privateNets, network)
}
}
func IsPrivate(host string) bool {
if host == "localhost" {
return true
}
return IsPrivateIP(net.ParseIP(host))
}
func IsPrivateIP(ip net.IP) bool {
if ip == nil {
return false
}
for _, privateNet := range privateNets {
if privateNet.Contains(ip) {
return true
}
}
return false
}

View File

@ -1,26 +0,0 @@
// Generated by egon.
// 🚫Edit at your own risk.
package server
import (
"io"
)
func IndexTemplate(w io.Writer, cssPath string, inlineScript string, scripts []string) error {
io.WriteString(w, "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><meta name=\"theme-color\" content=\"#f0f0f0\"><title>Dispatch</title><meta name=\"description\" content=\"Web-based IRC client.\"><link rel=\"preload\" href=\"/init\" as=\"fetch\" crossorigin><script>")
io.WriteString(w, inlineScript )
io.WriteString(w, "</script><link rel=\"preload\" href=\"/font/fontello.woff2?48901973\" as=\"font\" type=\"font/woff2\" crossorigin><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 cssPath != "" {
io.WriteString(w, "<link href=\"/")
io.WriteString(w, cssPath )
io.WriteString(w, "\" rel=\"stylesheet\">")
}
io.WriteString(w, "<link rel=\"manifest\" href=\"/manifest.json\"></head><body><div id=\"root\"></div>")
for _, script := range scripts {
io.WriteString(w, "<script src=\"/")
io.WriteString(w, script )
io.WriteString(w, "\"></script>")
}
io.WriteString(w, "<noscript>This page needs JavaScript enabled to function.</noscript></body></html>")
return nil
}

View File

@ -1,43 +1,48 @@
<%! cssPath string, inlineScript string, scripts []string %>
package server
type indexTemplateData struct {
InlineScript string
Stylesheet string
Scripts []string
}
const indexTemplate = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#f0f0f0">
<meta name="theme-color" content="#222">
<title>Dispatch</title>
<meta name="description" content="Web-based IRC client.">
<link rel="preload" href="/init" as="fetch" crossorigin>
<script>
<%== inlineScript %>
</script>
{{if .InlineScript}}
<script>{{.InlineScript}}</script>
{{end}}
{{range .Scripts}}
<script src="{{.}}" defer></script>
{{end}}
<link rel="preload" href="/font/fontello.woff2?48901973" as="font" type="font/woff2" crossorigin>
<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 cssPath != "" { %>
<link href="/<%== cssPath %>" rel="stylesheet">
<% } %>
{{if .Stylesheet}}
<link href="{{.Stylesheet}}" rel="stylesheet">
{{end}}
<link rel="manifest" href="/manifest.json">
</head>
<body>
<div id="root"></div>
<% for _, script := range scripts { %>
<script src="/<%== script %>"></script>
<% } %>
<noscript>This page needs JavaScript enabled to function.</noscript>
<div id="root"></div>
</body>
</html>
</html>`

Some files were not shown because too many files have changed in this diff Show More