diff --git a/cmd/setup-node/commands/root.go b/cmd/setup-node/commands/root.go index 570650971b..688035c355 100644 --- a/cmd/setup-node/commands/root.go +++ b/cmd/setup-node/commands/root.go @@ -12,6 +12,7 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" logrussyslog "github.com/sirupsen/logrus/hooks/syslog" + "github.com/skycoin/dmsg/discord" "github.com/skycoin/skycoin/src/util/logging" "github.com/spf13/cobra" @@ -44,6 +45,11 @@ var rootCmd = &cobra.Command{ logging.AddHook(hook) } + if discordWebhookURL := discord.GetWebhookURLFromEnv(); discordWebhookURL != "" { + hook := discord.NewHook(tag, discordWebhookURL) + logging.AddHook(hook) + } + var rdr io.Reader var err error diff --git a/cmd/skywire-visor/commands/root.go b/cmd/skywire-visor/commands/root.go index bc6f826611..6499d2bf5b 100644 --- a/cmd/skywire-visor/commands/root.go +++ b/cmd/skywire-visor/commands/root.go @@ -21,6 +21,7 @@ import ( "github.com/pkg/profile" logrussyslog "github.com/sirupsen/logrus/hooks/syslog" + "github.com/skycoin/dmsg/discord" "github.com/skycoin/skycoin/src/util/logging" "github.com/spf13/cobra" @@ -141,6 +142,11 @@ func (cfg *runCfg) startLogger() *runCfg { } } + if discordWebhookURL := discord.GetWebhookURLFromEnv(); discordWebhookURL != "" { + hook := discord.NewHook(cfg.tag, discordWebhookURL) + logging.AddHook(hook) + } + return cfg } diff --git a/go.mod b/go.mod index 76ea90b64e..e4dca9c31a 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/rakyll/statik v0.1.7 github.com/schollz/progressbar/v2 v2.15.0 github.com/sirupsen/logrus v1.4.2 - github.com/skycoin/dmsg v0.0.0-20200803194104-78ff5746d8a3 + github.com/skycoin/dmsg v0.0.0-20200807121748-518564603fc3 github.com/skycoin/skycoin v0.26.0 github.com/skycoin/yamux v0.0.0-20200803175205-571ceb89da9f github.com/spf13/cobra v0.0.5 diff --git a/go.sum b/go.sum index 5e0ba3f367..5f3cb9760d 100644 --- a/go.sum +++ b/go.sum @@ -124,6 +124,8 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn 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/kz/discordrus v1.2.0 h1:r5uplKozPR+TIJ1NUZT758Lv7eukf8+fp3L4uRj+6xs= +github.com/kz/discordrus v1.2.0/go.mod h1:cJ3TiJUUuY5Gm3DNYHnnaUa3iol8VBRPzztAeZm7exc= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -192,8 +194,8 @@ github.com/schollz/progressbar/v2 v2.15.0/go.mod h1:UdPq3prGkfQ7MOzZKlDRpYKcFqEM github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/skycoin/dmsg v0.0.0-20200803194104-78ff5746d8a3 h1:mSraFLdscptO1H9nJ1rrzXegC+hIC8TeWP4LoO9Oe6I= -github.com/skycoin/dmsg v0.0.0-20200803194104-78ff5746d8a3/go.mod h1:wyteEV9/z7kxG6nAWslHlQtEpixpB/RWtzFg7GLlIEQ= +github.com/skycoin/dmsg v0.0.0-20200807121748-518564603fc3 h1:9xY2P7ssFM33qIqK1mIdKlJ+IuAFHh96N83ttbPHJOM= +github.com/skycoin/dmsg v0.0.0-20200807121748-518564603fc3/go.mod h1:Bl7qEJ0iYn8Pg6yk3WhE15Rd7OdWmxcIE+NGpeQw2fA= github.com/skycoin/skycoin v0.26.0 h1:xDxe2r8AclMntZ550Y/vUQgwgLtwrf9Wu5UYiYcN5/o= github.com/skycoin/skycoin v0.26.0/go.mod h1:78nHjQzd8KG0jJJVL/j0xMmrihXi70ti63fh8vXScJw= github.com/skycoin/skywire v0.2.3-0.20200803142942-0f8b9981f6f9/go.mod h1:VtQTQdSwPvAZDzDgFK41PwGGQGeY3ehlFCtT0F8C7/8= diff --git a/vendor/github.com/kz/discordrus/.gitignore b/vendor/github.com/kz/discordrus/.gitignore new file mode 100644 index 0000000000..961e2f61b8 --- /dev/null +++ b/vendor/github.com/kz/discordrus/.gitignore @@ -0,0 +1,25 @@ +.idea/ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/kz/discordrus/.travis.yml b/vendor/github.com/kz/discordrus/.travis.yml new file mode 100644 index 0000000000..4d4b89db79 --- /dev/null +++ b/vendor/github.com/kz/discordrus/.travis.yml @@ -0,0 +1,10 @@ +language: go +go: + - 1.x + - 1.7.x + - 1.8 + - 1.10.x + - 1.12.x + - master +install: + - go get github.com/sirupsen/logrus diff --git a/vendor/github.com/kz/discordrus/LICENSE b/vendor/github.com/kz/discordrus/LICENSE new file mode 100644 index 0000000000..3ddc41e39f --- /dev/null +++ b/vendor/github.com/kz/discordrus/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Kelvin Zhang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/kz/discordrus/README.md b/vendor/github.com/kz/discordrus/README.md new file mode 100644 index 0000000000..7ba7a89c9a --- /dev/null +++ b/vendor/github.com/kz/discordrus/README.md @@ -0,0 +1,79 @@ +# discordrus | a [Discord](https://discordapp.com/) hook for [Logrus](https://github.com/Sirupsen/logrus) :walrus: [![Travis CI](https://api.travis-ci.org/kz/discordrus.svg?branch=master)](https://travis-ci.org/kz/discordrus) [![GoDoc](https://godoc.org/github.com/puddingfactory/logentrus?status.svg)](https://godoc.org/github.com/kz/discordrus) + +![Screenshot of discordrus in action](https://i.imgur.com/q8Tcmjn.png?1) + +## Install + +`go get -u github.com/kz/discordrus` + +## Setup + +In order to use this package, a Discord webhook URL is required. Find out how to obtain one [here](https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks). You will need to be a server administrator to do this. + +## Usage + +Below is an example of how this package may be used. The options below are used only for the purpose of demonstration and chances are that you will not need to use any options at all (or if any, only the `Username` option). + + +```go +package main + +import ( + "github.com/sirupsen/logrus" + "os" + "github.com/kz/discordrus" +) + +func init() { + logrus.SetFormatter(&logrus.TextFormatter{}) + logrus.SetOutput(os.Stderr) + logrus.SetLevel(logrus.TraceLevel) + + logrus.AddHook(discordrus.NewHook( + // Use environment variable for security reasons + os.Getenv("DISCORDRUS_WEBHOOK_URL"), + // Set minimum level to DebugLevel to receive all log entries + logrus.TraceLevel, + &discordrus.Opts{ + Username: "Test Username", + Author: "", // Setting this to a non-empty string adds the author text to the message header + DisableTimestamp: false, // Setting this to true will disable timestamps from appearing in the footer + TimestampFormat: "Jan 2 15:04:05.00000 MST", // The timestamp takes this format; if it is unset, it will take logrus' default format + TimestampLocale: nil, // The timestamp uses this locale; if it is unset, it will use time.Local + EnableCustomColors: true, // If set to true, the below CustomLevelColors will apply + CustomLevelColors: &discordrus.LevelColors{ + Trace: 3092790, + Debug: 10170623, + Info: 3581519, + Warn: 14327864, + Error: 13631488, + Panic: 13631488, + Fatal: 13631488, + }, + DisableInlineFields: false, // If set to true, fields will not appear in columns ("inline") + }, + )) +} + +func main() { + logrus.WithFields(logrus.Fields{"String": "hi", "Integer": 2, "Boolean": false}).Debug("Check this out! Awesome, right?") +} +``` + +All discordrus.Opts fields are optional. + +Option | Description | Default | Valid options +--- | --- | --- | --- +Username | Replaces the default username of the webhook bot for the sent message only | Username unchanged | Any non-empty string (2-32 chars. inclusive) +Author | Adds an author field to the header if set | Author not set | Any non-empty string (1-256 chars inclusive) +DisableInlineFields | Inline means whether Discord will display the field in a column (with maximum three columns to a row). Setting this to `true` will cause Discord to display the field in its own row. | false | bool +DisableTimestamp | Specifies whether the timestamp in the footer should be disabled | false | bool +TimestampFormat | Change the timestamp format | logrus's default time format | `"Jan 2 15:04:05.00000 MST"`, or any format accepted by Golang +TimestampLocale | Change the timestamp locale | `nil` | nil == time.Local, time.UTC, time.LoadLocation("America/New_York"), etc +EnableCustomColors | Specifies whether the `CustomLevelColors` opt value should be used instead of `discordrus.DefaultLevelColors`. If `true`, `CustomLevelColors` must be specified (or all colors will be set to the nil value of `0`, therefore displayed as white) | false | bool +CustomLevelColors | Replaces `discordrus.DefaultLevelColors`. All fields must be entered or they will default to the nil value of `0`. | Pointer to struct instance of `discordrus.LevelColors` + +In addition to the above character count constraints, Discord has a maximum of 25 fields with their name and value limits being 256 and 1024 respectively. Furthermore, the description (i.e., logrus' error message) must be a maximum of 2048. All of these constraints, including the option constraints above, will automatically be truncated with no further action required. + +## Acknowledgements +The following repositories have been helpful in creating this package: [puddingfactory/logentrus](https://github.com/puddingfactory/logentrus) for Logentries, [johntdyer/slackrus](https://github.com/johntdyer/slackrus) for Slack and [nubo/hiprus](https://github.com/nubo/hiprus) for Hipchat. Check them out! diff --git a/vendor/github.com/kz/discordrus/discordrus.go b/vendor/github.com/kz/discordrus/discordrus.go new file mode 100644 index 0000000000..2bf0807e6d --- /dev/null +++ b/vendor/github.com/kz/discordrus/discordrus.go @@ -0,0 +1,189 @@ +package discordrus + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/sirupsen/logrus" + "time" +) + +const ( + maxFieldNum = 25 + minUsernameChars = 2 + maxUsernameChars = 32 + maxAuthorChars = 256 + maxFieldNameChars = 256 + maxFieldValueChars = 1024 + maxDescriptionChars = 2048 + usernameTooShortMsg = " (USERNAME TOO SHORT)" +) + +// Opts contains the options available for the hook +type Opts struct { + // Username replaces the default username of the webhook bot for the sent message only if set (default: none) + Username string + // Author adds an author field if set (default: none) + Author string + // DisableInlineFields causes fields to be displayed one per line as opposed to being inline (i.e., in columns) (default: false) + DisableInlineFields bool + // EnableCustomColors specifies whether CustomLevelColors should be used instead of DefaultLevelColors (default: true) + EnableCustomColors bool + // CustomLevelColors is a LevelColors struct which replaces DefaultLevelColors if EnableCustomColors is set to true (default: none) + CustomLevelColors *LevelColors + // DisableTimestamp specifies whether the timestamp in the footer should be disabled (default: false) + DisableTimestamp bool + // TimestampFormat specifies a custom format for the footer + TimestampFormat string + // TimestampLocale specifies a custom locale for the timestamp + TimestampLocale *time.Location +} + +// Hook is a hook to send logs to Discord +type Hook struct { + // WebhookURL is the full Discord webhook URL + WebhookURL string + // MinLevel is the minimum priority level to enable logging for + MinLevel logrus.Level + // Opts contains the options available for the hook + Opts *Opts +} + +// NewHook creates a new instance of a hook, ensures correct string lengths and returns its pointer +func NewHook(webhookURL string, minLevel logrus.Level, opts *Opts) *Hook { + hook := Hook{ + WebhookURL: webhookURL, + MinLevel: minLevel, + Opts: opts, + } + + // Ensure correct username length + if hook.Opts.Username != "" && len(hook.Opts.Username) < minUsernameChars { + // Append "(USERNAME TOO SHORT)" in order not to disrupt logging operations + hook.Opts.Username = hook.Opts.Username + usernameTooShortMsg + } else if len(hook.Opts.Username) > maxUsernameChars { + hook.Opts.Username = hook.Opts.Username[:maxUsernameChars] + } + + // Truncate author + if len(hook.Opts.Author) > maxAuthorChars { + hook.Opts.Author = hook.Opts.Author[:maxAuthorChars] + } + + return &hook +} + +func (hook *Hook) Fire(entry *logrus.Entry) error { + // Parse the entry to a Discord webhook object in JSON form + webhookObject, err := hook.parseToJson(entry) + if err != nil { + return err + } + + err = hook.send(webhookObject) + if err != nil { + return err + } + + return nil +} + +func (hook *Hook) Levels() []logrus.Level { + return LevelThreshold(hook.MinLevel) +} + +func (hook *Hook) parseToJson(entry *logrus.Entry) (*[]byte, error) { + // Create struct mapping to Discord webhook object + var data = map[string]interface{}{ + "embeds": []map[string]interface{}{}, + } + var embed = map[string]interface{}{ + "title": strings.ToUpper(entry.Level.String()), + } + var fields = []map[string]interface{}{} + + // Add username to data + if hook.Opts.Username != "" { + data["username"] = hook.Opts.Username + } + + // Add description to embed + if len(entry.Message) > maxDescriptionChars { + entry.Message = entry.Message[:maxDescriptionChars] + } + embed["description"] = entry.Message + + // Add color to embed + if hook.Opts.EnableCustomColors { + embed["color"] = hook.Opts.CustomLevelColors.LevelColor(entry.Level) + } else { + embed["color"] = DefaultLevelColors.LevelColor(entry.Level) + } + + // Add author to embed + if hook.Opts.Author != "" { + embed["author"] = map[string]interface{}{"name": hook.Opts.Author} + } + + // Add footer to embed + if !hook.Opts.DisableTimestamp { + if hook.Opts.TimestampLocale != nil { + entry.Time = entry.Time.In(hook.Opts.TimestampLocale) + } + timestamp := "" + if hook.Opts.TimestampFormat != "" { + timestamp = entry.Time.Format(hook.Opts.TimestampFormat) + } else { + timestamp = entry.Time.String() + } + embed["footer"] = map[string]interface{}{ + "text": timestamp, + } + } + + // Add fields to embed + counter := 0 + for name, value := range entry.Data { + // Ensure that the maximum field number is not exceeded + if counter > maxFieldNum { + break + } + + // Make value a string + valueStr := fmt.Sprintf("%v", value) + + // Truncate names and values which are too long + if len(name) > maxFieldNameChars { + name = name[:maxFieldNameChars] + } + if len(valueStr) > maxFieldValueChars { + valueStr = valueStr[:maxFieldValueChars] + } + + var embedField = map[string]interface{}{ + "name": name, + "value": valueStr, + "inline": !hook.Opts.DisableInlineFields, + } + fields = append(fields, embedField) + counter++ + } + + // Merge fields and embed into data + embed["fields"] = fields + data["embeds"] = []map[string]interface{}{embed} + + marshaled, err := json.Marshal(data) + return &marshaled, err +} + +func (hook *Hook) send(webhookObject *[]byte) error { + _, err := http.Post(hook.WebhookURL, "application/json; charset=utf-8", bytes.NewBuffer(*webhookObject)) + if err != nil { + return err + } + return nil +} diff --git a/vendor/github.com/kz/discordrus/levels.go b/vendor/github.com/kz/discordrus/levels.go new file mode 100644 index 0000000000..67248eb9c0 --- /dev/null +++ b/vendor/github.com/kz/discordrus/levels.go @@ -0,0 +1,54 @@ +package discordrus + +import ( + "github.com/sirupsen/logrus" +) + +// LevelColors is a struct of the possible colors used in Discord color format (0x[RGB] converted to int) +type LevelColors struct { + Trace int + Debug int + Info int + Warn int + Error int + Panic int + Fatal int +} + +// DefaultLevelColors is a struct of the default colors used +var DefaultLevelColors = LevelColors{ + Trace: 3092790, + Debug: 10170623, + Info: 3581519, + Warn: 14327864, + Error: 13631488, + Panic: 13631488, + Fatal: 13631488, +} + +// LevelThreshold returns a slice of all the levels above and including the level specified +func LevelThreshold(l logrus.Level) []logrus.Level { + return logrus.AllLevels[:l+1] +} + +// LevelColor returns the respective color for the logrus level +func (lc LevelColors) LevelColor(l logrus.Level) int { + switch l { + case logrus.TraceLevel: + return lc.Trace + case logrus.DebugLevel: + return lc.Debug + case logrus.InfoLevel: + return lc.Info + case logrus.WarnLevel: + return lc.Warn + case logrus.ErrorLevel: + return lc.Error + case logrus.PanicLevel: + return lc.Panic + case logrus.FatalLevel: + return lc.Fatal + default: + return lc.Warn + } +} diff --git a/vendor/github.com/skycoin/dmsg/discord/discord.go b/vendor/github.com/skycoin/dmsg/discord/discord.go new file mode 100644 index 0000000000..674a836463 --- /dev/null +++ b/vendor/github.com/skycoin/dmsg/discord/discord.go @@ -0,0 +1,29 @@ +package discord + +import ( + "os" + "time" + + "github.com/kz/discordrus" + "github.com/sirupsen/logrus" +) + +const ( + webhookURLEnvName = "DISCORD_WEBHOOK_URL" +) + +func NewHook(tag, webHookURL string) logrus.Hook { + return discordrus.NewHook(webHookURL, logrus.ErrorLevel, discordOpts(tag)) +} + +func discordOpts(tag string) *discordrus.Opts { + return &discordrus.Opts{ + Username: tag, + TimestampFormat: time.RFC3339, + TimestampLocale: time.UTC, + } +} + +func GetWebhookURLFromEnv() string { + return os.Getenv(webhookURLEnvName) +} diff --git a/vendor/github.com/skycoin/dmsg/go.mod b/vendor/github.com/skycoin/dmsg/go.mod index 3dbb70749d..9d7198812f 100644 --- a/vendor/github.com/skycoin/dmsg/go.mod +++ b/vendor/github.com/skycoin/dmsg/go.mod @@ -7,6 +7,7 @@ require ( github.com/flynn/noise v0.0.0-20180327030543-2492fe189ae6 github.com/go-redis/redis v6.15.6+incompatible github.com/gorilla/handlers v1.4.2 + github.com/kz/discordrus v1.2.0 github.com/prometheus/client_golang v1.3.0 github.com/sirupsen/logrus v1.4.2 github.com/skycoin/skycoin v0.26.0 diff --git a/vendor/github.com/skycoin/dmsg/go.sum b/vendor/github.com/skycoin/dmsg/go.sum index cc583ed4e8..f58c93cfcf 100644 --- a/vendor/github.com/skycoin/dmsg/go.sum +++ b/vendor/github.com/skycoin/dmsg/go.sum @@ -120,6 +120,8 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn 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/kz/discordrus v1.2.0 h1:r5uplKozPR+TIJ1NUZT758Lv7eukf8+fp3L4uRj+6xs= +github.com/kz/discordrus v1.2.0/go.mod h1:cJ3TiJUUuY5Gm3DNYHnnaUa3iol8VBRPzztAeZm7exc= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= diff --git a/vendor/modules.txt b/vendor/modules.txt index 390e0dc5ac..2b0ea1ae3c 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -53,6 +53,8 @@ github.com/klauspost/compress/zstd/internal/xxhash github.com/klauspost/pgzip # github.com/konsorten/go-windows-terminal-sequences v1.0.2 github.com/konsorten/go-windows-terminal-sequences +# github.com/kz/discordrus v1.2.0 +github.com/kz/discordrus # github.com/mattn/go-colorable v0.1.6 github.com/mattn/go-colorable # github.com/mattn/go-isatty v0.0.12 @@ -97,10 +99,11 @@ github.com/schollz/progressbar/v2 # github.com/sirupsen/logrus v1.4.2 github.com/sirupsen/logrus github.com/sirupsen/logrus/hooks/syslog -# github.com/skycoin/dmsg v0.0.0-20200803194104-78ff5746d8a3 +# github.com/skycoin/dmsg v0.0.0-20200807121748-518564603fc3 github.com/skycoin/dmsg github.com/skycoin/dmsg/cipher github.com/skycoin/dmsg/disc +github.com/skycoin/dmsg/discord github.com/skycoin/dmsg/dmsgpty github.com/skycoin/dmsg/httputil github.com/skycoin/dmsg/ioutil