96 Commits
v0.5 ... v0.6.1

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
57704c9e89 v0.5.3 2018-11-26 02:09:29 +01:00
dd03e75be4 Add amd64 and darwin archive name replacements 2018-11-26 02:07:41 +01:00
4eac0609c2 Add bottom margin to settings version 2018-11-26 02:05:21 +01:00
bf2e7875a5 Add version flags and command, closes #38 2018-11-26 01:58:24 +01:00
fbbcf6457e Show version info in settings 2018-11-22 12:08:18 +01:00
5d896ae439 Print prettier version info 2018-11-22 11:31:02 +01:00
9b40934654 Grab referer in /init handler instead of getIndexData() 2018-11-22 10:40:29 +01:00
bd0541120f Improve document.title handling 2018-11-22 10:09:13 +01:00
09a1933131 Increase scrollbackDebounce to 150 2018-11-22 09:51:26 +01:00
8eb1f9cbb4 Fix some messages not rendering on MessageBox mount, handle scroll restoration edge case 2018-11-22 09:50:38 +01:00
9ca50b1546 v0.5.2 2018-11-22 08:48:29 +01:00
0699fc287d Fix scroll jumping on MessageBox mount 2018-11-22 08:48:29 +01:00
937480bfd4 Prevent rounded input corners in Safari 2018-11-22 08:48:29 +01:00
05527e41b7 Use absolute paths 2018-11-22 08:48:29 +01:00
65e103fd23 Delete release.sh 2018-11-21 02:17:51 +01:00
2a7b7572fa Update readme download links 2018-11-17 12:48:15 +01:00
2c1a6f2c44 Discard git changes when doing a release on travis 2018-11-17 12:34:12 +01:00
96f18117bd v0.5.1 2018-11-17 12:22:28 +01:00
90785aba20 Create sessions on boot requests instead of websocket handshakes 2018-11-17 11:52:36 +01:00
da87dccb53 Update react-hot-loader to 4.4.0, turn on pureSFC 2018-11-17 11:12:30 +01:00
b54cb42426 Setup goreleaser 2018-11-17 10:32:02 +01:00
4dcf674c4b Add robots.txt, clean up index template 2018-11-14 10:11:15 +01:00
764475b2a5 Turn on terserOptions.safari10 2018-11-14 09:24:40 +01:00
d867ca8477 Add worker-src csp directive 2018-11-14 08:34:35 +01:00
a783a87d04 Set overscanCount to 5 2018-11-14 08:33:01 +01:00
a7b6442ed9 Update dependencies 2018-11-14 08:25:20 +01:00
1389 changed files with 200736 additions and 119770 deletions

View File

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

4
.gitignore vendored
View File

@ -1,7 +1,5 @@
build
release
dist
dispatch
client/dist
client/node_modules
client/yarn-error.log
ca-certificates.crt

45
.goreleaser.yml Normal file
View File

@ -0,0 +1,45 @@
builds:
- ldflags:
- -s -w -X github.com/khlieng/dispatch/version.Tag=v{{.Version}} -X github.com/khlieng/dispatch/version.Commit={{.ShortCommit}} -X github.com/khlieng/dispatch/version.Date={{.Date}}
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm
- arm64
goarm:
- 6
- 7
archives:
- files:
- none*
format_overrides:
- goos: windows
format: zip
replacements:
amd64: x64
darwin: mac
checksum:
name_template: "checksums.txt"
changelog:
filters:
exclude:
- "(?i)^update.*dep"
- Merge pull request
- Merge branch
release:
name_template: "{{.Version}}"

View File

@ -1,8 +1,7 @@
language: go
go:
- "1.10.x"
- "1.11.x"
- 1.x
- tip
os:
@ -14,13 +13,13 @@ matrix:
- go: tip
install:
- go get github.com/jteeuwen/go-bindata/...
- GO111MODULE=off go get github.com/jteeuwen/go-bindata/...
- cd client
- nvm install 10.11.0
- nvm use 10.11.0
- nvm install 12.4.0
- nvm use 12.4.0
- npm install -g yarn
- yarn global add gulp@next
- yarn --ignore-engines
- yarn global add gulp
- yarn
script:
- yarn test:verbose
@ -28,3 +27,11 @@ script:
- cd ..
- go vet ./...
- go test -v -race ./...
deploy:
- provider: script
skip_cleanup: true
script: git checkout -- . && curl -sL https://git.io/goreleaser | bash
on:
tags: true
condition: $TRAVIS_OS_NAME = linux && $TRAVIS_GO_VERSION = 1.*

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/dispatch_windows_amd64.zip)**
- **[OS X (x64)](https://github.com/khlieng/dispatch/releases/download/v0.5/dispatch_darwin_amd64.zip)**
- **[Linux (x64)](https://github.com/khlieng/dispatch/releases/download/v0.5/dispatch_linux_amd64.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;
@ -31,7 +40,16 @@ input::-ms-clear {
display: none;
}
input,
textarea {
border-radius: 0;
appearance: none;
}
button {
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
height: 50px;
background: #6bb758;
@ -51,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);
}
@ -122,6 +167,10 @@ i[class*=' icon-']:before {
color: #f6546a !important;
}
.disabled {
color: #999 !important;
}
.textinput {
display: block;
position: relative;
@ -219,7 +268,8 @@ i[class*=' icon-']:before {
top: 0;
bottom: 50px;
width: 100%;
overflow: auto;
overflow-x: hidden;
overflow-y: auto;
}
.tablist p {
@ -227,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 {
@ -242,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;
@ -261,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 {
@ -278,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;
}
@ -325,7 +383,7 @@ i[class*=' icon-']:before {
.connect-form {
margin: auto 20px;
padding-top: 20px;
padding: 20px 0;
width: 350px;
text-align: center;
}
@ -362,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 {
@ -395,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;
}
@ -419,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 {
@ -427,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;
@ -452,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,
@ -470,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;
@ -492,7 +536,8 @@ input.chat-title {
.chat-topic-wrap {
flex: 1;
position: relative;
margin: 0 15px;
margin-left: 15px;
margin-right: 5px;
}
.chat-topic {
@ -501,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;
@ -523,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 {
@ -559,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;
@ -589,8 +628,6 @@ input.chat-title {
top: 50px;
bottom: 50px;
right: 0;
z-index: 1;
overflow: hidden;
}
.chat-channel .messagebox {
@ -609,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;
}
@ -631,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;
@ -643,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;
@ -689,7 +747,7 @@ input.message-input-nick.invalid {
flex: 1;
width: 100%;
height: 100%;
padding: 0 15px;
padding: 0 10px;
}
.userlist {
@ -700,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;
}
@ -748,7 +806,7 @@ input.message-input-nick.invalid {
color: #222;
}
.settings button {
.settings-button {
width: 200px;
}
@ -792,8 +850,8 @@ input.message-input-nick.invalid {
margin-right: 0;
}
.settings-button {
margin-top: 10px;
.settings-file:last-of-type {
margin-bottom: 10px;
}
}
@ -806,15 +864,187 @@ input.message-input-nick.invalid {
text-align: center;
}
.settings-version {
color: #999;
font-size: 12px;
margin-bottom: 20px;
}
.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;
@ -827,7 +1057,7 @@ input.message-input-nick.invalid {
}
.navicon {
display: inline-block;
display: block;
}
.main-container.off-canvas {
@ -842,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 {
@ -884,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

@ -1,9 +1,6 @@
/* eslint-disable no-underscore-dangle */
// This entrypoint gets inlined in the index page cached by service workers
// and is responsible for fetching the data we would otherwise embed
window.__env__ = fetch('/data', {
window.__init__ = fetch('/init', {
credentials: 'same-origin'
}).then(res => {
if (res.ok) {

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,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,17 +2,35 @@ 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';
const fetchThreshold = 600;
// The amount of time in ms that needs to pass without any
// scroll events happening before adding messages to the top,
// this is done to prevent the scroll from jumping all over the place
const scrollbackDebounce = 100;
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(() => {
@ -27,19 +45,9 @@ export default class MessageBox extends PureComponent {
this.loadScrollPos();
}
componentDidMount() {
const scrollToBottom = this.bottom;
window.requestAnimationFrame(() => {
const { messages } = this.props;
if (scrollToBottom && messages.length > 0) {
this.list.current.scrollToItem(messages.length + 1);
}
});
}
componentDidUpdate(prevProps) {
const { messages } = this.props;
if (prevProps.tab !== this.props.tab) {
this.loadScrollPos(true);
}
@ -47,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);
}
}
@ -69,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++) {
@ -98,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;
@ -109,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;
@ -123,6 +136,8 @@ export default class MessageBox extends PureComponent {
loadScrollPos = scroll => {
const pos = getScrollPos(this.updateScrollKey());
const { messages } = this.props;
if (pos >= 0) {
this.bottom = false;
if (scroll) {
@ -133,7 +148,17 @@ export default class MessageBox extends PureComponent {
} else {
this.bottom = true;
if (scroll) {
this.list.current.scrollToItem(this.props.messages.length + 1);
this.list.current.scrollToItem(messages.length + 1);
} else if (messages.length > 0) {
let totalHeight = 14;
for (let i = 0; i < messages.length; i++) {
totalHeight += messages[i].height;
}
const messageBoxHeight = windowHeight() - 100;
if (totalHeight > messageBoxHeight) {
this.initialScrollTop = totalHeight;
}
}
}
};
@ -142,7 +167,7 @@ export default class MessageBox extends PureComponent {
if (this.bottom) {
saveScrollPos(this.scrollKey, -1);
} else {
saveScrollPos(this.scrollKey, this.outer.current.scrollTop);
saveScrollPos(this.scrollKey, this.scrollTop);
}
};
@ -172,9 +197,21 @@ export default class MessageBox extends PureComponent {
const { clientHeight, scrollHeight } = this.outer.current;
this.scrollTop = scrollOffset;
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;
};
@ -201,7 +238,8 @@ export default class MessageBox extends PureComponent {
);
}
return null;
} else if (index === messages.length + 1) {
}
if (index === messages.length + 1) {
return null;
}
@ -219,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
@ -232,13 +285,15 @@ 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}
>
{this.renderMessage}
</List>

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;
@ -81,6 +83,7 @@ export default class UserList extends PureComponent {
itemKey={this.getItemKey}
itemSize={this.getItemHeight}
estimatedItemSize={24}
overscanCount={5}
>
{this.renderUser}
</List>

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';
@ -7,6 +7,7 @@ import FileInput from 'components/ui/FileInput';
const Settings = ({
settings,
installable,
version,
setSetting,
onCertChange,
onKeyChange,
@ -16,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">
@ -31,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>
)}
@ -71,6 +72,13 @@ const Settings = ({
{error ? <p className="error">{error}</p> : null}
</div>
</div>
{version && (
<div className="settings-version">
<p>{version.tag}</p>
<p>Commit: {version.commit}</p>
<p>Build Date: {version.date}</p>
</div>
)}
</div>
</div>
);

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

@ -12,7 +12,8 @@ import connect from 'utils/connect';
const mapState = createStructuredSelector({
settings: getSettings,
installable: state => state.app.installable
installable: state => state.app.installable,
version: state => state.app.version
});
const mapDispatch = {
@ -23,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

@ -8,16 +8,20 @@ export default function documentTitle({ store }) {
let title;
if (router.route === 'chat') {
const { name } = router.params;
const { server, name } = router.params;
if (name) {
title = `${name} @ ${serverName}`;
title = `${name} @ ${serverName || server}`;
} else {
title = serverName;
title = serverName || server;
}
} else {
title = capitalize(router.route);
}
document.title = `${title} | Dispatch`;
if (title) {
document.title = `${title} | Dispatch`;
} else {
document.title = 'Dispatch';
}
});
}

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,17 +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));
store.dispatch(setSettings(env.settings, true));
if (env.servers) {
@ -19,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) {
@ -60,21 +28,28 @@ 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));
}
});
}
export default function initialState(ctx) {
if (window.__env__) {
window.__env__.then(env => loadState(ctx, env));
} else {
const env = JSON.parse(document.getElementById('env').innerHTML);
loadState(ctx, env);
});
}
}
/* eslint-disable no-underscore-dangle */
export default async function initialState(ctx) {
const env = await window.__init__;
ctx.socket.connect();
loadState(ctx, env);
}

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

@ -29,7 +29,11 @@ const initialState = {
export default createReducer(initialState, {
[actions.APP_SET](state, { key, value }) {
state[key] = value;
if (typeof key === 'object') {
Object.assign(state, key);
} else {
state[key] = value;
}
},
[actions.UPDATE_MESSAGE_HEIGHT](state, action) {
@ -54,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 };
}
}
@ -95,9 +95,7 @@ export const getSortedChannels = createSelector(getChannels, channels =>
sortBy(
Object.keys(channels).map(server => ({
address: server,
channels: sortBy(Object.keys(channels[server]), channel =>
channel.toLowerCase()
)
channels: sortBy(channels[server], channel => channel.name.toLowerCase())
})),
server => server.address.toLowerCase()
)
@ -122,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]);
},
@ -129,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);
@ -158,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;
},
@ -189,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;
});
}
@ -223,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

@ -14,8 +14,6 @@ export default class Socket {
});
this.handlers = [];
this.connected = false;
this.connect();
}
connect() {

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,82 +12,88 @@
"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.1",
"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",
"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.3.11",
"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,src/css/*.css,src/**/*.test.js}",
"prettier:all": "prettier --write {.*,*.js,src/**/*.js,src/css/*.css}",
"prettier": "prettier --write {.*,*.js,css/*.css,**/*.test.js}",
"prettier:all": "prettier --write {.*,*.js,**/*.js,css/*.css}",
"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": {
@ -95,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

@ -4,6 +4,8 @@ var postcssPresetEnv = require('postcss-preset-env');
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',
@ -12,8 +14,8 @@ module.exports = {
boot: './js/boot'
},
output: {
filename: '[name].[chunkhash:8].js',
chunkFilename: '[name].[chunkhash:8].js',
filename: '[name].[chunkhash].js',
chunkFilename: '[name].[chunkhash].js',
publicPath: '/'
},
resolve: {
@ -75,24 +77,36 @@ module.exports = {
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css',
chunkFilename: '[name].[contenthash:8].css'
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$/
]
})
],
optimization: {
minimizer: [new TerserPlugin()],
minimizer: [
new TerserPlugin({
terserOptions: {
safari10: true
}
})
],
splitChunks: {
chunks: 'all',
cacheGroups: {

File diff suppressed because it is too large Load Diff

View File

@ -5,14 +5,15 @@ 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"
"github.com/khlieng/dispatch/storage/boltdb"
"github.com/khlieng/dispatch/version"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@ -24,43 +25,37 @@ const logo = `
| |_| || |\__ \| |_) || (_| || |_| (__ | | | |
|____/ |_||___/| .__/ \__,_| \__|\___||_| |_|
|_|
v0.5
%s
Commit: %s
Build Date: %s
Runtime: %s
`
var rootCmd = &cobra.Command{
Use: "dispatch",
Short: "Web-based IRC client in Go.",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if cmd.Use == "dispatch" {
fmt.Println(logo)
if viper.GetBool("version") {
printVersion()
os.Exit(0)
}
storage.Initialize(viper.GetString("dir"))
if cmd == cmd.Root() {
fmt.Printf(logo, version.Tag, version.Commit, version.Date, runtime.Version())
}
initConfig(storage.Path.Config(), viper.GetBool("reset_config"))
storage.Initialize(viper.GetString("dir"), viper.GetString("data"), viper.GetString("conf"))
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 {
@ -68,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()
},
}
@ -91,21 +95,22 @@ func Execute() {
func init() {
rootCmd.AddCommand(clearCmd)
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")
rootCmd.Flags().IntP("port", "p", 80, "port to listen on")
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) {

22
commands/version.go Normal file
View File

@ -0,0 +1,22 @@
package commands
import (
"fmt"
"runtime"
"github.com/khlieng/dispatch/version"
"github.com/spf13/cobra"
)
var versionCmd = &cobra.Command{
Use: "version",
Short: "Show version",
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
Run: func(cmd *cobra.Command, args []string) {
printVersion()
},
}
func printVersion() {
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
}

92
go.mod
View File

@ -1,58 +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-20180222194500-ef6db91d284a // 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-20181102132633-a4119e27a65d // 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.1.0
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 // indirect
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc
golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc // 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,152 +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-20180222194500-ef6db91d284a h1:JSvGDIbmil4Ui/dDdFBExb7/cmkNjyX5F97oglmvCDo=
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/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-20181102132633-a4119e27a65d h1:wnS/zl2nFbhU9r6hlfqYEy9qJ0gG89VJGigG6dq7+Eo=
github.com/syndtr/goleveldb v0.0.0-20181102132633-a4119e27a65d/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.1.0 h1:Ias1pE9hO98/fI23RLza0T3461YiM720d96oxTRPyuM=
github.com/xenolf/lego v1.1.0/go.mod h1:fwiGnfsIjG7OHPfOvgK7Y/Qo6+2Ox0iozjNTkZICKbY=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/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-20181102091132-c10e9556a7bc h1:ZMCWScCvS2fUVFw8LOpxyUUW5qiviqr4Dg5NdjLeiLU=
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/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/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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-20181031143558-9b800f95dbbc h1:SdCq5U4J+PpbSDIl9bM0V1e1Ug1jsnBkAFvTs1htn7U=
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=

9
install.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/sh -
Import="github.com/khlieng/dispatch/version"
Tag=$(git describe --tags)
Commit=$(git rev-parse --short HEAD)
Date=$(date +'%Y-%m-%dT%TZ')
go install -ldflags "-s -w -X $Import.Tag=$Tag -X $Import.Commit=$Commit -X $Import.Date=$Date"

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
}

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