Compare commits

...

51 Commits

Author SHA1 Message Date
Wim
3719dd93ab Use tulir/go-whatsapp fork 2021-01-23 19:56:27 +01:00
Wim
5dd15ef8e7 Add an even more debug option (discord) (#1368)
Enable discordgo debugging with debuglevel=1 under the [discord.xxx] section, for even more debugging fun.
2021-01-23 00:09:56 +01:00
Alexander
4ac6366706 Allow the XMPP bridge to use slack compatible webhooks (xmpp) (#1364)
* Add mod_slack_webhook support to the XMPP bridge

* Replace b.webhookURL with b.GetString

* Do not return a message ID on webhook POST

* Add the XMPP webhook to the sample configuration
2021-01-21 22:50:04 +01:00
Wim
adc0912efa Update README (#1362) 2021-01-15 23:21:48 +01:00
Wim
536823ce55 Optimize Dockerfile (#1361) 2021-01-15 22:44:01 +01:00
Peter Dave Hello
207cd24edb Unify/sync apk index cache control in Dockerfile (#1352)
The second stage is using a single `apk` command with `--no-cache` which is better.
2021-01-14 23:48:02 +01:00
Paul
b039da1eba Add jpe as valid image filename extension (telegram) (#1360) 2021-01-14 23:42:13 +01:00
Qais Patankar
8fcd0f3b6f Improve Markdown in transmitter docs (discord) (#1351) 2021-01-03 18:57:06 +00:00
Wim
16fde6935c Rename .oga audio files to .ogg (telegram) (#1349) 2021-01-02 00:55:20 +01:00
Wim
9592cff9fa Update README about unsupported steam chat 2021-01-02 00:12:10 +01:00
Wim
109148988c Bump version 2020-12-31 23:42:41 +01:00
Wim
cf13fff7d2 Release v1.21.0 (#1348) 2020-12-31 22:32:13 +01:00
Qais Patankar
a9d8ac8bc0 Refactor "msg-parent-not-found" to config.ParentIDNotFound (#1347) 2020-12-31 18:01:57 +00:00
Qais Patankar
1a4717b366 Reject cross-channel message references (discord) (#1345)
Discord message references have been designed in a way for this to
support cross-channel or even cross-guild references in the future.

This will ensure the ParentID is *not* set when the message refers to a
message that was sent in a different channel.
2020-12-31 16:21:37 +00:00
Qais Patankar
6cadf12260 Add a prefix handler for unthreaded messages (discord) (#1346) 2020-12-31 16:15:42 +00:00
Wim
19d47784bd Add threading support with token (discord) (#1342)
Webhooks don't support the threading yet, so this is token only.
In discord you can reply on each message of a thread, but this is not possible in mattermost (so some changes added there to make sure we always answer on the rootID of the thread).

Also needs some more testing with slack.

update : It now also uses the token when replying to a thread (even if webhooks are enabled), until webhooks have support for threads.
2020-12-31 16:59:47 +01:00
Qais Patankar
b89102c5fc Fix 'webook' typo in discord/webhook.go (#1344) 2020-12-31 16:51:49 +01:00
Wim
4f20ebead3 Update vendor for next release (#1343) 2020-12-31 14:48:12 +01:00
James Lu
a9f89dbc64 Add support for stateless bridging via draft/relaymsg (irc) (#1339)
* irc: add support for stateless bridging via draft/relaymsg

As discussed at https://github.com/42wim/matterbridge/issues/667#issuecomment-634214165

* irc: handle the draft/relaymsg tag in spoofed messages too

* Apply suggestions from code review

Co-authored-by: Wim <wim@42.be>

* Run gofmt on irc.go

* Document relaymsg in matterbridge.toml.sample

Co-authored-by: Wim <wim@42.be>
2020-12-30 18:21:32 +01:00
wschwab
58ea1e07d2 Update README.md (#1340)
added using `chmod a+x` to make the binary executable in the installation section
2020-12-29 20:42:00 +01:00
Qais Patankar
6de4c7e971 Update webhook documentation (discord) (#1335) 2020-12-17 20:27:52 +01:00
Qais Patankar
03dc51ffa2 Split Bdiscord.Send into handleEventWebhook and handleEventBotUser (discord) 2020-12-13 23:19:48 +01:00
Qais Patankar
aef2dcdfdd Move webhook specific logic to its own file (discord) 2020-12-13 23:19:48 +01:00
Qais Patankar
0494119bf4 Extract maybeGetLocalAvatar into its own function (discord) 2020-12-13 23:19:48 +01:00
Qais Patankar
0a17e21119 Remove WebhookURL support (discord) 2020-12-13 23:19:48 +01:00
Qais Patankar
52e2f926f4 Add initial transmitter implementation (discord)
This has been tested with one webhook in one channel.

Sends, edits and deletions work fine
2020-12-13 23:19:48 +01:00
Qais Patankar
611fb279bc Revert "Disable webhook editing (#1296)" (discord)
This reverts commit c23252ab53.
2020-12-13 23:19:48 +01:00
Gary Kim
41b4e64be9 Update go-nc-talk (nctalk) (#1333)
Signed-off-by: Gary Kim <gary@garykim.dev>
2020-12-10 00:06:27 +01:00
Wim
0d7315249d Update vendor (#1330) 2020-12-06 23:16:02 +01:00
Wim
4913766d58 Parse fencedcode in ParseMarkdown. Fixes #1127 (#1329) 2020-12-06 19:38:32 +01:00
Wim
92da8c7044 Mark messages as read (matrix). Fixes #1317 (#1328) 2020-12-06 17:49:35 +01:00
Wim
9dba3d5385 Update rocketchat vendor (#1327)
Contains fixes for #992 and adds more random ID
2020-12-06 17:23:37 +01:00
Wim
2d3c26a4b2 Implement ratelimiting (matrix). Fixes #1238 (#1326) 2020-12-06 17:18:25 +01:00
Wim
8eba2d3e50 Make handlers run async (irc) (#1325)
This makes the handlers run in a seperate go-routine in girc, and makes
sure that girc isn't blocked on executing PONG requests when
matterbridge takes a long time handling the incoming message.

This can happen when another bridge is in a backoff state where the
backoff time exceeds the IRC ping timeout.
2020-12-05 21:41:45 +01:00
Qais Patankar
a8d4a27de1 Remove locale from golangci misspell (#1324) 2020-12-05 15:22:23 +01:00
Qais Patankar
c42167c6f4 Refactor guild finding code (discord) (#1319) 2020-12-03 22:36:08 +01:00
Sebastian P
44d182e2f9 Add nil checks to text message handling (mumble) (#1321) 2020-12-03 22:25:33 +01:00
Wim
ad95e35687 Rename jfif to jpg (whatsapp). Fixes #1292 2020-11-29 15:37:20 +01:00
Wim
640a9995f4 Refactor handleTextMessage (whatsapp) 2020-11-29 15:37:20 +01:00
Wim
95625f6871 Refactor image downloads (whatsapp) 2020-11-29 15:37:20 +01:00
Wim
2c20f72a9c Handle audio downloads (whatsapp) 2020-11-29 15:37:20 +01:00
Wim
5ad788e768 Handle video downloads (whatsapp) 2020-11-29 15:37:20 +01:00
Wim
ed98c586c6 Add support for deleting messages (whatsapp) 2020-11-29 15:37:20 +01:00
Wim
3e865708d6 Refactor/cleanup code (whatsapp) 2020-11-29 15:37:20 +01:00
JeremyRand
c3bcbd63c0 Add UserID to RemoteNickFormat and Tengo (#1308)
* Add UserID to RemoteNickFormat and Tengo

* Use strings.ReplaceAll in gateway.modifyUsername

Fixes a warning from gocritic linter.

* Use Unicode escape sequence instead of raw ZWSP in gateway.modifyUsername

Fixes a warning from stylecheck linter.
2020-11-25 23:54:27 +01:00
Simon THOBY
29e29439ee Show mxids in case of clashing usernames (matrix) (#1309)
Fixes #1302.
2020-11-25 23:51:23 +01:00
Wim
0c19716f44 Join on invite (irc). Fixes #1231 (#1306) 2020-11-22 22:44:15 +01:00
Wim
b24e1bafa1 Add support for irc to irc notice (irc). Fixes #754 (#1305) 2020-11-22 22:21:02 +01:00
Wim
64b899ac89 Retry until we have contacts (whatsapp). Fixes #1122 (#1304) 2020-11-22 21:42:54 +01:00
Wim
aa274e5ab7 Update discordgo fork (#1303) 2020-11-22 19:21:34 +01:00
Wim
7b3eaf3ccf Bump version 2020-11-22 18:55:21 +01:00
303 changed files with 24711 additions and 7770 deletions

View File

@@ -91,7 +91,6 @@ linters-settings:
# Correct spellings using locale preferences for US or UK.
# Default is to use a neutral variety of English.
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
locale: US
lll:
# max line length, lines longer will be reported. Default is 120.
# '\t' is counted as 1 character by default, and can be changed with the tab-width option

View File

@@ -1,11 +1,9 @@
FROM alpine AS builder
COPY . /go/src/github.com/42wim/matterbridge
RUN apk update && apk add go git gcc musl-dev \
&& cd /go/src/github.com/42wim/matterbridge \
&& export GOPATH=/go \
&& go get \
&& go build -x -ldflags "-X main.githash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge
COPY . /go/src/matterbridge
RUN apk --no-cache add go git \
&& cd /go/src/matterbridge \
&& go build -mod vendor -ldflags "-X main.githash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge
FROM alpine
RUN apk --no-cache add ca-certificates mailcap

View File

@@ -70,7 +70,7 @@ And more...
- [Changelog](#changelog)
- [FAQ](#faq)
- [Related projects](#related-projects)
- [Articles](#articles)
- [Articles / Tutorials](#articles--tutorials)
- [Thanks](#thanks)
## Features
@@ -91,14 +91,15 @@ And more...
- [IRC](http://www.mirc.com/servers.html)
- [Keybase](https://keybase.io)
- [Matrix](https://matrix.org)
- [Mattermost](https://github.com/mattermost/mattermost-server/) 4.x, 5.x
- [Mattermost](https://github.com/mattermost/mattermost-server/)
- [Microsoft Teams](https://teams.microsoft.com)
- [Mumble](https://www.mumble.info/)
- [Nextcloud Talk](https://nextcloud.com/talk/)
- [Rocket.chat](https://rocket.chat)
- [Slack](https://slack.com)
- [Ssh-chat](https://github.com/shazow/ssh-chat)
- [Steam](https://store.steampowered.com/)
- ~~[Steam](https://store.steampowered.com/)~~
- Not supported anymore, see [here](https://github.com/Philipp15b/go-steam/issues/94) for more info.
- [Telegram](https://telegram.org)
- [Twitch](https://twitch.tv)
- [WhatsApp](https://www.whatsapp.com/)
@@ -110,9 +111,11 @@ And more...
- [Discourse](https://github.com/DeclanHoare/matterbabble)
- [Facebook messenger](https://github.com/VictorNine/fbridge)
- [Minecraft](https://github.com/elytra/MatterLink)
- [Minecraft](https://github.com/raws/mattercraft)
- [Reddit](https://github.com/bonehurtingjuice/mattereddit)
- [Counter-Strike, half-life and more](https://forums.alliedmods.net/showthread.php?t=319430)
- [MatterAMXX](https://github.com/GabeIggy/MatterAMXX)
- [Vintage Story](https://github.com/NikkyAI/vs-matterbridge)
### API
@@ -122,11 +125,13 @@ More info and examples on the [wiki](https://github.com/42wim/matterbridge/wiki/
Used by the projects below. Feel free to make a PR to add your project to this list.
- [MatterLink](https://github.com/elytra/MatterLink) (Matterbridge link for Minecraft Server chat)
- [Minecraft](https://github.com/raws/mattercraft) (Matterbridge link for Minecraft Server chat)
- [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
- [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support)
- [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
- [matterbabble](https://github.com/DeclanHoare/matterbabble) (Discourse support)
- [MatterAMXX](https://forums.alliedmods.net/showthread.php?t=319430) (Counter-Strike, half-life and more via AMXX mod)
- [Vintage Story](https://github.com/NikkyAI/vs-matterbridge)
## Chat with us
@@ -153,10 +158,10 @@ See <https://github.com/42wim/matterbridge/wiki>
### Binaries
- Latest stable release [v1.20.0](https://github.com/42wim/matterbridge/releases/latest)
- Latest stable release [v1.21.0](https://github.com/42wim/matterbridge/releases/latest)
- Development releases (follows master) can be downloaded [here](https://github.com/42wim/matterbridge/actions) selecting the latest green build and then artifacts.
To install or upgrade just download the latest [binary](https://github.com/42wim/matterbridge/releases/latest) and follow the instructions on the [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
To install or upgrade just download the latest [binary](https://github.com/42wim/matterbridge/releases/latest). On \*nix platforms you may need to make the binary executable - you can do this by running `chmod a+x` on the binary (example: `chmod a+x matterbridge-1.20.0-linux-64bit`). After downloading (and making the binary executable, if necessary), follow the instructions on the [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
### Packages
@@ -168,7 +173,7 @@ To install or upgrade just download the latest [binary](https://github.com/42wim
Most people just want to use binaries, you can find those [here](https://github.com/42wim/matterbridge/releases/latest)
If you really want to build from source, follow these instructions:
Go 1.12+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed.
Go 1.13+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed.
```bash
go get github.com/42wim/matterbridge
@@ -296,8 +301,11 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
- [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
- [isla](https://github.com/alphachung/isla) (Bot for Discord-Telegram groups used alongside matterbridge)
- [matterbabble](https://github.com/DeclanHoare/matterbabble) (Connect Discourse threads to Matterbridge)
- [nextcloud talk](https://github.com/nextcloud/talk_matterbridge) (Integrates matterbridge in Nextcloud Talk)
- [mattercraft](https://github.com/raws/mattercraft) (Minecraft bridge)
- [vs-matterbridge](https://github.com/NikkyAI/vs-matterbridge) (Vintage Story bridge)
## Articles
## Articles / Tutorials
- [matterbridge on kubernetes](https://medium.freecodecamp.org/using-kubernetes-to-deploy-a-chat-gateway-or-when-technology-works-like-its-supposed-to-a169a8cd69a3)
- <https://mattermost.com/blog/connect-irc-to-mattermost/>
@@ -308,6 +316,8 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
- <https://www.stitcher.com/s/?eid=52382713>
- <https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/>
- <https://userlinux.net/mattermost-and-matterbridge.html>
- <https://nextcloud.com/blog/bridging-chat-services-in-talk/>
- Youtube: [whatsapp - telegram bridging](https://www.youtube.com/watch?v=W-VXISoKtNc)
## Thanks

View File

@@ -26,8 +26,11 @@ const (
EventAPIConnected = "api_connected"
EventUserTyping = "user_typing"
EventGetChannelMembers = "get_channel_members"
EventNoticeIRC = "notice_irc"
)
const ParentIDNotFound = "msg-parent-not-found"
type Message struct {
Text string `json:"text"`
Channel string `json:"channel"`
@@ -44,6 +47,14 @@ type Message struct {
Extra map[string][]interface{}
}
func (m Message) ParentNotFound() bool {
return m.ParentID == ParentIDNotFound
}
func (m Message) ParentValid() bool {
return m.ParentID != "" && !m.ParentNotFound()
}
type FileInfo struct {
Name string
Data *[]byte

View File

@@ -2,13 +2,13 @@ package bdiscord
import (
"bytes"
"errors"
"fmt"
"strings"
"sync"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/discord/transmitter"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/matterbridge/discordgo"
)
@@ -20,12 +20,9 @@ type Bdiscord struct {
c *discordgo.Session
nick string
userID string
guildID string
webhookID string
webhookToken string
canEditWebhooks bool
nick string
userID string
guildID string
channelsMutex sync.RWMutex
channels []*discordgo.Channel
@@ -34,6 +31,10 @@ type Bdiscord struct {
membersMutex sync.RWMutex
userMemberMap map[string]*discordgo.Member
nickMemberMap map[string]*discordgo.Member
// Webhook specific logic
useAutoWebhooks bool
transmitter *transmitter.Transmitter
}
func New(cfg *bridge.Config) bridge.Bridger {
@@ -41,23 +42,18 @@ func New(cfg *bridge.Config) bridge.Bridger {
b.userMemberMap = make(map[string]*discordgo.Member)
b.nickMemberMap = make(map[string]*discordgo.Member)
b.channelInfoMap = make(map[string]*config.ChannelInfo)
if b.GetString("WebhookURL") != "" {
b.Log.Debug("Configuring Discord Incoming Webhook")
b.webhookID, b.webhookToken = b.splitURL(b.GetString("WebhookURL"))
b.useAutoWebhooks = b.GetBool("AutoWebhooks")
if b.useAutoWebhooks {
b.Log.Debug("Using automatic webhooks")
}
return b
}
func (b *Bdiscord) Connect() error {
var err error
var guildFound bool
token := b.GetString("Token")
b.Log.Info("Connecting")
if b.GetString("WebhookURL") == "" {
b.Log.Info("Connecting using token")
} else {
b.Log.Info("Connecting using webhookurl (for posting) and token")
}
if !strings.HasPrefix(b.GetString("Token"), "Bot ") {
token = "Bot " + b.GetString("Token")
}
@@ -94,66 +90,107 @@ func (b *Bdiscord) Connect() error {
serverName := strings.Replace(b.GetString("Server"), "ID:", "", -1)
b.nick = userinfo.Username
b.userID = userinfo.ID
// Try and find this account's guild, and populate channels
b.channelsMutex.Lock()
for _, guild := range guilds {
if guild.Name == serverName || guild.ID == serverName {
b.channels, err = b.c.GuildChannels(guild.ID)
if err != nil {
break
}
b.guildID = guild.ID
guildFound = true
// Skip, if the server name does not match the visible name or the ID
if guild.Name != serverName && guild.ID != serverName {
continue
}
// Complain about an ambiguous Server setting. Two Discord servers could have the same title!
// For IDs, practically this will never happen. It would only trigger if some server's name is also an ID.
if b.guildID != "" {
return fmt.Errorf("found multiple Discord servers with the same name %#v, expected to see only one", serverName)
}
// Getting this guild's channel could result in a permission error
b.channels, err = b.c.GuildChannels(guild.ID)
if err != nil {
return fmt.Errorf("could not get %#v's channels: %w", b.GetString("Server"), err)
}
b.guildID = guild.ID
}
b.channelsMutex.Unlock()
if !guildFound {
msg := fmt.Sprintf("Server \"%s\" not found", b.GetString("Server"))
err = errors.New(msg)
b.Log.Error(msg)
b.Log.Info("Possible values:")
// If we couldn't find a guild, we print extra debug information and return a nice error
if b.guildID == "" {
err = fmt.Errorf("could not find Discord server %#v", b.GetString("Server"))
b.Log.Error(err.Error())
// Print all of the possible server values
b.Log.Info("Possible server values:")
for _, guild := range guilds {
b.Log.Infof("Server=\"%s\" # Server name", guild.Name)
b.Log.Infof("Server=\"%s\" # Server ID", guild.ID)
b.Log.Infof("\t- Server=%#v # by name", guild.Name)
b.Log.Infof("\t- Server=%#v # by ID", guild.ID)
}
}
if err != nil {
// If there are no results, we should say that
if len(guilds) == 0 {
b.Log.Info("\t- (none found)")
}
return err
}
b.channelsMutex.RLock()
if b.GetString("WebhookURL") == "" {
for _, channel := range b.channels {
b.Log.Debugf("found channel %#v", channel)
}
} else {
manageWebhooks := discordgo.PermissionManageWebhooks
var channelsDenied []string
for _, info := range b.Channels {
id := b.getChannelID(info.Name) // note(qaisjp): this readlocks channelsMutex
b.Log.Debugf("Verifying PermissionManageWebhooks for %s with ID %s", info.ID, id)
perms, permsErr := b.c.UserChannelPermissions(userinfo.ID, id)
if permsErr != nil {
b.Log.Warnf("Failed to check PermissionManageWebhooks in channel \"%s\": %s", info.Name, permsErr.Error())
} else if perms&manageWebhooks == manageWebhooks {
continue
}
channelsDenied = append(channelsDenied, fmt.Sprintf("%#v", info.Name))
}
b.canEditWebhooks = len(channelsDenied) == 0
b.canEditWebhooks = false
b.Log.Info("Webhook editing is disabled because of ratelimit issues")
/*
if b.canEditWebhooks {
b.Log.Info("Can manage webhooks; will edit channel for global webhook on send")
} else {
b.Log.Warn("Can't manage webhooks; won't edit channel for global webhook on send")
b.Log.Warn("Can't manage webhooks in channels: ", strings.Join(channelsDenied, ", "))
}
*/
// Legacy note: WebhookURL used to have an actual webhook URL that we would edit,
// but we stopped doing that due to Discord making rate limits more aggressive.
//
// Even older: the same WebhookURL used to be used by every channel, which is usually unexpected.
// This is no longer possible.
if b.GetString("WebhookURL") != "" {
message := "The global WebhookURL setting has been removed. "
message += "You can get similar \"webhook editing\" behaviour by replacing this line with `AutoWebhooks=true`. "
message += "If you rely on the old-OLD (non-editing) behaviour, can move the WebhookURL to specific channel sections."
b.Log.Errorln(message)
return fmt.Errorf("use of removed WebhookURL setting")
}
if b.GetInt("debuglevel") > 0 {
b.Log.Debug("enabling even more discord debug")
b.c.Debug = true
}
// Initialise webhook management
b.transmitter = transmitter.New(b.c, b.guildID, "matterbridge", b.useAutoWebhooks)
b.transmitter.Log = b.Log
var webhookChannelIDs []string
for _, channel := range b.Channels {
channelID := b.getChannelID(channel.Name) // note(qaisjp): this readlocks channelsMutex
// If a WebhookURL was not explicitly provided for this channel,
// there are two options: just a regular bot message (ugly) or this is should be webhook sent
if channel.Options.WebhookURL == "" {
// If it should be webhook sent, we should enforce this via the transmitter
if b.useAutoWebhooks {
webhookChannelIDs = append(webhookChannelIDs, channelID)
}
continue
}
whID, whToken, ok := b.splitURL(channel.Options.WebhookURL)
if !ok {
return fmt.Errorf("failed to parse WebhookURL %#v for channel %#v", channel.Options.WebhookURL, channel.ID)
}
b.transmitter.AddWebhook(channelID, &discordgo.Webhook{
ID: whID,
Token: whToken,
GuildID: b.guildID,
ChannelID: channelID,
})
}
if b.useAutoWebhooks {
err = b.transmitter.RefreshGuildWebhooks(webhookChannelIDs)
if err != nil {
b.Log.WithError(err).Println("transmitter could not refresh guild webhooks")
return err
}
}
b.channelsMutex.RUnlock()
// Obtaining guild members and initializing nickname mapping.
b.membersMutex.Lock()
@@ -210,80 +247,23 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
msg.Text = "_" + msg.Text + "_"
}
// use initial webhook configured for the entire Discord account
isGlobalWebhook := true
wID := b.webhookID
wToken := b.webhookToken
// check if have a channel specific webhook
b.channelsMutex.RLock()
if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok {
if ci.Options.WebhookURL != "" {
wID, wToken = b.splitURL(ci.Options.WebhookURL)
isGlobalWebhook = false
}
// Handle prefix hint for unthreaded messages.
if msg.ParentNotFound() {
msg.ParentID = ""
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
}
b.channelsMutex.RUnlock()
// Use webhook to send the message
if wID != "" && msg.Event != config.EventMsgDelete {
// skip events
if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange {
return "", nil
}
// skip empty messages
if msg.Text == "" && (msg.Extra == nil || len(msg.Extra["file"]) == 0) {
b.Log.Debugf("Skipping empty message %#v", msg)
return "", nil
}
msg.Text = helper.ClipMessage(msg.Text, MessageLength)
msg.Text = b.replaceUserMentions(msg.Text)
// discord username must be [0..32] max
if len(msg.Username) > 32 {
msg.Username = msg.Username[0:32]
}
if msg.ID != "" {
b.Log.Debugf("Editing webhook message")
uri := discordgo.EndpointWebhookToken(wID, wToken) + "/messages/" + msg.ID
_, err := b.c.RequestWithBucketID("PATCH", uri, discordgo.WebhookParams{
Content: msg.Text,
Username: msg.Username,
}, discordgo.EndpointWebhookToken("", ""))
if err == nil {
return msg.ID, nil
}
b.Log.Errorf("Could not edit webhook message: %s", err)
}
b.Log.Debugf("Broadcasting using Webhook")
// if we have a global webhook for this Discord account, and permission
// to modify webhooks (previously verified), then set its channel to
// the message channel before using it.
if isGlobalWebhook && b.canEditWebhooks {
b.Log.Debugf("Setting webhook channel to \"%s\"", msg.Channel)
_, err := b.c.WebhookEdit(wID, "", "", channelID)
if err != nil {
b.Log.Errorf("Could not set webhook channel: %s", err)
return "", err
}
}
b.Log.Debugf("Processing webhook sending for message %#v", msg)
msg, err := b.webhookSend(&msg, wID, wToken)
if err != nil {
b.Log.Errorf("Could not broadcast via webook for message %#v: %s", msg, err)
return "", err
}
if msg == nil {
return "", nil
}
return msg.ID, nil
useWebhooks := b.shouldMessageUseWebhooks(&msg)
if useWebhooks && msg.Event != config.EventMsgDelete && msg.ParentID == "" {
return b.handleEventWebhook(&msg, channelID)
}
return b.handleEventBotUser(&msg, channelID)
}
// handleEventDirect handles events via the bot user
func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (string, error) {
b.Log.Debugf("Broadcasting using token (API)")
// Delete message
@@ -297,7 +277,7 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
for _, rmsg := range helper.HandleExtra(msg, b.General) {
rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength)
if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil {
b.Log.Errorf("Could not send message %#v: %s", rmsg, err)
@@ -305,7 +285,7 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
}
// check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 {
return b.handleUploadFile(&msg, channelID)
return b.handleUploadFile(msg, channelID)
}
}
@@ -318,54 +298,27 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
return msg.ID, err
}
m := discordgo.MessageSend{
Content: msg.Username + msg.Text,
}
if msg.ParentValid() {
m.Reference = &discordgo.MessageReference{
MessageID: msg.ParentID,
ChannelID: channelID,
GuildID: b.guildID,
}
}
// Post normal message
res, err := b.c.ChannelMessageSend(channelID, msg.Username+msg.Text)
res, err := b.c.ChannelMessageSendComplex(channelID, &m)
if err != nil {
return "", err
}
return res.ID, nil
}
// useWebhook returns true if we have a webhook defined somewhere
func (b *Bdiscord) useWebhook() bool {
if b.GetString("WebhookURL") != "" {
return true
}
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
for _, channel := range b.channelInfoMap {
if channel.Options.WebhookURL != "" {
return true
}
}
return false
}
// isWebhookID returns true if the specified id is used in a defined webhook
func (b *Bdiscord) isWebhookID(id string) bool {
if b.GetString("WebhookURL") != "" {
wID, _ := b.splitURL(b.GetString("WebhookURL"))
if wID == id {
return true
}
}
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
for _, channel := range b.channelInfoMap {
if channel.Options.WebhookURL != "" {
wID, _ := b.splitURL(channel.Options.WebhookURL)
if wID == id {
return true
}
}
}
return false
}
// handleUploadFile handles native upload of files
func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (string, error) {
var err error
@@ -387,83 +340,3 @@ func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (stri
}
return "", nil
}
// webhookSend send one or more message via webhook, taking care of file
// uploads (from slack, telegram or mattermost).
// Returns messageID and error.
func (b *Bdiscord) webhookSend(msg *config.Message, webhookID, token string) (*discordgo.Message, error) {
var (
res *discordgo.Message
err error
)
// If avatar is unset, check if UseLocalAvatar contains the message's
// account or protocol, and if so, try to find a local avatar
if msg.Avatar == "" {
for _, val := range b.GetStringSlice("UseLocalAvatar") {
if msg.Protocol == val || msg.Account == val {
if avatar := b.findAvatar(msg); avatar != "" {
msg.Avatar = avatar
}
break
}
}
}
// WebhookParams can have either `Content` or `File`.
// We can't send empty messages.
if msg.Text != "" {
res, err = b.c.WebhookExecute(
webhookID,
token,
true,
&discordgo.WebhookParams{
Content: msg.Text,
Username: msg.Username,
AvatarURL: msg.Avatar,
},
)
if err != nil {
b.Log.Errorf("Could not send text (%s) for message %#v: %s", msg.Text, msg, err)
}
}
if msg.Extra != nil {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
file := discordgo.File{
Name: fi.Name,
ContentType: "",
Reader: bytes.NewReader(*fi.Data),
}
content := ""
if msg.Text == "" {
content = fi.Comment
}
_, e2 := b.c.WebhookExecute(
webhookID,
token,
false,
&discordgo.WebhookParams{
Username: msg.Username,
AvatarURL: msg.Avatar,
File: &file,
Content: content,
},
)
if e2 != nil {
b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, e2)
}
}
}
return res, err
}
func (b *Bdiscord) findAvatar(m *config.Message) string {
member, err := b.getGuildMemberByNick(m.Username)
if err != nil {
return ""
}
return member.User.AvatarURL("")
}

View File

@@ -69,7 +69,7 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
return
}
// if using webhooks, do not relay if it's ours
if b.useWebhook() && m.Author.Bot && b.isWebhookID(m.Author.ID) {
if m.Author.Bot && b.transmitter.HasWebhook(m.Author.ID) {
return
}
@@ -127,6 +127,11 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
// Replace emotes
rmsg.Text = replaceEmotes(rmsg.Text)
// Add our parent id if it exists, and if it's not referring to a message in another channel
if ref := m.MessageReference; ref != nil && ref.ChannelID == m.ChannelID {
rmsg.ParentID = ref.MessageID
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", m.Author.Username, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg

View File

@@ -196,7 +196,7 @@ func (b *Bdiscord) replaceAction(text string) (string, bool) {
}
// splitURL splits a webhookURL and returns the ID and token.
func (b *Bdiscord) splitURL(url string) (string, string) {
func (b *Bdiscord) splitURL(url string) (string, string, bool) {
const (
expectedWebhookSplitCount = 7
webhookIdxID = 5
@@ -204,9 +204,9 @@ func (b *Bdiscord) splitURL(url string) (string, string) {
)
webhookURLSplit := strings.Split(url, "/")
if len(webhookURLSplit) != expectedWebhookSplitCount {
b.Log.Fatalf("%s is no correct discord WebhookURL", url)
return "", "", false
}
return webhookURLSplit[webhookIdxID], webhookURLSplit[webhookIdxToken]
return webhookURLSplit[webhookIdxID], webhookURLSplit[webhookIdxToken], true
}
func enumerateUsernames(s string) []string {

View File

@@ -0,0 +1,258 @@
// Package transmitter provides functionality for transmitting
// arbitrary webhook messages to Discord.
//
// The package provides the following functionality:
//
// - Creating new webhooks, whenever necessary
// - Loading webhooks that we have previously created
// - Sending new messages
// - Editing messages, via message ID
// - Deleting messages, via message ID
//
// The package has been designed for matterbridge, but with other
// Go bots in mind. The public API should be matterbridge-agnostic.
package transmitter
import (
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/matterbridge/discordgo"
log "github.com/sirupsen/logrus"
)
// A Transmitter represents a message manager for a single guild.
type Transmitter struct {
session *discordgo.Session
guild string
title string
autoCreate bool
// channelWebhooks maps from a channel ID to a webhook instance
channelWebhooks map[string]*discordgo.Webhook
mutex sync.RWMutex
Log *log.Entry
}
// ErrWebhookNotFound is returned when a valid webhook for this channel/message combination does not exist
var ErrWebhookNotFound = errors.New("webhook for this channel and message does not exist")
// ErrPermissionDenied is returned if the bot does not have permission to manage webhooks.
//
// Bots can be granted a guild-wide permission and channel-specific permissions to manage webhooks.
// Despite potentially having guild-wide permission, channel specific overrides could deny a bot's permission to manage webhooks.
var ErrPermissionDenied = errors.New("missing 'Manage Webhooks' permission")
// New returns a new Transmitter given a Discord session, guild ID, and title.
func New(session *discordgo.Session, guild string, title string, autoCreate bool) *Transmitter {
return &Transmitter{
session: session,
guild: guild,
title: title,
autoCreate: autoCreate,
channelWebhooks: make(map[string]*discordgo.Webhook),
Log: log.NewEntry(nil),
}
}
// Send transmits a message to the given channel with the provided webhook data, and waits until Discord responds with message data.
func (t *Transmitter) Send(channelID string, params *discordgo.WebhookParams) (*discordgo.Message, error) {
wh, err := t.getOrCreateWebhook(channelID)
if err != nil {
return nil, err
}
msg, err := t.session.WebhookExecute(wh.ID, wh.Token, true, params)
if err != nil {
return nil, fmt.Errorf("execute failed: %w", err)
}
return msg, nil
}
// Edit will edit a message in a channel, if possible.
func (t *Transmitter) Edit(channelID string, messageID string, params *discordgo.WebhookParams) error {
wh := t.getWebhook(channelID)
if wh == nil {
return ErrWebhookNotFound
}
uri := discordgo.EndpointWebhookToken(wh.ID, wh.Token) + "/messages/" + messageID
_, err := t.session.RequestWithBucketID("PATCH", uri, params, discordgo.EndpointWebhookToken("", ""))
if err != nil {
return err
}
return nil
}
// HasWebhook checks whether the transmitter is using a particular webhook.
func (t *Transmitter) HasWebhook(id string) bool {
t.mutex.RLock()
defer t.mutex.RUnlock()
for _, wh := range t.channelWebhooks {
if wh.ID == id {
return true
}
}
return false
}
// AddWebhook allows you to register a channel's webhook with the transmitter.
func (t *Transmitter) AddWebhook(channelID string, webhook *discordgo.Webhook) bool {
t.Log.Debugf("Manually added webhook %#v to channel %#v", webhook.ID, channelID)
t.mutex.Lock()
defer t.mutex.Unlock()
_, replaced := t.channelWebhooks[channelID]
t.channelWebhooks[channelID] = webhook
return replaced
}
// RefreshGuildWebhooks loads "relevant" webhooks into the transmitter, with careful permission handling.
//
// Notes:
//
// - A webhook is "relevant" if it was created by this bot -- the ApplicationID should match the bot's ID.
// - The term "having permission" means having the "Manage Webhooks" permission. See ErrPermissionDenied for more information.
// - This function is additive and will not unload previously loaded webhooks.
// - A nil channelIDs slice is treated the same as an empty one.
//
// If the bot has guild-wide permission:
//
// 1. it will load any "relevant" webhooks from the entire guild
// 2. the given slice is ignored
//
// If the bot does not have guild-wide permission:
//
// 1. it will load any "relevant" webhooks in each channel
// 2. a single error will be returned if any error occurs (incl. if there is no permission for any of these channels)
//
// If any channel has more than one "relevant" webhook, it will randomly pick one.
func (t *Transmitter) RefreshGuildWebhooks(channelIDs []string) error {
t.Log.Debugln("Refreshing guild webhooks")
botID, err := getDiscordUserID(t.session)
if err != nil {
return fmt.Errorf("could not get current user: %w", err)
}
// Get all existing webhooks
hooks, err := t.session.GuildWebhooks(t.guild)
if err != nil {
switch {
case isDiscordPermissionError(err):
// We fallback on manually fetching hooks from individual channels
// if we don't have the "Manage Webhooks" permission globally.
// We can only do this if we were provided channelIDs, though.
if len(channelIDs) == 0 {
return ErrPermissionDenied
}
t.Log.Debugln("Missing global 'Manage Webhooks' permission, falling back on per-channel permission")
return t.fetchChannelsHooks(channelIDs, botID)
default:
return fmt.Errorf("could not get webhooks: %w", err)
}
}
t.Log.Debugln("Refreshing guild webhooks using global permission")
t.assignHooksByAppID(hooks, botID, false)
return nil
}
// createWebhook creates a webhook for a specific channel.
func (t *Transmitter) createWebhook(channel string) (*discordgo.Webhook, error) {
t.mutex.Lock()
defer t.mutex.Unlock()
wh, err := t.session.WebhookCreate(channel, t.title+time.Now().Format(" 3:04:05PM"), "")
if err != nil {
return nil, err
}
t.channelWebhooks[channel] = wh
return wh, nil
}
func (t *Transmitter) getWebhook(channel string) *discordgo.Webhook {
t.mutex.RLock()
defer t.mutex.RUnlock()
return t.channelWebhooks[channel]
}
func (t *Transmitter) getOrCreateWebhook(channelID string) (*discordgo.Webhook, error) {
// If we have a webhook for this channel, immediately return it
wh := t.getWebhook(channelID)
if wh != nil {
return wh, nil
}
// Early exit if we don't want to automatically create one
if !t.autoCreate {
return nil, ErrWebhookNotFound
}
t.Log.Infof("Creating a webhook for %s\n", channelID)
wh, err := t.createWebhook(channelID)
if err != nil {
return nil, fmt.Errorf("could not create webhook: %w", err)
}
return wh, nil
}
// fetchChannelsHooks fetches hooks for the given channelIDs and calls assignHooksByAppID for each channel's hooks
func (t *Transmitter) fetchChannelsHooks(channelIDs []string, botID string) error {
// For each channel, search for relevant hooks
var failedHooks []string
for _, channelID := range channelIDs {
hooks, err := t.session.ChannelWebhooks(channelID)
if err != nil {
failedHooks = append(failedHooks, "\n- "+channelID+": "+err.Error())
continue
}
t.assignHooksByAppID(hooks, botID, true)
}
// Compose an error if any hooks failed
if len(failedHooks) > 0 {
return errors.New("failed to fetch hooks:" + strings.Join(failedHooks, ""))
}
return nil
}
func (t *Transmitter) assignHooksByAppID(hooks []*discordgo.Webhook, appID string, channelTargeted bool) {
logLine := "Picking up webhook"
if channelTargeted {
logLine += " (channel targeted)"
}
t.mutex.Lock()
defer t.mutex.Unlock()
for _, wh := range hooks {
if wh.ApplicationID != appID {
continue
}
t.channelWebhooks[wh.ChannelID] = wh
t.Log.WithFields(log.Fields{
"id": wh.ID,
"name": wh.Name,
"channel": wh.ChannelID,
}).Println(logLine)
break
}
}

View File

@@ -0,0 +1,32 @@
package transmitter
import (
"github.com/matterbridge/discordgo"
)
// isDiscordPermissionError returns false for nil, and true if a Discord RESTError with code discordgo.ErrorCodeMissionPermissions
func isDiscordPermissionError(err error) bool {
if err == nil {
return false
}
restErr, ok := err.(*discordgo.RESTError)
if !ok {
return false
}
return restErr.Message != nil && restErr.Message.Code == discordgo.ErrCodeMissingPermissions
}
// getDiscordUserID gets own user ID from state, and fallback on API request
func getDiscordUserID(session *discordgo.Session) (string, error) {
if user := session.State.User; user != nil {
return user.ID, nil
}
user, err := session.User("@me")
if err != nil {
return "", err
}
return user.ID, nil
}

147
bridge/discord/webhook.go Normal file
View File

@@ -0,0 +1,147 @@
package bdiscord
import (
"bytes"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/matterbridge/discordgo"
)
// shouldMessageUseWebhooks checks if have a channel specific webhook, if we're not using auto webhooks
func (b *Bdiscord) shouldMessageUseWebhooks(msg *config.Message) bool {
if b.useAutoWebhooks {
return true
}
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok {
if ci.Options.WebhookURL != "" {
return true
}
}
return false
}
// maybeGetLocalAvatar checks if UseLocalAvatar contains the message's
// account or protocol, and if so, returns the Discord avatar (if exists)
func (b *Bdiscord) maybeGetLocalAvatar(msg *config.Message) string {
for _, val := range b.GetStringSlice("UseLocalAvatar") {
if msg.Protocol != val && msg.Account != val {
continue
}
member, err := b.getGuildMemberByNick(msg.Username)
if err != nil {
return ""
}
return member.User.AvatarURL("")
}
return ""
}
// webhookSend send one or more message via webhook, taking care of file
// uploads (from slack, telegram or mattermost).
// Returns messageID and error.
func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordgo.Message, error) {
var (
res *discordgo.Message
err error
)
// If avatar is unset, mutate the message to include the local avatar (but only if settings say we should do this)
if msg.Avatar == "" {
msg.Avatar = b.maybeGetLocalAvatar(msg)
}
// WebhookParams can have either `Content` or `File`.
// We can't send empty messages.
if msg.Text != "" {
res, err = b.transmitter.Send(
channelID,
&discordgo.WebhookParams{
Content: msg.Text,
Username: msg.Username,
AvatarURL: msg.Avatar,
},
)
if err != nil {
b.Log.Errorf("Could not send text (%s) for message %#v: %s", msg.Text, msg, err)
}
}
if msg.Extra != nil {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
file := discordgo.File{
Name: fi.Name,
ContentType: "",
Reader: bytes.NewReader(*fi.Data),
}
content := ""
if msg.Text == "" {
content = fi.Comment
}
_, e2 := b.transmitter.Send(
channelID,
&discordgo.WebhookParams{
Username: msg.Username,
AvatarURL: msg.Avatar,
File: &file,
Content: content,
},
)
if e2 != nil {
b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, e2)
}
}
}
return res, err
}
func (b *Bdiscord) handleEventWebhook(msg *config.Message, channelID string) (string, error) {
// skip events
if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange {
return "", nil
}
// skip empty messages
if msg.Text == "" && (msg.Extra == nil || len(msg.Extra["file"]) == 0) {
b.Log.Debugf("Skipping empty message %#v", msg)
return "", nil
}
msg.Text = helper.ClipMessage(msg.Text, MessageLength)
msg.Text = b.replaceUserMentions(msg.Text)
// discord username must be [0..32] max
if len(msg.Username) > 32 {
msg.Username = msg.Username[0:32]
}
if msg.ID != "" {
b.Log.Debugf("Editing webhook message")
err := b.transmitter.Edit(channelID, msg.ID, &discordgo.WebhookParams{
Content: msg.Text,
Username: msg.Username,
})
if err == nil {
return msg.ID, nil
}
b.Log.Errorf("Could not edit webhook message: %s", err)
}
b.Log.Debugf("Processing webhook sending for message %#v", msg)
discordMsg, err := b.webhookSend(msg, channelID)
if err != nil {
b.Log.Errorf("Could not broadcast via webhook for message %#v: %s", msg, err)
return "", err
}
if discordMsg == nil {
return "", nil
}
return discordMsg.ID, nil
}

View File

@@ -183,7 +183,7 @@ func ClipMessage(text string, length int) string {
// ParseMarkdown takes in an input string as markdown and parses it to html
func ParseMarkdown(input string) string {
extensions := parser.HardLineBreak | parser.NoIntraEmphasis
extensions := parser.HardLineBreak | parser.NoIntraEmphasis | parser.FencedCode
markdownParser := parser.NewWithExtensions(extensions)
renderer := html.NewRenderer(html.RendererOptions{
Flags: 0,

View File

@@ -67,6 +67,20 @@ func (b *Birc) handleFiles(msg *config.Message) bool {
return true
}
func (b *Birc) handleInvite(client *girc.Client, event girc.Event) {
if len(event.Params) != 2 {
return
}
channel := event.Params[1]
b.Log.Debugf("got invite for %s", channel)
if _, ok := b.channels[channel]; ok {
b.i.Cmd.Join(channel)
}
}
func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
if len(event.Params) == 0 {
b.Log.Debugf("handleJoinPart: empty Params? %#v", event)
@@ -109,14 +123,15 @@ func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) {
i := b.i
b.Nick = event.Params[0]
i.Handlers.Add("PRIVMSG", b.handlePrivMsg)
i.Handlers.Add("CTCP_ACTION", b.handlePrivMsg)
i.Handlers.AddBg("PRIVMSG", b.handlePrivMsg)
i.Handlers.AddBg("CTCP_ACTION", b.handlePrivMsg)
i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
i.Handlers.Add(girc.NOTICE, b.handleNotice)
i.Handlers.Add("JOIN", b.handleJoinPart)
i.Handlers.Add("PART", b.handleJoinPart)
i.Handlers.Add("QUIT", b.handleJoinPart)
i.Handlers.Add("KICK", b.handleJoinPart)
i.Handlers.AddBg(girc.NOTICE, b.handleNotice)
i.Handlers.AddBg("JOIN", b.handleJoinPart)
i.Handlers.AddBg("PART", b.handleJoinPart)
i.Handlers.AddBg("QUIT", b.handleJoinPart)
i.Handlers.AddBg("KICK", b.handleJoinPart)
i.Handlers.Add("INVITE", b.handleInvite)
}
func (b *Birc) handleNickServ() {
@@ -170,7 +185,14 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
if b.skipPrivMsg(event) {
return
}
rmsg := config.Message{Username: event.Source.Name, Channel: strings.ToLower(event.Params[0]), Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host}
rmsg := config.Message{
Username: event.Source.Name,
Channel: strings.ToLower(event.Params[0]),
Account: b.Account,
UserID: event.Source.Ident + "@" + event.Source.Host,
}
b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Last(), event)
// set action event
@@ -178,6 +200,11 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
rmsg.Event = config.EventUserAction
}
// set NOTICE event
if event.Command == "NOTICE" {
rmsg.Event = config.EventNoticeIRC
}
// strip action, we made an event if it was an action
rmsg.Text += event.StripAction()

View File

@@ -30,6 +30,7 @@ type Birc struct {
Local chan config.Message // local queue for flood control
FirstConnection, authDone bool
MessageDelay, MessageQueue, MessageLength int
channels map[string]bool
*bridge.Config
}
@@ -40,6 +41,8 @@ func New(cfg *bridge.Config) bridge.Bridger {
b.Nick = b.GetString("Nick")
b.names = make(map[string][]string)
b.connected = make(chan error)
b.channels = make(map[string]bool)
if b.GetInt("MessageDelay") == 0 {
b.MessageDelay = 1300
} else {
@@ -112,6 +115,7 @@ func (b *Birc) Disconnect() error {
}
func (b *Birc) JoinChannel(channel config.ChannelInfo) error {
b.channels[channel.Name] = true
// need to check if we have nickserv auth done before joining channels
for {
if b.authDone {
@@ -201,22 +205,58 @@ func (b *Birc) doConnect() {
}
}
// Sanitize nicks for RELAYMSG: replace IRC characters with special meanings with "-"
func sanitizeNick(nick string) string {
sanitize := func(r rune) rune {
if strings.ContainsRune("!+%@&#$:'\"?*,. ", r) {
return '-'
}
return r
}
return strings.Map(sanitize, nick)
}
func (b *Birc) doSend() {
rate := time.Millisecond * time.Duration(b.MessageDelay)
throttle := time.NewTicker(rate)
for msg := range b.Local {
<-throttle.C
username := msg.Username
if b.GetBool("Colornicks") && len(username) > 1 {
checksum := crc32.ChecksumIEEE([]byte(msg.Username))
colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes
username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username)
}
if msg.Event == config.EventUserAction {
b.i.Cmd.Action(msg.Channel, username+msg.Text)
// Optional support for the proposed RELAYMSG extension, described at
// https://github.com/jlu5/ircv3-specifications/blob/master/extensions/relaymsg.md
// nolint:nestif
if (b.i.HasCapability("overdrivenetworks.com/relaymsg") || b.i.HasCapability("draft/relaymsg")) &&
b.GetBool("UseRelayMsg") {
username = sanitizeNick(username)
text := msg.Text
// Work around girc chomping leading commas on single word messages?
if strings.HasPrefix(text, ":") && !strings.ContainsRune(text, ' ') {
text = ":" + text
}
if msg.Event == config.EventUserAction {
b.i.Cmd.SendRawf("RELAYMSG %s %s :\x01ACTION %s\x01", msg.Channel, username, text) //nolint:errcheck
} else {
b.Log.Debugf("Sending RELAYMSG to channel %s: nick=%s", msg.Channel, username)
b.i.Cmd.SendRawf("RELAYMSG %s %s :%s", msg.Channel, username, text) //nolint:errcheck
}
} else {
b.Log.Debugf("Sending to channel %s", msg.Channel)
b.i.Cmd.Message(msg.Channel, username+msg.Text)
if b.GetBool("Colornicks") {
checksum := crc32.ChecksumIEEE([]byte(msg.Username))
colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes
username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username)
}
switch msg.Event {
case config.EventUserAction:
b.i.Cmd.Action(msg.Channel, username+msg.Text)
case config.EventNoticeIRC:
b.Log.Debugf("Sending notice to channel %s", msg.Channel)
b.i.Cmd.Notice(msg.Channel, username+msg.Text)
default:
b.Log.Debugf("Sending to channel %s", msg.Channel)
b.i.Cmd.Message(msg.Channel, username+msg.Text)
}
}
}
}
@@ -264,8 +304,9 @@ func (b *Birc) getClient() (*girc.Client, error) {
TLSConfig: &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server}, //nolint:gosec
PingDelay: pingDelay,
// skip gIRC internal rate limiting, since we have our own throttling
AllowFlood: true,
Debug: debug,
AllowFlood: true,
Debug: debug,
SupportedCaps: map[string][]string{"overdrivenetworks.com/relaymsg": nil, "draft/relaymsg": nil},
})
return i, nil
}
@@ -291,7 +332,7 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool {
b.Nick = b.i.GetNick()
// freenode doesn't send 001 as first reply
if event.Command == "NOTICE" {
if event.Command == "NOTICE" && len(event.Params) != 2 {
return true
}
// don't forward queries to the bot
@@ -302,6 +343,15 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool {
if event.Source.Name == b.Nick {
return true
}
// don't forward messages we sent via RELAYMSG
if relayedNick, ok := event.Tags.Get("draft/relaymsg"); ok && relayedNick == b.Nick {
return true
}
// This is the old name of the cap sent in spoofed messages; I've kept this in
// for compatibility reasons
if relayedNick, ok := event.Tags.Get("relaymsg"); ok && relayedNick == b.Nick {
return true
}
return false
}

View File

@@ -3,6 +3,7 @@ package bmatrix
import (
"encoding/json"
"errors"
"fmt"
"html"
"strings"
"time"
@@ -82,20 +83,36 @@ func (b *Bmatrix) getDisplayName(mxid string) string {
func (b *Bmatrix) cacheDisplayName(mxid string, displayName string) string {
now := time.Now()
// scan to delete old entries, to stop memory usage from becoming too high with old entries
// scan to delete old entries, to stop memory usage from becoming too high with old entries.
// In addition, we also detect if another user have the same username, and if so, we append their mxids to their usernames to differentiate them.
toDelete := []string{}
b.RLock()
for k, v := range b.NicknameMap {
if now.Sub(v.lastUpdated) > 10*time.Minute {
toDelete = append(toDelete, k)
}
}
b.RUnlock()
conflict := false
b.Lock()
for mxid, v := range b.NicknameMap {
// to prevent username reuse across matrix servers - or even on the same server, append
// the mxid to the username when there is a conflict
if v.displayName == displayName {
conflict = true
// TODO: it would be nice to be able to rename previous messages from this user.
// The current behavior is that only users with clashing usernames and *that have spoken since the bridge last started* will get their mxids shown, and I don't know if that's the expected behavior.
v.displayName = fmt.Sprintf("%s (%s)", displayName, mxid)
b.NicknameMap[mxid] = v
}
if now.Sub(v.lastUpdated) > 10*time.Minute {
toDelete = append(toDelete, mxid)
}
}
if conflict {
displayName = fmt.Sprintf("%s (%s)", displayName, mxid)
}
for _, v := range toDelete {
delete(b.NicknameMap, v)
}
b.NicknameMap[mxid] = NicknameCacheEntry{
displayName: displayName,
lastUpdated: now,
@@ -164,3 +181,35 @@ func (b *Bmatrix) getAvatarURL(sender string) string {
return url
}
// handleRatelimit handles the ratelimit errors and return if we're ratelimited and the amount of time to sleep
func (b *Bmatrix) handleRatelimit(err error) (time.Duration, bool) {
httpErr := handleError(err)
if httpErr.Errcode != "M_LIMIT_EXCEEDED" {
return 0, false
}
b.Log.Debugf("ratelimited: %s", httpErr.Err)
b.Log.Infof("getting ratelimited by matrix, sleeping approx %d seconds before retrying", httpErr.RetryAfterMs/1000)
return time.Duration(httpErr.RetryAfterMs) * time.Millisecond, true
}
// retry function will check if we're ratelimited and retries again when backoff time expired
// returns original error if not 429 ratelimit
func (b *Bmatrix) retry(f func() error) error {
b.rateMutex.Lock()
defer b.rateMutex.Unlock()
for {
if err := f(); err != nil {
if backoff, ok := b.handleRatelimit(err); ok {
time.Sleep(backoff)
} else {
return err
}
} else {
return nil
}
}
}

View File

@@ -30,6 +30,7 @@ type Bmatrix struct {
UserID string
NicknameMap map[string]NicknameCacheEntry
RoomMap map[string]string
rateMutex sync.RWMutex
sync.RWMutex
*bridge.Config
}
@@ -99,25 +100,18 @@ func (b *Bmatrix) Disconnect() error {
}
func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error {
retry:
resp, err := b.mc.JoinRoom(channel.Name, "", nil)
if err != nil {
httpErr := handleError(err)
if httpErr.Errcode == "M_LIMIT_EXCEEDED" {
b.Log.Infof("getting ratelimited by matrix, sleeping approx %d seconds before joining %s", httpErr.RetryAfterMs/1000, channel.Name)
time.Sleep((time.Duration(httpErr.RetryAfterMs) * time.Millisecond))
goto retry
return b.retry(func() error {
resp, err := b.mc.JoinRoom(channel.Name, "", nil)
if err != nil {
return err
}
return err
}
b.Lock()
b.RoomMap[resp.RoomID] = channel.Name
b.Unlock()
b.Lock()
b.RoomMap[resp.RoomID] = channel.Name
b.Unlock()
return nil
return nil
})
}
func (b *Bmatrix) Send(msg config.Message) (string, error) {
@@ -135,11 +129,21 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
Body: username.plain + msg.Text,
FormattedBody: username.formatted + msg.Text,
}
resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m)
if err != nil {
return "", err
}
return resp.EventID, err
msgID := ""
err := b.retry(func() error {
resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m)
if err != nil {
return err
}
msgID = resp.EventID
return err
})
return msgID, err
}
// Delete message
@@ -147,17 +151,34 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
if msg.ID == "" {
return "", nil
}
resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{})
if err != nil {
return "", err
}
return resp.EventID, err
msgID := ""
err := b.retry(func() error {
resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{})
if err != nil {
return err
}
msgID = resp.EventID
return err
})
return msgID, err
}
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
if _, err := b.mc.SendText(channel, rmsg.Username+rmsg.Text); err != nil {
rmsg := rmsg
err := b.retry(func() error {
_, err := b.mc.SendText(channel, rmsg.Username+rmsg.Text)
return err
})
if err != nil {
b.Log.Errorf("sendText failed: %s", err)
}
}
@@ -187,7 +208,12 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
EventID: msg.ID,
Type: "m.replace",
}
_, err := b.mc.SendMessageEvent(channel, "m.room.message", rmsg)
err := b.retry(func() error {
_, err := b.mc.SendMessageEvent(channel, "m.room.message", rmsg)
return err
})
if err != nil {
return "", err
}
@@ -202,26 +228,58 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
Body: username.plain + msg.Text,
FormattedBody: username.formatted + msg.Text,
}
resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m)
var (
resp *matrix.RespSendEvent
err error
)
err = b.retry(func() error {
resp, err = b.mc.SendMessageEvent(channel, "m.room.message", m)
return err
})
if err != nil {
return "", err
}
return resp.EventID, err
}
if b.GetBool("HTMLDisable") {
resp, err := b.mc.SendText(channel, username.plain+msg.Text)
var (
resp *matrix.RespSendEvent
err error
)
err = b.retry(func() error {
resp, err = b.mc.SendText(channel, username.plain+msg.Text)
return err
})
if err != nil {
return "", err
}
return resp.EventID, err
}
// Post normal message with HTML support (eg riot.im)
resp, err := b.mc.SendFormattedText(channel, username.plain+msg.Text, username.formatted+helper.ParseMarkdown(msg.Text))
var (
resp *matrix.RespSendEvent
err error
)
err = b.retry(func() error {
resp, err = b.mc.SendFormattedText(channel, username.plain+msg.Text,
username.formatted+helper.ParseMarkdown(msg.Text))
return err
})
if err != nil {
return "", err
}
return resp.EventID, err
}
@@ -341,6 +399,11 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Sender, b.Account)
b.Remote <- rmsg
// not crucial, so no ratelimit check here
if err := b.mc.MarkRead(ev.RoomID, ev.ID); err != nil {
b.Log.Errorf("couldn't mark message as read %s", err.Error())
}
}
}
@@ -420,13 +483,25 @@ func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *conf
sp := strings.Split(fi.Name, ".")
mtype := mime.TypeByExtension("." + sp[len(sp)-1])
// image and video uploads send no username, we have to do this ourself here #715
_, err := b.mc.SendFormattedText(channel, username.plain+fi.Comment, username.formatted+fi.Comment)
err := b.retry(func() error {
_, err := b.mc.SendFormattedText(channel, username.plain+fi.Comment, username.formatted+fi.Comment)
return err
})
if err != nil {
b.Log.Errorf("file comment failed: %#v", err)
}
b.Log.Debugf("uploading file: %s %s", fi.Name, mtype)
res, err := b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data)))
var res *matrix.RespMediaUpload
err = b.retry(func() error {
res, err = b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data)))
return err
})
if err != nil {
b.Log.Errorf("file upload failed: %#v", err)
return
@@ -435,40 +510,56 @@ func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *conf
switch {
case strings.Contains(mtype, "video"):
b.Log.Debugf("sendVideo %s", res.ContentURI)
_, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI)
err = b.retry(func() error {
_, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI)
return err
})
if err != nil {
b.Log.Errorf("sendVideo failed: %#v", err)
}
case strings.Contains(mtype, "image"):
b.Log.Debugf("sendImage %s", res.ContentURI)
_, err = b.mc.SendImage(channel, fi.Name, res.ContentURI)
err = b.retry(func() error {
_, err = b.mc.SendImage(channel, fi.Name, res.ContentURI)
return err
})
if err != nil {
b.Log.Errorf("sendImage failed: %#v", err)
}
case strings.Contains(mtype, "audio"):
b.Log.Debugf("sendAudio %s", res.ContentURI)
_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.AudioMessage{
MsgType: "m.audio",
Body: fi.Name,
URL: res.ContentURI,
Info: matrix.AudioInfo{
Mimetype: mtype,
Size: uint(len(*fi.Data)),
},
err = b.retry(func() error {
_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.AudioMessage{
MsgType: "m.audio",
Body: fi.Name,
URL: res.ContentURI,
Info: matrix.AudioInfo{
Mimetype: mtype,
Size: uint(len(*fi.Data)),
},
})
return err
})
if err != nil {
b.Log.Errorf("sendAudio failed: %#v", err)
}
default:
b.Log.Debugf("sendFile %s", res.ContentURI)
_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.FileMessage{
MsgType: "m.file",
Body: fi.Name,
URL: res.ContentURI,
Info: matrix.FileInfo{
Mimetype: mtype,
Size: uint(len(*fi.Data)),
},
err = b.retry(func() error {
_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.FileMessage{
MsgType: "m.file",
Body: fi.Name,
URL: res.ContentURI,
Info: matrix.FileInfo{
Mimetype: mtype,
Size: uint(len(*fi.Data)),
},
})
return err
})
if err != nil {
b.Log.Errorf("sendFile failed: %#v", err)

View File

@@ -108,7 +108,7 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
Channel: message.Channel,
Text: message.Text,
ID: message.Post.Id,
ParentID: message.Post.ParentId,
ParentID: message.Post.RootId, // ParentID is obsolete with mattermost
Extra: make(map[string][]interface{}),
}

View File

@@ -122,11 +122,20 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
}
// Handle prefix hint for unthreaded messages.
if msg.ParentID == "msg-parent-not-found" {
if msg.ParentNotFound() {
msg.ParentID = ""
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
}
// we only can reply to the root of the thread, not to a specific ID (like discord for example does)
if msg.ParentID != "" {
post, res := b.mc.Client.GetPost(msg.ParentID, "")
if res.Error != nil {
b.Log.Errorf("getting post %s failed: %s", msg.ParentID, res.Error.DetailedError)
}
msg.ParentID = post.RootId
}
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {

View File

@@ -86,13 +86,16 @@ func (b *Bmsteams) JoinChannel(channel config.ChannelInfo) error {
func (b *Bmsteams) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg)
if msg.ParentID != "" && msg.ParentID != "msg-parent-not-found" {
if msg.ParentValid() {
return b.sendReply(msg)
}
if msg.ParentID == "msg-parent-not-found" {
// Handle prefix hint for unthreaded messages.
if msg.ParentNotFound() {
msg.ParentID = ""
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
}
ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(msg.Channel).Messages().Request()
text := msg.Username + msg.Text
content := &msgraph.ItemBody{Content: &text}

View File

@@ -19,6 +19,12 @@ func (b *Bmumble) handleTextMessage(event *gumble.TextMessageEvent) {
if event.TextMessage.Sender != nil {
sender = event.TextMessage.Sender.Name
}
// If the text message is received before receiving a ServerSync
// and UserState, Client.Self or Self.Channel are nil
if event.Client.Self == nil || event.Client.Self.Channel == nil {
b.Log.Warn("Connection bootstrap not finished, discarding text message")
return
}
// Convert Mumble HTML messages to markdown
parts, err := b.convertHTMLtoMarkdown(event.TextMessage.Message)
if err != nil {

View File

@@ -83,6 +83,13 @@ func (b *Btalk) JoinChannel(channel config.ChannelInfo) error {
go func() {
for msg := range c {
msg := msg
if msg.Error != nil {
b.Log.Errorf("Fatal message poll error: %s\n", msg.Error)
return
}
// ignore messages that are one of the following
// * not a message from a user
// * from ourselves

View File

@@ -299,7 +299,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
}
// Handle prefix hint for unthreaded messages.
if msg.ParentID == "msg-parent-not-found" {
if msg.ParentNotFound() {
msg.ParentID = ""
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
}

View File

@@ -311,6 +311,11 @@ func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Messa
b.maybeConvertWebp(&name, data)
}
// rename .oga to .ogg https://github.com/42wim/matterbridge/issues/906#issuecomment-741793512
if strings.HasSuffix(name, ".oga") && message.Audio != nil {
name = strings.Replace(name, ".oga", ".ogg", 1)
}
helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General)
return nil
}
@@ -384,7 +389,7 @@ func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64) string {
Name: fi.Name,
Bytes: *fi.Data,
}
re := regexp.MustCompile(".(jpg|png)$")
re := regexp.MustCompile(".(jpg|jpe|png)$")
if re.MatchString(fi.Name) {
c = tgbotapi.NewPhotoUpload(chatid, file)
} else {

View File

@@ -24,7 +24,8 @@ Check:
func (b *Bwhatsapp) HandleError(err error) {
// ignore received invalid data errors. https://github.com/42wim/matterbridge/issues/843
// ignore tag 174 errors. https://github.com/42wim/matterbridge/issues/1094
if strings.Contains(err.Error(), "error processing data: received invalid data") || strings.Contains(err.Error(), "invalid string with tag 174") {
if strings.Contains(err.Error(), "error processing data: received invalid data") ||
strings.Contains(err.Error(), "invalid string with tag 174") {
return
}
@@ -47,16 +48,22 @@ func (b *Bwhatsapp) reconnect(err error) {
Max: 5 * time.Minute,
Jitter: true,
}
for {
d := bf.Duration()
b.Log.Errorf("Connection failed, underlying error: %v", err)
b.Log.Infof("Waiting %s...", d)
time.Sleep(d)
b.Log.Info("Reconnecting...")
err := b.conn.Restore()
if err == nil {
bf.Reset()
b.startedAt = uint64(time.Now().Unix())
return
}
}
@@ -64,7 +71,7 @@ func (b *Bwhatsapp) reconnect(err error) {
// HandleTextMessage sent from WhatsApp, relay it to the brige
func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
if message.Info.FromMe { // || !strings.Contains(strings.ToLower(message.Text), "@echo") {
if message.Info.FromMe {
return
}
// whatsapp sends last messages to show context , cut them
@@ -72,12 +79,10 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
return
}
messageTime := time.Unix(int64(message.Info.Timestamp), 0) // TODO check how behaves between timezones
groupJID := message.Info.RemoteJid
senderJID := message.Info.SenderJid
if len(senderJID) == 0 {
// TODO workaround till https://github.com/Rhymen/go-whatsapp/issues/86 resolved
if message.Info.Source != nil && message.Info.Source.Participant != nil {
senderJID = *message.Info.Source.Participant
}
@@ -101,110 +106,205 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
if mention == "" {
mention = "someone"
}
message.Text = strings.Replace(message.Text, "@"+numberAndSuffix[0], "@"+mention, 1)
}
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
rmsg := config.Message{
UserID: senderJID,
Username: senderName,
Text: message.Text,
Timestamp: messageTime,
Channel: groupJID,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
UserID: senderJID,
Username: senderName,
Text: message.Text,
Channel: groupJID,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
// ParentID: TODO, // TODO handle thread replies // map from Info.QuotedMessageID string
// Event string `json:"event"`
// Gateway string // will be added during message processing
ID: message.Info.Id}
ID: message.Info.Id,
}
if avatarURL, exists := b.userAvatars[senderJID]; exists {
rmsg.Avatar = avatarURL
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
// HandleImageMessage sent from WhatsApp, relay it to the brige
func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) {
if message.Info.FromMe { // || !strings.Contains(strings.ToLower(message.Text), "@echo") {
if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
return
}
// whatsapp sends last messages to show context , cut them
if message.Info.Timestamp < b.startedAt {
return
}
messageTime := time.Unix(int64(message.Info.Timestamp), 0) // TODO check how behaves between timezones
groupJID := message.Info.RemoteJid
senderJID := message.Info.SenderJid
if len(senderJID) == 0 {
// TODO workaround till https://github.com/Rhymen/go-whatsapp/issues/86 resolved
if message.Info.Source != nil && message.Info.Source.Participant != nil {
senderJID = *message.Info.Source.Participant
}
if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
senderJID = *message.Info.Source.Participant
}
// translate sender's Jid to the nicest username we can get
senderName := b.getSenderName(senderJID)
senderName := b.getSenderName(message.Info.SenderJid)
if senderName == "" {
senderName = "Someone" // don't expose telephone number
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
rmsg := config.Message{
UserID: senderJID,
Username: senderName,
Timestamp: messageTime,
Channel: groupJID,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
// ParentID: TODO, // TODO handle thread replies // map from Info.QuotedMessageID string
// Event string `json:"event"`
// Gateway string // will be added during message processing
ID: message.Info.Id}
UserID: senderJID,
Username: senderName,
Channel: message.Info.RemoteJid,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: message.Info.Id,
}
if avatarURL, exists := b.userAvatars[senderJID]; exists {
rmsg.Avatar = avatarURL
}
// Download and unencrypt content
data, err := message.Download()
fileExt, err := mime.ExtensionsByType(message.Type)
if err != nil {
b.Log.Errorf("%v", err)
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
// Get file extension by mimetype
// rename .jfif to .jpg https://github.com/42wim/matterbridge/issues/1292
if fileExt[0] == ".jfif" {
fileExt[0] = ".jpg"
}
filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
b.Log.Debugf("Trying to download %s with type %s", filename, message.Type)
data, err := message.Download()
if err != nil {
b.Log.Errorf("Download image failed: %s", err)
return
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
// HandleVideoMessage downloads video messages
func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) {
if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
return
}
senderJID := message.Info.SenderJid
if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
senderJID = *message.Info.Source.Participant
}
senderName := b.getSenderName(message.Info.SenderJid)
if senderName == "" {
senderName = "Someone" // don't expose telephone number
}
rmsg := config.Message{
UserID: senderJID,
Username: senderName,
Channel: message.Info.RemoteJid,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: message.Info.Id,
}
if avatarURL, exists := b.userAvatars[senderJID]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(message.Type)
if err != nil {
b.Log.Errorf("%v", err)
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
b.Log.Debugf("<= Image downloaded and unencrypted")
b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type)
data, err := message.Download()
if err != nil {
b.Log.Errorf("Download video failed: %s", err)
return
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General)
b.Log.Debugf("<= Image Message is %#v", rmsg)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
//func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) {
// fmt.Println(message) // TODO implement
//}
//
//func (b *Bwhatsapp) HandleJsonMessage(message string) {
// fmt.Println(message) // TODO implement
//}
// TODO HandleRawMessage
// TODO HandleAudioMessage
// HandleAudioMessage downloads audio messages
func (b *Bwhatsapp) HandleAudioMessage(message whatsapp.AudioMessage) {
if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
return
}
senderJID := message.Info.SenderJid
if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
senderJID = *message.Info.Source.Participant
}
senderName := b.getSenderName(message.Info.SenderJid)
if senderName == "" {
senderName = "Someone" // don't expose telephone number
}
rmsg := config.Message{
UserID: senderJID,
Username: senderName,
Channel: message.Info.RemoteJid,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: message.Info.Id,
}
if avatarURL, exists := b.userAvatars[senderJID]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(message.Type)
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type)
data, err := message.Download()
if err != nil {
b.Log.Errorf("Download audio failed: %s", err)
return
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, "audio message", "", &data, b.General)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}

View File

@@ -6,22 +6,24 @@ import (
"errors"
"fmt"
"os"
"strings"
qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go"
"github.com/Rhymen/go-whatsapp"
)
type ProfilePicInfo struct {
URL string `json:"eurl"`
Tag string `json:"tag"`
Status int16 `json:"status"`
URL string `json:"eurl"`
Tag string `json:"tag"`
Status int16 `json:"status"`
}
func qrFromTerminal(invert bool) chan string {
qr := make(chan string)
go func() {
terminal := qrcodeTerminal.New()
if invert {
terminal = qrcodeTerminal.New2(qrcodeTerminal.ConsoleColors.BrightWhite, qrcodeTerminal.ConsoleColors.BrightBlack, qrcodeTerminal.QRCodeRecoveryLevels.Medium)
}
@@ -44,13 +46,12 @@ func (b *Bwhatsapp) readSession() (whatsapp.Session, error) {
if err != nil {
return session, err
}
defer file.Close()
decoder := gob.NewDecoder(file)
err = decoder.Decode(&session)
if err != nil {
return session, err
}
return session, nil
return session, decoder.Decode(&session)
}
func (b *Bwhatsapp) writeSession(session whatsapp.Session) error {
@@ -65,11 +66,31 @@ func (b *Bwhatsapp) writeSession(session whatsapp.Session) error {
if err != nil {
return err
}
defer file.Close()
encoder := gob.NewEncoder(file)
err = encoder.Encode(session)
return err
defer file.Close()
encoder := gob.NewEncoder(file)
return encoder.Encode(session)
}
func (b *Bwhatsapp) restoreSession() (*whatsapp.Session, error) {
session, err := b.readSession()
if err != nil {
b.Log.Warn(err.Error())
}
b.Log.Debugln("Restoring WhatsApp session..")
session, err = b.conn.RestoreWithSession(session)
if err != nil {
// restore session connection timed out (I couldn't get over it without logging in again)
return nil, errors.New("failed to restore session: " + err.Error())
}
b.Log.Debugln("Session restored successfully!")
return &session, nil
}
func (b *Bwhatsapp) getSenderName(senderJid string) string {
@@ -114,6 +135,7 @@ func (b *Bwhatsapp) getSenderNotify(senderJid string) string {
if sender, exists := b.users[senderJid]; exists {
return sender.Notify
}
return ""
}
@@ -122,11 +144,20 @@ func (b *Bwhatsapp) GetProfilePicThumb(jid string) (*ProfilePicInfo, error) {
if err != nil {
return nil, fmt.Errorf("failed to get avatar: %v", err)
}
content := <-data
info := &ProfilePicInfo{}
err = json.Unmarshal([]byte(content), info)
if err != nil {
return info, fmt.Errorf("failed to unmarshal avatar info: %v", err)
}
return info, nil
}
func isGroupJid(identifier string) bool {
return strings.HasSuffix(identifier, "@g.us") ||
strings.HasSuffix(identifier, "@temp") ||
strings.HasSuffix(identifier, "@broadcast")
}

View File

@@ -28,7 +28,6 @@ const (
type Bwhatsapp struct {
*bridge.Config
// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L18-L21
session *whatsapp.Session
conn *whatsapp.Conn
startedAt uint64
@@ -40,6 +39,7 @@ type Bwhatsapp struct {
// New Create a new WhatsApp bridge. This will be called for each [whatsapp.<server>] entry you have in the config file
func New(cfg *bridge.Config) bridge.Bridger {
number := cfg.GetString(cfgNumber)
if number == "" {
cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number")
}
@@ -50,21 +50,17 @@ func New(cfg *bridge.Config) bridge.Bridger {
users: make(map[string]whatsapp.Contact),
userAvatars: make(map[string]string),
}
return b
}
// Connect to WhatsApp. Required implementation of the Bridger interface
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
func (b *Bwhatsapp) Connect() error {
b.RLock() // TODO do we need locking for Whatsapp?
defer b.RUnlock()
number := b.GetString(cfgNumber)
if number == "" {
return errors.New("WhatsApp's telephone Number need to be configured")
return errors.New("whatsapp's telephone number need to be configured")
}
// https://github.com/Rhymen/go-whatsapp#creating-a-connection
b.Log.Debugln("Connecting to WhatsApp..")
conn, err := whatsapp.NewConn(20 * time.Second)
if err != nil {
@@ -77,35 +73,18 @@ func (b *Bwhatsapp) Connect() error {
b.Log.Debugln("WhatsApp connection successful")
// load existing session in order to keep it between restarts
if b.session == nil {
var session whatsapp.Session
session, err = b.readSession()
if err == nil {
b.Log.Debugln("Restoring WhatsApp session..")
// https://github.com/Rhymen/go-whatsapp#restore
session, err = b.conn.RestoreWithSession(session)
if err != nil {
// TODO return or continue to normal login?
// restore session connection timed out (I couldn't get over it without logging in again)
return errors.New("failed to restore session: " + err.Error())
}
b.session = &session
b.Log.Debugln("Session restored successfully!")
} else {
b.Log.Warn(err.Error())
}
b.session, err = b.restoreSession()
if err != nil {
b.Log.Warn(err.Error())
}
// login to a new session
if b.session == nil {
err = b.Login()
if err != nil {
if err = b.Login(); err != nil {
return err
}
}
b.startedAt = uint64(time.Now().Unix())
_, err = b.conn.Contacts()
@@ -113,6 +92,13 @@ func (b *Bwhatsapp) Connect() error {
return fmt.Errorf("error on update of contacts: %v", err)
}
// see https://github.com/Rhymen/go-whatsapp/issues/137#issuecomment-480316013
for len(b.conn.Store.Contacts) == 0 {
b.conn.Contacts() // nolint:errcheck
<-time.After(1 * time.Second)
}
// map all the users
for id, contact := range b.conn.Store.Contacts {
if !isGroupJid(id) && id != "status@broadcast" {
@@ -129,12 +115,13 @@ func (b *Bwhatsapp) Connect() error {
info, err := b.GetProfilePicThumb(jid)
if err != nil {
b.Log.Warnf("Could not get profile photo of %s: %v", jid, err)
} else {
// TODO any race conditions here?
b.Lock()
b.userAvatars[jid] = info.URL
b.Unlock()
}
}
b.Log.Debug("Finished getting avatars..")
}()
@@ -151,8 +138,10 @@ func (b *Bwhatsapp) Login() error {
session, err := b.conn.Login(qrChan)
if err != nil {
b.Log.Warnln("Failed to log in:", err)
return err
}
b.session = &session
b.Log.Infof("Logged into session: %#v", session)
@@ -163,74 +152,62 @@ func (b *Bwhatsapp) Login() error {
fmt.Fprintf(os.Stderr, "error saving session: %v\n", err)
}
// TODO change connection strings to configured ones longClientName:"github.com/rhymen/go-whatsapp", shortClientName:"go-whatsapp"}" prefix=whatsapp
// TODO get also a nice logo
// TODO notification about unplugged and dead battery
// conn.Info: Wid, Pushname, Connected, Battery, Plugged
return nil
}
// Disconnect is called while reconnecting to the bridge
// TODO 42wim Documentation would be helpful on when reconnects happen and what should be done in this function
// Required implementation of the Bridger interface
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
func (b *Bwhatsapp) Disconnect() error {
// We could Logout, but that would close the session completely and would require a new QR code scan
// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L377-L381
return nil
}
func isGroupJid(identifier string) bool {
return strings.HasSuffix(identifier, "@g.us") || strings.HasSuffix(identifier, "@temp") || strings.HasSuffix(identifier, "@broadcast")
}
// JoinChannel Join a WhatsApp group specified in gateway config as channel='number-id@g.us' or channel='Channel name'
// Required implementation of the Bridger interface
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error {
byJid := isGroupJid(channel.Name)
// see https://github.com/Rhymen/go-whatsapp/issues/137#issuecomment-480316013
for len(b.conn.Store.Contacts) == 0 {
b.conn.Contacts() // nolint:errcheck
<-time.After(1 * time.Second)
}
// verify if we are member of the given group
if byJid {
// channel.Name specifies static group jID, not the name
if _, exists := b.conn.Store.Contacts[channel.Name]; !exists {
return fmt.Errorf("account doesn't belong to group with jid %s", channel.Name)
}
} else {
// channel.Name specifies group name that might change, warn about it
var jids []string
for id, contact := range b.conn.Store.Contacts {
if isGroupJid(id) && contact.Name == channel.Name {
jids = append(jids, id)
}
}
switch len(jids) {
case 0:
// didn't match any group - print out possibilites
// TODO sort
// copy b;
//sort.Slice(people, func(i, j int) bool {
// return people[i].Age > people[j].Age
//})
for id, contact := range b.conn.Store.Contacts {
if isGroupJid(id) {
b.Log.Infof("%s %s", contact.Jid, contact.Name)
}
}
return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name)
return nil
}
case 1:
return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", jids[0], channel.Name)
default:
return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, jids)
// channel.Name specifies group name that might change, warn about it
var jids []string
for id, contact := range b.conn.Store.Contacts {
if isGroupJid(id) && contact.Name == channel.Name {
jids = append(jids, id)
}
}
return nil
switch len(jids) {
case 0:
// didn't match any group - print out possibilites
for id, contact := range b.conn.Store.Contacts {
if isGroupJid(id) {
b.Log.Infof("%s %s", contact.Jid, contact.Name)
}
}
return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name)
case 1:
return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", jids[0], channel.Name)
default:
return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, jids)
}
}
// Post a document message from the bridge to WhatsApp
@@ -304,14 +281,12 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
if msg.ID == "" {
// No message ID in case action is executed on a message sent before the bridge was started
// and then the bridge cache doesn't have this message ID mapped
// TODO 42wim Doesn't the app get clogged with a ton of IDs after some time of running?
// WhatsApp allows to set any ID so in that case we could use external IDs and don't do mapping
// but external IDs are not set
return "", nil
}
// TODO delete message on WhatsApp https://github.com/Rhymen/go-whatsapp/issues/100
return "", nil
_, err := b.conn.RevokeMessage(msg.Channel, msg.ID, true)
return "", err
}
// Edit message
@@ -319,7 +294,6 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
b.Log.Debugf("updating message with id %s", msg.ID)
msg.Text += " (edited)"
// TODO handle edit as a message reply with updated text
}
// Handle Upload a file
@@ -349,16 +323,7 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Sending %#v", msg)
// create message ID
// TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented
idBytes := make([]byte, 10)
if _, err := rand.Read(idBytes); err != nil {
b.Log.Warn(err.Error())
}
message.Info.Id = strings.ToUpper(hex.EncodeToString(idBytes))
_, err := b.conn.Send(message)
return message.Info.Id, err
return b.conn.Send(message)
}
// TODO do we want that? to allow login with QR code from a bridged channel? https://github.com/tulir/mautrix-whatsapp/blob/513eb18e2d59bada0dd515ee1abaaf38a3bfe3d5/commands.go#L76

View File

@@ -1,8 +1,11 @@
package bxmpp
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"strings"
"sync"
"time"
@@ -86,14 +89,21 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
}
// Upload a file (in XMPP case send the upload URL because XMPP has no native upload support).
var err error
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.Log.Debugf("=> Sending attachement message %#v", rmsg)
if _, err := b.xc.Send(xmpp.Chat{
Type: "groupchat",
Remote: rmsg.Channel + "@" + b.GetString("Muc"),
Text: rmsg.Username + rmsg.Text,
}); err != nil {
if b.GetString("WebhookURL") != "" {
err = b.postSlackCompatibleWebhook(msg)
} else {
_, err = b.xc.Send(xmpp.Chat{
Type: "groupchat",
Remote: rmsg.Channel + "@" + b.GetString("Muc"),
Text: rmsg.Username + rmsg.Text,
})
}
if err != nil {
b.Log.WithError(err).Error("Unable to send message with share URL.")
}
}
@@ -102,13 +112,24 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
}
}
if b.GetString("WebhookURL") != "" {
b.Log.Debugf("Sending message using Webhook")
err := b.postSlackCompatibleWebhook(msg)
if err != nil {
b.Log.Errorf("Failed to send message using webhook: %s", err)
return "", err
}
return "", nil
}
// Post normal message.
var msgReplaceID string
msgID := xid.New().String()
if msg.ID != "" {
msgID = msg.ID
msgReplaceID = msg.ID
}
// Post normal message.
b.Log.Debugf("=> Sending message %#v", msg)
if _, err := b.xc.Send(xmpp.Chat{
Type: "groupchat",
@@ -122,6 +143,25 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
return msgID, nil
}
func (b *Bxmpp) postSlackCompatibleWebhook(msg config.Message) error {
type XMPPWebhook struct {
Username string `json:"username"`
Text string `json:"text"`
}
webhookBody, err := json.Marshal(XMPPWebhook{
Username: msg.Username,
Text: msg.Text,
})
if err != nil {
b.Log.Errorf("Failed to marshal webhook: %s", err)
return err
}
resp, err := http.Post(b.GetString("WebhookURL")+"/"+msg.Channel, "application/json", bytes.NewReader(webhookBody))
resp.Body.Close()
return err
}
func (b *Bxmpp) createXMPP() error {
if !strings.Contains(b.GetString("Jid"), "@") {
return fmt.Errorf("the Jid %s doesn't contain an @", b.GetString("Jid"))
@@ -378,6 +418,11 @@ func (b *Bxmpp) skipMessage(message xmpp.Chat) bool {
return true
}
// Ignore messages posted by our webhook
if b.GetString("WebhookURL") != "" && strings.Contains(message.ID, "webhookbot") {
return true
}
// skip delayed messages
return !message.Stamp.IsZero() && time.Since(message.Stamp).Minutes() > 5
}

View File

@@ -1,3 +1,49 @@
# v1.21.0
## Breaking Changes
- discord: Remove WebhookURL support (discord) (#1323)
`WebhookURL` global setting for discord is removed and will quit matterbridge.
New `AutoWebhooks=true` setting, which will automatically use (and create, if they do not exist) webhooks inside specific channels. This only works if the bot has Manage Webhooks permission in bridged channels (global permission or as a channel permission override). Backwards compatibility with channel-specific webhooks. More info [here](https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample#L862).
## New features
- discord: Create webhooks automatically (#1323)
- discord: Add threading support with token (discord) (#1342)
- irc: Join on invite (irc). Fixes #1231 (#1306)
- irc: Add support for stateless bridging via draft/relaymsg (irc) (#1339)
- whatsapp: Add support for deleting messages (whatsapp) (#1316)
- whatsapp: Handle video downloads (whatsapp) (#1316)
- whatsapp: Handle audio downloads (whatsapp) (#1316)
## Enhancements
- general: Parse fencedcode in ParseMarkdown. Fixes #1127 (#1329)
- discord: Refactor guild finding code (discord) (#1319)
- discord: Add a prefix handler for unthreaded messages (discord) (#1346)
- irc: Add support for irc to irc notice (irc). Fixes #754 (#1305)
- irc: Make handlers run async (irc) (#1325)
- matrix: Show mxids in case of clashing usernames (matrix) (#1309)
- matrix: Implement ratelimiting (matrix). Fixes #1238 (#1326)
- matrix: Mark messages as read (matrix). Fixes #1317 (#1328)
- nctalk: Update go-nc-talk (nctalk) (#1333)
- rocketchat: Update rocketchat vendor (#1327)
- tengo: Add UserID to RemoteNickFormat and Tengo (#1308)
- whatsapp: Retry until we have contacts (whatsapp). Fixes #1122 (#1304)
- whatsapp: Refactor/cleanup code (whatsapp)
- whatsapp: Refactor handleTextMessage (whatsapp)
- whatsapp: Refactor image downloads (whatsapp)
- whatsapp: Rename jfif to jpg (whatsapp). Fixes #1292
## Bugfix
- discord: Reject cross-channel message references (discord) (#1345)
- mumble: Add nil checks to text message handling (mumble) (#1321)
This release couldn't exist without the following contributors:
@nightmared, @qaisjp, @jlu5, @wschwab, @gary-kim, @s3lph, @JeremyRand
# v1.20.0
## Breaking

View File

@@ -337,20 +337,21 @@ func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) stri
}
i++
}
nick = strings.Replace(nick, "{NOPINGNICK}", msg.Username[:i]+""+msg.Username[i:], -1)
nick = strings.ReplaceAll(nick, "{NOPINGNICK}", msg.Username[:i]+"\u200b"+msg.Username[i:])
}
nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1)
nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1)
nick = strings.Replace(nick, "{GATEWAY}", gw.Name, -1)
nick = strings.Replace(nick, "{LABEL}", br.GetString("Label"), -1)
nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
nick = strings.Replace(nick, "{CHANNEL}", msg.Channel, -1)
nick = strings.ReplaceAll(nick, "{BRIDGE}", br.Name)
nick = strings.ReplaceAll(nick, "{PROTOCOL}", br.Protocol)
nick = strings.ReplaceAll(nick, "{GATEWAY}", gw.Name)
nick = strings.ReplaceAll(nick, "{LABEL}", br.GetString("Label"))
nick = strings.ReplaceAll(nick, "{NICK}", msg.Username)
nick = strings.ReplaceAll(nick, "{USERID}", msg.UserID)
nick = strings.ReplaceAll(nick, "{CHANNEL}", msg.Channel)
tengoNick, err := gw.modifyUsernameTengo(msg, br)
if err != nil {
gw.logger.Errorf("modifyUsernameTengo error: %s", err)
}
nick = strings.Replace(nick, "{TENGO}", tengoNick, -1) //nolint:gocritic
nick = strings.ReplaceAll(nick, "{TENGO}", tengoNick)
return nick
}
@@ -430,6 +431,11 @@ func (gw *Gateway) SendMessage(
}
}
// Only send irc notices to irc
if msg.Event == config.EventNoticeIRC && dest.Protocol != "irc" {
return "", nil
}
// Too noisy to log like other events
debugSendMessage := ""
if msg.Event != config.EventUserTyping {
@@ -453,9 +459,9 @@ func (gw *Gateway) SendMessage(
}
// if the parentID is still empty and we have a parentID set in the original message
// this means that we didn't find it in the cache so set it "msg-parent-not-found"
// this means that we didn't find it in the cache so set it to a "msg-parent-not-found" constant
if msg.ParentID == "" && rmsg.ParentID != "" {
msg.ParentID = "msg-parent-not-found"
msg.ParentID = config.ParentIDNotFound
}
drop, err := gw.modifyOutMessageTengo(rmsg, &msg, dest)
@@ -544,6 +550,7 @@ func modifyInMessageTengo(filename string, msg *config.Message) error {
s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...))
_ = s.Add("msgText", msg.Text)
_ = s.Add("msgUsername", msg.Username)
_ = s.Add("msgUserID", msg.UserID)
_ = s.Add("msgAccount", msg.Account)
_ = s.Add("msgChannel", msg.Channel)
c, err := s.Compile()
@@ -572,6 +579,7 @@ func (gw *Gateway) modifyUsernameTengo(msg *config.Message, br *bridge.Bridge) (
_ = s.Add("result", "")
_ = s.Add("msgText", msg.Text)
_ = s.Add("msgUsername", msg.Username)
_ = s.Add("msgUserID", msg.UserID)
_ = s.Add("nick", msg.Username)
_ = s.Add("msgAccount", msg.Account)
_ = s.Add("msgChannel", msg.Channel)
@@ -626,6 +634,7 @@ func (gw *Gateway) modifyOutMessageTengo(origmsg *config.Message, msg *config.Me
_ = s.Add("outEvent", msg.Event)
_ = s.Add("msgText", msg.Text)
_ = s.Add("msgUsername", msg.Username)
_ = s.Add("msgUserID", msg.UserID)
_ = s.Add("msgDrop", drop)
c, err := s.Compile()
if err != nil {

View File

@@ -160,7 +160,7 @@ func (r *Router) handleReceive() {
// For some bridges we always add/update the message ID.
// This is necessary as msgIDs will change if a bridge returns
// a different ID in response to edits.
if !exists || msg.Protocol == "discord" {
if !exists {
gw.Messages.Add(msg.Protocol+" "+msg.ID, msgIDs)
}
}

32
go.mod
View File

@@ -3,16 +3,15 @@ module github.com/42wim/matterbridge
require (
github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f
github.com/Jeffail/gabs v1.1.1 // indirect
github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560
github.com/Rhymen/go-whatsapp v0.1.2-0.20201122130733-6e5488ac98df
github.com/Rhymen/go-whatsapp v0.1.2-0.20201226125722-8029c28f5c5a
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20200922220614-e4a51dfb52e4 // indirect
github.com/d5/tengo/v2 v2.6.2
github.com/davecgh/go-spew v1.1.1
github.com/fsnotify/fsnotify v1.4.9
github.com/go-telegram-bot-api/telegram-bot-api v1.0.1-0.20200524105306-7434b0456e81
github.com/gomarkdown/markdown v0.0.0-20201113031856-722100d81a8e
github.com/google/gops v0.3.13
github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 // indirect
github.com/google/gops v0.3.14
github.com/gorilla/schema v1.2.0
github.com/gorilla/websocket v1.4.2
github.com/hashicorp/golang-lru v0.5.4
@@ -21,26 +20,31 @@ require (
github.com/labstack/echo/v4 v4.1.17
github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7
github.com/matrix-org/gomatrix v0.0.0-20200827122206-7dd5e2a05bcd
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20200411204219-d5c18ce75048
github.com/matterbridge/discordgo v0.22.0
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20201206215757-c1d86d75b9f8
github.com/matterbridge/discordgo v0.22.1
github.com/matterbridge/emoji v2.1.1-0.20191117213217-af507f6b02db+incompatible
github.com/matterbridge/go-xmpp v0.0.0-20200418225040-c8a3a57b4050
github.com/matterbridge/gozulipbot v0.0.0-20200820220548-be5824faa913
github.com/matterbridge/logrus-prefixed-formatter v0.5.3-0.20200523233437-d971309a77ba
github.com/mattermost/mattermost-server/v5 v5.29.0
github.com/mattn/godown v0.0.0-20201027140031-2c7783b24de7
github.com/mattermost/mattermost-server/v5 v5.30.1
github.com/mattn/godown v0.0.1
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/missdeer/golib v1.0.4
github.com/mitchellh/mapstructure v1.3.3 // indirect
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 // indirect
github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect
github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9
github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c
github.com/rs/xid v1.2.1
github.com/russross/blackfriday v1.5.2
github.com/russross/blackfriday v1.6.0
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca
github.com/shazow/ssh-chat v1.10.1
github.com/sirupsen/logrus v1.7.0
github.com/slack-go/slack v0.7.2
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/slack-go/slack v0.7.4
github.com/spf13/afero v1.3.4 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/viper v1.7.1
github.com/stretchr/testify v1.6.1
github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50
@@ -48,11 +52,13 @@ require (
github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
github.com/yaegashi/msgraph.go v0.1.4
github.com/zfjagann/golang-ring v0.0.0-20190304061218-d34796e0a6c2
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58
gomod.garykim.dev/nc-talk v0.1.5
golang.org/x/image v0.0.0-20201208152932-35266b937fa6
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5
gomod.garykim.dev/nc-talk v0.1.7
gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376
layeh.com/gumble v0.0.0-20200818122324-146f9205029b
)
replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.3.16
go 1.15

251
go.sum
View File

@@ -34,6 +34,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
code.cloudfoundry.org/bytefmt v0.0.0-20190710193110-1eb035ffe2b6/go.mod h1:wN/zk7mhREp/oviagqUXY3EwuHhWyOvAdsn5Y4CzOrc=
code.sajari.com/docconv v1.1.1-0.20200701232649-d9ea05fbd50a/go.mod h1:DooS873W9YwUjTwEYGpg55aDlvnx1VcEdr7IJ9rEW8g=
contrib.go.opencensus.io/exporter/ocagent v0.4.9/go.mod h1:ueLzZcP7LPhPulEBukGn4aLh7Mx9YJwpVJ9nL2FYltw=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
@@ -55,8 +57,10 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw=
github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/Jeffail/gabs v1.1.1 h1:V0uzR08Hj22EX8+8QMhyI9sX2hwRu+/RJhJUmnwda/E=
github.com/Jeffail/gabs v1.1.1/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc=
github.com/HdrHistogram/hdrhistogram-go v0.9.0/go.mod h1:nxrse8/Tzg2tg3DZcZjm6qEclQKK70g0KxO61gFFZD4=
github.com/JalfResi/justext v0.0.0-20170829062021-c0282dea7198/go.mod h1:0SURuH1rsE8aVWvutuMZghRNrNrYEUzibzJfhEYR8L0=
github.com/Jeffail/gabs v1.4.0 h1://5fYRRTq1edjfIrQGvdkcd22pkYUrHZ5YC/H2GJVAo=
github.com/Jeffail/gabs v1.4.0/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc=
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4KhCrqr6GRJjdC/gNfTdxkIXvuGZZda2VM=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
@@ -70,23 +74,20 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
github.com/PaulARoy/azurestoragecache v0.0.0-20170906084534-3c249a3ba788/go.mod h1:lY1dZd8HBzJ10eqKERHn3CU59tfhzcAVb2c0ZhIWSOk=
github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560 h1:ItnC9PEEMESzTbFayxrhKBbuFQOXDBI8yy7NudTcEWs=
github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560/go.mod h1:o38AwUFFS4gzbjSoyIgrZ1h9UeDrKwcci1Pj6baifvI=
github.com/Rhymen/go-whatsapp v0.0.0/go.mod h1:rdQr95g2C1xcOfM7QGOhza58HeI3I+tZ/bbluv7VazA=
github.com/Rhymen/go-whatsapp v0.1.1-0.20201007125822-005103751b7a h1:LW+rX0NY6LzMPa2hJcgmQlfiFJUihzOMAaIoCq+P3xc=
github.com/Rhymen/go-whatsapp v0.1.1-0.20201007125822-005103751b7a/go.mod h1:o7jjkvKnigfu432dMbQ/w4PH0Yp5u4Y6ysCNjUlcYCk=
github.com/Rhymen/go-whatsapp v0.1.2-0.20201122130733-6e5488ac98df h1:w1TSfL7OvTaHlYQlF8EGqnWKVn0It6Gyn7bXR0Mz7Qw=
github.com/Rhymen/go-whatsapp v0.1.2-0.20201122130733-6e5488ac98df/go.mod h1:o7jjkvKnigfu432dMbQ/w4PH0Yp5u4Y6ysCNjUlcYCk=
github.com/Rhymen/go-whatsapp/examples/echo v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:zgCiQtBtZ4P4gFWvwl9aashsdwOcbb/EHOGRmSzM8ME=
github.com/Rhymen/go-whatsapp/examples/restoreSession v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:5sCUSpG616ZoSJhlt9iBNI/KXBqrVLcNUJqg7J9+8pU=
github.com/Rhymen/go-whatsapp/examples/sendImage v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:RdiyhanVEGXTam+mZ3k6Y3VDCCvXYCwReOoxGozqhHw=
github.com/Rhymen/go-whatsapp/examples/sendTextMessages v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:suwzklatySS3Q0+NCxCDh5hYfgXdQUWU1DNcxwAxStM=
github.com/RoaringBitmap/roaring v0.4.21/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
github.com/RoaringBitmap/roaring v0.5.0/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
github.com/PuerkitoBio/goquery v1.4.1/go.mod h1:T9ezsOHcCrDCgA8aF1Cqr3sSYbO/xgdy8/R/XiIMAhA=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
github.com/RoaringBitmap/roaring v0.5.1/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20200922220614-e4a51dfb52e4 h1:u7UvmSK6McEMXFZB310/YZ6uvfDaSFrSoqWoy/qaOW0=
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20200922220614-e4a51dfb52e4/go.mod h1:wVff6N8s2foRPCYeynerOM/FF44uyI60/HMiboL0SXw=
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
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/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/advancedlogic/GoOse v0.0.0-20191112112754-e742535969c1/go.mod h1:f3HCSN1fBWjcpGtXyM119MJgeQl838v6so/PQOqvE1w=
github.com/advancedlogic/GoOse v0.0.0-20200830213114-1225d531e0ad/go.mod h1:f3HCSN1fBWjcpGtXyM119MJgeQl838v6so/PQOqvE1w=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -96,9 +97,15 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alexcesaro/log v0.0.0-20150915221235-61e686294e58 h1:MkpmYfld/S8kXqTYI68DfL8/hHXjHogL120Dy00TIxc=
github.com/alexcesaro/log v0.0.0-20150915221235-61e686294e58/go.mod h1:YNfsMyWSs+h+PaYkxGeMVmVCX75Zj/pqdjbu12ciCYE=
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI=
github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
@@ -109,6 +116,7 @@ github.com/avct/uasurfer v0.0.0-20191028135549-26b5daa857f1/go.mod h1:noBAuukeYO
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.19.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.35.5/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
@@ -119,16 +127,17 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/blevesearch/bleve v1.0.9/go.mod h1:tb04/rbU29clbtNgorgFd8XdJea4x3ybYaOjWKr+UBU=
github.com/blevesearch/bleve v1.0.12/go.mod h1:G0ErXWdIrUSYZLPoMpS9Z3saTnTsk4ebhPsVv/+0nxk=
github.com/blevesearch/blevex v0.0.0-20190916190636-152f0fe5c040/go.mod h1:WH+MU2F4T0VmSdaPX+Wu5GYoZBrYWdOZWSjzvYcDmqQ=
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+7LMvAB5IbSA=
github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ=
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
github.com/blevesearch/zap/v11 v11.0.9/go.mod h1:47hzinvmY2EvvJruzsSCJpro7so8L1neseaGjrtXHOY=
github.com/blevesearch/zap/v12 v12.0.9/go.mod h1:paQuvxy7yXor+0Mx8p2KNmJgygQbQNN+W6HRfL5Hvwc=
github.com/blevesearch/zap/v13 v13.0.1/go.mod h1:XmyNLMvMf8Z5FjLANXwUeDW3e1+o77TTGUWrth7T9WI=
github.com/blevesearch/zap/v14 v14.0.0/go.mod h1:sUc/gPGJlFbSQ2ZUh/wGRYwkKx+Dg/5p+dd+eq6QMXk=
github.com/blevesearch/zap/v11 v11.0.12/go.mod h1:JLfFhc8DWP01zMG/6VwEY2eAnlJsTN1vDE4S0rC5Y78=
github.com/blevesearch/zap/v12 v12.0.12/go.mod h1:1HrB4hhPfI8u8x4SPYbluhb8xhflpPvvj8EcWImNnJY=
github.com/blevesearch/zap/v13 v13.0.4/go.mod h1:YdB7UuG7TBWu/1dz9e2SaLp1RKfFfdJx+ulIK5HR1bA=
github.com/blevesearch/zap/v14 v14.0.3/go.mod h1:oObAhcDHw7p1ahiTCqhRkdxdl7UA8qpvX10pSgrTMHc=
github.com/blevesearch/zap/v15 v15.0.1/go.mod h1:ho0frqAex2ktT9cYFAxQpoQXsxb/KEfdjpx4s49rf/M=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
@@ -164,11 +173,12 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc
github.com/corpix/uarand v0.1.1/go.mod h1:SFKZvkcRoLqVRFZ4u25xPmp6m9ktANfbpXZ7SJ0/FNU=
github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k=
github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs=
github.com/couchbase/vellum v1.0.1/go.mod h1:FcwrEivFpNi24R3jLOs3n+fs5RnuQnQqCLBJ1uAg1W4=
github.com/couchbase/vellum v1.0.2/go.mod h1:FcwrEivFpNi24R3jLOs3n+fs5RnuQnQqCLBJ1uAg1W4=
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/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
@@ -183,11 +193,15 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3/go.mod h1:hEfFauPHz7+NnjR/yHJGhrKo1Za+zStgwUETx3yzqgY=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/die-net/lrucache v0.0.0-20181227122439-19a39ef22a11/go.mod h1:ew0MSjCVDdtGMjF3kzLK9hwdgF5mOE8SbYVF3Rc7mkU=
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
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 v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dyatlov/go-opengraph v0.0.0-20180429202543-816b6608b3c8 h1:6muCmMJat6z7qptVrIf/+OWPxsjAfvhw5/6t+FwEkgg=
@@ -209,7 +223,9 @@ github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+ne
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
@@ -217,6 +233,8 @@ github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJn
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/frankban/quicktest v1.4.0/go.mod h1:36zfPVQyHxymz4cH7wlDmVwDrJuljRB60qkgn7rorfQ=
github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
@@ -224,6 +242,7 @@ github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
github.com/getsentry/sentry-go v0.7.0/go.mod h1:pLFpD2Y5RHIKF9Bw3KH6/68DeN2K/XBJd8awjdPnUwg=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573/go.mod h1:eBvb3i++NHDH4Ugo9qCvMw8t0mTSctaEa5blJbWcNxs=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
@@ -247,6 +266,9 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-redis/redis/v8 v8.0.0/go.mod h1:isLoQT/NFSP7V67lyvM9GmdvLdyZ7pEhsXvvyQtnQTo=
github.com/go-resty/resty/v2 v2.0.0/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -277,7 +299,6 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -294,8 +315,7 @@ github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomarkdown/markdown v0.0.0-20200824053859-8c8b3816f167 h1:LP/6EfrZ/LyCc+SXvANDrIJ4sP9u2NAtqyv6QknetNQ=
github.com/gomarkdown/markdown v0.0.0-20200824053859-8c8b3816f167/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomarkdown/markdown v0.0.0-20201113031856-722100d81a8e h1:/Y3B7hM9H3TOWPhe8eWGBGS4r09pjvS5Z0uoPADyjmU=
github.com/gomarkdown/markdown v0.0.0-20201113031856-722100d81a8e/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
@@ -311,13 +331,13 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
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/gops v0.3.11 h1:TJThjZOyavVoT7C+tkjLMzcUGud9WiWHKPLRCcd2Sro=
github.com/google/gops v0.3.11/go.mod h1:38bMPVKFh+1X106CPpbLAWtZIR1+xwgzT9gew0kn6w4=
github.com/google/gops v0.3.13 h1:8lgvDd3tXe4UxVbmPPTGE0ToIpbh3hgXkt4EVZ8Y/hU=
github.com/google/gops v0.3.13/go.mod h1:38bMPVKFh+1X106CPpbLAWtZIR1+xwgzT9gew0kn6w4=
github.com/google/gops v0.3.14 h1:4Gpv4sABlEsVqrtKxiSynzD0//kzjTIUwUm5UgkGILI=
github.com/google/gops v0.3.14/go.mod h1:zjT9F4XsKzazOvdVad3+Zwga79UHKziX3r9TN05rVN8=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@@ -331,6 +351,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@@ -344,17 +366,14 @@ github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORR
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
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/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
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/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
@@ -376,12 +395,14 @@ github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyN
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.2.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
@@ -410,7 +431,6 @@ github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20200609203250-aecfd211c9ce/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@@ -425,15 +445,19 @@ github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/
github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI=
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
github.com/jamiealquiza/envy v1.1.0/go.mod h1:MP36BriGCLwEHhi1OU8E9569JNZrjWfCvzG7RsPnHus=
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
@@ -462,11 +486,16 @@ github.com/keybase/go-ps v0.0.0-20190827175125-91aafc93ba19 h1:WjT3fLi9n8YWh/Ih8
github.com/keybase/go-ps v0.0.0-20190827175125-91aafc93ba19/go.mod h1:hY+WOq6m2FpbvyrI93sMaypsttvaIL5nhVR92dTMUcQ=
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/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.1/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kljensen/snowball v0.6.0/go.mod h1:27N7E8fVU5H68RlUmnWwZCfxgt4POBJfENGMvNRhldw=
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.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -474,10 +503,14 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
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/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
github.com/labstack/echo/v4 v4.1.17 h1:PQIBaRplyRy3OjwILGkPg89JRtH2x5bssi59G2EL3fo=
github.com/labstack/echo/v4 v4.1.17/go.mod h1:Tn2yRQL/UclUalpb5rPdXDevbkJ+lp/2svdyFBg6CHQ=
@@ -485,6 +518,8 @@ github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/ledongthuc/pdf v0.0.0-20200323191019-23c5852adbd2/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/levigross/exp-html v0.0.0-20120902181939-8df60c69a8f5/go.mod h1:QMe2wuKJ0o7zIVE8AqiT8rd8epmm6WDIZ2wyuBqYPzM=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
@@ -498,14 +533,14 @@ github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP
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.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.3/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/marstr/guid v0.0.0-20170427235115-8bdf7d1a087c/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
github.com/matrix-org/gomatrix v0.0.0-20200827122206-7dd5e2a05bcd h1:xVrqJK3xHREMNjwjljkAUaadalWc0rRbmVuQatzmgwg=
github.com/matrix-org/gomatrix v0.0.0-20200827122206-7dd5e2a05bcd/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s=
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20200411204219-d5c18ce75048 h1:B9HaistmV+MD8/33BXmZe1zPIn+RImAFVXNNSOrwU2E=
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20200411204219-d5c18ce75048/go.mod h1:c6MxwqHD+0HvtAJjsHMIdPCiAwGiQwPRPTp69ACMg8A=
github.com/matterbridge/discordgo v0.22.0 h1:6EnYacrU28X5Bc6BQDOqR6O8/b+588RKqoQJPUXEJAw=
github.com/matterbridge/discordgo v0.22.0/go.mod h1:411nZYv0UMMrtppR5glXop1foboJiFAowy+42U+Ahvw=
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20201206215757-c1d86d75b9f8 h1:2iHni9FGPRRnaZrknLp+Kyr1CWbRIRUjp89TNpkqy3I=
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20201206215757-c1d86d75b9f8/go.mod h1:c6MxwqHD+0HvtAJjsHMIdPCiAwGiQwPRPTp69ACMg8A=
github.com/matterbridge/discordgo v0.22.1 h1:Wh2NXfvF4egJDxX7jEvtgxJgT/ZOqD/5tfcIsNnHJ9o=
github.com/matterbridge/discordgo v0.22.1/go.mod h1:411nZYv0UMMrtppR5glXop1foboJiFAowy+42U+Ahvw=
github.com/matterbridge/emoji v2.1.1-0.20191117213217-af507f6b02db+incompatible h1:oaOqwbg5HxHRxvAbd84ks0Okwoc1ISyUZ87EiVJFhGI=
github.com/matterbridge/emoji v2.1.1-0.20191117213217-af507f6b02db+incompatible/go.mod h1:igE6rUAn3jai2wCdsjFHfhUoekjrFthoEjFObKKwSb4=
github.com/matterbridge/go-xmpp v0.0.0-20200418225040-c8a3a57b4050 h1:kWkP1lXpkvtoNL08jkP3XQH/zvDOEXJpdCJd/DlIvMw=
@@ -522,21 +557,18 @@ github.com/mattermost/ldap v0.0.0-20191128190019-9f62ba4b8d4d h1:2DV7VIlEv6J5R5o
github.com/mattermost/ldap v0.0.0-20191128190019-9f62ba4b8d4d/go.mod h1:HLbgMEI5K131jpxGazJ97AxfPDt31osq36YS1oxFQPQ=
github.com/mattermost/logr v1.0.13 h1:6F/fM3csvH6Oy5sUpJuW7YyZSzZZAhJm5VcgKMxA2P8=
github.com/mattermost/logr v1.0.13/go.mod h1:Mt4DPu1NXMe6JxPdwCC0XBoxXmN9eXOIRPoZarU2PXs=
github.com/mattermost/mattermost-server/v5 v5.28.1 h1:zCPvSlEYCs0REHzGxaf06Dha4UnFlhBVutH3Fthw7J0=
github.com/mattermost/mattermost-server/v5 v5.28.1/go.mod h1:9FfgZY9Ywx64bzPBYo4mmR05ApyOxO+tr43eDhpWups=
github.com/mattermost/mattermost-server/v5 v5.29.0 h1:v+qGNpMkgYRJY1qn4Rx2u1W7dbkmes47NnDZLSTIRGU=
github.com/mattermost/mattermost-server/v5 v5.29.0/go.mod h1:9FfgZY9Ywx64bzPBYo4mmR05ApyOxO+tr43eDhpWups=
github.com/mattermost/mattermost-server/v5 v5.30.1 h1:vsTTMyQcsZGevgsvR1EbQM4/RAiYC0lF4gEPcXhe84w=
github.com/mattermost/mattermost-server/v5 v5.30.1/go.mod h1:+6oGzqA4hEsoYpmFHT9j+3BtAscj7LJa/qNDxbGvrp4=
github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs=
github.com/mattermost/viper v1.0.4/go.mod h1:uc5hKG9lv4/KRwPOt2c1omOyirS/UnuA2TytiZQSFHM=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
@@ -545,6 +577,7 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
@@ -552,29 +585,27 @@ github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/Qd
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/godown v0.0.0-20200217152941-afc959f6a561 h1:0YGo77enc6tJpXQxUeQWs9bPIQPTH1lbOmc5tgRuq4o=
github.com/mattn/godown v0.0.0-20200217152941-afc959f6a561/go.mod h1:/ivCKurgV/bx6yqtP/Jtc2Xmrv3beCYBvlfAUl4X5g4=
github.com/mattn/godown v0.0.0-20201027140031-2c7783b24de7 h1:UeXqxG2wTENf2BataGYSoX3jUrJ5PI0ceUzdL1L+BPI=
github.com/mattn/godown v0.0.0-20201027140031-2c7783b24de7/go.mod h1:/ivCKurgV/bx6yqtP/Jtc2Xmrv3beCYBvlfAUl4X5g4=
github.com/mattn/godown v0.0.1 h1:39uk50ufLVQFs0eapIJVX5fCS74a1Fs2g5f1MVqIHdE=
github.com/mattn/godown v0.0.1/go.mod h1:/ivCKurgV/bx6yqtP/Jtc2Xmrv3beCYBvlfAUl4X5g4=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed/go.mod h1:dSsfyI2zABAdhcbvkXqgxOxrCsbYeHCPgrZkku60dSg=
github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/archiver/v3 v3.4.0/go.mod h1:00RcBMhNszoMqnda/5VHHL9+cBoTgj+AkLnmwr3HGrA=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.31/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw=
github.com/minio/minio-go/v7 v7.0.4/go.mod h1:CSt2ETZNs+bIIhWTse0mcZKZWMGrFU7Er7RR0TmkDYk=
github.com/minio/minio-go/v7 v7.0.5/go.mod h1:TA0CQCjJZHM5SJj9IjqR0NmpmQJ6bCbXifAJ3mUU6Hw=
github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
github.com/missdeer/golib v1.0.3 h1:+kz/tn1lXlPS8i+gjHHVAZC8YcgrmfiMTqELyvOwI4g=
github.com/missdeer/golib v1.0.3/go.mod h1:Cys1ITPPZxIk2eTcQcKT3jDsBdhICAfrrw+ki/eRXxA=
github.com/missdeer/golib v1.0.4 h1:tM7MJIPffXSmwFCTOCMjL5C7JsT5SQ+OmZwzssZQOa8=
github.com/missdeer/golib v1.0.4/go.mod h1:mPN/UcszFq0GxKfQsZI3aFOiRjnzXCBZ392od3guGEY=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
@@ -592,8 +623,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
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/monaco-io/request v1.0.4 h1:AbogA+IvPOWqyGZIFU7kSb8YS2Jv5Dnl5ncMj8cQV+o=
github.com/monaco-io/request v1.0.4/go.mod h1:EmggwHktBsbJmCgwZXqy7o0H1NNsAstQBWZrFVd3xtQ=
github.com/monaco-io/request v1.0.5 h1:QAJb5m1pCPZUGv3zzTZn7GlQI3q+uJWi7fH9QxDGbm4=
github.com/monaco-io/request v1.0.5/go.mod h1:EmggwHktBsbJmCgwZXqy7o0H1NNsAstQBWZrFVd3xtQ=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 h1:oKIteTqeSpenyTrOVj5zkiyCaflLa8B+CD0324otT+o=
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
@@ -620,6 +651,7 @@ github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9 h1:mp6tU1r0xLostUGL
github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9/go.mod h1:A5SRAcpTemjGgIuBq6Kic2yHcoeUFWUinOAlMP/i9xo=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/ngdinhtoan/glide-cleanup v0.2.0/go.mod h1:UQzsmiDOb8YV3nOsCxK/c9zPpCZVNoHScRE3EO9pVMM=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
@@ -627,22 +659,29 @@ github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQ
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/olivere/elastic v6.2.34+incompatible/go.mod h1:J+q1zQJTgAz9woqsbVRqGeB5G1iqDKVBWLNSYW8yfJ8=
github.com/olivere/elastic v6.2.35+incompatible/go.mod h1:J+q1zQJTgAz9woqsbVRqGeB5G1iqDKVBWLNSYW8yfJ8=
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/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/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.13.0 h1:M76yO2HkZASFjXL0HSoZJ1AYEmQxNJmY41Jx1zNUq1Y=
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4=
github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
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/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/oov/psd v0.0.0-20200705094106-99303fb2511f/go.mod h1:GHI1bnmAcbp96z6LNfBJvtrjxhaXGkbsk967utPlvL8=
github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs=
github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/oov/psd v0.0.0-20201002182931-74231384897f/go.mod h1:GHI1bnmAcbp96z6LNfBJvtrjxhaXGkbsk967utPlvL8=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
@@ -655,6 +694,9 @@ github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTm
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/gosseract/v2 v2.2.4/go.mod h1:ahOp/kHojnOMGv1RaUnR0jwY5JVa6BYKhYAS8nbMLSo=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@@ -666,14 +708,16 @@ github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
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.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw=
github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs=
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/peterbourgon/diskv v0.0.0-20171120014656-2973218375c3/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
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/cmdflag v0.0.2/go.mod h1:a3zKGZ3cdQUfxjd0RGMLZr8xI3nvpJOB+m6o/1X5BmU=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4/v3 v3.3.2/go.mod h1:280XNCGS8jAcG++AHdd6SeWnzyJ1w9oow2vbORyey8Q=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
@@ -708,7 +752,7 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.12.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
github.com/prometheus/common v0.14.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
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=
@@ -716,6 +760,7 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
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=
@@ -731,9 +776,11 @@ github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rudderlabs/analytics-go v3.2.1+incompatible/go.mod h1:LF8/ty9kUX4PTY3l5c97K3nZZaX5Hwsvt+NBaRL/f30=
github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7/go.mod h1:Oz4y6ImuOQZxynhbSXk7btjEfNBtGlj2dcaOvXl2FSM=
github.com/russellhaering/goxmldsig v1.1.0/go.mod h1:QK8GhXPB3+AfuCrfo0oRISa9NfzeCpWmxeGnqEpDF9o=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
@@ -742,16 +789,13 @@ github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxT
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/satori/go.uuid v0.0.0-20180103174451-36e9d2ebbde5/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/schollz/progressbar/v2 v2.13.2/go.mod h1:6YZjqdthH6SCZKv2rqGryrxPtfmRB/DWZxSMfCXPyD8=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shazow/rateio v0.0.0-20150116013248-e8e00881e5c1 h1:Lx3BlDGFElJt4u/zKc9A3BuGYbQAGlEFyPuUA3jeMD0=
github.com/shazow/rateio v0.0.0-20150116013248-e8e00881e5c1/go.mod h1:vt2jWY/3Qw1bIzle5thrJWucsLuuX9iUNnp20CqCciI=
github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4 h1:zwQ1HBo5FYwn1ksMd19qBCKO8JAWE9wmHivEpkw/DvE=
github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4/go.mod h1:vt2jWY/3Qw1bIzle5thrJWucsLuuX9iUNnp20CqCciI=
github.com/shazow/ssh-chat v1.8.3-0.20200308224626-80ddf1f43a98 h1:sN07ff+PSRsUNhpSod4uGKAQ+Nc0FXsBPG9FmYMNg4w=
github.com/shazow/ssh-chat v1.8.3-0.20200308224626-80ddf1f43a98/go.mod h1:xkTgfD+WP+KR4HuG76oal25BBEeu5kJyi2EOsgiu/4Q=
github.com/shazow/ssh-chat v1.10.1 h1:ePS+ngEYqm+yUuXegDPutysqLV2WoI22XDOeRgI6CE0=
github.com/shazow/ssh-chat v1.10.1/go.mod h1:0+7szsKylcre0vljkVnbuI6q7Odtc+QCDHxa+fFNV54=
github.com/shirou/gopsutil v2.20.4+incompatible h1:cMT4rxS55zx9NVUnCkrmXCsEB/RNfG9SwHY9evtX8Ng=
@@ -779,15 +823,16 @@ github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/simplereach/timeutils v1.2.0/go.mod h1:VVbQDfN/FHRZa1LSqcwo4kNZ62OOyqLLGQKYB3pB0Q8=
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/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 h1:lpEzuenPuO1XNTeikEmvqYFcU37GVLl8SRNblzyvGBE=
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo=
github.com/slack-go/slack v0.7.2 h1:oLy2a2YqrtoHSSxbjRhrtLDGbCKcZJwgbuQ826BWxaI=
github.com/slack-go/slack v0.7.2/go.mod h1:FGqNzJBmxIsZURAxh2a8D21AnOVvvXZvGligs4npPUM=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/slack-go/slack v0.7.4 h1:Z+7CmUDV+ym4lYLA4NNLFIpr3+nDgViHrx8xsuXgrYs=
github.com/slack-go/slack v0.7.4/go.mod h1:FGqNzJBmxIsZURAxh2a8D21AnOVvvXZvGligs4npPUM=
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/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8=
@@ -824,6 +869,9 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/splitio/go-client/v6 v6.0.0/go.mod h1:dTs/FpMfHb01sLso64uvudHFItiAW59ypqsK2Pbhuq4=
github.com/splitio/go-split-commons/v2 v2.0.0/go.mod h1:iJVknIQ96Ezic+5FJ4vHYdqDfOV5w7s+w2wKjwgjFB0=
github.com/splitio/go-toolkit/v3 v3.0.0/go.mod h1:HGgawLnM2RlM84zVRbATpPMjF7H6u9CUYG6RlpwOlOk=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/steveyen/gtreap v0.1.0/go.mod h1:kl/5J7XbrOmlIbYIXdRHDDE5QxHqpk0cmkT7Z4dM9/Y=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
@@ -831,6 +879,7 @@ github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
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/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As=
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -852,14 +901,18 @@ 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-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tulir/go-whatsapp v0.3.16 h1:NfcXC2DQXwls3qkAjbFqSeoMX+rUbbpBBGGvCXI3RUw=
github.com/tulir/go-whatsapp v0.3.16/go.mod h1:U5+sm33vrv3wz62YyRM/VS7q2ObXkxU4Xqj/3KOmN9o=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/tylerb/graceful v1.2.15/go.mod h1:LPYTbOYmUTdabwRt0TGhLllQ0MUNbs0Y5q1WXJOI9II=
github.com/uber/jaeger-client-go v2.25.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/uber/jaeger-lib v2.4.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
@@ -877,6 +930,7 @@ github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50/go.mod h
github.com/vmihailenco/msgpack/v4 v4.3.11/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1/go.mod h1:xlngVLeyQ/Qi05oQxhQ+oTuqa03RjMwMfk/7/TCs+QI=
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/wiggin77/cfg v1.0.2 h1:NBUX+iJRr+RTncTqTNvajHwzduqbhCQjEqxLHr6Fk7A=
github.com/wiggin77/cfg v1.0.2/go.mod h1:b3gotba2e5bXTqTW48DwIFoLc+4lWKP7WPi/CdvZ4aE=
github.com/wiggin77/merror v1.0.2 h1:V0nH9eFp64ASyaXC+pB5WpvBoCg7NUwvaCSKdzlcHqw=
@@ -892,6 +946,7 @@ github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7V
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.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xlab/treeprint v1.0.0 h1:J0TkWtiuYgtdlrkkrDLISYBQ92M+X5m4LrIIMKrbDTs=
github.com/xlab/treeprint v1.0.0/go.mod h1:IoImgRak9i3zJyuxOKUP1v4UZd1tMoKkq/Cimt1uhCg=
@@ -907,12 +962,12 @@ github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZ
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zfjagann/golang-ring v0.0.0-20190304061218-d34796e0a6c2 h1:UQwvu7FjUEdVYofx0U6bsc5odNE7wa5TSA0fl559GcA=
github.com/zfjagann/golang-ring v0.0.0-20190304061218-d34796e0a6c2/go.mod h1:0MsIttMJIF/8Y7x0XjonJP7K99t3sR6bjj4m5S4JmqU=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
@@ -925,24 +980,29 @@ 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.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM=
go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM=
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
@@ -951,22 +1011,21 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/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-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -979,14 +1038,16 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
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/exp v0.0.0-20200908183739-ae8ad444f925/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200801110659-972c09e46d76/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI=
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -1010,6 +1071,9 @@ golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449 h1:xUIPaMhvROX9dhPvRCenIJtU78+lbEenGbgqB5hfHCQ=
golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180530234432-1e491301e022/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=
@@ -1052,11 +1116,15 @@ golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M=
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -1066,10 +1134,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58 h1:Mj83v+wSRNEar42a/MQgxk9X42TdEmrOl9i+y8WbxLo=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 h1:Lm4OryKCca1vehdsWogr9N4t7NfZxLbJoc/H0w4K4S4=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1095,7 +1161,6 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/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=
@@ -1136,11 +1201,14 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8=
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201007165808-a893ed343c85/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65 h1:Qo9oJ566/Sq7N4hrGftVXs8GI2CXBCuOd4S2wHE/e0M=
golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d h1:MiWWjyhUzZ+jvhZvloX6ZrUsdEghn8a64Upd8EMHglE=
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/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/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1210,17 +1278,18 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200731060945-b5fad4ed8dd6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200818005847-188abfa75333/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20201008025239-9df69603baec h1:RY2OghEV/7X1MLaecgm1mwFd3sGvUddm5pGVSxQvX0c=
golang.org/x/tools v0.0.0-20201008025239-9df69603baec/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
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=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gomod.garykim.dev/nc-talk v0.1.5 h1:zZ/FviVpwJuhD/YrKiAvs6Z3Oew/DL/w6RKbKaanhFA=
gomod.garykim.dev/nc-talk v0.1.5/go.mod h1:zKg8yxCk2KaTy6aPDEfRac0Jik72czX+nRsG8CZuhtc=
gomod.garykim.dev/nc-talk v0.1.7 h1:G2qsiRcyaj5FEADQlulsBAFJHs27tPmH9VtKK+at9SM=
gomod.garykim.dev/nc-talk v0.1.7/go.mod h1:DNucAJ6zeaumBEwV5NiYk+Eea8Ca+Q5f+plhz9F7d58=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
@@ -1289,8 +1358,8 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200815001618-f69a88009b70/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201007142714-5c0e72c5e71e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
@@ -1312,6 +1381,7 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
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=
@@ -1342,15 +1412,16 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.60.0 h1:P5ZzC7RJO04094NJYlEnBdFK2wwmnCAy/+7sAzvWs60=
gopkg.in/ini.v1 v1.60.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376 h1:sY2a+y0j4iDrajJcorb+a0hJIQ6uakU5gybjfLWHlXo=
gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376/go.mod h1:BHKOc1m5wm8WwQkMqYBoo4vNxhmF7xg8+xhG8L+Cy3M=
gopkg.in/olivere/elastic.v6 v6.2.34/go.mod h1:2cTT8Z+/LcArSWpCgvZqBgt3VOqXiy7v00w12Lz8bd4=
gopkg.in/olivere/elastic.v6 v6.2.35/go.mod h1:2cTT8Z+/LcArSWpCgvZqBgt3VOqXiy7v00w12Lz8bd4=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
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=

View File

@@ -16,7 +16,7 @@ import (
)
var (
version = "1.20.0"
version = "1.21.1-dev"
githash string
flagConfig = flag.String("conf", "matterbridge.toml", "config file")

View File

@@ -193,6 +193,19 @@ ShowTopicChange=false
#OPTIONAL (default 0)
JoinDelay=0
#Use the optional RELAYMSG extension for username spoofing on IRC.
#This requires an IRCd that supports the draft/relaymsg specification: currently this includes
#Oragono 2.4.0+ and InspIRCd 3 with the m_relaymsg contrib module.
#See https://github.com/42wim/matterbridge/issues/667#issuecomment-634214165 for more details.
#Spoofed nicks will use the configured RemoteNickFormat, replacing reserved IRC characters
#(!+%@&#$:'"?*,.) with a hyphen (-).
#On most configurations, the RemoteNickFormat must include a separator character such as "/".
#You should make sure that the settings here match your IRCd.
#This option overrides ColorNicks.
#OPTIONAL (default false)
UseRelayMsg=false
#RemoteNickFormat="{NICK}/{PROTOCOL}"
###################################################################
#XMPP section
###################################################################
@@ -297,6 +310,11 @@ StripNick=false
#OPTIONAL (default false)
ShowTopicChange=false
#Enable sending messages using a webhook instead of regular MUC messages.
#Only works with a prosody server using mod_slack_webhook. Does not support editing.
#OPTIONAL (default "")
WebhookURL="https://yourdomain/prosody/msg/someid"
###################################################################
#mattermost section
###################################################################
@@ -846,10 +864,11 @@ UseUserName=false
# UseDiscriminator appends the `#xxxx` discriminator when used with UseUserName
UseDiscriminator=false
# WebhookURL sends messages in the style of puppets.
# This only works if you have one discord channel, if you have multiple discord channels you'll have to specify it in the gateway config
# Example: "https://discordapp.com/api/webhooks/1234/abcd_xyzw"
WebhookURL=""
# AutoWebhooks automatically configures message sending in the style of puppets.
# This is an easier alternative to manually configuring "WebhookURL" for each gateway,
# as turning this on will automatically load or create webhooks for each channel.
# This feature requires the "Manage Webhooks" permission (either globally or as per-channel).
AutoWebhooks=false
# EditDisable disables sending of edits to other bridges
EditDisable=false
@@ -1435,7 +1454,7 @@ Server = "mumble.yourdomain.me:64738"
# Nickname to log in as
Nick = "matterbridge"
# Some servers require a password
# Some servers require a password
# OPTIONAL (default empty)
Password = "serverpasswordhere"
@@ -1632,7 +1651,8 @@ RemoteNickFormat="{NICK}"
## Settings below can be reloaded by editing the file
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{NICK}" (case sensitive) will be replaced by the actual nick.
#The string "{USERID}" (case sensitive) will be replaced by the user ID.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
@@ -1707,7 +1727,7 @@ LogFile="/var/log/matterbridge.log"
#This script will receive every incoming message and can be used to modify the Username and the Text of that message.
#The script will have the following global variables:
#to modify: msgUsername and msgText
#to read: msgChannel and msgAccount
#to read: msgUserID, msgChannel, msgAccount
#
#The script is reloaded on every message, so you can modify the script on the fly.
#
@@ -1731,6 +1751,7 @@ InMessage="example.tengo"
#read-only:
#inAccount, inProtocol, inChannel, inGateway, inEvent
#outAccount, outProtocol, outChannel, outGateway, outEvent
#msgUserID
#
#read-write:
#msgText, msgUsername, msgDrop
@@ -1748,7 +1769,7 @@ OutMessage="example.tengo"
#RemoteNickFormat allows you to specify the location of a tengo (https://github.com/d5/tengo/) script.
#The script will have the following global variables:
#to modify: result
#to read: channel, bridge, gateway, protocol, nick
#to read: channel, bridge, gateway, protocol, nick, msgUserID
#
#The result will be set in {TENGO} in the RemoteNickFormat key of every bridge where {TENGO} is specified
#
@@ -1861,13 +1882,16 @@ enable=true
#OPTIONAL - your irc / xmpp channel key
key="yourkey"
# Discord specific gateway options
[[gateway.inout]]
account="discord.game"
channel="mygreatgame"
#OPTIONAL - webhookurl only works for discord (it needs a different URL for each cahnnel)
[gateway.inout.options]
webhookurl="https://discordapp.com/api/webhooks/123456789123456789/C9WPqExYWONPDZabcdef-def1434FGFjstasJX9pYht73y"
# WebhookURL sends messages in the style of "puppets". You must configure a webhook URL for each channel you want to bridge.
# If you have more than one channel and don't wnat to configure each channel manually, see the "AutoWebhooks" option in the gateway config.
# Example: "https://discord.com/api/webhooks/1234/abcd_xyzw"
WebhookURL=""
[[gateway.inout]]
account="zulip.streamchat"

View File

@@ -7,21 +7,17 @@ It does nothing spectacular except for being fabulous.
https://godoc.org/github.com/Jeffail/gabs
## How to install:
## Install
``` bash
go get github.com/Jeffail/gabs
```
## How to use
## Use
### Parsing and searching JSON
``` go
...
import "github.com/Jeffail/gabs"
jsonParsed, err := gabs.ParseJSON([]byte(`{
"outter":{
"inner":{
@@ -29,7 +25,10 @@ jsonParsed, err := gabs.ParseJSON([]byte(`{
"value2":22
},
"alsoInner":{
"value1":20
"value1":20,
"array1":[
30, 40
]
}
}
}`))
@@ -43,26 +42,26 @@ value, ok = jsonParsed.Path("outter.inner.value1").Data().(float64)
value, ok = jsonParsed.Search("outter", "inner", "value1").Data().(float64)
// value == 10.0, ok == true
gObj, err := jsonParsed.JSONPointer("/outter/alsoInner/array1/1")
if err != nil {
panic(err)
}
value, ok = gObj.Data().(float64)
// value == 40.0, ok == true
value, ok = jsonParsed.Path("does.not.exist").Data().(float64)
// value == 0.0, ok == false
exists := jsonParsed.Exists("outter", "inner", "value1")
// exists == true
exists := jsonParsed.Exists("does", "not", "exist")
// exists == false
exists := jsonParsed.ExistsP("does.not.exist")
// exists == false
...
```
### Iterating objects
``` go
...
jsonParsed, _ := gabs.ParseJSON([]byte(`{"object":{ "first": 1, "second": 2, "third": 3 }}`))
// S is shorthand for Search
@@ -70,24 +69,25 @@ children, _ := jsonParsed.S("object").ChildrenMap()
for key, child := range children {
fmt.Printf("key: %v, value: %v\n", key, child.Data().(string))
}
...
```
### Iterating arrays
``` go
...
jsonParsed, _ := gabs.ParseJSON([]byte(`{"array":[ "first", "second", "third" ]}`))
jsonParsed, err := gabs.ParseJSON([]byte(`{"array":[ "first", "second", "third" ]}`))
if err != nil {
panic(err)
}
// S is shorthand for Search
children, _ := jsonParsed.S("array").Children()
children, err := jsonParsed.S("array").Children()
if err != nil {
panic(err)
}
for _, child := range children {
fmt.Println(child.Data().(string))
}
...
```
Will print:
@@ -108,12 +108,11 @@ objects within the array, this returns a JSON array containing the results for
each element.
``` go
...
jsonParsed, _ := gabs.ParseJSON([]byte(`{"array":[ {"value":1}, {"value":2}, {"value":3} ]}`))
jsonParsed, err := gabs.ParseJSON([]byte(`{"array":[ {"value":1}, {"value":2}, {"value":3} ]}`))
if err != nil {
panic(err)
}
fmt.Println(jsonParsed.Path("array.value").String())
...
```
Will print:
@@ -125,8 +124,6 @@ Will print:
### Generating JSON
``` go
...
jsonObj := gabs.New()
// or gabs.Consume(jsonObject) to work on an existing map[string]interface{}
@@ -135,8 +132,6 @@ jsonObj.SetP(20, "outter.inner.value2")
jsonObj.Set(30, "outter", "inner2", "value3")
fmt.Println(jsonObj.String())
...
```
Will print:
@@ -148,11 +143,7 @@ Will print:
To pretty-print:
``` go
...
fmt.Println(jsonObj.StringIndent("", " "))
...
```
Will print:
@@ -174,8 +165,6 @@ Will print:
### Generating Arrays
``` go
...
jsonObj := gabs.New()
jsonObj.Array("foo", "array")
@@ -186,8 +175,6 @@ jsonObj.ArrayAppend(20, "foo", "array")
jsonObj.ArrayAppend(30, "foo", "array")
fmt.Println(jsonObj.String())
...
```
Will print:
@@ -199,8 +186,6 @@ Will print:
Working with arrays by index:
``` go
...
jsonObj := gabs.New()
// Create an array with the length of 3
@@ -217,8 +202,6 @@ jsonObj.S("foo").Index(2).SetIndex(2, 1)
jsonObj.S("foo").Index(2).SetIndex(3, 2)
fmt.Println(jsonObj.String())
...
```
Will print:
@@ -232,8 +215,6 @@ Will print:
This is the easiest part:
``` go
...
jsonParsedObj, _ := gabs.ParseJSON([]byte(`{
"outter":{
"values":{
@@ -246,15 +227,11 @@ jsonParsedObj, _ := gabs.ParseJSON([]byte(`{
jsonOutput := jsonParsedObj.String()
// Becomes `{"outter":{"values":{"first":10,"second":11}},"outter2":"hello world"}`
...
```
And to serialize a specific segment is as simple as:
``` go
...
jsonParsedObj := gabs.ParseJSON([]byte(`{
"outter":{
"values":{
@@ -267,8 +244,6 @@ jsonParsedObj := gabs.ParseJSON([]byte(`{
jsonOutput := jsonParsedObj.Search("outter").String()
// Becomes `{"values":{"first":10,"second":11}}`
...
```
### Merge two containers

View File

@@ -20,58 +20,85 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
// Package gabs implements a simplified wrapper around creating and parsing JSON.
// Package gabs implements a simplified wrapper around creating and parsing
// unknown or dynamic JSON.
package gabs
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"strconv"
"strings"
)
//--------------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------
var (
// ErrOutOfBounds - Index out of bounds.
// ErrOutOfBounds indicates an index was out of bounds.
ErrOutOfBounds = errors.New("out of bounds")
// ErrNotObjOrArray - The target is not an object or array type.
// ErrNotObjOrArray is returned when a target is not an object or array type
// but needs to be for the intended operation.
ErrNotObjOrArray = errors.New("not an object or array")
// ErrNotObj - The target is not an object type.
// ErrNotObj is returned when a target is not an object but needs to be for
// the intended operation.
ErrNotObj = errors.New("not an object")
// ErrNotArray - The target is not an array type.
// ErrNotArray is returned when a target is not an array but needs to be for
// the intended operation.
ErrNotArray = errors.New("not an array")
// ErrPathCollision - Creating a path failed because an element collided with an existing value.
// ErrPathCollision is returned when creating a path failed because an
// element collided with an existing value.
ErrPathCollision = errors.New("encountered value collision whilst building path")
// ErrInvalidInputObj - The input value was not a map[string]interface{}.
// ErrInvalidInputObj is returned when the input value was not a
// map[string]interface{}.
ErrInvalidInputObj = errors.New("invalid input object")
// ErrInvalidInputText - The input data could not be parsed.
// ErrInvalidInputText is returned when the input data could not be parsed.
ErrInvalidInputText = errors.New("input text could not be parsed")
// ErrInvalidPath - The filepath was not valid.
// ErrInvalidPath is returned when the filepath was not valid.
ErrInvalidPath = errors.New("invalid file path")
// ErrInvalidBuffer - The input buffer contained an invalid JSON string
// ErrInvalidBuffer is returned when the input buffer contained an invalid
// JSON string.
ErrInvalidBuffer = errors.New("input buffer contained invalid JSON")
)
//--------------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// Container - an internal structure that holds a reference to the core interface map of the parsed
// json. Use this container to move context.
func resolveJSONPointerHierarchy(path string) ([]string, error) {
if len(path) < 1 {
return nil, errors.New("failed to resolve JSON pointer: path must not be empty")
}
if path[0] != '/' {
return nil, errors.New("failed to resolve JSON pointer: path must begin with '/'")
}
hierarchy := strings.Split(path, "/")[1:]
for i, v := range hierarchy {
v = strings.Replace(v, "~1", "/", -1)
v = strings.Replace(v, "~0", "~", -1)
hierarchy[i] = v
}
return hierarchy, nil
}
//------------------------------------------------------------------------------
// Container references a specific element within a JSON structure.
type Container struct {
object interface{}
}
// Data - Return the contained data as an interface{}.
// Data returns the underlying interface{} of the target element in the JSON
// structure.
func (g *Container) Data() interface{} {
if g == nil {
return nil
@@ -79,17 +106,18 @@ func (g *Container) Data() interface{} {
return g.object
}
//--------------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// Path - Search for a value using dot notation.
// Path searches the JSON structure following a path in dot notation.
func (g *Container) Path(path string) *Container {
return g.Search(strings.Split(path, ".")...)
}
// Search - Attempt to find and return an object within the JSON structure by specifying the
// hierarchy of field names to locate the target. If the search encounters an array and has not
// reached the end target then it will iterate each object of the array for the target and return
// all of the results in a JSON array.
// Search attempts to find and return an object within the JSON structure by
// following a provided hierarchy of field names to locate the target. If the
// search encounters an array and has not reached the end target then it will
// iterate each object of the array for the target and return all of the results
// in a JSON array.
func (g *Container) Search(hierarchy ...string) *Container {
var object interface{}
@@ -120,22 +148,55 @@ func (g *Container) Search(hierarchy ...string) *Container {
return &Container{object}
}
// S - Shorthand method, does the same thing as Search.
// JSONPointer parses a JSON pointer path (https://tools.ietf.org/html/rfc6901)
// and either returns a *gabs.Container containing the result or an error if the
// referenced item could not be found.
func (g *Container) JSONPointer(path string) (*Container, error) {
hierarchy, err := resolveJSONPointerHierarchy(path)
if err != nil {
return nil, err
}
object := g.Data()
for target := 0; target < len(hierarchy); target++ {
pathSeg := hierarchy[target]
if mmap, ok := object.(map[string]interface{}); ok {
object, ok = mmap[pathSeg]
if !ok {
return nil, fmt.Errorf("failed to resolve JSON pointer: index '%v' value '%v' was not found", target, pathSeg)
}
} else if marray, ok := object.([]interface{}); ok {
index, err := strconv.Atoi(pathSeg)
if err != nil {
return nil, fmt.Errorf("failed to resolve JSON pointer: could not parse index '%v' value '%v' into array index: %v", target, pathSeg, err)
}
if len(marray) <= index {
return nil, fmt.Errorf("failed to resolve JSON pointer: index '%v' value '%v' exceeded target array size of '%v'", target, pathSeg, len(marray))
}
object = marray[index]
} else {
return &Container{nil}, fmt.Errorf("failed to resolve JSON pointer: index '%v' field '%v' was not found", target, pathSeg)
}
}
return &Container{object}, nil
}
// S is a shorthand alias for Search.
func (g *Container) S(hierarchy ...string) *Container {
return g.Search(hierarchy...)
}
// Exists - Checks whether a path exists.
// Exists checks whether a path exists.
func (g *Container) Exists(hierarchy ...string) bool {
return g.Search(hierarchy...) != nil
}
// ExistsP - Checks whether a dot notation path exists.
// ExistsP checks whether a dot notation path exists.
func (g *Container) ExistsP(path string) bool {
return g.Exists(strings.Split(path, ".")...)
}
// Index - Attempt to find and return an object within a JSON array by index.
// Index attempts to find and return an element within a JSON array by an index.
func (g *Container) Index(index int) *Container {
if array, ok := g.Data().([]interface{}); ok {
if index >= len(array) {
@@ -146,9 +207,9 @@ func (g *Container) Index(index int) *Container {
return &Container{nil}
}
// Children - Return a slice of all the children of the array. This also works for objects, however,
// the children returned for an object will NOT be in order and you lose the names of the returned
// objects this way.
// Children returns a slice of all children of an array element. This also works
// for objects, however, the children returned for an object will be in a random
// order and you lose the names of the returned objects this way.
func (g *Container) Children() ([]*Container, error) {
if array, ok := g.Data().([]interface{}); ok {
children := make([]*Container, len(array))
@@ -167,7 +228,7 @@ func (g *Container) Children() ([]*Container, error) {
return nil, ErrNotObjOrArray
}
// ChildrenMap - Return a map of all the children of an object.
// ChildrenMap returns a map of all the children of an object element.
func (g *Container) ChildrenMap() (map[string]*Container, error) {
if mmap, ok := g.Data().(map[string]interface{}); ok {
children := map[string]*Container{}
@@ -179,11 +240,11 @@ func (g *Container) ChildrenMap() (map[string]*Container, error) {
return nil, ErrNotObj
}
//--------------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// Set - Set the value of a field at a JSON path, any parts of the path that do not exist will be
// constructed, and if a collision occurs with a non object type whilst iterating the path an error
// is returned.
// Set the value of a field at a JSON path, any parts of the path that do not
// exist will be constructed, and if a collision occurs with a non object type
// whilst iterating the path an error is returned.
func (g *Container) Set(value interface{}, path ...string) (*Container, error) {
if len(path) == 0 {
g.object = value
@@ -209,12 +270,14 @@ func (g *Container) Set(value interface{}, path ...string) (*Container, error) {
return &Container{object}, nil
}
// SetP - Does the same as Set, but using a dot notation JSON path.
// SetP sets the value of a field at a JSON path using dot notation, any parts
// of the path that do not exist will be constructed, and if a collision occurs
// with a non object type whilst iterating the path an error is returned.
func (g *Container) SetP(value interface{}, path string) (*Container, error) {
return g.Set(value, strings.Split(path, ".")...)
}
// SetIndex - Set a value of an array element based on the index.
// SetIndex attempts to set a value of an array element based on an index.
func (g *Container) SetIndex(value interface{}, index int) (*Container, error) {
if array, ok := g.Data().([]interface{}); ok {
if index >= len(array) {
@@ -226,60 +289,112 @@ func (g *Container) SetIndex(value interface{}, index int) (*Container, error) {
return &Container{nil}, ErrNotArray
}
// Object - Create a new JSON object at a path. Returns an error if the path contains a collision
// with a non object type.
// SetJSONPointer parses a JSON pointer path
// (https://tools.ietf.org/html/rfc6901) and sets the leaf to a value. Returns
// an error if the pointer could not be resolved due to missing fields.
func (g *Container) SetJSONPointer(value interface{}, path string) error {
hierarchy, err := resolveJSONPointerHierarchy(path)
if err != nil {
return err
}
if len(hierarchy) == 0 {
g.object = value
return nil
}
object := g.object
for target := 0; target < len(hierarchy); target++ {
pathSeg := hierarchy[target]
if mmap, ok := object.(map[string]interface{}); ok {
if target == len(hierarchy)-1 {
object = value
mmap[pathSeg] = object
} else if object = mmap[pathSeg]; object == nil {
return fmt.Errorf("failed to resolve JSON pointer: index '%v' value '%v' was not found", target, pathSeg)
}
} else if marray, ok := object.([]interface{}); ok {
index, err := strconv.Atoi(pathSeg)
if err != nil {
return fmt.Errorf("failed to resolve JSON pointer: could not parse index '%v' value '%v' into array index: %v", target, pathSeg, err)
}
if len(marray) <= index {
return fmt.Errorf("failed to resolve JSON pointer: index '%v' value '%v' exceeded target array size of '%v'", target, pathSeg, len(marray))
}
if target == len(hierarchy)-1 {
object = value
marray[index] = object
} else if object = marray[index]; object == nil {
return fmt.Errorf("failed to resolve JSON pointer: index '%v' value '%v' was not found", target, pathSeg)
}
} else {
return fmt.Errorf("failed to resolve JSON pointer: index '%v' value '%v' was not found", target, pathSeg)
}
}
return nil
}
// Object creates a new JSON object at a target path. Returns an error if the
// path contains a collision with a non object type.
func (g *Container) Object(path ...string) (*Container, error) {
return g.Set(map[string]interface{}{}, path...)
}
// ObjectP - Does the same as Object, but using a dot notation JSON path.
// ObjectP creates a new JSON object at a target path using dot notation.
// Returns an error if the path contains a collision with a non object type.
func (g *Container) ObjectP(path string) (*Container, error) {
return g.Object(strings.Split(path, ".")...)
}
// ObjectI - Create a new JSON object at an array index. Returns an error if the object is not an
// array or the index is out of bounds.
// ObjectI creates a new JSON object at an array index. Returns an error if the
// object is not an array or the index is out of bounds.
func (g *Container) ObjectI(index int) (*Container, error) {
return g.SetIndex(map[string]interface{}{}, index)
}
// Array - Create a new JSON array at a path. Returns an error if the path contains a collision with
// a non object type.
// Array creates a new JSON array at a path. Returns an error if the path
// contains a collision with a non object type.
func (g *Container) Array(path ...string) (*Container, error) {
return g.Set([]interface{}{}, path...)
}
// ArrayP - Does the same as Array, but using a dot notation JSON path.
// ArrayP creates a new JSON array at a path using dot notation. Returns an
// error if the path contains a collision with a non object type.
func (g *Container) ArrayP(path string) (*Container, error) {
return g.Array(strings.Split(path, ".")...)
}
// ArrayI - Create a new JSON array at an array index. Returns an error if the object is not an
// array or the index is out of bounds.
// ArrayI creates a new JSON array within an array at an index. Returns an error
// if the element is not an array or the index is out of bounds.
func (g *Container) ArrayI(index int) (*Container, error) {
return g.SetIndex([]interface{}{}, index)
}
// ArrayOfSize - Create a new JSON array of a particular size at a path. Returns an error if the
// path contains a collision with a non object type.
// ArrayOfSize creates a new JSON array of a particular size at a path. Returns
// an error if the path contains a collision with a non object type.
func (g *Container) ArrayOfSize(size int, path ...string) (*Container, error) {
a := make([]interface{}, size)
return g.Set(a, path...)
}
// ArrayOfSizeP - Does the same as ArrayOfSize, but using a dot notation JSON path.
// ArrayOfSizeP creates a new JSON array of a particular size at a path using
// dot notation. Returns an error if the path contains a collision with a non
// object type.
func (g *Container) ArrayOfSizeP(size int, path string) (*Container, error) {
return g.ArrayOfSize(size, strings.Split(path, ".")...)
}
// ArrayOfSizeI - Create a new JSON array of a particular size at an array index. Returns an error
// if the object is not an array or the index is out of bounds.
// ArrayOfSizeI create a new JSON array of a particular size within an array at
// an index. Returns an error if the element is not an array or the index is out
// of bounds.
func (g *Container) ArrayOfSizeI(size, index int) (*Container, error) {
a := make([]interface{}, size)
return g.SetIndex(a, index)
}
// Delete - Delete an element at a JSON path, an error is returned if the element does not exist.
// Delete an element at a path, an error is returned if the element does not
// exist.
func (g *Container) Delete(path ...string) error {
var object interface{}
@@ -304,45 +419,40 @@ func (g *Container) Delete(path ...string) error {
return nil
}
// DeleteP - Does the same as Delete, but using a dot notation JSON path.
// DeleteP deletes an element at a path using dot notation, an error is returned
// if the element does not exist.
func (g *Container) DeleteP(path string) error {
return g.Delete(strings.Split(path, ".")...)
}
// Merge - Merges two gabs-containers
func (g *Container) Merge(toMerge *Container) error {
// MergeFn merges two objects using a provided function to resolve collisions.
//
// The collision function receives two interface{} arguments, destination (the
// original object) and source (the object being merged into the destination).
// Which ever value is returned becomes the new value in the destination object
// at the location of the collision.
func (g *Container) MergeFn(source *Container, collisionFn func(destination, source interface{}) interface{}) error {
var recursiveFnc func(map[string]interface{}, []string) error
recursiveFnc = func(mmap map[string]interface{}, path []string) error {
for key, value := range mmap {
newPath := append(path, key)
if g.Exists(newPath...) {
target := g.Search(newPath...)
existingData := g.Search(newPath...).Data()
switch t := value.(type) {
case map[string]interface{}:
switch targetV := target.Data().(type) {
switch existingVal := existingData.(type) {
case map[string]interface{}:
if err := recursiveFnc(t, newPath); err != nil {
return err
}
case []interface{}:
g.Set(append(targetV, t), newPath...)
default:
newSlice := append([]interface{}{}, targetV)
g.Set(append(newSlice, t), newPath...)
}
case []interface{}:
for _, valueOfSlice := range t {
if err := g.ArrayAppend(valueOfSlice, newPath...); err != nil {
if _, err := g.Set(collisionFn(existingVal, t), newPath...); err != nil {
return err
}
}
default:
switch targetV := target.Data().(type) {
case []interface{}:
g.Set(append(targetV, t), newPath...)
default:
newSlice := append([]interface{}{}, targetV)
g.Set(append(newSlice, t), newPath...)
if _, err := g.Set(collisionFn(existingData, t), newPath...); err != nil {
return err
}
}
} else {
@@ -354,21 +464,48 @@ func (g *Container) Merge(toMerge *Container) error {
}
return nil
}
if mmap, ok := toMerge.Data().(map[string]interface{}); ok {
if mmap, ok := source.Data().(map[string]interface{}); ok {
return recursiveFnc(mmap, []string{})
}
return nil
}
//--------------------------------------------------------------------------------------------------
// Merge a source object into an existing destination object. When a collision
// is found within the merged structures (both a source and destination object
// contain the same non-object keys) the result will be an array containing both
// values, where values that are already arrays will be expanded into the
// resulting array.
//
// It is possible to merge structures will different collision behaviours with
// MergeFn.
func (g *Container) Merge(source *Container) error {
return g.MergeFn(source, func(dest, source interface{}) interface{} {
destArr, destIsArray := dest.([]interface{})
sourceArr, sourceIsArray := source.([]interface{})
if destIsArray {
if sourceIsArray {
return append(destArr, sourceArr...)
}
return append(destArr, source)
}
if sourceIsArray {
return append(append([]interface{}{}, dest), sourceArr...)
}
return []interface{}{dest, source}
})
}
//------------------------------------------------------------------------------
/*
Array modification/search - Keeping these options simple right now, no need for anything more
complicated since you can just cast to []interface{}, modify and then reassign with Set.
Array modification/search - Keeping these options simple right now, no need for
anything more complicated since you can just cast to []interface{}, modify and
then reassign with Set.
*/
// ArrayAppend - Append a value onto a JSON array. If the target is not a JSON array then it will be
// converted into one, with its contents as the first element of the array.
// ArrayAppend attempts to append a value onto a JSON array at a path. If the
// target is not a JSON array then it will be converted into one, with its
// original contents set to the first element of the array.
func (g *Container) ArrayAppend(value interface{}, path ...string) error {
if array, ok := g.Search(path...).Data().([]interface{}); ok {
array = append(array, value)
@@ -386,12 +523,15 @@ func (g *Container) ArrayAppend(value interface{}, path ...string) error {
return err
}
// ArrayAppendP - Append a value onto a JSON array using a dot notation JSON path.
// ArrayAppendP attempts to append a value onto a JSON array at a path using dot
// notation. If the target is not a JSON array then it will be converted into
// one, with its original contents set to the first element of the array.
func (g *Container) ArrayAppendP(value interface{}, path string) error {
return g.ArrayAppend(value, strings.Split(path, ".")...)
}
// ArrayRemove - Remove an element from a JSON array.
// ArrayRemove attempts to remove an element identified by an index from a JSON
// array at a path.
func (g *Container) ArrayRemove(index int, path ...string) error {
if index < 0 {
return ErrOutOfBounds
@@ -409,12 +549,14 @@ func (g *Container) ArrayRemove(index int, path ...string) error {
return err
}
// ArrayRemoveP - Remove an element from a JSON array using a dot notation JSON path.
// ArrayRemoveP attempts to remove an element identified by an index from a JSON
// array at a path using dot notation.
func (g *Container) ArrayRemoveP(index int, path string) error {
return g.ArrayRemove(index, strings.Split(path, ".")...)
}
// ArrayElement - Access an element from a JSON array.
// ArrayElement attempts to access an element by an index from a JSON array at a
// path.
func (g *Container) ArrayElement(index int, path ...string) (*Container, error) {
if index < 0 {
return &Container{nil}, ErrOutOfBounds
@@ -429,12 +571,13 @@ func (g *Container) ArrayElement(index int, path ...string) (*Container, error)
return &Container{nil}, ErrOutOfBounds
}
// ArrayElementP - Access an element from a JSON array using a dot notation JSON path.
// ArrayElementP attempts to access an element by an index from a JSON array at
// a path using dot notation.
func (g *Container) ArrayElementP(index int, path string) (*Container, error) {
return g.ArrayElement(index, strings.Split(path, ".")...)
}
// ArrayCount - Count the number of elements in a JSON array.
// ArrayCount counts the number of elements in a JSON array at a path.
func (g *Container) ArrayCount(path ...string) (int, error) {
if array, ok := g.Search(path...).Data().([]interface{}); ok {
return len(array), nil
@@ -442,14 +585,15 @@ func (g *Container) ArrayCount(path ...string) (int, error) {
return 0, ErrNotArray
}
// ArrayCountP - Count the number of elements in a JSON array using a dot notation JSON path.
// ArrayCountP counts the number of elements in a JSON array at a path using dot
// notation.
func (g *Container) ArrayCountP(path string) (int, error) {
return g.ArrayCount(strings.Split(path, ".")...)
}
//--------------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// Bytes - Converts the contained object back to a JSON []byte blob.
// Bytes marshals an element to a JSON []byte blob.
func (g *Container) Bytes() []byte {
if g.Data() != nil {
if bytes, err := json.Marshal(g.object); err == nil {
@@ -459,7 +603,8 @@ func (g *Container) Bytes() []byte {
return []byte("{}")
}
// BytesIndent - Converts the contained object to a JSON []byte blob formatted with prefix, indent.
// BytesIndent marshals an element to a JSON []byte blob formatted with a prefix
// and indent string.
func (g *Container) BytesIndent(prefix string, indent string) []byte {
if g.object != nil {
if bytes, err := json.MarshalIndent(g.object, prefix, indent); err == nil {
@@ -469,12 +614,13 @@ func (g *Container) BytesIndent(prefix string, indent string) []byte {
return []byte("{}")
}
// String - Converts the contained object to a JSON formatted string.
// String marshals an element to a JSON formatted string.
func (g *Container) String() string {
return string(g.Bytes())
}
// StringIndent - Converts the contained object back to a JSON formatted string with prefix, indent.
// StringIndent marshals an element to a JSON string formatted with a prefix and
// indent string.
func (g *Container) StringIndent(prefix string, indent string) string {
return string(g.BytesIndent(prefix, indent))
}
@@ -496,10 +642,9 @@ func EncodeOptIndent(prefix string, indent string) EncodeOpt {
}
}
// EncodeJSON - Encodes the contained object back to a JSON formatted []byte
// using a variant list of modifier functions for the encoder being used.
// Functions for modifying the output are prefixed with EncodeOpt, e.g.
// EncodeOptHTMLEscape.
// EncodeJSON marshals an element to a JSON formatted []byte using a variant
// list of modifier functions for the encoder being used. Functions for
// modifying the output are prefixed with EncodeOpt, e.g. EncodeOptHTMLEscape.
func (g *Container) EncodeJSON(encodeOpts ...EncodeOpt) []byte {
var b bytes.Buffer
encoder := json.NewEncoder(&b)
@@ -517,17 +662,18 @@ func (g *Container) EncodeJSON(encodeOpts ...EncodeOpt) []byte {
return result
}
// New - Create a new gabs JSON object.
// New creates a new gabs JSON object.
func New() *Container {
return &Container{map[string]interface{}{}}
}
// Consume - Gobble up an already converted JSON object, or a fresh map[string]interface{} object.
// Consume an already unmarshalled JSON object (or a new map[string]interface{})
// into a *Container.
func Consume(root interface{}) (*Container, error) {
return &Container{root}, nil
}
// ParseJSON - Convert a string into a representation of the parsed JSON.
// ParseJSON unmarshals a JSON byte slice into a *Container.
func ParseJSON(sample []byte) (*Container, error) {
var gabs Container
@@ -538,7 +684,7 @@ func ParseJSON(sample []byte) (*Container, error) {
return &gabs, nil
}
// ParseJSONDecoder - Convert a json.Decoder into a representation of the parsed JSON.
// ParseJSONDecoder applies a json.Decoder to a *Container.
func ParseJSONDecoder(decoder *json.Decoder) (*Container, error) {
var gabs Container
@@ -549,7 +695,7 @@ func ParseJSONDecoder(decoder *json.Decoder) (*Container, error) {
return &gabs, nil
}
// ParseJSONFile - Read a file and convert into a representation of the parsed JSON.
// ParseJSONFile reads a file and unmarshals the contents into a *Container.
func ParseJSONFile(path string) (*Container, error) {
if len(path) > 0 {
cBytes, err := ioutil.ReadFile(path)
@@ -567,7 +713,7 @@ func ParseJSONFile(path string) (*Container, error) {
return nil, ErrInvalidPath
}
// ParseJSONBuffer - Read the contents of a buffer into a representation of the parsed JSON.
// ParseJSONBuffer reads a buffer and unmarshals the contents into a *Container.
func ParseJSONBuffer(buffer io.Reader) (*Container, error) {
var gabs Container
jsonDecoder := json.NewDecoder(buffer)
@@ -578,4 +724,4 @@ func ParseJSONBuffer(buffer io.Reader) (*Container, error) {
return &gabs, nil
}
//--------------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------

1
vendor/github.com/Jeffail/gabs/go.mod generated vendored Normal file
View File

@@ -0,0 +1 @@
module github.com/Jeffail/gabs

File diff suppressed because it is too large Load Diff

View File

@@ -69,18 +69,46 @@ message InteractiveAnnotation {
}
}
message DeviceListMetadata {
optional bytes senderKeyHash = 1;
optional uint64 senderTimestamp = 2;
optional bytes recipientKeyHash = 8;
optional uint64 recipientTimestamp = 9;
}
message MessageContextInfo {
optional DeviceListMetadata deviceListMetadata = 1;
}
message AdReplyInfo {
optional string advertiserName = 1;
enum AD_REPLY_INFO_MEDIATYPE {
enum AdReplyInfoMediaType {
NONE = 0;
IMAGE = 1;
VIDEO = 2;
}
optional AD_REPLY_INFO_MEDIATYPE mediaType = 2;
optional AdReplyInfoMediaType mediaType = 2;
optional bytes jpegThumbnail = 16;
optional string caption = 17;
}
message ExternalAdReplyInfo {
optional string title = 1;
optional string body = 2;
enum ExternalAdReplyInfoMediaType {
NONE = 0;
IMAGE = 1;
VIDEO = 2;
}
optional ExternalAdReplyInfoMediaType mediaType = 3;
optional string thumbnailUrl = 4;
optional string mediaUrl = 5;
optional bytes thumbnail = 6;
optional string sourceType = 7;
optional string sourceId = 8;
optional string sourceUrl = 9;
}
message ContextInfo {
optional string stanzaId = 1;
optional string participant = 2;
@@ -96,6 +124,8 @@ message ContextInfo {
optional MessageKey placeholderKey = 24;
optional uint32 expiration = 25;
optional int64 ephemeralSettingTimestamp = 26;
optional bytes ephemeralSharedSecret = 27;
optional ExternalAdReplyInfo externalAdReply = 28;
}
message SenderKeyDistributionMessage {
@@ -125,6 +155,24 @@ message ImageMessage {
repeated uint32 scanLengths = 22;
optional bytes midQualityFileSha256 = 23;
optional bytes midQualityFileEncSha256 = 24;
optional bool viewOnce = 25;
}
message InvoiceMessage {
optional string note = 1;
optional string token = 2;
enum InvoiceMessageAttachmentType {
IMAGE = 0;
PDF = 1;
}
optional InvoiceMessageAttachmentType attachmentType = 3;
optional string attachmentMimetype = 4;
optional bytes attachmentMediaKey = 5;
optional int64 attachmentMediaKeyTimestamp = 6;
optional bytes attachmentFileSha256 = 7;
optional bytes attachmentFileEncSha256 = 8;
optional string attachmentDirectPath = 9;
optional bytes attachmentJpegThumbnail = 10;
}
message ContactMessage {
@@ -156,7 +204,7 @@ message ExtendedTextMessage {
optional string title = 6;
optional fixed32 textArgb = 7;
optional fixed32 backgroundArgb = 8;
enum EXTENDED_TEXT_MESSAGE_FONTTYPE {
enum ExtendedTextMessageFontType {
SANS_SERIF = 0;
SERIF = 1;
NORICAN_REGULAR = 2;
@@ -164,12 +212,12 @@ message ExtendedTextMessage {
BEBASNEUE_REGULAR = 4;
OSWALD_HEAVY = 5;
}
optional EXTENDED_TEXT_MESSAGE_FONTTYPE font = 9;
enum EXTENDED_TEXT_MESSAGE_PREVIEWTYPE {
optional ExtendedTextMessageFontType font = 9;
enum ExtendedTextMessagePreviewType {
NONE = 0;
VIDEO = 1;
}
optional EXTENDED_TEXT_MESSAGE_PREVIEWTYPE previewType = 10;
optional ExtendedTextMessagePreviewType previewType = 10;
optional bytes jpegThumbnail = 16;
optional ContextInfo contextInfo = 17;
optional bool doNotPlayInline = 18;
@@ -187,8 +235,14 @@ message DocumentMessage {
optional bytes fileEncSha256 = 9;
optional string directPath = 10;
optional int64 mediaKeyTimestamp = 11;
optional bool contactVcard = 12;
optional string thumbnailDirectPath = 13;
optional bytes thumbnailSha256 = 14;
optional bytes thumbnailEncSha256 = 15;
optional bytes jpegThumbnail = 16;
optional ContextInfo contextInfo = 17;
optional uint32 thumbnailHeight = 18;
optional uint32 thumbnailWidth = 19;
}
message AudioMessage {
@@ -224,12 +278,13 @@ message VideoMessage {
optional bytes jpegThumbnail = 16;
optional ContextInfo contextInfo = 17;
optional bytes streamingSidecar = 18;
enum VIDEO_MESSAGE_ATTRIBUTION {
enum VideoMessageAttribution {
NONE = 0;
GIPHY = 1;
TENOR = 2;
}
optional VIDEO_MESSAGE_ATTRIBUTION gifAttribution = 19;
optional VideoMessageAttribution gifAttribution = 19;
optional bool viewOnce = 20;
}
message Call {
@@ -243,16 +298,23 @@ message Chat {
message ProtocolMessage {
optional MessageKey key = 1;
enum PROTOCOL_MESSAGE_TYPE {
enum ProtocolMessageType {
REVOKE = 0;
EPHEMERAL_SETTING = 3;
EPHEMERAL_SYNC_RESPONSE = 4;
HISTORY_SYNC_NOTIFICATION = 5;
APP_STATE_SYNC_KEY_SHARE = 6;
APP_STATE_SYNC_KEY_REQUEST = 7;
MSG_FANOUT_BACKFILL_REQUEST = 8;
INITIAL_SECURITY_NOTIFICATION_SETTING_SYNC = 9;
}
optional PROTOCOL_MESSAGE_TYPE type = 2;
optional ProtocolMessageType type = 2;
optional uint32 ephemeralExpiration = 4;
optional int64 ephemeralSettingTimestamp = 5;
optional HistorySyncNotification historySyncNotification = 6;
optional AppStateSyncKeyShare appStateSyncKeyShare = 7;
optional AppStateSyncKeyRequest appStateSyncKeyRequest = 8;
optional InitialSecurityNotificationSettingSync initialSecurityNotificationSettingSync = 9;
}
message HistorySyncNotification {
@@ -261,14 +323,49 @@ message HistorySyncNotification {
optional bytes mediaKey = 3;
optional bytes fileEncSha256 = 4;
optional string directPath = 5;
enum HISTORY_SYNC_NOTIFICATION_HISTORYSYNCTYPE {
enum HistorySyncNotificationHistorySyncType {
INITIAL_BOOTSTRAP = 0;
INITIAL_STATUS_V3 = 1;
FULL = 2;
RECENT = 3;
PUSH_NAME = 4;
}
optional HISTORY_SYNC_NOTIFICATION_HISTORYSYNCTYPE syncType = 6;
optional HistorySyncNotificationHistorySyncType syncType = 6;
optional uint32 chunkOrder = 7;
optional string originalMessageId = 8;
}
message AppStateSyncKey {
optional AppStateSyncKeyId keyId = 1;
optional AppStateSyncKeyData keyData = 2;
}
message AppStateSyncKeyId {
optional bytes keyId = 1;
}
message AppStateSyncKeyFingerprint {
optional uint32 rawId = 1;
optional uint32 currentIndex = 2;
repeated uint32 deviceIndexes = 3 [packed=true];
}
message AppStateSyncKeyData {
optional bytes keyData = 1;
optional AppStateSyncKeyFingerprint fingerprint = 2;
optional int64 timestamp = 3;
}
message AppStateSyncKeyShare {
repeated AppStateSyncKey keys = 1;
}
message AppStateSyncKeyRequest {
repeated AppStateSyncKeyId keyIds = 1;
}
message InitialSecurityNotificationSettingSync {
optional bool securityNotificationEnabled = 1;
}
message ContactsArrayMessage {
@@ -283,7 +380,7 @@ message HSMCurrency {
}
message HSMDateTimeComponent {
enum HSM_DATE_TIME_COMPONENT_DAYOFWEEKTYPE {
enum HSMDateTimeComponentDayOfWeekType {
MONDAY = 1;
TUESDAY = 2;
WEDNESDAY = 3;
@@ -292,17 +389,17 @@ message HSMDateTimeComponent {
SATURDAY = 6;
SUNDAY = 7;
}
optional HSM_DATE_TIME_COMPONENT_DAYOFWEEKTYPE dayOfWeek = 1;
optional HSMDateTimeComponentDayOfWeekType dayOfWeek = 1;
optional uint32 year = 2;
optional uint32 month = 3;
optional uint32 dayOfMonth = 4;
optional uint32 hour = 5;
optional uint32 minute = 6;
enum HSM_DATE_TIME_COMPONENT_CALENDARTYPE {
enum HSMDateTimeComponentCalendarType {
GREGORIAN = 1;
SOLAR_HIJRI = 2;
}
optional HSM_DATE_TIME_COMPONENT_CALENDARTYPE calendar = 7;
optional HSMDateTimeComponentCalendarType calendar = 7;
}
message HSMDateTimeUnixEpoch {
@@ -347,6 +444,13 @@ message RequestPaymentMessage {
optional uint64 amount1000 = 2;
optional string requestFrom = 3;
optional int64 expiryTimestamp = 5;
optional PaymentMoney amount = 6;
}
message PaymentMoney {
optional int64 value = 1;
optional uint32 offset = 2;
optional string currencyCode = 3;
}
message DeclinePaymentRequestMessage {
@@ -457,6 +561,64 @@ message ProductMessage {
optional ContextInfo contextInfo = 17;
}
message OrderMessage {
optional string orderId = 1;
optional bytes thumbnail = 2;
optional int32 itemCount = 3;
enum OrderMessageOrderStatus {
INQUIRY = 1;
}
optional OrderMessageOrderStatus status = 4;
enum OrderMessageOrderSurface {
CATALOG = 1;
}
optional OrderMessageOrderSurface surface = 5;
optional string message = 6;
optional string orderTitle = 7;
optional string sellerJid = 8;
optional string token = 9;
optional ContextInfo contextInfo = 17;
}
message Row {
optional string title = 1;
optional string description = 2;
optional string rowId = 3;
}
message Section {
optional string title = 1;
repeated Row rows = 2;
}
message ListMessage {
optional string title = 1;
optional string description = 2;
optional string buttonText = 3;
enum ListMessageListType {
UNKNOWN = 0;
SINGLE_SELECT = 1;
}
optional ListMessageListType listType = 4;
repeated Section sections = 5;
}
message SingleSelectReply {
optional string selectedRowId = 1;
}
message ListResponseMessage {
optional string title = 1;
enum ListResponseMessageListType {
UNKNOWN = 0;
SINGLE_SELECT = 1;
}
optional ListResponseMessageListType listType = 2;
optional SingleSelectReply singleSelectReply = 3;
optional ContextInfo contextInfo = 4;
optional string description = 5;
}
message GroupInviteMessage {
optional string groupJid = 1;
optional string inviteCode = 2;
@@ -467,13 +629,50 @@ message GroupInviteMessage {
optional ContextInfo contextInfo = 7;
}
message EphemeralSetting {
optional string chatJid = 1;
optional uint32 ephemeralExpiration = 2;
optional int64 ephemeralSettingTimestamp = 3;
}
message DeviceSentMessage {
optional string destinationJid = 1;
optional Message message = 2;
optional string phash = 3;
repeated EphemeralSetting broadcastEphemeralSettings = 4;
}
message DeviceSyncMessage {
optional bytes serializedXmlBytes = 1;
message FutureProofMessage {
optional Message message = 1;
}
message ButtonText {
optional string displayText = 1;
}
message Button {
optional string buttonId = 1;
optional ButtonText buttonText = 2;
}
message ButtonsMessage {
optional string contentText = 6;
optional string footerText = 7;
optional ContextInfo contextInfo = 8;
repeated Button buttons = 9;
oneof title {
string titleText = 1;
DocumentMessage documentMessage = 2;
ImageMessage imageMessage = 3;
VideoMessage videoMessage = 4;
LocationMessage locationMessage = 5;
}
}
message ButtonsResponseMessage {
optional string selectedButtonId = 1;
optional string selectedDisplayText = 2;
optional ContextInfo contextInfo = 3;
}
message Message {
@@ -503,7 +702,15 @@ message Message {
optional TemplateButtonReplyMessage templateButtonReplyMessage = 29;
optional ProductMessage productMessage = 30;
optional DeviceSentMessage deviceSentMessage = 31;
optional DeviceSyncMessage deviceSyncMessage = 32;
optional MessageContextInfo messageContextInfo = 35;
optional ListMessage listMessage = 36;
optional FutureProofMessage viewOnceMessage = 37;
optional OrderMessage orderMessage = 38;
optional ListResponseMessage listResponseMessage = 39;
optional FutureProofMessage ephemeralMessage = 40;
optional InvoiceMessage invoiceMessage = 41;
optional ButtonsMessage buttonsMessage = 42;
optional ButtonsResponseMessage buttonsResponseMessage = 43;
}
message MessageKey {
@@ -514,51 +721,52 @@ message MessageKey {
}
message WebFeatures {
enum WEB_FEATURES_FLAG {
enum WebFeaturesFlag {
NOT_STARTED = 0;
FORCE_UPGRADE = 1;
DEVELOPMENT = 2;
PRODUCTION = 3;
}
optional WEB_FEATURES_FLAG labelsDisplay = 1;
optional WEB_FEATURES_FLAG voipIndividualOutgoing = 2;
optional WEB_FEATURES_FLAG groupsV3 = 3;
optional WEB_FEATURES_FLAG groupsV3Create = 4;
optional WEB_FEATURES_FLAG changeNumberV2 = 5;
optional WEB_FEATURES_FLAG queryStatusV3Thumbnail = 6;
optional WEB_FEATURES_FLAG liveLocations = 7;
optional WEB_FEATURES_FLAG queryVname = 8;
optional WEB_FEATURES_FLAG voipIndividualIncoming = 9;
optional WEB_FEATURES_FLAG quickRepliesQuery = 10;
optional WEB_FEATURES_FLAG payments = 11;
optional WEB_FEATURES_FLAG stickerPackQuery = 12;
optional WEB_FEATURES_FLAG liveLocationsFinal = 13;
optional WEB_FEATURES_FLAG labelsEdit = 14;
optional WEB_FEATURES_FLAG mediaUpload = 15;
optional WEB_FEATURES_FLAG mediaUploadRichQuickReplies = 18;
optional WEB_FEATURES_FLAG vnameV2 = 19;
optional WEB_FEATURES_FLAG videoPlaybackUrl = 20;
optional WEB_FEATURES_FLAG statusRanking = 21;
optional WEB_FEATURES_FLAG voipIndividualVideo = 22;
optional WEB_FEATURES_FLAG thirdPartyStickers = 23;
optional WEB_FEATURES_FLAG frequentlyForwardedSetting = 24;
optional WEB_FEATURES_FLAG groupsV4JoinPermission = 25;
optional WEB_FEATURES_FLAG recentStickers = 26;
optional WEB_FEATURES_FLAG catalog = 27;
optional WEB_FEATURES_FLAG starredStickers = 28;
optional WEB_FEATURES_FLAG voipGroupCall = 29;
optional WEB_FEATURES_FLAG templateMessage = 30;
optional WEB_FEATURES_FLAG templateMessageInteractivity = 31;
optional WEB_FEATURES_FLAG ephemeralMessages = 32;
optional WEB_FEATURES_FLAG e2ENotificationSync = 33;
optional WEB_FEATURES_FLAG recentStickersV2 = 34;
}
message TabletNotificationsInfo {
optional uint64 timestamp = 2;
optional uint32 unreadChats = 3;
optional uint32 notifyMessageCount = 4;
repeated NotificationMessageInfo notifyMessage = 5;
optional WebFeaturesFlag labelsDisplay = 1;
optional WebFeaturesFlag voipIndividualOutgoing = 2;
optional WebFeaturesFlag groupsV3 = 3;
optional WebFeaturesFlag groupsV3Create = 4;
optional WebFeaturesFlag changeNumberV2 = 5;
optional WebFeaturesFlag queryStatusV3Thumbnail = 6;
optional WebFeaturesFlag liveLocations = 7;
optional WebFeaturesFlag queryVname = 8;
optional WebFeaturesFlag voipIndividualIncoming = 9;
optional WebFeaturesFlag quickRepliesQuery = 10;
optional WebFeaturesFlag payments = 11;
optional WebFeaturesFlag stickerPackQuery = 12;
optional WebFeaturesFlag liveLocationsFinal = 13;
optional WebFeaturesFlag labelsEdit = 14;
optional WebFeaturesFlag mediaUpload = 15;
optional WebFeaturesFlag mediaUploadRichQuickReplies = 18;
optional WebFeaturesFlag vnameV2 = 19;
optional WebFeaturesFlag videoPlaybackUrl = 20;
optional WebFeaturesFlag statusRanking = 21;
optional WebFeaturesFlag voipIndividualVideo = 22;
optional WebFeaturesFlag thirdPartyStickers = 23;
optional WebFeaturesFlag frequentlyForwardedSetting = 24;
optional WebFeaturesFlag groupsV4JoinPermission = 25;
optional WebFeaturesFlag recentStickers = 26;
optional WebFeaturesFlag catalog = 27;
optional WebFeaturesFlag starredStickers = 28;
optional WebFeaturesFlag voipGroupCall = 29;
optional WebFeaturesFlag templateMessage = 30;
optional WebFeaturesFlag templateMessageInteractivity = 31;
optional WebFeaturesFlag ephemeralMessages = 32;
optional WebFeaturesFlag e2ENotificationSync = 33;
optional WebFeaturesFlag recentStickersV2 = 34;
optional WebFeaturesFlag syncdRelease1 = 35;
optional WebFeaturesFlag recentStickersV3 = 36;
optional WebFeaturesFlag userNotice = 37;
optional WebFeaturesFlag syncdRelease11 = 38;
optional WebFeaturesFlag support = 39;
optional WebFeaturesFlag groupUiiCleanup = 40;
optional WebFeaturesFlag groupDogfoodingInternalOnly = 41;
optional WebFeaturesFlag settingsSync = 42;
}
message NotificationMessageInfo {
@@ -576,14 +784,14 @@ message WebNotificationsInfo {
}
message PaymentInfo {
enum PAYMENT_INFO_CURRENCY {
enum PaymentInfoCurrency {
UNKNOWN_CURRENCY = 0;
INR = 1;
}
optional PAYMENT_INFO_CURRENCY currencyDeprecated = 1;
optional PaymentInfoCurrency currencyDeprecated = 1;
optional uint64 amount1000 = 2;
optional string receiverJid = 3;
enum PAYMENT_INFO_STATUS {
enum PaymentInfoStatus {
UNKNOWN_STATUS = 0;
PROCESSING = 1;
SENT = 2;
@@ -597,13 +805,13 @@ message PaymentInfo {
WAITING_FOR_PAYER = 10;
WAITING = 11;
}
optional PAYMENT_INFO_STATUS status = 4;
optional PaymentInfoStatus status = 4;
optional uint64 transactionTimestamp = 5;
optional MessageKey requestMessageKey = 6;
optional uint64 expiryTimestamp = 7;
optional bool futureproofed = 8;
optional string currency = 9;
enum PAYMENT_INFO_TXNSTATUS {
enum PaymentInfoTxnStatus {
UNKNOWN = 0;
PENDING_SETUP = 1;
PENDING_RECEIVER_SETUP = 2;
@@ -633,14 +841,14 @@ message PaymentInfo {
COLLECT_CANCELED = 26;
COLLECT_CANCELLING = 27;
}
optional PAYMENT_INFO_TXNSTATUS txnStatus = 10;
optional PaymentInfoTxnStatus txnStatus = 10;
}
message WebMessageInfo {
required MessageKey key = 1;
optional Message message = 2;
optional uint64 messageTimestamp = 3;
enum WEB_MESSAGE_INFO_STATUS {
enum WebMessageInfoStatus {
ERROR = 0;
PENDING = 1;
SERVER_ACK = 2;
@@ -648,7 +856,7 @@ message WebMessageInfo {
READ = 4;
PLAYED = 5;
}
optional WEB_MESSAGE_INFO_STATUS status = 4;
optional WebMessageInfoStatus status = 4;
optional string participant = 5;
optional bool ignore = 16;
optional bool starred = 17;
@@ -658,7 +866,7 @@ message WebMessageInfo {
optional bool multicast = 21;
optional bool urlText = 22;
optional bool urlNumber = 23;
enum WEB_MESSAGE_INFO_STUBTYPE {
enum WebMessageInfoStubType {
UNKNOWN = 0;
REVOKE = 1;
CIPHERTEXT = 2;
@@ -732,8 +940,54 @@ message WebMessageInfo {
GROUP_V4_ADD_INVITE_SENT = 70;
GROUP_PARTICIPANT_ADD_REQUEST_JOIN = 71;
CHANGE_EPHEMERAL_SETTING = 72;
E2E_DEVICE_CHANGED = 73;
VIEWED_ONCE = 74;
E2E_ENCRYPTED_NOW = 75;
BLUE_MSG_BSP_FB_TO_BSP_PREMISE = 76;
BLUE_MSG_BSP_FB_TO_SELF_FB = 77;
BLUE_MSG_BSP_FB_TO_SELF_PREMISE = 78;
BLUE_MSG_BSP_FB_UNVERIFIED = 79;
BLUE_MSG_BSP_FB_UNVERIFIED_TO_SELF_PREMISE_VERIFIED = 80;
BLUE_MSG_BSP_FB_VERIFIED = 81;
BLUE_MSG_BSP_FB_VERIFIED_TO_SELF_PREMISE_UNVERIFIED = 82;
BLUE_MSG_BSP_PREMISE_TO_SELF_PREMISE = 83;
BLUE_MSG_BSP_PREMISE_UNVERIFIED = 84;
BLUE_MSG_BSP_PREMISE_UNVERIFIED_TO_SELF_PREMISE_VERIFIED = 85;
BLUE_MSG_BSP_PREMISE_VERIFIED = 86;
BLUE_MSG_BSP_PREMISE_VERIFIED_TO_SELF_PREMISE_UNVERIFIED = 87;
BLUE_MSG_CONSUMER_TO_BSP_FB_UNVERIFIED = 88;
BLUE_MSG_CONSUMER_TO_BSP_PREMISE_UNVERIFIED = 89;
BLUE_MSG_CONSUMER_TO_SELF_FB_UNVERIFIED = 90;
BLUE_MSG_CONSUMER_TO_SELF_PREMISE_UNVERIFIED = 91;
BLUE_MSG_SELF_FB_TO_BSP_PREMISE = 92;
BLUE_MSG_SELF_FB_TO_SELF_PREMISE = 93;
BLUE_MSG_SELF_FB_UNVERIFIED = 94;
BLUE_MSG_SELF_FB_UNVERIFIED_TO_SELF_PREMISE_VERIFIED = 95;
BLUE_MSG_SELF_FB_VERIFIED = 96;
BLUE_MSG_SELF_FB_VERIFIED_TO_SELF_PREMISE_UNVERIFIED = 97;
BLUE_MSG_SELF_PREMISE_TO_BSP_PREMISE = 98;
BLUE_MSG_SELF_PREMISE_UNVERIFIED = 99;
BLUE_MSG_SELF_PREMISE_VERIFIED = 100;
BLUE_MSG_TO_BSP_FB = 101;
BLUE_MSG_TO_CONSUMER = 102;
BLUE_MSG_TO_SELF_FB = 103;
BLUE_MSG_UNVERIFIED_TO_BSP_FB_VERIFIED = 104;
BLUE_MSG_UNVERIFIED_TO_BSP_PREMISE_VERIFIED = 105;
BLUE_MSG_UNVERIFIED_TO_SELF_FB_VERIFIED = 106;
BLUE_MSG_UNVERIFIED_TO_VERIFIED = 107;
BLUE_MSG_VERIFIED_TO_BSP_FB_UNVERIFIED = 108;
BLUE_MSG_VERIFIED_TO_BSP_PREMISE_UNVERIFIED = 109;
BLUE_MSG_VERIFIED_TO_SELF_FB_UNVERIFIED = 110;
BLUE_MSG_VERIFIED_TO_UNVERIFIED = 111;
BLUE_MSG_BSP_FB_UNVERIFIED_TO_BSP_PREMISE_VERIFIED = 112;
BLUE_MSG_BSP_FB_UNVERIFIED_TO_SELF_FB_VERIFIED = 113;
BLUE_MSG_BSP_FB_VERIFIED_TO_BSP_PREMISE_UNVERIFIED = 114;
BLUE_MSG_BSP_FB_VERIFIED_TO_SELF_FB_UNVERIFIED = 115;
BLUE_MSG_SELF_FB_UNVERIFIED_TO_BSP_PREMISE_VERIFIED = 116;
BLUE_MSG_SELF_FB_VERIFIED_TO_BSP_PREMISE_UNVERIFIED = 117;
E2E_IDENTITY_UNAVAILABLE = 118;
}
optional WEB_MESSAGE_INFO_STUBTYPE messageStubType = 24;
optional WebMessageInfoStubType messageStubType = 24;
optional bool clearMedia = 25;
repeated string messageStubParameters = 26;
optional uint32 duration = 27;
@@ -743,5 +997,15 @@ message WebMessageInfo {
optional PaymentInfo quotedPaymentInfo = 31;
optional uint64 ephemeralStartTimestamp = 32;
optional uint32 ephemeralDuration = 33;
optional bool ephemeralOffToOn = 34;
optional bool ephemeralOutOfSync = 35;
enum WebMessageInfoBizPrivacyStatus {
E2EE = 0;
FB = 2;
BSP = 1;
BSP_AND_FB = 3;
}
optional WebMessageInfoBizPrivacyStatus bizPrivacyStatus = 36;
optional string verifiedBizName = 37;
}

View File

@@ -24,7 +24,10 @@ var SingleByteTokens = [...]string{"", "", "", "200", "400", "404", "500", "501"
"invite", "gif", "vcard", "frequent", "privacy", "blacklist", "whitelist",
"verify", "location", "document", "elapsed", "revoke_invite", "expiration",
"unsubscribe", "disable", "vname", "old_jid", "new_jid", "announcement",
"locked", "prop", "label", "color", "call", "offer", "call-id"}
"locked", "prop", "label", "color", "call", "offer", "call-id",
"quick_reply", "sticker", "pay_t", "accept", "reject", "sticker_pack",
"invalid", "canceled", "missed", "connected", "result", "audio",
"video", "recent"}
var doubleByteTokens = [...]string{}

View File

@@ -2,6 +2,7 @@
package whatsapp
import (
"fmt"
"math/rand"
"net/http"
"net/url"
@@ -9,7 +10,6 @@ import (
"time"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
)
type metric byte
@@ -195,7 +195,7 @@ func (wac *Conn) connect() (err error) {
headers := http.Header{"Origin": []string{"https://web.whatsapp.com"}}
wsConn, _, err := dialer.Dial("wss://web.whatsapp.com/ws", headers)
if err != nil {
return errors.Wrap(err, "couldn't dial whatsapp web websocket")
return fmt.Errorf("couldn't dial whatsapp web websocket: %w", err)
}
wsConn.SetCloseHandler(func(code int, text string) error {
@@ -221,7 +221,7 @@ func (wac *Conn) connect() (err error) {
wac.wg = &sync.WaitGroup{}
wac.wg.Add(2)
go wac.readPump()
go wac.keepAlive(20000, 60000)
go wac.keepAlive(20000, 55000)
wac.loggedIn = false
return nil
@@ -237,7 +237,10 @@ func (wac *Conn) Disconnect() (Session, error) {
close(wac.ws.close) //signal close
wac.wg.Wait() //wait for close
err := wac.ws.conn.Close()
var err error
if wac.ws != nil && wac.ws.conn != nil {
err = wac.ws.conn.Close()
}
wac.ws = nil
if wac.session == nil {
@@ -246,17 +249,20 @@ func (wac *Conn) Disconnect() (Session, error) {
return *wac.session, err
}
func (wac *Conn) AdminTest() (bool, error) {
func (wac *Conn) IsLoginInProgress() bool {
return wac.sessionLock == 1
}
func (wac *Conn) AdminTest() error {
if !wac.connected {
return false, ErrNotConnected
return ErrNotConnected
}
if !wac.loggedIn {
return false, ErrInvalidSession
return ErrInvalidSession
}
result, err := wac.sendAdminTest()
return result, err
return wac.sendAdminTest()
}
func (wac *Conn) keepAlive(minIntervalMs int, maxIntervalMs int) {
@@ -265,7 +271,7 @@ func (wac *Conn) keepAlive(minIntervalMs int, maxIntervalMs int) {
for {
err := wac.sendKeepAlive()
if err != nil {
wac.handle(errors.Wrap(err, "keepAlive failed"))
wac.handle(fmt.Errorf("keepAlive failed: %w", err))
//TODO: Consequences?
}
interval := rand.Intn(maxIntervalMs-minIntervalMs) + minIntervalMs

View File

@@ -39,20 +39,20 @@ func (wac *Conn) Search(search string, count, page int) (*binary.Node, error) {
return wac.query("search", "", "", "", "", search, count, page)
}
func (wac *Conn) LoadMessages(jid, messageId string, count int) (*binary.Node, error) {
func (wac *Conn) LoadMessages(jid string, count int) (*binary.Node, error) {
return wac.query("message", jid, "", "before", "true", "", count, 0)
}
func (wac *Conn) LoadMessagesBefore(jid, messageId string, count int) (*binary.Node, error) {
return wac.query("message", jid, messageId, "before", "true", "", count, 0)
func (wac *Conn) LoadMessagesBefore(jid, messageId string, fromMe bool, count int) (*binary.Node, error) {
return wac.query("message", jid, messageId, "before", strconv.FormatBool(fromMe), "", count, 0)
}
func (wac *Conn) LoadMessagesAfter(jid, messageId string, count int) (*binary.Node, error) {
return wac.query("message", jid, messageId, "after", "true", "", count, 0)
func (wac *Conn) LoadMessagesAfter(jid, messageId string, fromMe bool, count int) (*binary.Node, error) {
return wac.query("message", jid, messageId, "after", strconv.FormatBool(fromMe), "", count, 0)
}
func (wac *Conn) LoadMediaInfo(jid, messageId, owner string) (*binary.Node, error) {
return wac.query("media", jid, messageId, "", owner, "", 0, 0)
func (wac *Conn) LoadMediaInfo(jid, messageId string, fromMe bool) (*binary.Node, error) {
return wac.query("media", jid, messageId, "", strconv.FormatBool(fromMe), "", 0, 0)
}
func (wac *Conn) Presence(jid string, presence Presence) (<-chan string, error) {
@@ -96,11 +96,19 @@ func (wac *Conn) Emoji() (*binary.Node, error) {
}
func (wac *Conn) Contacts() (*binary.Node, error) {
return wac.query("contacts", "", "", "", "", "", 0, 0)
node, err := wac.query("contacts", "", "", "", "", "", 0, 0)
if node != nil && node.Description == "response" && node.Attributes["type"] == "contacts" {
wac.updateContacts(node.Content)
}
return node, err
}
func (wac *Conn) Chats() (*binary.Node, error) {
return wac.query("chat", "", "", "", "", "", 0, 0)
node, err := wac.query("chat", "", "", "", "", "", 0, 0)
if node != nil && node.Description == "response" && node.Attributes["type"] == "chat" {
wac.updateChats(node.Content)
}
return node, err
}
func (wac *Conn) Read(jid, id string) (<-chan string, error) {
@@ -177,13 +185,18 @@ func (wac *Conn) query(t, jid, messageId, kind, owner, search string, count, pag
return nil, err
}
msg, err := wac.decryptBinaryMessage([]byte(<-ch))
if err != nil {
return nil, err
}
select {
case response := <-ch:
msg, err := wac.decryptBinaryMessage([]byte(response))
if err != nil {
return nil, err
}
//TODO: use parseProtoMessage
return msg, nil
//TODO: use parseProtoMessage
return msg, nil
case <-time.After(3 * time.Minute):
return nil, ErrQueryTimeout
}
}
func (wac *Conn) setGroup(t, jid, subject string, participants []string) (<-chan string, error) {

View File

@@ -1,9 +1,9 @@
package whatsapp
import (
"encoding/json"
"errors"
"fmt"
"github.com/pkg/errors"
)
var (
@@ -21,9 +21,28 @@ var (
ErrServerRespondedWith404 = errors.New("server responded with status 404")
ErrMediaDownloadFailedWith404 = errors.New("download failed with status code 404")
ErrMediaDownloadFailedWith410 = errors.New("download failed with status code 410")
ErrInvalidWebsocket = errors.New("invalid websocket")
ErrMessageTypeNotImplemented = errors.New("message type not implemented")
ErrOptionsNotProvided = errors.New("new conn options not provided")
ErrLoginTimedOut = errors.New("login timed out")
ErrQueryTimeout = errors.New("query timed out")
ErrBadRequest = errors.New("400 (bad request)")
ErrUnpaired = errors.New("401 (unpaired from phone)")
ErrAccessDenied = errors.New("403 (access denied)")
ErrLoggedIn = errors.New("405 (already logged in)")
ErrReplaced = errors.New("409 (logged in from another location)")
ErrNoURLPresent = errors.New("no url present")
ErrFileLengthMismatch = errors.New("file length does not match")
ErrInvalidHashLength = errors.New("hash too short")
ErrTooShortFile = errors.New("file too short")
ErrInvalidMediaHMAC = errors.New("invalid media hmac")
ErrCantGetInviteLink = errors.New("you don't have the permission to view the invite link")
ErrJoinUnauthorized = errors.New("you're not allowed to join that group")
ErrInvalidWebsocket = errors.New("invalid websocket")
ErrMessageTypeNotImplemented = errors.New("message type not implemented")
ErrOptionsNotProvided = errors.New("new conn options not provided")
)
type ErrConnectionFailed struct {
@@ -42,3 +61,31 @@ type ErrConnectionClosed struct {
func (e *ErrConnectionClosed) Error() string {
return fmt.Sprintf("server closed connection,code: %d,text: %s", e.Code, e.Text)
}
type StatusResponseFields struct {
// The response status code. This is always expected to be present.
Status int `json:"status"`
// Some error messages include a "tos" value. If it's higher than 0, it
// might mean the user has been banned for breaking the terms of service.
TermsOfService int `json:"tos,omitempty"`
// This is a timestamp that's at least present in message send responses.
Timestamp int64 `json:"t,omitempty"`
}
type StatusResponse struct {
StatusResponseFields
RequestType string `json:"-"`
Extra map[string]interface{} `json:"-"`
}
func (sr *StatusResponse) UnmarshalJSON(data []byte) error {
err := json.Unmarshal(data, &sr.Extra)
if err != nil {
return err
}
return json.Unmarshal(data, &sr.StatusResponseFields)
}
func (sr StatusResponse) Error() string {
return fmt.Sprintf("%s responded with %d", sr.RequestType, sr.Status)
}

View File

@@ -1,14 +1,10 @@
module github.com/Rhymen/go-whatsapp
require (
github.com/Rhymen/go-whatsapp/examples/echo v0.0.0-20190325075644-cc2581bbf24d // indirect
github.com/Rhymen/go-whatsapp/examples/restoreSession v0.0.0-20190325075644-cc2581bbf24d // indirect
github.com/Rhymen/go-whatsapp/examples/sendImage v0.0.0-20190325075644-cc2581bbf24d // indirect
github.com/Rhymen/go-whatsapp/examples/sendTextMessages v0.0.0-20190325075644-cc2581bbf24d // indirect
github.com/golang/protobuf v1.3.0
github.com/gorilla/websocket v1.4.1
github.com/pkg/errors v0.8.1
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
)
go 1.11
go 1.13
require (
github.com/golang/protobuf v1.4.2
github.com/gorilla/websocket v1.4.2
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9
google.golang.org/protobuf v1.24.0
)

View File

@@ -1,37 +1,70 @@
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f h1:2dk3eOnYllh+wUOuDhOoC2vUVoJF/5z478ryJ+wzEII=
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f/go.mod h1:4a58ifQTEe2uwwsaqbh3i2un5/CBPg+At/qHpt18Tmk=
github.com/Rhymen/go-whatsapp v0.0.0/go.mod h1:rdQr95g2C1xcOfM7QGOhza58HeI3I+tZ/bbluv7VazA=
github.com/Rhymen/go-whatsapp/examples/echo v0.0.0-20190325075644-cc2581bbf24d h1:m3wkrunHupL9XzzM+JZu1pgoDV1d9LFtD0gedNTHVDU=
github.com/Rhymen/go-whatsapp/examples/echo v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:zgCiQtBtZ4P4gFWvwl9aashsdwOcbb/EHOGRmSzM8ME=
github.com/Rhymen/go-whatsapp/examples/restoreSession v0.0.0-20190325075644-cc2581bbf24d h1:muQlzqfZxjptOBjPdv+UoxVMr8Y1rPx7VMGPJIAFc5w=
github.com/Rhymen/go-whatsapp/examples/restoreSession v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:5sCUSpG616ZoSJhlt9iBNI/KXBqrVLcNUJqg7J9+8pU=
github.com/Rhymen/go-whatsapp/examples/sendImage v0.0.0-20190325075644-cc2581bbf24d h1:xP//3V77YvHd1cj2Z3ttuQWAvs5WmIwBbjKe/t0g/tM=
github.com/Rhymen/go-whatsapp/examples/sendImage v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:RdiyhanVEGXTam+mZ3k6Y3VDCCvXYCwReOoxGozqhHw=
github.com/Rhymen/go-whatsapp/examples/sendTextMessages v0.0.0-20190325075644-cc2581bbf24d h1:IRmRE0SPMByczwE2dhnTcVojje3w2TCSKwFrboLUbDg=
github.com/Rhymen/go-whatsapp/examples/sendTextMessages v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:suwzklatySS3Q0+NCxCDh5hYfgXdQUWU1DNcxwAxStM=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
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/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk=
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 h1:lpEzuenPuO1XNTeikEmvqYFcU37GVLl8SRNblzyvGBE=
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/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-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
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-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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/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/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -3,7 +3,10 @@ package whatsapp
import (
"encoding/json"
"fmt"
"strconv"
"time"
"github.com/Rhymen/go-whatsapp/binary"
)
func (wac *Conn) GetGroupMetaData(jid string) (<-chan string, error) {
@@ -57,8 +60,11 @@ func (wac *Conn) GroupInviteLink(jid string) (string, error) {
return "", fmt.Errorf("request timed out")
}
if int(response["status"].(float64)) != 200 {
return "", fmt.Errorf("request responded with %d", response["status"])
status := int(response["status"].(float64))
if status == 401 {
return "", ErrCantGetInviteLink
} else if status != 200 {
return "", fmt.Errorf("request responded with %d", status)
}
return response["code"].(string), nil
@@ -82,9 +88,77 @@ func (wac *Conn) GroupAcceptInviteCode(code string) (jid string, err error) {
return "", fmt.Errorf("request timed out")
}
if int(response["status"].(float64)) != 200 {
return "", fmt.Errorf("request responded with %d", response["status"])
status := int(response["status"].(float64))
if status == 401 {
return "", ErrJoinUnauthorized
} else if status != 200 {
return "", fmt.Errorf("request responded with %d", status)
}
return response["gid"].(string), nil
}
type descriptionID struct {
DescID string `json:"descId"`
}
func (wac *Conn) getDescriptionID(jid string) (string, error) {
data, err := wac.GetGroupMetaData(jid)
if err != nil {
return "none", err
}
var oldData descriptionID
err = json.Unmarshal([]byte(<-data), &oldData)
if err != nil {
return "none", err
}
if oldData.DescID == "" {
return "none", nil
}
return oldData.DescID, nil
}
func (wac *Conn) UpdateGroupDescription(jid, description string) (<-chan string, error) {
prevID, err := wac.getDescriptionID(jid)
if err != nil {
return nil, err
}
newData := map[string]string{
"prev": prevID,
}
var desc interface{} = description
if description == "" {
newData["delete"] = "true"
desc = nil
} else {
newData["id"] = fmt.Sprintf("%d-%d", time.Now().Unix(), wac.msgCount*19)
}
tag := fmt.Sprintf("%d.--%d", time.Now().Unix(), wac.msgCount*19)
n := binary.Node{
Description: "action",
Attributes: map[string]string{
"type": "set",
"epoch": strconv.Itoa(wac.msgCount),
},
Content: []interface{}{
binary.Node{
Description: "group",
Attributes: map[string]string{
"id": tag,
"jid": jid,
"type": "description",
"author": wac.Info.Wid,
},
Content: []binary.Node{
{
Description: "description",
Attributes: newData,
Content: desc,
},
},
},
},
}
return wac.writeBinary(n, group, 136, tag)
}

View File

@@ -117,6 +117,12 @@ type RawMessageHandler interface {
HandleRawMessage(message *proto.WebMessageInfo)
}
// The UnknownBinaryHandler interface needs to be implemented to receive unhandled binary messages.
type UnknownBinaryHandler interface {
Handler
HandleUnknownBinaryNode(message *binary.Node)
}
/**
The ContactListHandler interface needs to be implemented to applky custom actions to contact lists dispatched by the dispatcher.
*/
@@ -141,6 +147,16 @@ type BatteryMessageHandler interface {
HandleBatteryMessage(battery BatteryMessage)
}
type ReadMessageHandler interface {
Handler
HandleReadMessage(read ReadMessage)
}
type ReceivedMessageHandler interface {
Handler
HandleReceivedMessage(received ReceivedMessage)
}
/**
The NewContactHandler interface needs to be implemented to receive the contact's name for the first time.
*/
@@ -186,6 +202,19 @@ func (wac *Conn) shouldCallSynchronously(handler Handler) bool {
}
func (wac *Conn) handle(message interface{}) {
defer func() {
if errIfc := recover(); errIfc != nil {
if err, ok := errIfc.(error); ok {
wac.unsafeHandle(fmt.Errorf("panic in WhatsApp handler: %w", err))
} else {
wac.unsafeHandle(fmt.Errorf("panic in WhatsApp handler: %v", errIfc))
}
}
}()
wac.unsafeHandle(message)
}
func (wac *Conn) unsafeHandle(message interface{}) {
wac.handleWithCustomHandlers(message, wac.handler)
}
@@ -324,6 +353,28 @@ func (wac *Conn) handleWithCustomHandlers(message interface{}, handlers []Handle
}
}
case ReadMessage:
for _, h := range handlers {
if x, ok := h.(ReadMessageHandler); ok {
if wac.shouldCallSynchronously(h) {
x.HandleReadMessage(m)
} else {
go x.HandleReadMessage(m)
}
}
}
case ReceivedMessage:
for _, h := range handlers {
if x, ok := h.(ReceivedMessageHandler); ok {
if wac.shouldCallSynchronously(h) {
x.HandleReceivedMessage(m)
} else {
go x.HandleReceivedMessage(m)
}
}
}
case *proto.WebMessageInfo:
for _, h := range handlers {
if x, ok := h.(RawMessageHandler); ok {
@@ -334,6 +385,17 @@ func (wac *Conn) handleWithCustomHandlers(message interface{}, handlers []Handle
}
}
}
case *binary.Node:
for _, h := range handlers {
if x, ok := h.(UnknownBinaryHandler); ok {
if wac.shouldCallSynchronously(h) {
x.HandleUnknownBinaryNode(m)
} else {
go x.HandleUnknownBinaryNode(m)
}
}
}
}
}
@@ -425,6 +487,8 @@ func (wac *Conn) dispatch(msg interface{}) {
for a := range con {
wac.handle(ParseNodeMessage(con[a]))
}
} else {
wac.handle(message)
}
} else if message.Description == "response" && message.Attributes["type"] == "contacts" {
wac.updateContacts(message.Content)
@@ -432,6 +496,8 @@ func (wac *Conn) dispatch(msg interface{}) {
} else if message.Description == "response" && message.Attributes["type"] == "chat" {
wac.updateChats(message.Content)
wac.handleChats(message.Content)
} else {
wac.handle(message)
}
case error:
wac.handle(message)

View File

@@ -10,6 +10,7 @@ import (
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"time"
@@ -20,7 +21,7 @@ import (
func Download(url string, mediaKey []byte, appInfo MediaType, fileLength int) ([]byte, error) {
if url == "" {
return nil, fmt.Errorf("no url present")
return nil, ErrNoURLPresent
}
file, mac, err := downloadMedia(url)
if err != nil {
@@ -38,7 +39,7 @@ func Download(url string, mediaKey []byte, appInfo MediaType, fileLength int) ([
return nil, err
}
if len(data) != fileLength {
return nil, fmt.Errorf("file length does not match. Expected: %v, got: %v", fileLength, len(data))
return nil, ErrFileLengthMismatch
}
return data, nil
}
@@ -50,10 +51,10 @@ func validateMedia(iv []byte, file []byte, macKey []byte, mac []byte) error {
return err
}
if n < 10 {
return fmt.Errorf("hash to short")
return ErrInvalidHashLength
}
if !hmac.Equal(h.Sum(nil)[:10], mac) {
return fmt.Errorf("invalid media hmac")
return ErrInvalidMediaHMAC
}
return nil
}
@@ -71,18 +72,18 @@ func downloadMedia(url string) (file []byte, mac []byte, err error) {
if err != nil {
return nil, nil, err
}
if resp.StatusCode != 200 {
if resp.StatusCode == 404 {
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusNotFound {
return nil, nil, ErrMediaDownloadFailedWith404
}
if resp.StatusCode == 410 {
if resp.StatusCode == http.StatusGone {
return nil, nil, ErrMediaDownloadFailedWith410
}
return nil, nil, fmt.Errorf("download failed with status code %d", resp.StatusCode)
}
defer resp.Body.Close()
if resp.ContentLength <= 10 {
return nil, nil, fmt.Errorf("file to short")
return nil, nil, ErrTooShortFile
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
@@ -101,8 +102,8 @@ type MediaConn struct {
Hosts []struct {
Hostname string `json:"hostname"`
IPs []struct {
IP4 string `json:"ip4"`
IP6 string `json:"ip6"`
IP4 net.IP `json:"ip4"`
IP6 net.IP `json:"ip6"`
} `json:"ips"`
} `json:"hosts"`
} `json:"media_conn"`
@@ -125,21 +126,17 @@ func (wac *Conn) queryMediaConn() (hostname, auth string, ttl int, err error) {
return "", "", 0, fmt.Errorf("query media conn timed out")
}
if resp.Status != 200 {
if resp.Status != http.StatusOK {
return "", "", 0, fmt.Errorf("query media conn responded with %d", resp.Status)
}
var host string
for _, h := range resp.MediaConn.Hosts {
if h.Hostname != "" {
host = h.Hostname
break
return h.Hostname, resp.MediaConn.Auth, resp.MediaConn.TTL, nil
}
}
if host == "" {
return "", "", 0, fmt.Errorf("query media conn responded with no host")
}
return host, resp.MediaConn.Auth, resp.MediaConn.TTL, nil
return "", "", 0, fmt.Errorf("query media conn responded with no host")
}
var mediaTypeMap = map[MediaType]string{
@@ -202,7 +199,7 @@ func (wac *Conn) Upload(reader io.Reader, appInfo MediaType) (downloadURL string
body := bytes.NewReader(append(enc, mac...))
req, err := http.NewRequest("POST", uploadURL.String(), body)
req, err := http.NewRequest(http.MethodPost, uploadURL.String(), body)
if err != nil {
return "", nil, nil, nil, 0, err
}
@@ -222,7 +219,9 @@ func (wac *Conn) Upload(reader io.Reader, appInfo MediaType) (downloadURL string
}
var jsonRes map[string]string
json.NewDecoder(res.Body).Decode(&jsonRes)
if err := json.NewDecoder(res.Body).Decode(&jsonRes); err != nil {
return "", nil, nil, nil, 0, err
}
return jsonRes["url"], mediaKey, fileEncSha256, fileSha256, fileLength, nil
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/Rhymen/go-whatsapp/binary"
"github.com/Rhymen/go-whatsapp/binary/proto"
"github.com/davecgh/go-spew/spew"
)
type MediaType string
@@ -23,6 +24,23 @@ const (
MediaDocument MediaType = "WhatsApp Document Keys"
)
func (wac *Conn) SendRaw(msg *proto.WebMessageInfo, output chan<- error) {
ch, err := wac.sendProto(msg)
if err != nil {
output <- fmt.Errorf("could not send proto: %w", err)
return
}
response := <-ch
resp := StatusResponse{RequestType: "message sending"}
if err = json.Unmarshal([]byte(response), &resp); err != nil {
output <- fmt.Errorf("error decoding sending response: %w", err)
} else if resp.Status != 200 {
output <- resp
} else {
output <- nil
}
}
func (wac *Conn) Send(msg interface{}) (string, error) {
var msgProto *proto.WebMessageInfo
@@ -76,21 +94,16 @@ func (wac *Conn) Send(msg interface{}) (string, error) {
select {
case response := <-ch:
var resp map[string]interface{}
resp := StatusResponse{RequestType: "message sending"}
if err = json.Unmarshal([]byte(response), &resp); err != nil {
return "ERROR", fmt.Errorf("error decoding sending response: %v\n", err)
} else if resp.Status != 200 {
return "ERROR", resp
}
if int(resp["status"].(float64)) != 200 {
return "ERROR", fmt.Errorf("message sending responded with %v", resp["status"])
}
if int(resp["status"].(float64)) == 200 {
return getMessageInfo(msgProto).Id, nil
}
return getMessageInfo(msgProto).Id, nil
case <-time.After(wac.msgTimeout):
return "ERROR", fmt.Errorf("sending message timed out")
}
return "ERROR", nil
}
func (wac *Conn) sendProto(p *proto.WebMessageInfo) (<-chan string, error) {
@@ -151,21 +164,16 @@ func (wac *Conn) DeleteMessage(remotejid, msgid string, fromMe bool) error {
select {
case response := <-ch:
var resp map[string]interface{}
resp := StatusResponse{RequestType: "message deletion"}
if err = json.Unmarshal([]byte(response), &resp); err != nil {
return fmt.Errorf("error decoding deletion response: %v", err)
} else if resp.Status != 200 {
return resp
}
if int(resp["status"].(float64)) != 200 {
return fmt.Errorf("message deletion responded with %v", resp["status"])
}
if int(resp["status"].(float64)) == 200 {
return nil
}
return nil
case <-time.After(wac.msgTimeout):
return fmt.Errorf("deleting message timed out")
}
return nil
}
func (wac *Conn) deleteChatProto(remotejid, msgid string, fromMe bool) (<-chan string, error) {
@@ -258,7 +266,7 @@ func getInfoProto(info *MessageInfo) *proto.WebMessageInfo {
}
info.FromMe = true
status := proto.WebMessageInfo_WEB_MESSAGE_INFO_STATUS(info.Status)
status := proto.WebMessageInfo_WebMessageInfoStatus(info.Status)
return &proto.WebMessageInfo{
Key: &proto.MessageKey{
@@ -275,19 +283,20 @@ func getInfoProto(info *MessageInfo) *proto.WebMessageInfo {
ContextInfo represents contextinfo of every message
*/
type ContextInfo struct {
QuotedMessageID string //StanzaId
QuotedMessageID string // StanzaId
QuotedMessage *proto.Message
Participant string
IsForwarded bool
MentionedJID []string
}
func getMessageContext(msg *proto.ContextInfo) ContextInfo {
return ContextInfo{
QuotedMessageID: msg.GetStanzaId(), //StanzaId
QuotedMessageID: msg.GetStanzaId(), // StanzaId
QuotedMessage: msg.GetQuotedMessage(),
Participant: msg.GetParticipant(),
IsForwarded: msg.GetIsForwarded(),
MentionedJID: msg.GetMentionedJid(),
}
}
@@ -325,7 +334,6 @@ func getTextMessage(msg *proto.WebMessageInfo) TextMessage {
text.ContextInfo = getMessageContext(m.GetContextInfo())
} else {
text.Text = msg.GetMessage().GetConversation()
}
return text
@@ -803,7 +811,6 @@ func getContactMessageProto(msg ContactMessage) *proto.WebMessageInfo {
}
func ParseProtoMessage(msg *proto.WebMessageInfo) interface{} {
switch {
case msg.GetMessage().GetAudioMessage() != nil:
@@ -837,7 +844,8 @@ func ParseProtoMessage(msg *proto.WebMessageInfo) interface{} {
return getContactMessage(msg)
default:
//cannot match message
// cannot match message
spew.Dump(msg)
return ErrMessageTypeNotImplemented
}
}
@@ -873,15 +881,50 @@ func getNewContact(msg map[string]string) Contact {
return contact
}
// ReadMessage represents a chat that the user read on the WhatsApp mobile app.
type ReadMessage struct {
Jid string
}
func getReadMessage(msg map[string]string) ReadMessage {
return ReadMessage{
Jid: msg["jid"],
}
}
// ReceivedMessage probably represents a message that the user read on the WhatsApp mobile app.
type ReceivedMessage struct {
Index string
Jid string
Owner bool
Participant string
Type string
}
func getReceivedMessage(msg map[string]string) ReceivedMessage {
owner, _ := strconv.ParseBool(msg["owner"])
// This field might not exist
participant, _ := msg["participant"]
return ReceivedMessage{
Index: msg["index"],
Jid: msg["jid"],
Owner: owner,
Participant: participant,
Type: msg["type"],
}
}
func ParseNodeMessage(msg binary.Node) interface{} {
switch msg.Description {
case "battery":
return getBatteryMessage(msg.Attributes)
case "user":
return getNewContact(msg.Attributes)
case "read":
return getReadMessage(msg.Attributes)
case "received":
return getReceivedMessage(msg.Attributes)
default:
//cannot match message
return &msg
}
return nil
}

View File

@@ -7,12 +7,13 @@ import (
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"github.com/gorilla/websocket"
"github.com/Rhymen/go-whatsapp/binary"
"github.com/Rhymen/go-whatsapp/crypto/cbc"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
)
func (wac *Conn) readPump() {
@@ -41,12 +42,12 @@ func (wac *Conn) readPump() {
}
msg, err := ioutil.ReadAll(reader)
if err != nil {
wac.handle(errors.Wrap(err, "error reading message from Reader"))
wac.handle(fmt.Errorf("error reading message from Reader: %w", err))
continue
}
err = wac.processReadData(msgType, msg)
if err != nil {
wac.handle(errors.Wrap(err, "error processing data"))
wac.handle(fmt.Errorf("error processing data: %w", err))
}
case <-wac.ws.close:
return
@@ -95,7 +96,7 @@ func (wac *Conn) processReadData(msgType int, msg []byte) error {
}
message, err := wac.decryptBinaryMessage([]byte(data[1]))
if err != nil {
return errors.Wrap(err, "error decoding binary")
return fmt.Errorf("error decoding binary: %w", err)
}
wac.dispatch(message)
} else { //RAW json status updates
@@ -111,16 +112,18 @@ func (wac *Conn) decryptBinaryMessage(msg []byte) (*binary.Node, error) {
var response struct {
Status int `json:"status"`
}
err := json.Unmarshal(msg, &response)
if err == nil {
if response.Status == 404 {
if err := json.Unmarshal(msg, &response); err == nil {
if response.Status == http.StatusNotFound {
return nil, ErrServerRespondedWith404
}
return nil, errors.New(fmt.Sprintf("server responded with %d", response.Status))
return nil, fmt.Errorf("server responded with %d", response.Status)
} else {
return nil, ErrInvalidServerResponse
}
return nil, ErrInvalidServerResponse
}
h2.Write([]byte(msg[32:]))
if !hmac.Equal(h2.Sum(nil), msg[:32]) {
@@ -130,13 +133,13 @@ func (wac *Conn) decryptBinaryMessage(msg []byte) (*binary.Node, error) {
// message decrypt
d, err := cbc.Decrypt(wac.session.EncKey, nil, msg[32:])
if err != nil {
return nil, errors.Wrap(err, "decrypting message with AES-CBC failed")
return nil, fmt.Errorf("decrypting message with AES-CBC failed: %w", err)
}
// message unmarshal
message, err := binary.Unmarshal(d)
if err != nil {
return nil, errors.Wrap(err, "could not decode binary")
return nil, fmt.Errorf("could not decode binary: %w", err)
}
return message, nil

View File

@@ -18,7 +18,7 @@ import (
)
//represents the WhatsAppWeb client version
var waVersion = []int{2, 2039, 9}
var waVersion = []int{2, 2100, 6}
/*
Session contains session individual information. To be able to resume the connection without scanning the qr code
@@ -141,7 +141,7 @@ func CheckCurrentServerVersion() ([]int, error) {
SetClientName sets the long and short client names that are sent to WhatsApp when logging in and displayed in the
WhatsApp Web device list. As the values are only sent when logging in, changing them after logging in is not possible.
*/
func (wac *Conn) SetClientName(long, short string, version string) error {
func (wac *Conn) SetClientName(long, short, version string) error {
if wac.session != nil && (wac.session.EncKey != nil || wac.session.MacKey != nil) {
return fmt.Errorf("cannot change client name after logging in")
}
@@ -157,6 +157,28 @@ func (wac *Conn) SetClientVersion(major int, minor int, patch int) {
waVersion = []int{major, minor, patch}
}
func (wac *Conn) adminInitRequest(clientId string) (string, time.Duration, error) {
login := []interface{}{"admin", "init", waVersion, []string{wac.longClientName, wac.shortClientName, wac.clientVersion}, clientId, true}
loginChan, err := wac.writeJson(login)
if err != nil {
return "", 0, fmt.Errorf("error writing login: %v\n", err)
}
var r string
select {
case r = <-loginChan:
case <-time.After(wac.msgTimeout):
return "", 0, fmt.Errorf("login connection timed out")
}
var resp map[string]interface{}
if err = json.Unmarshal([]byte(r), &resp); err != nil {
return "", 0, fmt.Errorf("error decoding login resp: %v\n", err)
}
return resp["ref"].(string), time.Duration(resp["ttl"].(float64)) * time.Millisecond, nil
}
// GetClientVersion returns WhatsApp client version
func (wac *Conn) GetClientVersion() []int {
return waVersion
@@ -186,6 +208,10 @@ github.com/Baozisoftware/qrcode-terminal-go Example login procedure:
fmt.Printf("login successful, session: %v\n", session)
*/
func (wac *Conn) Login(qrChan chan<- string) (Session, error) {
return wac.LoginWithRetry(qrChan, 0)
}
func (wac *Conn) LoginWithRetry(qrChan chan<- string, maxRetries int) (Session, error) {
session := Session{}
//Makes sure that only a single Login or Restore can happen at the same time
if !atomic.CompareAndSwapUint32(&wac.sessionLock, 0, 1) {
@@ -213,30 +239,6 @@ func (wac *Conn) Login(qrChan chan<- string) (Session, error) {
}
session.ClientId = base64.StdEncoding.EncodeToString(clientId)
login := []interface{}{"admin", "init", waVersion, []string{wac.longClientName, wac.shortClientName, wac.clientVersion}, session.ClientId, true}
loginChan, err := wac.writeJson(login)
if err != nil {
return session, fmt.Errorf("error writing login: %v\n", err)
}
var r string
select {
case r = <-loginChan:
case <-time.After(wac.msgTimeout):
return session, fmt.Errorf("login connection timed out")
}
var resp map[string]interface{}
if err = json.Unmarshal([]byte(r), &resp); err != nil {
return session, fmt.Errorf("error decoding login resp: %v\n", err)
}
var ref string
if rref, ok := resp["ref"].(string); ok {
ref = rref
} else {
return session, fmt.Errorf("error decoding login resp: invalid resp['ref']\n")
}
priv, pub, err := curve25519.GenerateKey()
if err != nil {
@@ -249,18 +251,35 @@ func (wac *Conn) Login(qrChan chan<- string) (Session, error) {
wac.listener.m["s1"] = s1
wac.listener.Unlock()
ref, ttl, err := wac.adminInitRequest(session.ClientId)
if err != nil {
return session, err
}
qrChan <- fmt.Sprintf("%v,%v,%v", ref, base64.StdEncoding.EncodeToString(pub[:]), session.ClientId)
wac.loginSessionLock.Lock()
defer wac.loginSessionLock.Unlock()
var resp2 []interface{}
select {
case r1 := <-s1:
wac.loginSessionLock.Lock()
defer wac.loginSessionLock.Unlock()
if err := json.Unmarshal([]byte(r1), &resp2); err != nil {
return session, fmt.Errorf("error decoding qr code resp: %v", err)
For:
for {
select {
case r1 := <-s1:
if err := json.Unmarshal([]byte(r1), &resp2); err != nil {
return session, fmt.Errorf("error decoding qr code resp: %v", err)
}
break For
case <-time.After(ttl):
maxRetries--
if maxRetries < 0 {
_, _ = wac.Disconnect()
return session, ErrLoginTimedOut
}
ref, ttl, err = wac.adminInitRequest(session.ClientId)
if err != nil {
return session, err
}
qrChan <- fmt.Sprintf("%v,%v,%v", ref, base64.StdEncoding.EncodeToString(pub[:]), session.ClientId)
}
case <-time.After(time.Duration(resp["ttl"].(float64)) * time.Millisecond):
return session, fmt.Errorf("qr code scan timed out")
}
info := resp2[1].(map[string]interface{})
@@ -389,14 +408,12 @@ func (wac *Conn) Restore() error {
select {
case r := <-initChan:
var resp map[string]interface{}
resp := StatusResponse{RequestType: "init"}
if err = json.Unmarshal([]byte(r), &resp); err != nil {
return fmt.Errorf("error decoding login connResp: %v\n", err)
}
if int(resp["status"].(float64)) != 200 {
} else if resp.Status != 200 {
wac.timeTag = ""
return fmt.Errorf("init responded with %d", resp["status"])
return resp
}
case <-time.After(wac.msgTimeout):
wac.timeTag = ""
@@ -416,12 +433,11 @@ func (wac *Conn) Restore() error {
//check for an error message
select {
case r := <-loginChan:
var resp map[string]interface{}
resp := StatusResponse{RequestType: "admin login"}
if err = json.Unmarshal([]byte(r), &resp); err != nil {
return fmt.Errorf("error decoding login connResp: %v\n", err)
}
if int(resp["status"].(float64)) != 200 {
return fmt.Errorf("admin login responded with %d", int(resp["status"].(float64)))
} else if resp.Status != 200 {
return fmt.Errorf("admin login errored: %w", wac.getAdminLoginResponseError(resp))
}
default:
// not even an error message assume timeout
@@ -456,15 +472,13 @@ func (wac *Conn) Restore() error {
//check for login 200 --> login success
select {
case r := <-loginChan:
var resp map[string]interface{}
resp := StatusResponse{RequestType: "admin login"}
if err = json.Unmarshal([]byte(r), &resp); err != nil {
wac.timeTag = ""
return fmt.Errorf("error decoding login connResp: %v\n", err)
}
if int(resp["status"].(float64)) != 200 {
} else if resp.Status != 200 {
wac.timeTag = ""
return fmt.Errorf("admin login responded with %d", resp["status"])
return fmt.Errorf("admin login errored: %w", wac.getAdminLoginResponseError(resp))
}
case <-time.After(wac.msgTimeout):
wac.timeTag = ""
@@ -484,6 +498,22 @@ func (wac *Conn) Restore() error {
return nil
}
func (wac *Conn) getAdminLoginResponseError(resp StatusResponse) error {
switch resp.Status {
case 400:
return ErrBadRequest
case 401:
return ErrUnpaired
case 403:
return fmt.Errorf("%w - tos: %d", ErrAccessDenied, resp.TermsOfService)
case 405:
return ErrLoggedIn
case 409:
return ErrReplaced
}
return fmt.Errorf("%d (unknown error)", status)
}
func (wac *Conn) resolveChallenge(challenge string) error {
decoded, err := base64.StdEncoding.DecodeString(challenge)
if err != nil {
@@ -501,12 +531,11 @@ func (wac *Conn) resolveChallenge(challenge string) error {
select {
case r := <-challengeChan:
var resp map[string]interface{}
resp := StatusResponse{RequestType: "login challenge"}
if err := json.Unmarshal([]byte(r), &resp); err != nil {
return fmt.Errorf("error decoding login resp: %v\n", err)
}
if int(resp["status"].(float64)) != 200 {
return fmt.Errorf("challenge responded with %d\n", resp["status"])
} else if resp.Status != 200 {
return resp
}
case <-time.After(wac.msgTimeout):
return fmt.Errorf("connection timed out")

View File

@@ -6,13 +6,12 @@ import (
"encoding/json"
"fmt"
"strconv"
"time"
"github.com/gorilla/websocket"
"github.com/Rhymen/go-whatsapp/binary"
"github.com/Rhymen/go-whatsapp/crypto/cbc"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
)
//writeJson enqueues a json message into the writeChan
@@ -54,7 +53,7 @@ func (wac *Conn) writeBinary(node binary.Node, metric metric, flag flag, message
data, err := wac.encryptBinaryMessage(node)
if err != nil {
return nil, errors.Wrap(err, "encryptBinaryMessage(node) failed")
return nil, fmt.Errorf("encryptBinaryMessage(node) failed: %w", err)
}
bytes := []byte(messageTag + ",")
@@ -63,7 +62,7 @@ func (wac *Conn) writeBinary(node binary.Node, metric metric, flag flag, message
ch, err := wac.write(websocket.BinaryMessage, messageTag, bytes)
if err != nil {
return nil, errors.Wrap(err, "failed to write message")
return nil, fmt.Errorf("failed to write message: %w", err)
}
wac.msgCount++
@@ -74,14 +73,14 @@ func (wac *Conn) sendKeepAlive() error {
bytes := []byte("?,,")
respChan, err := wac.write(websocket.TextMessage, "!", bytes)
if err != nil {
return errors.Wrap(err, "error sending keepAlive")
return fmt.Errorf("error sending keepAlive: %w", err)
}
select {
case resp := <-respChan:
msecs, err := strconv.ParseInt(resp, 10, 64)
if err != nil {
return errors.Wrap(err, "Error converting time string to uint")
return fmt.Errorf("Error converting time string to uint: %w", err)
}
wac.ServerLastSeen = time.Unix(msecs/1000, (msecs%1000)*int64(time.Millisecond))
@@ -96,29 +95,30 @@ func (wac *Conn) sendKeepAlive() error {
When phone is unreachable, WhatsAppWeb sends ["admin","test"] time after time to try a successful contact.
Tested with Airplane mode and no connection at all.
*/
func (wac *Conn) sendAdminTest() (bool, error) {
func (wac *Conn) sendAdminTest() error {
data := []interface{}{"admin", "test"}
r, err := wac.writeJson(data)
if err != nil {
return false, errors.Wrap(err, "error sending admin test")
return fmt.Errorf("error sending admin test: %w", err)
}
var response []interface{}
var resp string
select {
case resp := <-r:
case resp = <-r:
if err := json.Unmarshal([]byte(resp), &response); err != nil {
return false, fmt.Errorf("error decoding response message: %v\n", err)
return fmt.Errorf("error decoding response message: %v\n", err)
}
case <-time.After(wac.msgTimeout):
return false, ErrConnectionTimeout
return ErrConnectionTimeout
}
if len(response) == 2 && response[0].(string) == "Pong" && response[1].(bool) == true {
return true, nil
return nil
} else {
return false, nil
return fmt.Errorf("unexpected ping response: %s", resp)
}
}
@@ -145,7 +145,7 @@ func (wac *Conn) write(messageType int, answerMessageTag string, data []byte) (<
delete(wac.listener.m, answerMessageTag)
wac.listener.Unlock()
}
return nil, errors.Wrap(err, "error writing to websocket")
return nil, fmt.Errorf("error writing to websocket: %w", err)
}
return ch, nil
}
@@ -153,12 +153,12 @@ func (wac *Conn) write(messageType int, answerMessageTag string, data []byte) (<
func (wac *Conn) encryptBinaryMessage(node binary.Node) (data []byte, err error) {
b, err := binary.Marshal(node)
if err != nil {
return nil, errors.Wrap(err, "binary node marshal failed")
return nil, fmt.Errorf("binary node marshal failed: %w", err)
}
cipher, err := cbc.Encrypt(wac.session.EncKey, nil, b)
if err != nil {
return nil, errors.Wrap(err, "encrypt failed")
return nil, fmt.Errorf("encrypt failed: %w", err)
}
h := hmac.New(sha256.New, wac.session.MacKey)

12
vendor/github.com/disintegration/imaging/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,12 @@
language: go
go:
- "1.10.x"
- "1.11.x"
- "1.12.x"
before_install:
- go get github.com/mattn/goveralls
script:
- go test -v -race -cover
- $GOPATH/bin/goveralls -service=travis-ci

21
vendor/github.com/disintegration/imaging/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2012 Grigory Dryapak
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.

226
vendor/github.com/disintegration/imaging/README.md generated vendored Normal file
View File

@@ -0,0 +1,226 @@
# Imaging
[![GoDoc](https://godoc.org/github.com/disintegration/imaging?status.svg)](https://godoc.org/github.com/disintegration/imaging)
[![Build Status](https://travis-ci.org/disintegration/imaging.svg?branch=master)](https://travis-ci.org/disintegration/imaging)
[![Coverage Status](https://coveralls.io/repos/github/disintegration/imaging/badge.svg?branch=master&service=github)](https://coveralls.io/github/disintegration/imaging?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/disintegration/imaging)](https://goreportcard.com/report/github.com/disintegration/imaging)
Package imaging provides basic image processing functions (resize, rotate, crop, brightness/contrast adjustments, etc.).
All the image processing functions provided by the package accept any image type that implements `image.Image` interface
as an input, and return a new image of `*image.NRGBA` type (32bit RGBA colors, non-premultiplied alpha).
## Installation
go get -u github.com/disintegration/imaging
## Documentation
http://godoc.org/github.com/disintegration/imaging
## Usage examples
A few usage examples can be found below. See the documentation for the full list of supported functions.
### Image resizing
```go
// Resize srcImage to size = 128x128px using the Lanczos filter.
dstImage128 := imaging.Resize(srcImage, 128, 128, imaging.Lanczos)
// Resize srcImage to width = 800px preserving the aspect ratio.
dstImage800 := imaging.Resize(srcImage, 800, 0, imaging.Lanczos)
// Scale down srcImage to fit the 800x600px bounding box.
dstImageFit := imaging.Fit(srcImage, 800, 600, imaging.Lanczos)
// Resize and crop the srcImage to fill the 100x100px area.
dstImageFill := imaging.Fill(srcImage, 100, 100, imaging.Center, imaging.Lanczos)
```
Imaging supports image resizing using various resampling filters. The most notable ones:
- `Lanczos` - A high-quality resampling filter for photographic images yielding sharp results.
- `CatmullRom` - A sharp cubic filter that is faster than Lanczos filter while providing similar results.
- `MitchellNetravali` - A cubic filter that produces smoother results with less ringing artifacts than CatmullRom.
- `Linear` - Bilinear resampling filter, produces smooth output. Faster than cubic filters.
- `Box` - Simple and fast averaging filter appropriate for downscaling. When upscaling it's similar to NearestNeighbor.
- `NearestNeighbor` - Fastest resampling filter, no antialiasing.
The full list of supported filters: NearestNeighbor, Box, Linear, Hermite, MitchellNetravali, CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine. Custom filters can be created using ResampleFilter struct.
**Resampling filters comparison**
Original image:
![srcImage](testdata/branches.png)
The same image resized from 600x400px to 150x100px using different resampling filters.
From faster (lower quality) to slower (higher quality):
Filter | Resize result
--------------------------|---------------------------------------------
`imaging.NearestNeighbor` | ![dstImage](testdata/out_resize_nearest.png)
`imaging.Linear` | ![dstImage](testdata/out_resize_linear.png)
`imaging.CatmullRom` | ![dstImage](testdata/out_resize_catrom.png)
`imaging.Lanczos` | ![dstImage](testdata/out_resize_lanczos.png)
### Gaussian Blur
```go
dstImage := imaging.Blur(srcImage, 0.5)
```
Sigma parameter allows to control the strength of the blurring effect.
Original image | Sigma = 0.5 | Sigma = 1.5
-----------------------------------|----------------------------------------|---------------------------------------
![srcImage](testdata/flowers_small.png) | ![dstImage](testdata/out_blur_0.5.png) | ![dstImage](testdata/out_blur_1.5.png)
### Sharpening
```go
dstImage := imaging.Sharpen(srcImage, 0.5)
```
`Sharpen` uses gaussian function internally. Sigma parameter allows to control the strength of the sharpening effect.
Original image | Sigma = 0.5 | Sigma = 1.5
-----------------------------------|-------------------------------------------|------------------------------------------
![srcImage](testdata/flowers_small.png) | ![dstImage](testdata/out_sharpen_0.5.png) | ![dstImage](testdata/out_sharpen_1.5.png)
### Gamma correction
```go
dstImage := imaging.AdjustGamma(srcImage, 0.75)
```
Original image | Gamma = 0.75 | Gamma = 1.25
-----------------------------------|------------------------------------------|-----------------------------------------
![srcImage](testdata/flowers_small.png) | ![dstImage](testdata/out_gamma_0.75.png) | ![dstImage](testdata/out_gamma_1.25.png)
### Contrast adjustment
```go
dstImage := imaging.AdjustContrast(srcImage, 20)
```
Original image | Contrast = 15 | Contrast = -15
-----------------------------------|--------------------------------------------|-------------------------------------------
![srcImage](testdata/flowers_small.png) | ![dstImage](testdata/out_contrast_p15.png) | ![dstImage](testdata/out_contrast_m15.png)
### Brightness adjustment
```go
dstImage := imaging.AdjustBrightness(srcImage, 20)
```
Original image | Brightness = 10 | Brightness = -10
-----------------------------------|----------------------------------------------|---------------------------------------------
![srcImage](testdata/flowers_small.png) | ![dstImage](testdata/out_brightness_p10.png) | ![dstImage](testdata/out_brightness_m10.png)
### Saturation adjustment
```go
dstImage := imaging.AdjustSaturation(srcImage, 20)
```
Original image | Saturation = 30 | Saturation = -30
-----------------------------------|----------------------------------------------|---------------------------------------------
![srcImage](testdata/flowers_small.png) | ![dstImage](testdata/out_saturation_p30.png) | ![dstImage](testdata/out_saturation_m30.png)
## FAQ
### Incorrect image orientation after processing (e.g. an image appears rotated after resizing)
Most probably, the given image contains the EXIF orientation tag.
The stadard `image/*` packages do not support loading and saving
this kind of information. To fix the issue, try opening images with
the `AutoOrientation` decode option. If this option is set to `true`,
the image orientation is changed after decoding, according to the
orientation tag (if present). Here's the example:
```go
img, err := imaging.Open("test.jpg", imaging.AutoOrientation(true))
```
### What's the difference between `imaging` and `gift` packages?
[imaging](https://github.com/disintegration/imaging)
is designed to be a lightweight and simple image manipulation package.
It provides basic image processing functions and a few helper functions
such as `Open` and `Save`. It consistently returns *image.NRGBA image
type (8 bits per channel, RGBA).
[gift](https://github.com/disintegration/gift)
supports more advanced image processing, for example, sRGB/Linear color
space conversions. It also supports different output image types
(e.g. 16 bits per channel) and provides easy-to-use API for chaining
multiple processing steps together.
## Example code
```go
package main
import (
"image"
"image/color"
"log"
"github.com/disintegration/imaging"
)
func main() {
// Open a test image.
src, err := imaging.Open("testdata/flowers.png")
if err != nil {
log.Fatalf("failed to open image: %v", err)
}
// Crop the original image to 300x300px size using the center anchor.
src = imaging.CropAnchor(src, 300, 300, imaging.Center)
// Resize the cropped image to width = 200px preserving the aspect ratio.
src = imaging.Resize(src, 200, 0, imaging.Lanczos)
// Create a blurred version of the image.
img1 := imaging.Blur(src, 5)
// Create a grayscale version of the image with higher contrast and sharpness.
img2 := imaging.Grayscale(src)
img2 = imaging.AdjustContrast(img2, 20)
img2 = imaging.Sharpen(img2, 2)
// Create an inverted version of the image.
img3 := imaging.Invert(src)
// Create an embossed version of the image using a convolution filter.
img4 := imaging.Convolve3x3(
src,
[9]float64{
-1, -1, 0,
-1, 1, 1,
0, 1, 1,
},
nil,
)
// Create a new image and paste the four produced images into it.
dst := imaging.New(400, 400, color.NRGBA{0, 0, 0, 0})
dst = imaging.Paste(dst, img1, image.Pt(0, 0))
dst = imaging.Paste(dst, img2, image.Pt(0, 200))
dst = imaging.Paste(dst, img3, image.Pt(200, 0))
dst = imaging.Paste(dst, img4, image.Pt(200, 200))
// Save the resulting image as JPEG.
err = imaging.Save(dst, "testdata/out_example.jpg")
if err != nil {
log.Fatalf("failed to save image: %v", err)
}
}
```
Output:
![dstImage](testdata/out_example.jpg)

253
vendor/github.com/disintegration/imaging/adjust.go generated vendored Normal file
View File

@@ -0,0 +1,253 @@
package imaging
import (
"image"
"image/color"
"math"
)
// Grayscale produces a grayscale version of the image.
func Grayscale(img image.Image) *image.NRGBA {
src := newScanner(img)
dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
parallel(0, src.h, func(ys <-chan int) {
for y := range ys {
i := y * dst.Stride
src.scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4])
for x := 0; x < src.w; x++ {
d := dst.Pix[i : i+3 : i+3]
r := d[0]
g := d[1]
b := d[2]
f := 0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)
y := uint8(f + 0.5)
d[0] = y
d[1] = y
d[2] = y
i += 4
}
}
})
return dst
}
// Invert produces an inverted (negated) version of the image.
func Invert(img image.Image) *image.NRGBA {
src := newScanner(img)
dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
parallel(0, src.h, func(ys <-chan int) {
for y := range ys {
i := y * dst.Stride
src.scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4])
for x := 0; x < src.w; x++ {
d := dst.Pix[i : i+3 : i+3]
d[0] = 255 - d[0]
d[1] = 255 - d[1]
d[2] = 255 - d[2]
i += 4
}
}
})
return dst
}
// AdjustSaturation changes the saturation of the image using the percentage parameter and returns the adjusted image.
// The percentage must be in the range (-100, 100).
// The percentage = 0 gives the original image.
// The percentage = 100 gives the image with the saturation value doubled for each pixel.
// The percentage = -100 gives the image with the saturation value zeroed for each pixel (grayscale).
//
// Examples:
// dstImage = imaging.AdjustSaturation(srcImage, 25) // Increase image saturation by 25%.
// dstImage = imaging.AdjustSaturation(srcImage, -10) // Decrease image saturation by 10%.
//
func AdjustSaturation(img image.Image, percentage float64) *image.NRGBA {
percentage = math.Min(math.Max(percentage, -100), 100)
multiplier := 1 + percentage/100
return AdjustFunc(img, func(c color.NRGBA) color.NRGBA {
h, s, l := rgbToHSL(c.R, c.G, c.B)
s *= multiplier
if s > 1 {
s = 1
}
r, g, b := hslToRGB(h, s, l)
return color.NRGBA{r, g, b, c.A}
})
}
// AdjustContrast changes the contrast of the image using the percentage parameter and returns the adjusted image.
// The percentage must be in range (-100, 100). The percentage = 0 gives the original image.
// The percentage = -100 gives solid gray image.
//
// Examples:
//
// dstImage = imaging.AdjustContrast(srcImage, -10) // Decrease image contrast by 10%.
// dstImage = imaging.AdjustContrast(srcImage, 20) // Increase image contrast by 20%.
//
func AdjustContrast(img image.Image, percentage float64) *image.NRGBA {
percentage = math.Min(math.Max(percentage, -100.0), 100.0)
lut := make([]uint8, 256)
v := (100.0 + percentage) / 100.0
for i := 0; i < 256; i++ {
switch {
case 0 <= v && v <= 1:
lut[i] = clamp((0.5 + (float64(i)/255.0-0.5)*v) * 255.0)
case 1 < v && v < 2:
lut[i] = clamp((0.5 + (float64(i)/255.0-0.5)*(1/(2.0-v))) * 255.0)
default:
lut[i] = uint8(float64(i)/255.0+0.5) * 255
}
}
return adjustLUT(img, lut)
}
// AdjustBrightness changes the brightness of the image using the percentage parameter and returns the adjusted image.
// The percentage must be in range (-100, 100). The percentage = 0 gives the original image.
// The percentage = -100 gives solid black image. The percentage = 100 gives solid white image.
//
// Examples:
//
// dstImage = imaging.AdjustBrightness(srcImage, -15) // Decrease image brightness by 15%.
// dstImage = imaging.AdjustBrightness(srcImage, 10) // Increase image brightness by 10%.
//
func AdjustBrightness(img image.Image, percentage float64) *image.NRGBA {
percentage = math.Min(math.Max(percentage, -100.0), 100.0)
lut := make([]uint8, 256)
shift := 255.0 * percentage / 100.0
for i := 0; i < 256; i++ {
lut[i] = clamp(float64(i) + shift)
}
return adjustLUT(img, lut)
}
// AdjustGamma performs a gamma correction on the image and returns the adjusted image.
// Gamma parameter must be positive. Gamma = 1.0 gives the original image.
// Gamma less than 1.0 darkens the image and gamma greater than 1.0 lightens it.
//
// Example:
//
// dstImage = imaging.AdjustGamma(srcImage, 0.7)
//
func AdjustGamma(img image.Image, gamma float64) *image.NRGBA {
e := 1.0 / math.Max(gamma, 0.0001)
lut := make([]uint8, 256)
for i := 0; i < 256; i++ {
lut[i] = clamp(math.Pow(float64(i)/255.0, e) * 255.0)
}
return adjustLUT(img, lut)
}
// AdjustSigmoid changes the contrast of the image using a sigmoidal function and returns the adjusted image.
// It's a non-linear contrast change useful for photo adjustments as it preserves highlight and shadow detail.
// The midpoint parameter is the midpoint of contrast that must be between 0 and 1, typically 0.5.
// The factor parameter indicates how much to increase or decrease the contrast, typically in range (-10, 10).
// If the factor parameter is positive the image contrast is increased otherwise the contrast is decreased.
//
// Examples:
//
// dstImage = imaging.AdjustSigmoid(srcImage, 0.5, 3.0) // Increase the contrast.
// dstImage = imaging.AdjustSigmoid(srcImage, 0.5, -3.0) // Decrease the contrast.
//
func AdjustSigmoid(img image.Image, midpoint, factor float64) *image.NRGBA {
if factor == 0 {
return Clone(img)
}
lut := make([]uint8, 256)
a := math.Min(math.Max(midpoint, 0.0), 1.0)
b := math.Abs(factor)
sig0 := sigmoid(a, b, 0)
sig1 := sigmoid(a, b, 1)
e := 1.0e-6
if factor > 0 {
for i := 0; i < 256; i++ {
x := float64(i) / 255.0
sigX := sigmoid(a, b, x)
f := (sigX - sig0) / (sig1 - sig0)
lut[i] = clamp(f * 255.0)
}
} else {
for i := 0; i < 256; i++ {
x := float64(i) / 255.0
arg := math.Min(math.Max((sig1-sig0)*x+sig0, e), 1.0-e)
f := a - math.Log(1.0/arg-1.0)/b
lut[i] = clamp(f * 255.0)
}
}
return adjustLUT(img, lut)
}
func sigmoid(a, b, x float64) float64 {
return 1 / (1 + math.Exp(b*(a-x)))
}
// adjustLUT applies the given lookup table to the colors of the image.
func adjustLUT(img image.Image, lut []uint8) *image.NRGBA {
src := newScanner(img)
dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
lut = lut[0:256]
parallel(0, src.h, func(ys <-chan int) {
for y := range ys {
i := y * dst.Stride
src.scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4])
for x := 0; x < src.w; x++ {
d := dst.Pix[i : i+3 : i+3]
d[0] = lut[d[0]]
d[1] = lut[d[1]]
d[2] = lut[d[2]]
i += 4
}
}
})
return dst
}
// AdjustFunc applies the fn function to each pixel of the img image and returns the adjusted image.
//
// Example:
//
// dstImage = imaging.AdjustFunc(
// srcImage,
// func(c color.NRGBA) color.NRGBA {
// // Shift the red channel by 16.
// r := int(c.R) + 16
// if r > 255 {
// r = 255
// }
// return color.NRGBA{uint8(r), c.G, c.B, c.A}
// }
// )
//
func AdjustFunc(img image.Image, fn func(c color.NRGBA) color.NRGBA) *image.NRGBA {
src := newScanner(img)
dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
parallel(0, src.h, func(ys <-chan int) {
for y := range ys {
i := y * dst.Stride
src.scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4])
for x := 0; x < src.w; x++ {
d := dst.Pix[i : i+4 : i+4]
r := d[0]
g := d[1]
b := d[2]
a := d[3]
c := fn(color.NRGBA{r, g, b, a})
d[0] = c.R
d[1] = c.G
d[2] = c.B
d[3] = c.A
i += 4
}
}
})
return dst
}

148
vendor/github.com/disintegration/imaging/convolution.go generated vendored Normal file
View File

@@ -0,0 +1,148 @@
package imaging
import (
"image"
)
// ConvolveOptions are convolution parameters.
type ConvolveOptions struct {
// If Normalize is true the kernel is normalized before convolution.
Normalize bool
// If Abs is true the absolute value of each color channel is taken after convolution.
Abs bool
// Bias is added to each color channel value after convolution.
Bias int
}
// Convolve3x3 convolves the image with the specified 3x3 convolution kernel.
// Default parameters are used if a nil *ConvolveOptions is passed.
func Convolve3x3(img image.Image, kernel [9]float64, options *ConvolveOptions) *image.NRGBA {
return convolve(img, kernel[:], options)
}
// Convolve5x5 convolves the image with the specified 5x5 convolution kernel.
// Default parameters are used if a nil *ConvolveOptions is passed.
func Convolve5x5(img image.Image, kernel [25]float64, options *ConvolveOptions) *image.NRGBA {
return convolve(img, kernel[:], options)
}
func convolve(img image.Image, kernel []float64, options *ConvolveOptions) *image.NRGBA {
src := toNRGBA(img)
w := src.Bounds().Max.X
h := src.Bounds().Max.Y
dst := image.NewNRGBA(image.Rect(0, 0, w, h))
if w < 1 || h < 1 {
return dst
}
if options == nil {
options = &ConvolveOptions{}
}
if options.Normalize {
normalizeKernel(kernel)
}
type coef struct {
x, y int
k float64
}
var coefs []coef
var m int
switch len(kernel) {
case 9:
m = 1
case 25:
m = 2
}
i := 0
for y := -m; y <= m; y++ {
for x := -m; x <= m; x++ {
if kernel[i] != 0 {
coefs = append(coefs, coef{x: x, y: y, k: kernel[i]})
}
i++
}
}
parallel(0, h, func(ys <-chan int) {
for y := range ys {
for x := 0; x < w; x++ {
var r, g, b float64
for _, c := range coefs {
ix := x + c.x
if ix < 0 {
ix = 0
} else if ix >= w {
ix = w - 1
}
iy := y + c.y
if iy < 0 {
iy = 0
} else if iy >= h {
iy = h - 1
}
off := iy*src.Stride + ix*4
s := src.Pix[off : off+3 : off+3]
r += float64(s[0]) * c.k
g += float64(s[1]) * c.k
b += float64(s[2]) * c.k
}
if options.Abs {
if r < 0 {
r = -r
}
if g < 0 {
g = -g
}
if b < 0 {
b = -b
}
}
if options.Bias != 0 {
r += float64(options.Bias)
g += float64(options.Bias)
b += float64(options.Bias)
}
srcOff := y*src.Stride + x*4
dstOff := y*dst.Stride + x*4
d := dst.Pix[dstOff : dstOff+4 : dstOff+4]
d[0] = clamp(r)
d[1] = clamp(g)
d[2] = clamp(b)
d[3] = src.Pix[srcOff+3]
}
}
})
return dst
}
func normalizeKernel(kernel []float64) {
var sum, sumpos float64
for i := range kernel {
sum += kernel[i]
if kernel[i] > 0 {
sumpos += kernel[i]
}
}
if sum != 0 {
for i := range kernel {
kernel[i] /= sum
}
} else if sumpos != 0 {
for i := range kernel {
kernel[i] /= sumpos
}
}
}

7
vendor/github.com/disintegration/imaging/doc.go generated vendored Normal file
View File

@@ -0,0 +1,7 @@
/*
Package imaging provides basic image processing functions (resize, rotate, crop, brightness/contrast adjustments, etc.).
All the image processing functions provided by the package accept any image type that implements image.Image interface
as an input, and return a new image of *image.NRGBA type (32bit RGBA colors, non-premultiplied alpha).
*/
package imaging

169
vendor/github.com/disintegration/imaging/effects.go generated vendored Normal file
View File

@@ -0,0 +1,169 @@
package imaging
import (
"image"
"math"
)
func gaussianBlurKernel(x, sigma float64) float64 {
return math.Exp(-(x*x)/(2*sigma*sigma)) / (sigma * math.Sqrt(2*math.Pi))
}
// Blur produces a blurred version of the image using a Gaussian function.
// Sigma parameter must be positive and indicates how much the image will be blurred.
//
// Example:
//
// dstImage := imaging.Blur(srcImage, 3.5)
//
func Blur(img image.Image, sigma float64) *image.NRGBA {
if sigma <= 0 {
return Clone(img)
}
radius := int(math.Ceil(sigma * 3.0))
kernel := make([]float64, radius+1)
for i := 0; i <= radius; i++ {
kernel[i] = gaussianBlurKernel(float64(i), sigma)
}
return blurVertical(blurHorizontal(img, kernel), kernel)
}
func blurHorizontal(img image.Image, kernel []float64) *image.NRGBA {
src := newScanner(img)
dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
radius := len(kernel) - 1
parallel(0, src.h, func(ys <-chan int) {
scanLine := make([]uint8, src.w*4)
scanLineF := make([]float64, len(scanLine))
for y := range ys {
src.scan(0, y, src.w, y+1, scanLine)
for i, v := range scanLine {
scanLineF[i] = float64(v)
}
for x := 0; x < src.w; x++ {
min := x - radius
if min < 0 {
min = 0
}
max := x + radius
if max > src.w-1 {
max = src.w - 1
}
var r, g, b, a, wsum float64
for ix := min; ix <= max; ix++ {
i := ix * 4
weight := kernel[absint(x-ix)]
wsum += weight
s := scanLineF[i : i+4 : i+4]
wa := s[3] * weight
r += s[0] * wa
g += s[1] * wa
b += s[2] * wa
a += wa
}
if a != 0 {
aInv := 1 / a
j := y*dst.Stride + x*4
d := dst.Pix[j : j+4 : j+4]
d[0] = clamp(r * aInv)
d[1] = clamp(g * aInv)
d[2] = clamp(b * aInv)
d[3] = clamp(a / wsum)
}
}
}
})
return dst
}
func blurVertical(img image.Image, kernel []float64) *image.NRGBA {
src := newScanner(img)
dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
radius := len(kernel) - 1
parallel(0, src.w, func(xs <-chan int) {
scanLine := make([]uint8, src.h*4)
scanLineF := make([]float64, len(scanLine))
for x := range xs {
src.scan(x, 0, x+1, src.h, scanLine)
for i, v := range scanLine {
scanLineF[i] = float64(v)
}
for y := 0; y < src.h; y++ {
min := y - radius
if min < 0 {
min = 0
}
max := y + radius
if max > src.h-1 {
max = src.h - 1
}
var r, g, b, a, wsum float64
for iy := min; iy <= max; iy++ {
i := iy * 4
weight := kernel[absint(y-iy)]
wsum += weight
s := scanLineF[i : i+4 : i+4]
wa := s[3] * weight
r += s[0] * wa
g += s[1] * wa
b += s[2] * wa
a += wa
}
if a != 0 {
aInv := 1 / a
j := y*dst.Stride + x*4
d := dst.Pix[j : j+4 : j+4]
d[0] = clamp(r * aInv)
d[1] = clamp(g * aInv)
d[2] = clamp(b * aInv)
d[3] = clamp(a / wsum)
}
}
}
})
return dst
}
// Sharpen produces a sharpened version of the image.
// Sigma parameter must be positive and indicates how much the image will be sharpened.
//
// Example:
//
// dstImage := imaging.Sharpen(srcImage, 3.5)
//
func Sharpen(img image.Image, sigma float64) *image.NRGBA {
if sigma <= 0 {
return Clone(img)
}
src := newScanner(img)
dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
blurred := Blur(img, sigma)
parallel(0, src.h, func(ys <-chan int) {
scanLine := make([]uint8, src.w*4)
for y := range ys {
src.scan(0, y, src.w, y+1, scanLine)
j := y * dst.Stride
for i := 0; i < src.w*4; i++ {
val := int(scanLine[i])<<1 - int(blurred.Pix[j])
if val < 0 {
val = 0
} else if val > 0xff {
val = 0xff
}
dst.Pix[j] = uint8(val)
j++
}
}
})
return dst
}

3
vendor/github.com/disintegration/imaging/go.mod generated vendored Normal file
View File

@@ -0,0 +1,3 @@
module github.com/disintegration/imaging
require golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8

3
vendor/github.com/disintegration/imaging/go.sum generated vendored Normal file
View File

@@ -0,0 +1,3 @@
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

52
vendor/github.com/disintegration/imaging/histogram.go generated vendored Normal file
View File

@@ -0,0 +1,52 @@
package imaging
import (
"image"
"sync"
)
// Histogram returns a normalized histogram of an image.
//
// Resulting histogram is represented as an array of 256 floats, where
// histogram[i] is a probability of a pixel being of a particular luminance i.
func Histogram(img image.Image) [256]float64 {
var mu sync.Mutex
var histogram [256]float64
var total float64
src := newScanner(img)
if src.w == 0 || src.h == 0 {
return histogram
}
parallel(0, src.h, func(ys <-chan int) {
var tmpHistogram [256]float64
var tmpTotal float64
scanLine := make([]uint8, src.w*4)
for y := range ys {
src.scan(0, y, src.w, y+1, scanLine)
i := 0
for x := 0; x < src.w; x++ {
s := scanLine[i : i+3 : i+3]
r := s[0]
g := s[1]
b := s[2]
y := 0.299*float32(r) + 0.587*float32(g) + 0.114*float32(b)
tmpHistogram[int(y+0.5)]++
tmpTotal++
i += 4
}
}
mu.Lock()
for i := 0; i < 256; i++ {
histogram[i] += tmpHistogram[i]
}
total += tmpTotal
mu.Unlock()
})
for i := 0; i < 256; i++ {
histogram[i] = histogram[i] / total
}
return histogram
}

444
vendor/github.com/disintegration/imaging/io.go generated vendored Normal file
View File

@@ -0,0 +1,444 @@
package imaging
import (
"encoding/binary"
"errors"
"image"
"image/draw"
"image/gif"
"image/jpeg"
"image/png"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"golang.org/x/image/bmp"
"golang.org/x/image/tiff"
)
type fileSystem interface {
Create(string) (io.WriteCloser, error)
Open(string) (io.ReadCloser, error)
}
type localFS struct{}
func (localFS) Create(name string) (io.WriteCloser, error) { return os.Create(name) }
func (localFS) Open(name string) (io.ReadCloser, error) { return os.Open(name) }
var fs fileSystem = localFS{}
type decodeConfig struct {
autoOrientation bool
}
var defaultDecodeConfig = decodeConfig{
autoOrientation: false,
}
// DecodeOption sets an optional parameter for the Decode and Open functions.
type DecodeOption func(*decodeConfig)
// AutoOrientation returns a DecodeOption that sets the auto-orientation mode.
// If auto-orientation is enabled, the image will be transformed after decoding
// according to the EXIF orientation tag (if present). By default it's disabled.
func AutoOrientation(enabled bool) DecodeOption {
return func(c *decodeConfig) {
c.autoOrientation = enabled
}
}
// Decode reads an image from r.
func Decode(r io.Reader, opts ...DecodeOption) (image.Image, error) {
cfg := defaultDecodeConfig
for _, option := range opts {
option(&cfg)
}
if !cfg.autoOrientation {
img, _, err := image.Decode(r)
return img, err
}
var orient orientation
pr, pw := io.Pipe()
r = io.TeeReader(r, pw)
done := make(chan struct{})
go func() {
defer close(done)
orient = readOrientation(pr)
io.Copy(ioutil.Discard, pr)
}()
img, _, err := image.Decode(r)
pw.Close()
<-done
if err != nil {
return nil, err
}
return fixOrientation(img, orient), nil
}
// Open loads an image from file.
//
// Examples:
//
// // Load an image from file.
// img, err := imaging.Open("test.jpg")
//
// // Load an image and transform it depending on the EXIF orientation tag (if present).
// img, err := imaging.Open("test.jpg", imaging.AutoOrientation(true))
//
func Open(filename string, opts ...DecodeOption) (image.Image, error) {
file, err := fs.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
return Decode(file, opts...)
}
// Format is an image file format.
type Format int
// Image file formats.
const (
JPEG Format = iota
PNG
GIF
TIFF
BMP
)
var formatExts = map[string]Format{
"jpg": JPEG,
"jpeg": JPEG,
"png": PNG,
"gif": GIF,
"tif": TIFF,
"tiff": TIFF,
"bmp": BMP,
}
var formatNames = map[Format]string{
JPEG: "JPEG",
PNG: "PNG",
GIF: "GIF",
TIFF: "TIFF",
BMP: "BMP",
}
func (f Format) String() string {
return formatNames[f]
}
// ErrUnsupportedFormat means the given image format is not supported.
var ErrUnsupportedFormat = errors.New("imaging: unsupported image format")
// FormatFromExtension parses image format from filename extension:
// "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported.
func FormatFromExtension(ext string) (Format, error) {
if f, ok := formatExts[strings.ToLower(strings.TrimPrefix(ext, "."))]; ok {
return f, nil
}
return -1, ErrUnsupportedFormat
}
// FormatFromFilename parses image format from filename:
// "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported.
func FormatFromFilename(filename string) (Format, error) {
ext := filepath.Ext(filename)
return FormatFromExtension(ext)
}
type encodeConfig struct {
jpegQuality int
gifNumColors int
gifQuantizer draw.Quantizer
gifDrawer draw.Drawer
pngCompressionLevel png.CompressionLevel
}
var defaultEncodeConfig = encodeConfig{
jpegQuality: 95,
gifNumColors: 256,
gifQuantizer: nil,
gifDrawer: nil,
pngCompressionLevel: png.DefaultCompression,
}
// EncodeOption sets an optional parameter for the Encode and Save functions.
type EncodeOption func(*encodeConfig)
// JPEGQuality returns an EncodeOption that sets the output JPEG quality.
// Quality ranges from 1 to 100 inclusive, higher is better. Default is 95.
func JPEGQuality(quality int) EncodeOption {
return func(c *encodeConfig) {
c.jpegQuality = quality
}
}
// GIFNumColors returns an EncodeOption that sets the maximum number of colors
// used in the GIF-encoded image. It ranges from 1 to 256. Default is 256.
func GIFNumColors(numColors int) EncodeOption {
return func(c *encodeConfig) {
c.gifNumColors = numColors
}
}
// GIFQuantizer returns an EncodeOption that sets the quantizer that is used to produce
// a palette of the GIF-encoded image.
func GIFQuantizer(quantizer draw.Quantizer) EncodeOption {
return func(c *encodeConfig) {
c.gifQuantizer = quantizer
}
}
// GIFDrawer returns an EncodeOption that sets the drawer that is used to convert
// the source image to the desired palette of the GIF-encoded image.
func GIFDrawer(drawer draw.Drawer) EncodeOption {
return func(c *encodeConfig) {
c.gifDrawer = drawer
}
}
// PNGCompressionLevel returns an EncodeOption that sets the compression level
// of the PNG-encoded image. Default is png.DefaultCompression.
func PNGCompressionLevel(level png.CompressionLevel) EncodeOption {
return func(c *encodeConfig) {
c.pngCompressionLevel = level
}
}
// Encode writes the image img to w in the specified format (JPEG, PNG, GIF, TIFF or BMP).
func Encode(w io.Writer, img image.Image, format Format, opts ...EncodeOption) error {
cfg := defaultEncodeConfig
for _, option := range opts {
option(&cfg)
}
switch format {
case JPEG:
if nrgba, ok := img.(*image.NRGBA); ok && nrgba.Opaque() {
rgba := &image.RGBA{
Pix: nrgba.Pix,
Stride: nrgba.Stride,
Rect: nrgba.Rect,
}
return jpeg.Encode(w, rgba, &jpeg.Options{Quality: cfg.jpegQuality})
}
return jpeg.Encode(w, img, &jpeg.Options{Quality: cfg.jpegQuality})
case PNG:
encoder := png.Encoder{CompressionLevel: cfg.pngCompressionLevel}
return encoder.Encode(w, img)
case GIF:
return gif.Encode(w, img, &gif.Options{
NumColors: cfg.gifNumColors,
Quantizer: cfg.gifQuantizer,
Drawer: cfg.gifDrawer,
})
case TIFF:
return tiff.Encode(w, img, &tiff.Options{Compression: tiff.Deflate, Predictor: true})
case BMP:
return bmp.Encode(w, img)
}
return ErrUnsupportedFormat
}
// Save saves the image to file with the specified filename.
// The format is determined from the filename extension:
// "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported.
//
// Examples:
//
// // Save the image as PNG.
// err := imaging.Save(img, "out.png")
//
// // Save the image as JPEG with optional quality parameter set to 80.
// err := imaging.Save(img, "out.jpg", imaging.JPEGQuality(80))
//
func Save(img image.Image, filename string, opts ...EncodeOption) (err error) {
f, err := FormatFromFilename(filename)
if err != nil {
return err
}
file, err := fs.Create(filename)
if err != nil {
return err
}
err = Encode(file, img, f, opts...)
errc := file.Close()
if err == nil {
err = errc
}
return err
}
// orientation is an EXIF flag that specifies the transformation
// that should be applied to image to display it correctly.
type orientation int
const (
orientationUnspecified = 0
orientationNormal = 1
orientationFlipH = 2
orientationRotate180 = 3
orientationFlipV = 4
orientationTranspose = 5
orientationRotate270 = 6
orientationTransverse = 7
orientationRotate90 = 8
)
// readOrientation tries to read the orientation EXIF flag from image data in r.
// If the EXIF data block is not found or the orientation flag is not found
// or any other error occures while reading the data, it returns the
// orientationUnspecified (0) value.
func readOrientation(r io.Reader) orientation {
const (
markerSOI = 0xffd8
markerAPP1 = 0xffe1
exifHeader = 0x45786966
byteOrderBE = 0x4d4d
byteOrderLE = 0x4949
orientationTag = 0x0112
)
// Check if JPEG SOI marker is present.
var soi uint16
if err := binary.Read(r, binary.BigEndian, &soi); err != nil {
return orientationUnspecified
}
if soi != markerSOI {
return orientationUnspecified // Missing JPEG SOI marker.
}
// Find JPEG APP1 marker.
for {
var marker, size uint16
if err := binary.Read(r, binary.BigEndian, &marker); err != nil {
return orientationUnspecified
}
if err := binary.Read(r, binary.BigEndian, &size); err != nil {
return orientationUnspecified
}
if marker>>8 != 0xff {
return orientationUnspecified // Invalid JPEG marker.
}
if marker == markerAPP1 {
break
}
if size < 2 {
return orientationUnspecified // Invalid block size.
}
if _, err := io.CopyN(ioutil.Discard, r, int64(size-2)); err != nil {
return orientationUnspecified
}
}
// Check if EXIF header is present.
var header uint32
if err := binary.Read(r, binary.BigEndian, &header); err != nil {
return orientationUnspecified
}
if header != exifHeader {
return orientationUnspecified
}
if _, err := io.CopyN(ioutil.Discard, r, 2); err != nil {
return orientationUnspecified
}
// Read byte order information.
var (
byteOrderTag uint16
byteOrder binary.ByteOrder
)
if err := binary.Read(r, binary.BigEndian, &byteOrderTag); err != nil {
return orientationUnspecified
}
switch byteOrderTag {
case byteOrderBE:
byteOrder = binary.BigEndian
case byteOrderLE:
byteOrder = binary.LittleEndian
default:
return orientationUnspecified // Invalid byte order flag.
}
if _, err := io.CopyN(ioutil.Discard, r, 2); err != nil {
return orientationUnspecified
}
// Skip the EXIF offset.
var offset uint32
if err := binary.Read(r, byteOrder, &offset); err != nil {
return orientationUnspecified
}
if offset < 8 {
return orientationUnspecified // Invalid offset value.
}
if _, err := io.CopyN(ioutil.Discard, r, int64(offset-8)); err != nil {
return orientationUnspecified
}
// Read the number of tags.
var numTags uint16
if err := binary.Read(r, byteOrder, &numTags); err != nil {
return orientationUnspecified
}
// Find the orientation tag.
for i := 0; i < int(numTags); i++ {
var tag uint16
if err := binary.Read(r, byteOrder, &tag); err != nil {
return orientationUnspecified
}
if tag != orientationTag {
if _, err := io.CopyN(ioutil.Discard, r, 10); err != nil {
return orientationUnspecified
}
continue
}
if _, err := io.CopyN(ioutil.Discard, r, 6); err != nil {
return orientationUnspecified
}
var val uint16
if err := binary.Read(r, byteOrder, &val); err != nil {
return orientationUnspecified
}
if val < 1 || val > 8 {
return orientationUnspecified // Invalid tag value.
}
return orientation(val)
}
return orientationUnspecified // Missing orientation tag.
}
// fixOrientation applies a transform to img corresponding to the given orientation flag.
func fixOrientation(img image.Image, o orientation) image.Image {
switch o {
case orientationNormal:
case orientationFlipH:
img = FlipH(img)
case orientationFlipV:
img = FlipV(img)
case orientationRotate90:
img = Rotate90(img)
case orientationRotate180:
img = Rotate180(img)
case orientationRotate270:
img = Rotate270(img)
case orientationTranspose:
img = Transpose(img)
case orientationTransverse:
img = Transverse(img)
}
return img
}

595
vendor/github.com/disintegration/imaging/resize.go generated vendored Normal file
View File

@@ -0,0 +1,595 @@
package imaging
import (
"image"
"math"
)
type indexWeight struct {
index int
weight float64
}
func precomputeWeights(dstSize, srcSize int, filter ResampleFilter) [][]indexWeight {
du := float64(srcSize) / float64(dstSize)
scale := du
if scale < 1.0 {
scale = 1.0
}
ru := math.Ceil(scale * filter.Support)
out := make([][]indexWeight, dstSize)
tmp := make([]indexWeight, 0, dstSize*int(ru+2)*2)
for v := 0; v < dstSize; v++ {
fu := (float64(v)+0.5)*du - 0.5
begin := int(math.Ceil(fu - ru))
if begin < 0 {
begin = 0
}
end := int(math.Floor(fu + ru))
if end > srcSize-1 {
end = srcSize - 1
}
var sum float64
for u := begin; u <= end; u++ {
w := filter.Kernel((float64(u) - fu) / scale)
if w != 0 {
sum += w
tmp = append(tmp, indexWeight{index: u, weight: w})
}
}
if sum != 0 {
for i := range tmp {
tmp[i].weight /= sum
}
}
out[v] = tmp
tmp = tmp[len(tmp):]
}
return out
}
// Resize resizes the image to the specified width and height using the specified resampling
// filter and returns the transformed image. If one of width or height is 0, the image aspect
// ratio is preserved.
//
// Example:
//
// dstImage := imaging.Resize(srcImage, 800, 600, imaging.Lanczos)
//
func Resize(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
dstW, dstH := width, height
if dstW < 0 || dstH < 0 {
return &image.NRGBA{}
}
if dstW == 0 && dstH == 0 {
return &image.NRGBA{}
}
srcW := img.Bounds().Dx()
srcH := img.Bounds().Dy()
if srcW <= 0 || srcH <= 0 {
return &image.NRGBA{}
}
// If new width or height is 0 then preserve aspect ratio, minimum 1px.
if dstW == 0 {
tmpW := float64(dstH) * float64(srcW) / float64(srcH)
dstW = int(math.Max(1.0, math.Floor(tmpW+0.5)))
}
if dstH == 0 {
tmpH := float64(dstW) * float64(srcH) / float64(srcW)
dstH = int(math.Max(1.0, math.Floor(tmpH+0.5)))
}
if filter.Support <= 0 {
// Nearest-neighbor special case.
return resizeNearest(img, dstW, dstH)
}
if srcW != dstW && srcH != dstH {
return resizeVertical(resizeHorizontal(img, dstW, filter), dstH, filter)
}
if srcW != dstW {
return resizeHorizontal(img, dstW, filter)
}
if srcH != dstH {
return resizeVertical(img, dstH, filter)
}
return Clone(img)
}
func resizeHorizontal(img image.Image, width int, filter ResampleFilter) *image.NRGBA {
src := newScanner(img)
dst := image.NewNRGBA(image.Rect(0, 0, width, src.h))
weights := precomputeWeights(width, src.w, filter)
parallel(0, src.h, func(ys <-chan int) {
scanLine := make([]uint8, src.w*4)
for y := range ys {
src.scan(0, y, src.w, y+1, scanLine)
j0 := y * dst.Stride
for x := range weights {
var r, g, b, a float64
for _, w := range weights[x] {
i := w.index * 4
s := scanLine[i : i+4 : i+4]
aw := float64(s[3]) * w.weight
r += float64(s[0]) * aw
g += float64(s[1]) * aw
b += float64(s[2]) * aw
a += aw
}
if a != 0 {
aInv := 1 / a
j := j0 + x*4
d := dst.Pix[j : j+4 : j+4]
d[0] = clamp(r * aInv)
d[1] = clamp(g * aInv)
d[2] = clamp(b * aInv)
d[3] = clamp(a)
}
}
}
})
return dst
}
func resizeVertical(img image.Image, height int, filter ResampleFilter) *image.NRGBA {
src := newScanner(img)
dst := image.NewNRGBA(image.Rect(0, 0, src.w, height))
weights := precomputeWeights(height, src.h, filter)
parallel(0, src.w, func(xs <-chan int) {
scanLine := make([]uint8, src.h*4)
for x := range xs {
src.scan(x, 0, x+1, src.h, scanLine)
for y := range weights {
var r, g, b, a float64
for _, w := range weights[y] {
i := w.index * 4
s := scanLine[i : i+4 : i+4]
aw := float64(s[3]) * w.weight
r += float64(s[0]) * aw
g += float64(s[1]) * aw
b += float64(s[2]) * aw
a += aw
}
if a != 0 {
aInv := 1 / a
j := y*dst.Stride + x*4
d := dst.Pix[j : j+4 : j+4]
d[0] = clamp(r * aInv)
d[1] = clamp(g * aInv)
d[2] = clamp(b * aInv)
d[3] = clamp(a)
}
}
}
})
return dst
}
// resizeNearest is a fast nearest-neighbor resize, no filtering.
func resizeNearest(img image.Image, width, height int) *image.NRGBA {
dst := image.NewNRGBA(image.Rect(0, 0, width, height))
dx := float64(img.Bounds().Dx()) / float64(width)
dy := float64(img.Bounds().Dy()) / float64(height)
if dx > 1 && dy > 1 {
src := newScanner(img)
parallel(0, height, func(ys <-chan int) {
for y := range ys {
srcY := int((float64(y) + 0.5) * dy)
dstOff := y * dst.Stride
for x := 0; x < width; x++ {
srcX := int((float64(x) + 0.5) * dx)
src.scan(srcX, srcY, srcX+1, srcY+1, dst.Pix[dstOff:dstOff+4])
dstOff += 4
}
}
})
} else {
src := toNRGBA(img)
parallel(0, height, func(ys <-chan int) {
for y := range ys {
srcY := int((float64(y) + 0.5) * dy)
srcOff0 := srcY * src.Stride
dstOff := y * dst.Stride
for x := 0; x < width; x++ {
srcX := int((float64(x) + 0.5) * dx)
srcOff := srcOff0 + srcX*4
copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
dstOff += 4
}
}
})
}
return dst
}
// Fit scales down the image using the specified resample filter to fit the specified
// maximum width and height and returns the transformed image.
//
// Example:
//
// dstImage := imaging.Fit(srcImage, 800, 600, imaging.Lanczos)
//
func Fit(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
maxW, maxH := width, height
if maxW <= 0 || maxH <= 0 {
return &image.NRGBA{}
}
srcBounds := img.Bounds()
srcW := srcBounds.Dx()
srcH := srcBounds.Dy()
if srcW <= 0 || srcH <= 0 {
return &image.NRGBA{}
}
if srcW <= maxW && srcH <= maxH {
return Clone(img)
}
srcAspectRatio := float64(srcW) / float64(srcH)
maxAspectRatio := float64(maxW) / float64(maxH)
var newW, newH int
if srcAspectRatio > maxAspectRatio {
newW = maxW
newH = int(float64(newW) / srcAspectRatio)
} else {
newH = maxH
newW = int(float64(newH) * srcAspectRatio)
}
return Resize(img, newW, newH, filter)
}
// Fill creates an image with the specified dimensions and fills it with the scaled source image.
// To achieve the correct aspect ratio without stretching, the source image will be cropped.
//
// Example:
//
// dstImage := imaging.Fill(srcImage, 800, 600, imaging.Center, imaging.Lanczos)
//
func Fill(img image.Image, width, height int, anchor Anchor, filter ResampleFilter) *image.NRGBA {
dstW, dstH := width, height
if dstW <= 0 || dstH <= 0 {
return &image.NRGBA{}
}
srcBounds := img.Bounds()
srcW := srcBounds.Dx()
srcH := srcBounds.Dy()
if srcW <= 0 || srcH <= 0 {
return &image.NRGBA{}
}
if srcW == dstW && srcH == dstH {
return Clone(img)
}
if srcW >= 100 && srcH >= 100 {
return cropAndResize(img, dstW, dstH, anchor, filter)
}
return resizeAndCrop(img, dstW, dstH, anchor, filter)
}
// cropAndResize crops the image to the smallest possible size that has the required aspect ratio using
// the given anchor point, then scales it to the specified dimensions and returns the transformed image.
//
// This is generally faster than resizing first, but may result in inaccuracies when used on small source images.
func cropAndResize(img image.Image, width, height int, anchor Anchor, filter ResampleFilter) *image.NRGBA {
dstW, dstH := width, height
srcBounds := img.Bounds()
srcW := srcBounds.Dx()
srcH := srcBounds.Dy()
srcAspectRatio := float64(srcW) / float64(srcH)
dstAspectRatio := float64(dstW) / float64(dstH)
var tmp *image.NRGBA
if srcAspectRatio < dstAspectRatio {
cropH := float64(srcW) * float64(dstH) / float64(dstW)
tmp = CropAnchor(img, srcW, int(math.Max(1, cropH)+0.5), anchor)
} else {
cropW := float64(srcH) * float64(dstW) / float64(dstH)
tmp = CropAnchor(img, int(math.Max(1, cropW)+0.5), srcH, anchor)
}
return Resize(tmp, dstW, dstH, filter)
}
// resizeAndCrop resizes the image to the smallest possible size that will cover the specified dimensions,
// crops the resized image to the specified dimensions using the given anchor point and returns
// the transformed image.
func resizeAndCrop(img image.Image, width, height int, anchor Anchor, filter ResampleFilter) *image.NRGBA {
dstW, dstH := width, height
srcBounds := img.Bounds()
srcW := srcBounds.Dx()
srcH := srcBounds.Dy()
srcAspectRatio := float64(srcW) / float64(srcH)
dstAspectRatio := float64(dstW) / float64(dstH)
var tmp *image.NRGBA
if srcAspectRatio < dstAspectRatio {
tmp = Resize(img, dstW, 0, filter)
} else {
tmp = Resize(img, 0, dstH, filter)
}
return CropAnchor(tmp, dstW, dstH, anchor)
}
// Thumbnail scales the image up or down using the specified resample filter, crops it
// to the specified width and hight and returns the transformed image.
//
// Example:
//
// dstImage := imaging.Thumbnail(srcImage, 100, 100, imaging.Lanczos)
//
func Thumbnail(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
return Fill(img, width, height, Center, filter)
}
// ResampleFilter specifies a resampling filter to be used for image resizing.
//
// General filter recommendations:
//
// - Lanczos
// A high-quality resampling filter for photographic images yielding sharp results.
//
// - CatmullRom
// A sharp cubic filter that is faster than Lanczos filter while providing similar results.
//
// - MitchellNetravali
// A cubic filter that produces smoother results with less ringing artifacts than CatmullRom.
//
// - Linear
// Bilinear resampling filter, produces a smooth output. Faster than cubic filters.
//
// - Box
// Simple and fast averaging filter appropriate for downscaling.
// When upscaling it's similar to NearestNeighbor.
//
// - NearestNeighbor
// Fastest resampling filter, no antialiasing.
//
type ResampleFilter struct {
Support float64
Kernel func(float64) float64
}
// NearestNeighbor is a nearest-neighbor filter (no anti-aliasing).
var NearestNeighbor ResampleFilter
// Box filter (averaging pixels).
var Box ResampleFilter
// Linear filter.
var Linear ResampleFilter
// Hermite cubic spline filter (BC-spline; B=0; C=0).
var Hermite ResampleFilter
// MitchellNetravali is Mitchell-Netravali cubic filter (BC-spline; B=1/3; C=1/3).
var MitchellNetravali ResampleFilter
// CatmullRom is a Catmull-Rom - sharp cubic filter (BC-spline; B=0; C=0.5).
var CatmullRom ResampleFilter
// BSpline is a smooth cubic filter (BC-spline; B=1; C=0).
var BSpline ResampleFilter
// Gaussian is a Gaussian blurring filter.
var Gaussian ResampleFilter
// Bartlett is a Bartlett-windowed sinc filter (3 lobes).
var Bartlett ResampleFilter
// Lanczos filter (3 lobes).
var Lanczos ResampleFilter
// Hann is a Hann-windowed sinc filter (3 lobes).
var Hann ResampleFilter
// Hamming is a Hamming-windowed sinc filter (3 lobes).
var Hamming ResampleFilter
// Blackman is a Blackman-windowed sinc filter (3 lobes).
var Blackman ResampleFilter
// Welch is a Welch-windowed sinc filter (parabolic window, 3 lobes).
var Welch ResampleFilter
// Cosine is a Cosine-windowed sinc filter (3 lobes).
var Cosine ResampleFilter
func bcspline(x, b, c float64) float64 {
var y float64
x = math.Abs(x)
if x < 1.0 {
y = ((12-9*b-6*c)*x*x*x + (-18+12*b+6*c)*x*x + (6 - 2*b)) / 6
} else if x < 2.0 {
y = ((-b-6*c)*x*x*x + (6*b+30*c)*x*x + (-12*b-48*c)*x + (8*b + 24*c)) / 6
}
return y
}
func sinc(x float64) float64 {
if x == 0 {
return 1
}
return math.Sin(math.Pi*x) / (math.Pi * x)
}
func init() {
NearestNeighbor = ResampleFilter{
Support: 0.0, // special case - not applying the filter
}
Box = ResampleFilter{
Support: 0.5,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x <= 0.5 {
return 1.0
}
return 0
},
}
Linear = ResampleFilter{
Support: 1.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 1.0 {
return 1.0 - x
}
return 0
},
}
Hermite = ResampleFilter{
Support: 1.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 1.0 {
return bcspline(x, 0.0, 0.0)
}
return 0
},
}
MitchellNetravali = ResampleFilter{
Support: 2.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 2.0 {
return bcspline(x, 1.0/3.0, 1.0/3.0)
}
return 0
},
}
CatmullRom = ResampleFilter{
Support: 2.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 2.0 {
return bcspline(x, 0.0, 0.5)
}
return 0
},
}
BSpline = ResampleFilter{
Support: 2.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 2.0 {
return bcspline(x, 1.0, 0.0)
}
return 0
},
}
Gaussian = ResampleFilter{
Support: 2.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 2.0 {
return math.Exp(-2 * x * x)
}
return 0
},
}
Bartlett = ResampleFilter{
Support: 3.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 3.0 {
return sinc(x) * (3.0 - x) / 3.0
}
return 0
},
}
Lanczos = ResampleFilter{
Support: 3.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 3.0 {
return sinc(x) * sinc(x/3.0)
}
return 0
},
}
Hann = ResampleFilter{
Support: 3.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 3.0 {
return sinc(x) * (0.5 + 0.5*math.Cos(math.Pi*x/3.0))
}
return 0
},
}
Hamming = ResampleFilter{
Support: 3.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 3.0 {
return sinc(x) * (0.54 + 0.46*math.Cos(math.Pi*x/3.0))
}
return 0
},
}
Blackman = ResampleFilter{
Support: 3.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 3.0 {
return sinc(x) * (0.42 - 0.5*math.Cos(math.Pi*x/3.0+math.Pi) + 0.08*math.Cos(2.0*math.Pi*x/3.0))
}
return 0
},
}
Welch = ResampleFilter{
Support: 3.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 3.0 {
return sinc(x) * (1.0 - (x * x / 9.0))
}
return 0
},
}
Cosine = ResampleFilter{
Support: 3.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 3.0 {
return sinc(x) * math.Cos((math.Pi/2.0)*(x/3.0))
}
return 0
},
}
}

285
vendor/github.com/disintegration/imaging/scanner.go generated vendored Normal file
View File

@@ -0,0 +1,285 @@
package imaging
import (
"image"
"image/color"
)
type scanner struct {
image image.Image
w, h int
palette []color.NRGBA
}
func newScanner(img image.Image) *scanner {
s := &scanner{
image: img,
w: img.Bounds().Dx(),
h: img.Bounds().Dy(),
}
if img, ok := img.(*image.Paletted); ok {
s.palette = make([]color.NRGBA, len(img.Palette))
for i := 0; i < len(img.Palette); i++ {
s.palette[i] = color.NRGBAModel.Convert(img.Palette[i]).(color.NRGBA)
}
}
return s
}
// scan scans the given rectangular region of the image into dst.
func (s *scanner) scan(x1, y1, x2, y2 int, dst []uint8) {
switch img := s.image.(type) {
case *image.NRGBA:
size := (x2 - x1) * 4
j := 0
i := y1*img.Stride + x1*4
if size == 4 {
for y := y1; y < y2; y++ {
d := dst[j : j+4 : j+4]
s := img.Pix[i : i+4 : i+4]
d[0] = s[0]
d[1] = s[1]
d[2] = s[2]
d[3] = s[3]
j += size
i += img.Stride
}
} else {
for y := y1; y < y2; y++ {
copy(dst[j:j+size], img.Pix[i:i+size])
j += size
i += img.Stride
}
}
case *image.NRGBA64:
j := 0
for y := y1; y < y2; y++ {
i := y*img.Stride + x1*8
for x := x1; x < x2; x++ {
s := img.Pix[i : i+8 : i+8]
d := dst[j : j+4 : j+4]
d[0] = s[0]
d[1] = s[2]
d[2] = s[4]
d[3] = s[6]
j += 4
i += 8
}
}
case *image.RGBA:
j := 0
for y := y1; y < y2; y++ {
i := y*img.Stride + x1*4
for x := x1; x < x2; x++ {
d := dst[j : j+4 : j+4]
a := img.Pix[i+3]
switch a {
case 0:
d[0] = 0
d[1] = 0
d[2] = 0
d[3] = a
case 0xff:
s := img.Pix[i : i+4 : i+4]
d[0] = s[0]
d[1] = s[1]
d[2] = s[2]
d[3] = a
default:
s := img.Pix[i : i+4 : i+4]
r16 := uint16(s[0])
g16 := uint16(s[1])
b16 := uint16(s[2])
a16 := uint16(a)
d[0] = uint8(r16 * 0xff / a16)
d[1] = uint8(g16 * 0xff / a16)
d[2] = uint8(b16 * 0xff / a16)
d[3] = a
}
j += 4
i += 4
}
}
case *image.RGBA64:
j := 0
for y := y1; y < y2; y++ {
i := y*img.Stride + x1*8
for x := x1; x < x2; x++ {
s := img.Pix[i : i+8 : i+8]
d := dst[j : j+4 : j+4]
a := s[6]
switch a {
case 0:
d[0] = 0
d[1] = 0
d[2] = 0
case 0xff:
d[0] = s[0]
d[1] = s[2]
d[2] = s[4]
default:
r32 := uint32(s[0])<<8 | uint32(s[1])
g32 := uint32(s[2])<<8 | uint32(s[3])
b32 := uint32(s[4])<<8 | uint32(s[5])
a32 := uint32(s[6])<<8 | uint32(s[7])
d[0] = uint8((r32 * 0xffff / a32) >> 8)
d[1] = uint8((g32 * 0xffff / a32) >> 8)
d[2] = uint8((b32 * 0xffff / a32) >> 8)
}
d[3] = a
j += 4
i += 8
}
}
case *image.Gray:
j := 0
for y := y1; y < y2; y++ {
i := y*img.Stride + x1
for x := x1; x < x2; x++ {
c := img.Pix[i]
d := dst[j : j+4 : j+4]
d[0] = c
d[1] = c
d[2] = c
d[3] = 0xff
j += 4
i++
}
}
case *image.Gray16:
j := 0
for y := y1; y < y2; y++ {
i := y*img.Stride + x1*2
for x := x1; x < x2; x++ {
c := img.Pix[i]
d := dst[j : j+4 : j+4]
d[0] = c
d[1] = c
d[2] = c
d[3] = 0xff
j += 4
i += 2
}
}
case *image.YCbCr:
j := 0
x1 += img.Rect.Min.X
x2 += img.Rect.Min.X
y1 += img.Rect.Min.Y
y2 += img.Rect.Min.Y
hy := img.Rect.Min.Y / 2
hx := img.Rect.Min.X / 2
for y := y1; y < y2; y++ {
iy := (y-img.Rect.Min.Y)*img.YStride + (x1 - img.Rect.Min.X)
var yBase int
switch img.SubsampleRatio {
case image.YCbCrSubsampleRatio444, image.YCbCrSubsampleRatio422:
yBase = (y - img.Rect.Min.Y) * img.CStride
case image.YCbCrSubsampleRatio420, image.YCbCrSubsampleRatio440:
yBase = (y/2 - hy) * img.CStride
}
for x := x1; x < x2; x++ {
var ic int
switch img.SubsampleRatio {
case image.YCbCrSubsampleRatio444, image.YCbCrSubsampleRatio440:
ic = yBase + (x - img.Rect.Min.X)
case image.YCbCrSubsampleRatio422, image.YCbCrSubsampleRatio420:
ic = yBase + (x/2 - hx)
default:
ic = img.COffset(x, y)
}
yy1 := int32(img.Y[iy]) * 0x10101
cb1 := int32(img.Cb[ic]) - 128
cr1 := int32(img.Cr[ic]) - 128
r := yy1 + 91881*cr1
if uint32(r)&0xff000000 == 0 {
r >>= 16
} else {
r = ^(r >> 31)
}
g := yy1 - 22554*cb1 - 46802*cr1
if uint32(g)&0xff000000 == 0 {
g >>= 16
} else {
g = ^(g >> 31)
}
b := yy1 + 116130*cb1
if uint32(b)&0xff000000 == 0 {
b >>= 16
} else {
b = ^(b >> 31)
}
d := dst[j : j+4 : j+4]
d[0] = uint8(r)
d[1] = uint8(g)
d[2] = uint8(b)
d[3] = 0xff
iy++
j += 4
}
}
case *image.Paletted:
j := 0
for y := y1; y < y2; y++ {
i := y*img.Stride + x1
for x := x1; x < x2; x++ {
c := s.palette[img.Pix[i]]
d := dst[j : j+4 : j+4]
d[0] = c.R
d[1] = c.G
d[2] = c.B
d[3] = c.A
j += 4
i++
}
}
default:
j := 0
b := s.image.Bounds()
x1 += b.Min.X
x2 += b.Min.X
y1 += b.Min.Y
y2 += b.Min.Y
for y := y1; y < y2; y++ {
for x := x1; x < x2; x++ {
r16, g16, b16, a16 := s.image.At(x, y).RGBA()
d := dst[j : j+4 : j+4]
switch a16 {
case 0xffff:
d[0] = uint8(r16 >> 8)
d[1] = uint8(g16 >> 8)
d[2] = uint8(b16 >> 8)
d[3] = 0xff
case 0:
d[0] = 0
d[1] = 0
d[2] = 0
d[3] = 0
default:
d[0] = uint8(((r16 * 0xffff) / a16) >> 8)
d[1] = uint8(((g16 * 0xffff) / a16) >> 8)
d[2] = uint8(((b16 * 0xffff) / a16) >> 8)
d[3] = uint8(a16 >> 8)
}
j += 4
}
}
}
}

249
vendor/github.com/disintegration/imaging/tools.go generated vendored Normal file
View File

@@ -0,0 +1,249 @@
package imaging
import (
"bytes"
"image"
"image/color"
"math"
)
// New creates a new image with the specified width and height, and fills it with the specified color.
func New(width, height int, fillColor color.Color) *image.NRGBA {
if width <= 0 || height <= 0 {
return &image.NRGBA{}
}
c := color.NRGBAModel.Convert(fillColor).(color.NRGBA)
if (c == color.NRGBA{0, 0, 0, 0}) {
return image.NewNRGBA(image.Rect(0, 0, width, height))
}
return &image.NRGBA{
Pix: bytes.Repeat([]byte{c.R, c.G, c.B, c.A}, width*height),
Stride: 4 * width,
Rect: image.Rect(0, 0, width, height),
}
}
// Clone returns a copy of the given image.
func Clone(img image.Image) *image.NRGBA {
src := newScanner(img)
dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
size := src.w * 4
parallel(0, src.h, func(ys <-chan int) {
for y := range ys {
i := y * dst.Stride
src.scan(0, y, src.w, y+1, dst.Pix[i:i+size])
}
})
return dst
}
// Anchor is the anchor point for image alignment.
type Anchor int
// Anchor point positions.
const (
Center Anchor = iota
TopLeft
Top
TopRight
Left
Right
BottomLeft
Bottom
BottomRight
)
func anchorPt(b image.Rectangle, w, h int, anchor Anchor) image.Point {
var x, y int
switch anchor {
case TopLeft:
x = b.Min.X
y = b.Min.Y
case Top:
x = b.Min.X + (b.Dx()-w)/2
y = b.Min.Y
case TopRight:
x = b.Max.X - w
y = b.Min.Y
case Left:
x = b.Min.X
y = b.Min.Y + (b.Dy()-h)/2
case Right:
x = b.Max.X - w
y = b.Min.Y + (b.Dy()-h)/2
case BottomLeft:
x = b.Min.X
y = b.Max.Y - h
case Bottom:
x = b.Min.X + (b.Dx()-w)/2
y = b.Max.Y - h
case BottomRight:
x = b.Max.X - w
y = b.Max.Y - h
default:
x = b.Min.X + (b.Dx()-w)/2
y = b.Min.Y + (b.Dy()-h)/2
}
return image.Pt(x, y)
}
// Crop cuts out a rectangular region with the specified bounds
// from the image and returns the cropped image.
func Crop(img image.Image, rect image.Rectangle) *image.NRGBA {
r := rect.Intersect(img.Bounds()).Sub(img.Bounds().Min)
if r.Empty() {
return &image.NRGBA{}
}
src := newScanner(img)
dst := image.NewNRGBA(image.Rect(0, 0, r.Dx(), r.Dy()))
rowSize := r.Dx() * 4
parallel(r.Min.Y, r.Max.Y, func(ys <-chan int) {
for y := range ys {
i := (y - r.Min.Y) * dst.Stride
src.scan(r.Min.X, y, r.Max.X, y+1, dst.Pix[i:i+rowSize])
}
})
return dst
}
// CropAnchor cuts out a rectangular region with the specified size
// from the image using the specified anchor point and returns the cropped image.
func CropAnchor(img image.Image, width, height int, anchor Anchor) *image.NRGBA {
srcBounds := img.Bounds()
pt := anchorPt(srcBounds, width, height, anchor)
r := image.Rect(0, 0, width, height).Add(pt)
b := srcBounds.Intersect(r)
return Crop(img, b)
}
// CropCenter cuts out a rectangular region with the specified size
// from the center of the image and returns the cropped image.
func CropCenter(img image.Image, width, height int) *image.NRGBA {
return CropAnchor(img, width, height, Center)
}
// Paste pastes the img image to the background image at the specified position and returns the combined image.
func Paste(background, img image.Image, pos image.Point) *image.NRGBA {
dst := Clone(background)
pos = pos.Sub(background.Bounds().Min)
pasteRect := image.Rectangle{Min: pos, Max: pos.Add(img.Bounds().Size())}
interRect := pasteRect.Intersect(dst.Bounds())
if interRect.Empty() {
return dst
}
src := newScanner(img)
parallel(interRect.Min.Y, interRect.Max.Y, func(ys <-chan int) {
for y := range ys {
x1 := interRect.Min.X - pasteRect.Min.X
x2 := interRect.Max.X - pasteRect.Min.X
y1 := y - pasteRect.Min.Y
y2 := y1 + 1
i1 := y*dst.Stride + interRect.Min.X*4
i2 := i1 + interRect.Dx()*4
src.scan(x1, y1, x2, y2, dst.Pix[i1:i2])
}
})
return dst
}
// PasteCenter pastes the img image to the center of the background image and returns the combined image.
func PasteCenter(background, img image.Image) *image.NRGBA {
bgBounds := background.Bounds()
bgW := bgBounds.Dx()
bgH := bgBounds.Dy()
bgMinX := bgBounds.Min.X
bgMinY := bgBounds.Min.Y
centerX := bgMinX + bgW/2
centerY := bgMinY + bgH/2
x0 := centerX - img.Bounds().Dx()/2
y0 := centerY - img.Bounds().Dy()/2
return Paste(background, img, image.Pt(x0, y0))
}
// Overlay draws the img image over the background image at given position
// and returns the combined image. Opacity parameter is the opacity of the img
// image layer, used to compose the images, it must be from 0.0 to 1.0.
//
// Examples:
//
// // Draw spriteImage over backgroundImage at the given position (x=50, y=50).
// dstImage := imaging.Overlay(backgroundImage, spriteImage, image.Pt(50, 50), 1.0)
//
// // Blend two opaque images of the same size.
// dstImage := imaging.Overlay(imageOne, imageTwo, image.Pt(0, 0), 0.5)
//
func Overlay(background, img image.Image, pos image.Point, opacity float64) *image.NRGBA {
opacity = math.Min(math.Max(opacity, 0.0), 1.0) // Ensure 0.0 <= opacity <= 1.0.
dst := Clone(background)
pos = pos.Sub(background.Bounds().Min)
pasteRect := image.Rectangle{Min: pos, Max: pos.Add(img.Bounds().Size())}
interRect := pasteRect.Intersect(dst.Bounds())
if interRect.Empty() {
return dst
}
src := newScanner(img)
parallel(interRect.Min.Y, interRect.Max.Y, func(ys <-chan int) {
scanLine := make([]uint8, interRect.Dx()*4)
for y := range ys {
x1 := interRect.Min.X - pasteRect.Min.X
x2 := interRect.Max.X - pasteRect.Min.X
y1 := y - pasteRect.Min.Y
y2 := y1 + 1
src.scan(x1, y1, x2, y2, scanLine)
i := y*dst.Stride + interRect.Min.X*4
j := 0
for x := interRect.Min.X; x < interRect.Max.X; x++ {
d := dst.Pix[i : i+4 : i+4]
r1 := float64(d[0])
g1 := float64(d[1])
b1 := float64(d[2])
a1 := float64(d[3])
s := scanLine[j : j+4 : j+4]
r2 := float64(s[0])
g2 := float64(s[1])
b2 := float64(s[2])
a2 := float64(s[3])
coef2 := opacity * a2 / 255
coef1 := (1 - coef2) * a1 / 255
coefSum := coef1 + coef2
coef1 /= coefSum
coef2 /= coefSum
d[0] = uint8(r1*coef1 + r2*coef2)
d[1] = uint8(g1*coef1 + g2*coef2)
d[2] = uint8(b1*coef1 + b2*coef2)
d[3] = uint8(math.Min(a1+a2*opacity*(255-a1)/255, 255))
i += 4
j += 4
}
}
})
return dst
}
// OverlayCenter overlays the img image to the center of the background image and
// returns the combined image. Opacity parameter is the opacity of the img
// image layer, used to compose the images, it must be from 0.0 to 1.0.
func OverlayCenter(background, img image.Image, opacity float64) *image.NRGBA {
bgBounds := background.Bounds()
bgW := bgBounds.Dx()
bgH := bgBounds.Dy()
bgMinX := bgBounds.Min.X
bgMinY := bgBounds.Min.Y
centerX := bgMinX + bgW/2
centerY := bgMinY + bgH/2
x0 := centerX - img.Bounds().Dx()/2
y0 := centerY - img.Bounds().Dy()/2
return Overlay(background, img, image.Point{x0, y0}, opacity)
}

268
vendor/github.com/disintegration/imaging/transform.go generated vendored Normal file
View File

@@ -0,0 +1,268 @@
package imaging
import (
"image"
"image/color"
"math"
)
// FlipH flips the image horizontally (from left to right) and returns the transformed image.
func FlipH(img image.Image) *image.NRGBA {
src := newScanner(img)
dstW := src.w
dstH := src.h
rowSize := dstW * 4
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
parallel(0, dstH, func(ys <-chan int) {
for dstY := range ys {
i := dstY * dst.Stride
srcY := dstY
src.scan(0, srcY, src.w, srcY+1, dst.Pix[i:i+rowSize])
reverse(dst.Pix[i : i+rowSize])
}
})
return dst
}
// FlipV flips the image vertically (from top to bottom) and returns the transformed image.
func FlipV(img image.Image) *image.NRGBA {
src := newScanner(img)
dstW := src.w
dstH := src.h
rowSize := dstW * 4
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
parallel(0, dstH, func(ys <-chan int) {
for dstY := range ys {
i := dstY * dst.Stride
srcY := dstH - dstY - 1
src.scan(0, srcY, src.w, srcY+1, dst.Pix[i:i+rowSize])
}
})
return dst
}
// Transpose flips the image horizontally and rotates 90 degrees counter-clockwise.
func Transpose(img image.Image) *image.NRGBA {
src := newScanner(img)
dstW := src.h
dstH := src.w
rowSize := dstW * 4
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
parallel(0, dstH, func(ys <-chan int) {
for dstY := range ys {
i := dstY * dst.Stride
srcX := dstY
src.scan(srcX, 0, srcX+1, src.h, dst.Pix[i:i+rowSize])
}
})
return dst
}
// Transverse flips the image vertically and rotates 90 degrees counter-clockwise.
func Transverse(img image.Image) *image.NRGBA {
src := newScanner(img)
dstW := src.h
dstH := src.w
rowSize := dstW * 4
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
parallel(0, dstH, func(ys <-chan int) {
for dstY := range ys {
i := dstY * dst.Stride
srcX := dstH - dstY - 1
src.scan(srcX, 0, srcX+1, src.h, dst.Pix[i:i+rowSize])
reverse(dst.Pix[i : i+rowSize])
}
})
return dst
}
// Rotate90 rotates the image 90 degrees counter-clockwise and returns the transformed image.
func Rotate90(img image.Image) *image.NRGBA {
src := newScanner(img)
dstW := src.h
dstH := src.w
rowSize := dstW * 4
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
parallel(0, dstH, func(ys <-chan int) {
for dstY := range ys {
i := dstY * dst.Stride
srcX := dstH - dstY - 1
src.scan(srcX, 0, srcX+1, src.h, dst.Pix[i:i+rowSize])
}
})
return dst
}
// Rotate180 rotates the image 180 degrees counter-clockwise and returns the transformed image.
func Rotate180(img image.Image) *image.NRGBA {
src := newScanner(img)
dstW := src.w
dstH := src.h
rowSize := dstW * 4
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
parallel(0, dstH, func(ys <-chan int) {
for dstY := range ys {
i := dstY * dst.Stride
srcY := dstH - dstY - 1
src.scan(0, srcY, src.w, srcY+1, dst.Pix[i:i+rowSize])
reverse(dst.Pix[i : i+rowSize])
}
})
return dst
}
// Rotate270 rotates the image 270 degrees counter-clockwise and returns the transformed image.
func Rotate270(img image.Image) *image.NRGBA {
src := newScanner(img)
dstW := src.h
dstH := src.w
rowSize := dstW * 4
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
parallel(0, dstH, func(ys <-chan int) {
for dstY := range ys {
i := dstY * dst.Stride
srcX := dstY
src.scan(srcX, 0, srcX+1, src.h, dst.Pix[i:i+rowSize])
reverse(dst.Pix[i : i+rowSize])
}
})
return dst
}
// Rotate rotates an image by the given angle counter-clockwise .
// The angle parameter is the rotation angle in degrees.
// The bgColor parameter specifies the color of the uncovered zone after the rotation.
func Rotate(img image.Image, angle float64, bgColor color.Color) *image.NRGBA {
angle = angle - math.Floor(angle/360)*360
switch angle {
case 0:
return Clone(img)
case 90:
return Rotate90(img)
case 180:
return Rotate180(img)
case 270:
return Rotate270(img)
}
src := toNRGBA(img)
srcW := src.Bounds().Max.X
srcH := src.Bounds().Max.Y
dstW, dstH := rotatedSize(srcW, srcH, angle)
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
if dstW <= 0 || dstH <= 0 {
return dst
}
srcXOff := float64(srcW)/2 - 0.5
srcYOff := float64(srcH)/2 - 0.5
dstXOff := float64(dstW)/2 - 0.5
dstYOff := float64(dstH)/2 - 0.5
bgColorNRGBA := color.NRGBAModel.Convert(bgColor).(color.NRGBA)
sin, cos := math.Sincos(math.Pi * angle / 180)
parallel(0, dstH, func(ys <-chan int) {
for dstY := range ys {
for dstX := 0; dstX < dstW; dstX++ {
xf, yf := rotatePoint(float64(dstX)-dstXOff, float64(dstY)-dstYOff, sin, cos)
xf, yf = xf+srcXOff, yf+srcYOff
interpolatePoint(dst, dstX, dstY, src, xf, yf, bgColorNRGBA)
}
}
})
return dst
}
func rotatePoint(x, y, sin, cos float64) (float64, float64) {
return x*cos - y*sin, x*sin + y*cos
}
func rotatedSize(w, h int, angle float64) (int, int) {
if w <= 0 || h <= 0 {
return 0, 0
}
sin, cos := math.Sincos(math.Pi * angle / 180)
x1, y1 := rotatePoint(float64(w-1), 0, sin, cos)
x2, y2 := rotatePoint(float64(w-1), float64(h-1), sin, cos)
x3, y3 := rotatePoint(0, float64(h-1), sin, cos)
minx := math.Min(x1, math.Min(x2, math.Min(x3, 0)))
maxx := math.Max(x1, math.Max(x2, math.Max(x3, 0)))
miny := math.Min(y1, math.Min(y2, math.Min(y3, 0)))
maxy := math.Max(y1, math.Max(y2, math.Max(y3, 0)))
neww := maxx - minx + 1
if neww-math.Floor(neww) > 0.1 {
neww++
}
newh := maxy - miny + 1
if newh-math.Floor(newh) > 0.1 {
newh++
}
return int(neww), int(newh)
}
func interpolatePoint(dst *image.NRGBA, dstX, dstY int, src *image.NRGBA, xf, yf float64, bgColor color.NRGBA) {
j := dstY*dst.Stride + dstX*4
d := dst.Pix[j : j+4 : j+4]
x0 := int(math.Floor(xf))
y0 := int(math.Floor(yf))
bounds := src.Bounds()
if !image.Pt(x0, y0).In(image.Rect(bounds.Min.X-1, bounds.Min.Y-1, bounds.Max.X, bounds.Max.Y)) {
d[0] = bgColor.R
d[1] = bgColor.G
d[2] = bgColor.B
d[3] = bgColor.A
return
}
xq := xf - float64(x0)
yq := yf - float64(y0)
points := [4]image.Point{
{x0, y0},
{x0 + 1, y0},
{x0, y0 + 1},
{x0 + 1, y0 + 1},
}
weights := [4]float64{
(1 - xq) * (1 - yq),
xq * (1 - yq),
(1 - xq) * yq,
xq * yq,
}
var r, g, b, a float64
for i := 0; i < 4; i++ {
p := points[i]
w := weights[i]
if p.In(bounds) {
i := p.Y*src.Stride + p.X*4
s := src.Pix[i : i+4 : i+4]
wa := float64(s[3]) * w
r += float64(s[0]) * wa
g += float64(s[1]) * wa
b += float64(s[2]) * wa
a += wa
} else {
wa := float64(bgColor.A) * w
r += float64(bgColor.R) * wa
g += float64(bgColor.G) * wa
b += float64(bgColor.B) * wa
a += wa
}
}
if a != 0 {
aInv := 1 / a
d[0] = clamp(r * aInv)
d[1] = clamp(g * aInv)
d[2] = clamp(b * aInv)
d[3] = clamp(a)
}
}

167
vendor/github.com/disintegration/imaging/utils.go generated vendored Normal file
View File

@@ -0,0 +1,167 @@
package imaging
import (
"image"
"math"
"runtime"
"sync"
)
// parallel processes the data in separate goroutines.
func parallel(start, stop int, fn func(<-chan int)) {
count := stop - start
if count < 1 {
return
}
procs := runtime.GOMAXPROCS(0)
if procs > count {
procs = count
}
c := make(chan int, count)
for i := start; i < stop; i++ {
c <- i
}
close(c)
var wg sync.WaitGroup
for i := 0; i < procs; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fn(c)
}()
}
wg.Wait()
}
// absint returns the absolute value of i.
func absint(i int) int {
if i < 0 {
return -i
}
return i
}
// clamp rounds and clamps float64 value to fit into uint8.
func clamp(x float64) uint8 {
v := int64(x + 0.5)
if v > 255 {
return 255
}
if v > 0 {
return uint8(v)
}
return 0
}
func reverse(pix []uint8) {
if len(pix) <= 4 {
return
}
i := 0
j := len(pix) - 4
for i < j {
pi := pix[i : i+4 : i+4]
pj := pix[j : j+4 : j+4]
pi[0], pj[0] = pj[0], pi[0]
pi[1], pj[1] = pj[1], pi[1]
pi[2], pj[2] = pj[2], pi[2]
pi[3], pj[3] = pj[3], pi[3]
i += 4
j -= 4
}
}
func toNRGBA(img image.Image) *image.NRGBA {
if img, ok := img.(*image.NRGBA); ok {
return &image.NRGBA{
Pix: img.Pix,
Stride: img.Stride,
Rect: img.Rect.Sub(img.Rect.Min),
}
}
return Clone(img)
}
// rgbToHSL converts a color from RGB to HSL.
func rgbToHSL(r, g, b uint8) (float64, float64, float64) {
rr := float64(r) / 255
gg := float64(g) / 255
bb := float64(b) / 255
max := math.Max(rr, math.Max(gg, bb))
min := math.Min(rr, math.Min(gg, bb))
l := (max + min) / 2
if max == min {
return 0, 0, l
}
var h, s float64
d := max - min
if l > 0.5 {
s = d / (2 - max - min)
} else {
s = d / (max + min)
}
switch max {
case rr:
h = (gg - bb) / d
if g < b {
h += 6
}
case gg:
h = (bb-rr)/d + 2
case bb:
h = (rr-gg)/d + 4
}
h /= 6
return h, s, l
}
// hslToRGB converts a color from HSL to RGB.
func hslToRGB(h, s, l float64) (uint8, uint8, uint8) {
var r, g, b float64
if s == 0 {
v := clamp(l * 255)
return v, v, v
}
var q float64
if l < 0.5 {
q = l * (1 + s)
} else {
q = l + s - l*s
}
p := 2*l - q
r = hueToRGB(p, q, h+1/3.0)
g = hueToRGB(p, q, h)
b = hueToRGB(p, q, h-1/3.0)
return clamp(r * 255), clamp(g * 255), clamp(b * 255)
}
func hueToRGB(p, q, t float64) float64 {
if t < 0 {
t++
}
if t > 1 {
t--
}
if t < 1/6.0 {
return p + (q-p)*6*t
}
if t < 1/2.0 {
return q
}
if t < 2/3.0 {
return p + (q-p)*(2/3.0-t)*6
}
return p
}

View File

@@ -8,6 +8,7 @@ package agent
import (
"bufio"
"context"
"encoding/binary"
"fmt"
"io"
@@ -55,6 +56,13 @@ type Options struct {
// can call Close before shutting down.
// Optional.
ShutdownCleanup bool
// ReuseSocketAddrAndPort determines whether the SO_REUSEADDR and
// SO_REUSEADDR socket options should be set on the listening socket of
// the agent. This option is only effective on unix-like OSes and if
// Addr is set to a fixed host:port.
// Optional.
ReuseSocketAddrAndPort bool
}
// Listen starts the gops agent on a host process. Once agent started, users
@@ -96,11 +104,14 @@ func Listen(opts Options) error {
if addr == "" {
addr = defaultAddr
}
ln, err := net.Listen("tcp", addr)
var lc net.ListenConfig
if opts.ReuseSocketAddrAndPort {
lc.Control = setsockoptReuseAddrAndPort
}
listener, err = lc.Listen(context.Background(), "tcp", addr)
if err != nil {
return err
}
listener = ln
port := listener.Addr().(*net.TCPAddr).Port
portfile = fmt.Sprintf("%s/%d", gopsdir, os.Getpid())
err = ioutil.WriteFile(portfile, []byte(strconv.Itoa(port)), os.ModePerm)

36
vendor/github.com/google/gops/agent/sockopt_unix.go generated vendored Normal file
View File

@@ -0,0 +1,36 @@
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !js,!plan9,!windows
package agent
import (
"syscall"
"golang.org/x/sys/unix"
)
// setsockoptReuseAddrAndPort sets the SO_REUSEADDR and SO_REUSEPORT socket
// options on c's underlying socket in order to increase the chance to re-bind()
// to the same address and port upon agent restart.
func setsockoptReuseAddrAndPort(network, address string, c syscall.RawConn) error {
var soerr error
if err := c.Control(func(su uintptr) {
sock := int(su)
// Allow reuse of recently-used addresses. This socket option is
// set by default on listeners in Go's net package, see
// net.setDefaultSockopts.
soerr = unix.SetsockoptInt(sock, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
if soerr != nil {
return
}
// Allow reuse of recently-used ports. This gives the agent a
// better chance to re-bind upon restarts.
soerr = unix.SetsockoptInt(sock, unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
}); err != nil {
return err
}
return soerr
}

View File

@@ -0,0 +1,13 @@
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build js,wasm plan9 windows
package agent
import "syscall"
func setsockoptReuseAddrAndPort(network, address string, c syscall.RawConn) error {
return nil
}

View File

@@ -16,4 +16,4 @@ change is the ability to represent an invalid UUID (vs a NIL UUID).
Full `go doc` style documentation for the package can be viewed online without
installing this package by using the GoDoc site here:
http://godoc.org/github.com/google/uuid
http://pkg.go.dev/github.com/google/uuid

View File

@@ -16,10 +16,11 @@ func (uuid UUID) MarshalText() ([]byte, error) {
// UnmarshalText implements encoding.TextUnmarshaler.
func (uuid *UUID) UnmarshalText(data []byte) error {
id, err := ParseBytes(data)
if err == nil {
*uuid = id
if err != nil {
return err
}
return err
*uuid = id
return nil
}
// MarshalBinary implements encoding.BinaryMarshaler.

View File

@@ -17,12 +17,6 @@ import (
//
// In most cases, New should be used.
func NewUUID() (UUID, error) {
nodeMu.Lock()
if nodeID == zeroID {
setNodeInterface("")
}
nodeMu.Unlock()
var uuid UUID
now, seq, err := GetTime()
if err != nil {
@@ -38,7 +32,13 @@ func NewUUID() (UUID, error) {
binary.BigEndian.PutUint16(uuid[4:], timeMid)
binary.BigEndian.PutUint16(uuid[6:], timeHi)
binary.BigEndian.PutUint16(uuid[8:], seq)
nodeMu.Lock()
if nodeID == zeroID {
setNodeInterface("")
}
copy(uuid[10:], nodeID[:])
nodeMu.Unlock()
return uuid, nil
}

View File

@@ -27,8 +27,13 @@ func New() UUID {
// equivalent to the odds of creating a few tens of trillions of UUIDs in a
// year and having one duplicate.
func NewRandom() (UUID, error) {
return NewRandomFromReader(rander)
}
// NewRandomFromReader returns a UUID based on bytes read from a given io.Reader.
func NewRandomFromReader(r io.Reader) (UUID, error) {
var uuid UUID
_, err := io.ReadFull(rander, uuid[:])
_, err := io.ReadFull(r, uuid[:])
if err != nil {
return Nil, err
}

View File

@@ -44,6 +44,8 @@ func Wrap(outer, inner error) error {
//
// format is the format of the error message. The string '{{err}}' will
// be replaced with the original error message.
//
// Deprecated: Use fmt.Errorf()
func Wrapf(format string, err error) error {
outerMsg := "<nil>"
if err != nil {
@@ -148,6 +150,9 @@ func Walk(err error, cb WalkFunc) {
for _, err := range e.WrappedErrors() {
Walk(err, cb)
}
case interface{ Unwrap() error }:
cb(err)
Walk(e.Unwrap(), cb)
default:
cb(err)
}
@@ -167,3 +172,7 @@ func (w *wrappedError) Error() string {
func (w *wrappedError) WrappedErrors() []error {
return []error{w.Outer, w.Inner}
}
func (w *wrappedError) Unwrap() error {
return w.Inner
}

View File

@@ -66,6 +66,9 @@ type Attachment struct {
AudioURL string `json:"audio_url,omitempty"`
VideoURL string `json:"video_url,omitempty"`
Actions []AttachmentAction `json:"actions,omitempty"`
ActionButtonsAlignment AttachmentActionButtonsAlignment `json:"button_alignment,omitempty"`
Fields []AttachmentField `json:"fields,omitempty"`
}
@@ -77,3 +80,37 @@ type AttachmentField struct {
Title string `json:"title"`
Value string `json:"value"`
}
type AttachmentActionType string
const (
AttachmentActionTypeButton AttachmentActionType = "button"
)
// AttachmentAction are action buttons on message attachments
type AttachmentAction struct {
Type AttachmentActionType `json:"type"`
Text string `json:"text"`
Url string `json:"url"`
ImageURL string `json:"image_url"`
IsWebView bool `json:"is_webview"`
WebviewHeightRatio string `json:"webview_height_ratio"`
Msg string `json:"msg"`
MsgInChatWindow bool `json:"msg_in_chat_window"`
MsgProcessingType MessageProcessingType `json:"msg_processing_type"`
}
// AttachmentActionButtonAlignment configures how the actions buttons will be aligned
type AttachmentActionButtonsAlignment string
const (
ActionButtonAlignVertical AttachmentActionButtonsAlignment = "vertical"
ActionButtonAlignHorizontal AttachmentActionButtonsAlignment = "horizontal"
)
type MessageProcessingType string
const (
ProcessingTypeSendMessage MessageProcessingType = "sendMessage"
ProcessingTypeRespondWithMessage MessageProcessingType = "respondWithMessage"
)

View File

@@ -14,6 +14,7 @@ type CreateUserRequest struct {
Email string `json:"email"`
Password string `json:"password"`
Username string `json:"username"`
Roles []string `json:"roles,omitempty"`
CustomFields map[string]string `json:"customFields,omitempty"`
}

View File

@@ -72,6 +72,7 @@ func (c *Client) GetChannelSubscriptions() ([]models.ChannelSubscription, error)
DisplayName: stringOrZero(sub.Path("fname").Data()),
Open: sub.Path("open").Data().(bool),
Type: stringOrZero(sub.Path("t").Data()),
RoomId: stringOrZero(sub.Path("rid").Data()),
User: models.User{
ID: stringOrZero(sub.Path("u._id").Data()),
UserName: stringOrZero(sub.Path("u.username").Data()),

View File

@@ -92,5 +92,5 @@ func (c *Client) Close() {
// Some of the rocketchat objects need unique IDs specified by the client
func (c *Client) newRandomId() string {
return fmt.Sprintf("%f", rand.Float64())
return fmt.Sprintf("%x%x", rand.Uint64(), time.Now().UTC().UnixNano())
}

View File

@@ -2,6 +2,7 @@ package realtime
import (
"fmt"
"log"
"strconv"
"time"
@@ -16,31 +17,62 @@ const (
default_buffer_size = 100
)
var messageListenerAdded = false
// NewMessage creates basic message with an ID, a RoomID, and a Msg
// Takes channel and text
func (c *Client) NewMessage(channel *models.Channel, text string) *models.Message {
return &models.Message{
ID: c.newRandomId(),
RoomID: channel.ID,
Msg: text,
}
}
// LoadHistory loads history
// Takes roomId
// Takes roomID
//
// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/load-history
func (c *Client) LoadHistory(roomId string) error {
_, err := c.ddp.Call("loadHistory", roomId)
func (c *Client) LoadHistory(roomID string) ([]models.Message, error) {
m, err := c.ddp.Call("loadHistory", roomID)
if err != nil {
return err
return nil, err
}
return nil
history := m.(map[string]interface{})
document, _ := gabs.Consume(history["messages"])
msgs, err := document.Children()
if err != nil {
log.Printf("response is in an unexpected format: %v", err)
return make([]models.Message, 0), nil
}
messages := make([]models.Message, len(msgs))
for i, arg := range msgs {
messages[i] = *getMessageFromDocument(arg)
}
// log.Println(messages)
return messages, nil
}
// SendMessage sends message to channel
// takes channel and message
// takes message
//
// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/send-message
func (c *Client) SendMessage(m *models.Message) (*models.Message, error) {
m.ID = c.newRandomId()
rawResponse, err := c.ddp.Call("sendMessage", m)
func (c *Client) SendMessage(message *models.Message) (*models.Message, error) {
rawResponse, err := c.ddp.Call("sendMessage", message)
if err != nil {
return nil, err
}
if rawResponse == nil {
return nil, fmt.Errorf("rawResponse is %#v", rawResponse)
}
return getMessageFromData(rawResponse.(map[string]interface{})), nil
}
@@ -95,7 +127,6 @@ func (c *Client) StarMessage(message *models.Message) error {
"rid": message.RoomID,
"starred": true,
})
if err != nil {
return err
}
@@ -113,7 +144,6 @@ func (c *Client) UnStarMessage(message *models.Message) error {
"rid": message.RoomID,
"starred": false,
})
if err != nil {
return err
}
@@ -127,7 +157,6 @@ func (c *Client) UnStarMessage(message *models.Message) error {
// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/pin-message
func (c *Client) PinMessage(message *models.Message) error {
_, err := c.ddp.Call("pinMessage", message)
if err != nil {
return err
}
@@ -141,7 +170,6 @@ func (c *Client) PinMessage(message *models.Message) error {
// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/unpin-messages
func (c *Client) UnPinMessage(message *models.Message) error {
_, err := c.ddp.Call("unpinMessage", message)
if err != nil {
return err
}
@@ -154,13 +182,14 @@ func (c *Client) UnPinMessage(message *models.Message) error {
//
// https://rocket.chat/docs/developer-guides/realtime-api/subscriptions/stream-room-messages/
func (c *Client) SubscribeToMessageStream(channel *models.Channel, msgChannel chan models.Message) error {
if err := c.ddp.Sub("stream-room-messages", channel.ID, send_added_event); err != nil {
return err
}
//msgChannel := make(chan models.Message, default_buffer_size)
c.ddp.CollectionByName("stream-room-messages").AddUpdateListener(messageExtractor{msgChannel, "update"})
if !messageListenerAdded {
c.ddp.CollectionByName("stream-room-messages").AddUpdateListener(messageExtractor{msgChannel, "update"})
messageListenerAdded = true
}
return nil
}
@@ -168,7 +197,6 @@ func (c *Client) SubscribeToMessageStream(channel *models.Channel, msgChannel ch
func getMessagesFromUpdateEvent(update ddp.Update) []models.Message {
document, _ := gabs.Consume(update["args"])
args, err := document.Children()
if err != nil {
// log.Printf("Event arguments are in an unexpected format: %v", err)
return make([]models.Message, 0)
@@ -206,7 +234,7 @@ func getMessageFromDocument(arg *gabs.Container) *models.Message {
TitleLinkDownload: stringOrZero(attachment.Path("title_link_download").Data()),
ImageURL: stringOrZero(attachment.Path("image_url").Data()),
AuthorName: stringOrZero(arg.Path("u.name").Data()),
AuthorName: stringOrZero(arg.Path("u.name").Data()),
}
}
}

View File

@@ -56,9 +56,17 @@ func (c *Client) LeaveChannel(channel *models.Channel) error {
// https://rocket.chat/docs/developer-guides/rest-api/channels/info
func (c *Client) GetChannelInfo(channel *models.Channel) (*models.Channel, error) {
response := new(ChannelResponse)
if err := c.Get("channels.info", url.Values{"roomId": []string{channel.ID}}, response); err != nil {
return nil, err
switch {
case channel.Name != "" && channel.ID == "":
if err := c.Get("channels.info", url.Values{"roomName": []string{channel.Name}}, response); err != nil {
return nil, err
}
default:
if err := c.Get("channels.info", url.Values{"roomId": []string{channel.ID}}, response); err != nil {
return nil, err
}
}
return &response.Channel, nil
}

View File

@@ -0,0 +1,33 @@
package rest
import (
"bytes"
"encoding/json"
"github.com/matterbridge/Rocket.Chat.Go.SDK/models"
)
type UpdatePermissionsRequest struct {
Permissions []models.Permission `json:"permissions"`
}
type UpdatePermissionsResponse struct {
Status
Permissions []models.Permission `json:"permissions"`
}
// UpdatePermissions updates permissions
//
// https://rocket.chat/docs/developer-guides/rest-api/permissions/update/
func (c *Client) UpdatePermissions(req *UpdatePermissionsRequest) (*UpdatePermissionsResponse, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, err
}
response := new(UpdatePermissionsResponse)
if err := c.Post("permissions.update", bytes.NewBuffer(body), response); err != nil {
return nil, err
}
return response, nil
}

View File

@@ -4,6 +4,7 @@ go:
- 1.12.x
- 1.13.x
- 1.14.x
- 1.15.x
env:
- GO111MODULE=on
install:

View File

@@ -112,6 +112,8 @@ var (
EndpointChannelMessagesBulkDelete = func(cID string) string { return EndpointChannel(cID) + "/messages/bulk-delete" }
EndpointChannelMessagesPins = func(cID string) string { return EndpointChannel(cID) + "/pins" }
EndpointChannelMessagePin = func(cID, mID string) string { return EndpointChannel(cID) + "/pins/" + mID }
EndpointChannelMessageCrosspost = func(cID, mID string) string { return EndpointChannel(cID) + "/messages/" + mID + "/crosspost" }
EndpointChannelFollow = func(cID string) string { return EndpointChannel(cID) + "/followers" }
EndpointGroupIcon = func(cID, hash string) string { return EndpointCDNChannelIcons + cID + "/" + hash + ".png" }
@@ -139,8 +141,8 @@ var (
EndpointIntegrationsJoin = func(iID string) string { return EndpointAPI + "integrations/" + iID + "/join" }
EndpointEmoji = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".png" }
EndpointEmojiAnimated = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".gif" }
EndpointEmoji = func(eID string) string { return EndpointCDN + "emojis/" + eID + ".png" }
EndpointEmojiAnimated = func(eID string) string { return EndpointCDN + "emojis/" + eID + ".gif" }
EndpointOauth2 = EndpointAPI + "oauth2/"
EndpointApplications = EndpointOauth2 + "applications"

View File

@@ -252,6 +252,8 @@ type VoiceServerUpdate struct {
// VoiceStateUpdate is the data for a VoiceStateUpdate event.
type VoiceStateUpdate struct {
*VoiceState
// BeforeUpdate will be nil if the VoiceState was not previously cached in the state cache.
BeforeUpdate *VoiceState `json:"-"`
}
// MessageDeleteBulk is the data for a MessageDeleteBulk event

View File

@@ -150,6 +150,7 @@ type MessageSend struct {
TTS bool `json:"tts"`
Files []*File `json:"-"`
AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"`
Reference *MessageReference `json:"message_reference,omitempty"`
// TODO: Remove this when compatibility is not required.
File *File `json:"-"`
@@ -371,6 +372,15 @@ type MessageReference struct {
GuildID string `json:"guild_id"`
}
// Reference returns MessageReference of given message
func (m *Message) Reference() *MessageReference {
return &MessageReference{
GuildID: m.GuildID,
ChannelID: m.ChannelID,
MessageID: m.ID,
}
}
// ContentWithMentionsReplaced will replace all @<id> mentions with the
// username of the mention.
func (m *Message) ContentWithMentionsReplaced() (content string) {

View File

@@ -502,14 +502,12 @@ func (s *Session) UserChannelPermissions(userID, channelID string) (apermissions
}
}
return memberPermissions(guild, channel, member), nil
return memberPermissions(guild, channel, userID, member.Roles), nil
}
// Calculates the permissions for a member.
// https://support.discord.com/hc/en-us/articles/206141927-How-is-the-permission-hierarchy-structured-
func memberPermissions(guild *Guild, channel *Channel, member *Member) (apermissions int) {
userID := member.User.ID
func memberPermissions(guild *Guild, channel *Channel, userID string, roles []string) (apermissions int) {
if userID == guild.OwnerID {
apermissions = PermissionAll
return
@@ -523,7 +521,7 @@ func memberPermissions(guild *Guild, channel *Channel, member *Member) (apermiss
}
for _, role := range guild.Roles {
for _, roleID := range member.Roles {
for _, roleID := range roles {
if role.ID == roleID {
apermissions |= role.Permissions
break
@@ -549,7 +547,7 @@ func memberPermissions(guild *Guild, channel *Channel, member *Member) (apermiss
// Member overwrites can override role overrides, so do two passes
for _, overwrite := range channel.PermissionOverwrites {
for _, roleID := range member.Roles {
for _, roleID := range roles {
if overwrite.Type == "role" && roleID == overwrite.ID {
denies |= overwrite.Deny
allows |= overwrite.Allow
@@ -834,10 +832,6 @@ func (s *Session) GuildMemberEdit(guildID, userID string, roles []string) (err e
}{roles}
_, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, ""))
if err != nil {
return
}
return
}
@@ -848,16 +842,11 @@ func (s *Session) GuildMemberEdit(guildID, userID string, roles []string) (err e
// NOTE : I am not entirely set on the name of this function and it may change
// prior to the final 1.0.0 release of Discordgo
func (s *Session) GuildMemberMove(guildID string, userID string, channelID *string) (err error) {
data := struct {
ChannelID *string `json:"channel_id"`
}{channelID}
_, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, ""))
if err != nil {
return
}
return
}
@@ -865,6 +854,7 @@ func (s *Session) GuildMemberMove(guildID string, userID string, channelID *stri
// guildID : The ID of a guild
// userID : The ID of a user
// userID : The ID of a user or "@me" which is a shortcut of the current user ID
// nickname : The nickname of the member, "" will reset their nickname
func (s *Session) GuildMemberNickname(guildID, userID, nickname string) (err error) {
data := struct {
@@ -879,6 +869,32 @@ func (s *Session) GuildMemberNickname(guildID, userID, nickname string) (err err
return
}
// GuildMemberMute server mutes a guild member
// guildID : The ID of a Guild.
// userID : The ID of a User.
// mute : boolean value for if the user should be muted
func (s *Session) GuildMemberMute(guildID string, userID string, mute bool) (err error) {
data := struct {
Mute bool `json:"mute"`
}{mute}
_, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, ""))
return
}
// GuildMemberDeafen server deafens a guild member
// guildID : The ID of a Guild.
// userID : The ID of a User.
// deaf : boolean value for if the user should be deafened
func (s *Session) GuildMemberDeafen(guildID string, userID string, deaf bool) (err error) {
data := struct {
Deaf bool `json:"deaf"`
}{deaf}
_, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, ""))
return
}
// GuildMemberRoleAdd adds the specified role to a given member
// guildID : The ID of a Guild.
// userID : The ID of a User.
@@ -1613,6 +1629,17 @@ func (s *Session) ChannelMessageSendEmbed(channelID string, embed *MessageEmbed)
})
}
// ChannelMessageSendReply sends a message to the given channel with reference data.
// channelID : The ID of a Channel.
// content : The message to send.
// reference : The message reference to send.
func (s *Session) ChannelMessageSendReply(channelID string, content string, reference *MessageReference) (*Message, error) {
return s.ChannelMessageSendComplex(channelID, &MessageSend{
Content: content,
Reference: reference,
})
}
// ChannelMessageEdit edits an existing message, replacing it entirely with
// the given content.
// channelID : The ID of a Channel
@@ -1790,6 +1817,43 @@ func (s *Session) ChannelPermissionDelete(channelID, targetID string) (err error
return
}
// ChannelMessageCrosspost cross posts a message in a news channel to followers
// of the channel
// channelID : The ID of a Channel
// messageID : The ID of a Message
func (s *Session) ChannelMessageCrosspost(channelID, messageID string) (st *Message, err error) {
endpoint := EndpointChannelMessageCrosspost(channelID, messageID)
body, err := s.RequestWithBucketID("POST", endpoint, nil, endpoint)
if err != nil {
return
}
err = unmarshal(body, &st)
return
}
// ChannelNewsFollow follows a news channel in the targetID
// channelID : The ID of a News Channel
// targetID : The ID of a Channel where the News Channel should post to
func (s *Session) ChannelNewsFollow(channelID, targetID string) (st *ChannelFollow, err error) {
endpoint := EndpointChannelFollow(channelID)
data := struct {
WebhookChannelID string `json:"webhook_channel_id"`
}{targetID}
body, err := s.RequestWithBucketID("POST", endpoint, data, endpoint)
if err != nil {
return
}
err = unmarshal(body, &st)
return
}
// ------------------------------------------------------------------------------------------------
// Functions specific to Discord Invites
// ------------------------------------------------------------------------------------------------

View File

@@ -25,6 +25,11 @@ var ErrNilState = errors.New("state not instantiated, please use discordgo.New()
// requested is not found
var ErrStateNotFound = errors.New("state cache not found")
// ErrMessageIncompletePermissions is returned when the message
// requested for permissions does not contain enough data to
// generate the permissions.
var ErrMessageIncompletePermissions = errors.New("message incomplete, unable to determine permissions")
// A State contains the current known state.
// As discord sends this in a READY blob, it seems reasonable to simply
// use that struct as the data store.
@@ -727,6 +732,26 @@ func (s *State) voiceStateUpdate(update *VoiceStateUpdate) error {
return nil
}
// VoiceState gets a VoiceState by guild and user ID.
func (s *State) VoiceState(guildID, userID string) (*VoiceState, error) {
if s == nil {
return nil, ErrNilState
}
guild, err := s.Guild(guildID)
if err != nil {
return nil, err
}
for _, state := range guild.VoiceStates {
if state.UserID == userID {
return state, nil
}
}
return nil, ErrStateNotFound
}
// Message gets a message by channel and message ID.
func (s *State) Message(channelID, messageID string) (*Message, error) {
if s == nil {
@@ -916,6 +941,13 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) {
}
case *VoiceStateUpdate:
if s.TrackVoice {
var old *VoiceState
old, err = s.VoiceState(t.GuildID, t.UserID)
if err == nil {
oldCopy := *old
t.BeforeUpdate = &oldCopy
}
err = s.voiceStateUpdate(t)
}
case *PresenceUpdate:
@@ -980,17 +1012,36 @@ func (s *State) UserChannelPermissions(userID, channelID string) (apermissions i
return
}
if userID == guild.OwnerID {
apermissions = PermissionAll
return
}
member, err := s.Member(guild.ID, userID)
if err != nil {
return
}
return memberPermissions(guild, channel, member), nil
return memberPermissions(guild, channel, userID, member.Roles), nil
}
// MessagePermissions returns the permissions of the author of the message
// in the channel in which it was sent.
func (s *State) MessagePermissions(message *Message) (apermissions int, err error) {
if s == nil {
return 0, ErrNilState
}
if message.Author == nil || message.Member == nil {
return 0, ErrMessageIncompletePermissions
}
channel, err := s.Channel(message.ChannelID)
if err != nil {
return
}
guild, err := s.Guild(channel.GuildID)
if err != nil {
return
}
return memberPermissions(guild, channel, message.Author.ID, message.Member.Roles), nil
}
// UserColor returns the color of a user in a channel.
@@ -1018,11 +1069,39 @@ func (s *State) UserColor(userID, channelID string) int {
return 0
}
return firstRoleColorColor(guild, member.Roles)
}
// MessageColor returns the color of the author's name as displayed
// in the client associated with this message.
func (s *State) MessageColor(message *Message) int {
if s == nil {
return 0
}
if message.Member == nil || message.Member.Roles == nil {
return 0
}
channel, err := s.Channel(message.ChannelID)
if err != nil {
return 0
}
guild, err := s.Guild(channel.GuildID)
if err != nil {
return 0
}
return firstRoleColorColor(guild, message.Member.Roles)
}
func firstRoleColorColor(guild *Guild, memberRoles []string) int {
roles := Roles(guild.Roles)
sort.Sort(roles)
for _, role := range roles {
for _, roleID := range member.Roles {
for _, roleID := range memberRoles {
if role.ID == roleID {
if role.Color != 0 {
return role.Color
@@ -1031,5 +1110,11 @@ func (s *State) UserColor(userID, channelID string) int {
}
}
for _, role := range roles {
if role.ID == guild.ID {
return role.Color
}
}
return 0
}

View File

@@ -316,6 +316,12 @@ type ChannelEdit struct {
RateLimitPerUser int `json:"rate_limit_per_user,omitempty"`
}
// A ChannelFollow holds data returned after following a news channel
type ChannelFollow struct {
ChannelID string `json:"channel_id"`
WebhookID string `json:"webhook_id"`
}
// A PermissionOverwrite holds permission overwrite data for a Channel
type PermissionOverwrite struct {
ID string `json:"id"`
@@ -614,6 +620,7 @@ type GuildParams struct {
Icon string `json:"icon,omitempty"`
OwnerID string `json:"owner_id,omitempty"`
Splash string `json:"splash,omitempty"`
Banner string `json:"banner,omitempty"`
}
// A Role stores information about Discord guild member roles.
@@ -1057,6 +1064,9 @@ type Webhook struct {
Name string `json:"name"`
Avatar string `json:"avatar"`
Token string `json:"token"`
// ApplicationID is the bot/OAuth2 application that created this webhook
ApplicationID string `json:"application_id,omitempty"`
}
// WebhookType is the type of Webhook (see WebhookType* consts) in the Webhook struct

View File

@@ -2,6 +2,27 @@ package discordgo
import "strings"
// UserFlags is the flags of "user" (see UserFlags* consts)
// https://discord.com/developers/docs/resources/user#user-object-user-flags
type UserFlags int
// Valid UserFlags values
const (
UserFlagDiscordEmployee UserFlags = 1 << 0
UserFlagDiscordPartner = 1 << 1
UserFlagHypeSquadEvents = 1 << 2
UserFlagBugHunterLevel1 = 1 << 3
UserFlagHouseBravery = 1 << 6
UserFlagHouseBrilliance = 1 << 7
UserFlagHouseBalance = 1 << 8
UserFlagEarlySupporter = 1 << 9
UserFlagTeamUser = 1 << 10
UserFlagSystem = 1 << 12
UserFlagBugHunterLevel2 = 1 << 14
UserFlagVerifiedBot = 1 << 16
UserFlagVerifiedBotDeveloper = 1 << 17
)
// A User stores all data for an individual Discord user.
type User struct {
// The ID of the user.
@@ -36,6 +57,22 @@ type User struct {
// Whether the user is a bot.
Bot bool `json:"bot"`
// The public flags on a user's account.
// This is a combination of bit masks; the presence of a certain flag can
// be checked by performing a bitwise AND between this int and the flag.
PublicFlags UserFlags `json:"public_flags"`
// The type of Nitro subscription on a user's account.
// Only available when the request is authorized via a Bearer token.
PremiumType int `json:"premium_type"`
// Whether the user is an Official Discord System user (part of the urgent message system).
System bool `json:"system"`
// The flags on a user's account.
// Only available when the request is authorized via a Bearer token.
Flags int `json:"flags"`
}
// String returns a unique identifier of the form username#discriminator

View File

@@ -139,6 +139,7 @@ func (v *VoiceConnection) ChangeChannel(channelID string, mute, deaf bool) (err
func (v *VoiceConnection) Disconnect() (err error) {
// Send a OP4 with a nil channel to disconnect
v.Lock()
if v.sessionID != "" {
data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, nil, true, true}}
v.session.wsMutex.Lock()
@@ -146,6 +147,7 @@ func (v *VoiceConnection) Disconnect() (err error) {
v.session.wsMutex.Unlock()
v.sessionID = ""
}
v.Unlock()
// Close websocket and udp connections
v.Close()

View File

@@ -864,7 +864,7 @@ Mozilla Public License Version 2.0
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
@@ -1387,7 +1387,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
This product contains 'durafmt' by Wesley Hill.
:clock8: Better time duration formatting in Go!
:clock8: Better time duration formatting in Go!
* HOMEPAGE:
* https://github.com/hako/durafmt
@@ -3657,7 +3657,7 @@ lumberjack is a log rolling package for Go
The MIT License (MIT)
Copyright (c) 2014 Nate Finch
Copyright (c) 2014 Nate Finch
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -4206,6 +4206,220 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## AWS SDK for Go
This product contains 'aws-sdk' by Amazon.
AWS-SDK support for the Go language.
* HOMEPAGE:
* https://github.com/aws/aws-sdk-go
* LICENSE:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
---
## semver
@@ -4274,3 +4488,328 @@ 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.
---
## Archiver
This product contains 'archiver' by Matthew Holt
A library to handle direferen archive files (zip, rar, tar.gz...)
* HOMEPAGE:
* https://github.com/mholt/archiver
* LICENSE:
MIT License
Copyright (c) 2016 Matthew Holt
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.
---
## PDF Reader library
This product contains 'pdf' by the Go team and modified by Thuc Le
A library to provide pdf reading support
* HOMEPAGE:
* https://github.com/ledongthuc/pdf
* LICENSE:
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---
## GoOse
This product contains 'GoOse' by Antonio Linari
A library to provide html text extraction support
* HOMEPAGE:
* https://github.com/advancedlogic/GoOse
* LICENSE:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
---
## Docconv
This product contains 'docconv' by Sajari Pty Ltd
A library to provide text extraction support for different documents
* HOMEPAGE:
* https://github.com/sajari/docconv
* LICENSE:
The MIT License (MIT)
Copyright (c) 2014 Sajari Pty Ltd
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.

View File

@@ -164,6 +164,18 @@ func (o *ChannelMember) GetRoles() []string {
return strings.Fields(o.Roles)
}
func (o *ChannelMember) SetChannelMuted(muted bool) {
if o.IsChannelMuted() {
o.NotifyProps[MARK_UNREAD_NOTIFY_PROP] = CHANNEL_MARK_UNREAD_ALL
} else {
o.NotifyProps[MARK_UNREAD_NOTIFY_PROP] = CHANNEL_MARK_UNREAD_MENTION
}
}
func (o *ChannelMember) IsChannelMuted() bool {
return o.NotifyProps[MARK_UNREAD_NOTIFY_PROP] == CHANNEL_MARK_UNREAD_MENTION
}
func IsChannelNotifyLevelValid(notifyLevel string) bool {
return notifyLevel == CHANNEL_NOTIFY_DEFAULT ||
notifyLevel == CHANNEL_NOTIFY_ALL ||

View File

@@ -46,6 +46,7 @@ type SidebarCategory struct {
Sorting SidebarCategorySorting `json:"sorting"`
Type SidebarCategoryType `json:"type"`
DisplayName string `json:"display_name"`
Muted bool `json:"muted"`
}
// SidebarCategoryWithChannels combines data from SidebarCategory table with the Channel IDs that belong to that category

View File

@@ -189,6 +189,14 @@ func (c *Client4) GetUserRoute(userId string) string {
return fmt.Sprintf(c.GetUsersRoute()+"/%v", userId)
}
func (c *Client4) GetUserThreadsRoute(userId string) string {
return fmt.Sprintf(c.GetUsersRoute()+"/%v/threads", userId)
}
func (c *Client4) GetUserThreadRoute(userId, threadId string) string {
return fmt.Sprintf(c.GetUserThreadsRoute(userId)+"/%v", threadId)
}
func (c *Client4) GetUserCategoryRoute(userID, teamID string) string {
return c.GetUserRoute(userID) + c.GetTeamRoute(teamID) + "/channels/categories"
}
@@ -334,6 +342,10 @@ func (c *Client4) GetSystemRoute() string {
return "/system"
}
func (c *Client4) GetCloudRoute() string {
return "/cloud"
}
func (c *Client4) GetTestEmailRoute() string {
return "/email/test"
}
@@ -3268,6 +3280,21 @@ func (c *Client4) GetPingWithServerStatus() (string, *Response) {
return MapFromJson(r.Body)["status"], BuildResponse(r)
}
// GetPingWithFullServerStatus will return the full status if several basic server
// health checks all pass successfully.
func (c *Client4) GetPingWithFullServerStatus() (map[string]string, *Response) {
r, err := c.DoApiGet(c.GetSystemRoute()+"/ping?get_server_status="+c.boolString(true), "")
if r != nil && r.StatusCode == 500 {
defer r.Body.Close()
return map[string]string{"status": STATUS_UNHEALTHY}, BuildErrorResponse(r, err)
}
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return MapFromJson(r.Body), BuildResponse(r)
}
// TestEmail will attempt to connect to the configured SMTP server.
func (c *Client4) TestEmail(config *Config) (bool, *Response) {
r, err := c.DoApiPost(c.GetTestEmailRoute(), config.ToJson())
@@ -4090,7 +4117,7 @@ func (c *Client4) MigrateAuthToSaml(fromAuthService string, usersMap map[string]
// UploadLdapPublicCertificate will upload a public certificate for LDAP and set the config to use it.
func (c *Client4) UploadLdapPublicCertificate(data []byte) (bool, *Response) {
body, writer, err := fileToMultipart(data, LDAP_PUBIC_CERTIFICATE_NAME)
body, writer, err := fileToMultipart(data, LDAP_PUBLIC_CERTIFICATE_NAME)
if err != nil {
return false, &Response{Error: NewAppError("UploadLdapPublicCertificate", "model.client.upload_ldap_cert.app_error", nil, err.Error(), http.StatusBadRequest)}
}
@@ -5616,3 +5643,183 @@ func (c *Client4) UpdatePassword(userId, currentPassword, newPassword string) *R
defer closeBody(r)
return BuildResponse(r)
}
// Cloud Section
func (c *Client4) GetCloudProducts() ([]*Product, *Response) {
r, appErr := c.DoApiGet(c.GetCloudRoute()+"/products", "")
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
var cloudProducts []*Product
json.NewDecoder(r.Body).Decode(&cloudProducts)
return cloudProducts, BuildResponse(r)
}
func (c *Client4) CreateCustomerPayment() (*StripeSetupIntent, *Response) {
r, appErr := c.DoApiPost(c.GetCloudRoute()+"/payment", "")
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
var setupIntent *StripeSetupIntent
json.NewDecoder(r.Body).Decode(&setupIntent)
return setupIntent, BuildResponse(r)
}
func (c *Client4) ConfirmCustomerPayment(confirmRequest *ConfirmPaymentMethodRequest) *Response {
json, _ := json.Marshal(confirmRequest)
r, appErr := c.doApiPostBytes(c.GetCloudRoute()+"/payment/confirm", json)
if appErr != nil {
return BuildErrorResponse(r, appErr)
}
defer closeBody(r)
return BuildResponse(r)
}
func (c *Client4) GetCloudCustomer() (*CloudCustomer, *Response) {
r, appErr := c.DoApiGet(c.GetCloudRoute()+"/customer", "")
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
var cloudCustomer *CloudCustomer
json.NewDecoder(r.Body).Decode(&cloudCustomer)
return cloudCustomer, BuildResponse(r)
}
func (c *Client4) GetSubscription() (*Subscription, *Response) {
r, appErr := c.DoApiGet(c.GetCloudRoute()+"/subscription", "")
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
var subscription *Subscription
json.NewDecoder(r.Body).Decode(&subscription)
return subscription, BuildResponse(r)
}
func (c *Client4) GetInvoicesForSubscription() ([]*Invoice, *Response) {
r, appErr := c.DoApiGet(c.GetCloudRoute()+"/subscription/invoices", "")
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
var invoices []*Invoice
json.NewDecoder(r.Body).Decode(&invoices)
return invoices, BuildResponse(r)
}
func (c *Client4) UpdateCloudCustomer(customerInfo *CloudCustomerInfo) (*CloudCustomer, *Response) {
customerBytes, _ := json.Marshal(customerInfo)
r, appErr := c.doApiPutBytes(c.GetCloudRoute()+"/customer", customerBytes)
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
var customer *CloudCustomer
json.NewDecoder(r.Body).Decode(&customer)
return customer, BuildResponse(r)
}
func (c *Client4) UpdateCloudCustomerAddress(address *Address) (*CloudCustomer, *Response) {
addressBytes, _ := json.Marshal(address)
r, appErr := c.doApiPutBytes(c.GetCloudRoute()+"/customer/address", addressBytes)
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
var customer *CloudCustomer
json.NewDecoder(r.Body).Decode(&customer)
return customer, BuildResponse(r)
}
func (c *Client4) GetUserThreads(userId string, options GetUserThreadsOpts) (*Threads, *Response) {
v := url.Values{}
if options.Since != 0 {
v.Set("since", fmt.Sprintf("%d", options.Since))
}
if options.Page != 0 {
v.Set("page", fmt.Sprintf("%d", options.Page))
}
if options.PageSize != 0 {
v.Set("pageSize", fmt.Sprintf("%d", options.PageSize))
}
if options.Extended {
v.Set("extended", "true")
}
if options.Deleted {
v.Set("deleted", "true")
}
url := c.GetUserThreadsRoute(userId)
if len(v) > 0 {
url += "?" + v.Encode()
}
r, appErr := c.DoApiGet(url, "")
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
var threads Threads
json.NewDecoder(r.Body).Decode(&threads)
return &threads, BuildResponse(r)
}
func (c *Client4) UpdateThreadsReadForUser(userId string, timestamp int64) *Response {
r, appErr := c.DoApiPut(fmt.Sprintf("%s/read/%d", c.GetUserThreadsRoute(userId), timestamp), "")
if appErr != nil {
return BuildErrorResponse(r, appErr)
}
defer closeBody(r)
return BuildResponse(r)
}
func (c *Client4) UpdateThreadReadForUser(userId, threadId string, timestamp int64) *Response {
r, appErr := c.DoApiPut(fmt.Sprintf("%s/read/%d", c.GetUserThreadRoute(userId, threadId), timestamp), "")
if appErr != nil {
return BuildErrorResponse(r, appErr)
}
defer closeBody(r)
return BuildResponse(r)
}
func (c *Client4) UpdateThreadFollowForUser(userId, threadId string, state bool) *Response {
var appErr *AppError
var r *http.Response
if state {
r, appErr = c.DoApiPut(c.GetUserThreadRoute(userId, threadId)+"/following", "")
} else {
r, appErr = c.DoApiDelete(c.GetUserThreadRoute(userId, threadId) + "/following")
}
if appErr != nil {
return BuildErrorResponse(r, appErr)
}
defer closeBody(r)
return BuildResponse(r)
}

View File

@@ -0,0 +1,114 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
// Product model represents a product on the cloud system.
type Product struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
PricePerSeat float64 `json:"price_per_seat"`
AddOns []*AddOn `json:"add_ons"`
}
// AddOn represents an addon to a product.
type AddOn struct {
ID string `json:"id"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
PricePerSeat float64 `json:"price_per_seat"`
}
// StripeSetupIntent represents the SetupIntent model from Stripe for updating payment methods.
type StripeSetupIntent struct {
ID string `json:"id"`
ClientSecret string `json:"client_secret"`
}
// ConfirmPaymentMethodRequest contains the fields for the customer payment update API.
type ConfirmPaymentMethodRequest struct {
StripeSetupIntentID string `json:"stripe_setup_intent_id"`
}
// Customer model represents a customer on the system.
type CloudCustomer struct {
CloudCustomerInfo
ID string `json:"id"`
CreatorID string `json:"creator_id"`
CreateAt int64 `json:"create_at"`
BillingAddress *Address `json:"billing_address"`
CompanyAddress *Address `json:"company_address"`
PaymentMethod *PaymentMethod `json:"payment_method"`
}
// CloudCustomerInfo represents editable info of a customer.
type CloudCustomerInfo struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
ContactFirstName string `json:"contact_first_name,omitempty"`
ContactLastName string `json:"contact_last_name,omitempty"`
NumEmployees int `json:"num_employees"`
}
// Address model represents a customer's address.
type Address struct {
City string `json:"city"`
Country string `json:"country"`
Line1 string `json:"line1"`
Line2 string `json:"line2"`
PostalCode string `json:"postal_code"`
State string `json:"state"`
}
// PaymentMethod represents methods of payment for a customer.
type PaymentMethod struct {
Type string `json:"type"`
LastFour int `json:"last_four"`
ExpMonth int `json:"exp_month"`
ExpYear int `json:"exp_year"`
CardBrand string `json:"card_brand"`
Name string `json:"name"`
}
// Subscription model represents a subscription on the system.
type Subscription struct {
ID string `json:"id"`
CustomerID string `json:"customer_id"`
ProductID string `json:"product_id"`
AddOns []string `json:"add_ons"`
StartAt int64 `json:"start_at"`
EndAt int64 `json:"end_at"`
CreateAt int64 `json:"create_at"`
Seats int `json:"seats"`
Status string `json:"status"`
DNS string `json:"dns"`
IsPaidTier string `json:"is_paid_tier"`
LastInvoice *Invoice `json:"last_invoice"`
}
// Invoice model represents a cloud invoice
type Invoice struct {
ID string `json:"id"`
Number string `json:"number"`
CreateAt int64 `json:"create_at"`
Total int64 `json:"total"`
Tax int64 `json:"tax"`
Status string `json:"status"`
Description string `json:"description"`
PeriodStart int64 `json:"period_start"`
PeriodEnd int64 `json:"period_end"`
SubscriptionID string `json:"subscription_id"`
Items []*InvoiceLineItem `json:"line_items"`
}
// InvoiceLineItem model represents a cloud invoice lineitem tied to an invoice.
type InvoiceLineItem struct {
PriceID string `json:"price_id"`
Total int64 `json:"total"`
Quantity int64 `json:"quantity"`
PricePerUnit int64 `json:"price_per_unit"`
Description string `json:"description"`
Type string `json:"type"`
Metadata map[string]interface{} `json:"metadata"`
}

View File

@@ -12,12 +12,14 @@ import (
"net/http"
"net/url"
"os"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"github.com/mattermost/ldap"
"github.com/mattermost/mattermost-server/v5/mlog"
)
const (
@@ -107,7 +109,7 @@ const (
TEAM_SETTINGS_DEFAULT_CUSTOM_DESCRIPTION_TEXT = ""
TEAM_SETTINGS_DEFAULT_USER_STATUS_AWAY_TIMEOUT = 300
SQL_SETTINGS_DEFAULT_DATA_SOURCE = "mmuser:mostest@tcp(localhost:3306)/mattermost_test?charset=utf8mb4,utf8&readTimeout=30s&writeTimeout=30s"
SQL_SETTINGS_DEFAULT_DATA_SOURCE = "postgres://mmuser:mostest@localhost/mattermost_test?sslmode=disable&connect_timeout=10"
FILE_SETTINGS_DEFAULT_DIRECTORY = "./data/"
@@ -220,6 +222,8 @@ const (
OFFICE365_SETTINGS_DEFAULT_TOKEN_ENDPOINT = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
OFFICE365_SETTINGS_DEFAULT_USER_API_ENDPOINT = "https://graph.microsoft.com/v1.0/me"
CLOUD_SETTINGS_DEFAULT_CWS_URL = "https://customers.mattermost.com"
LOCAL_MODE_SOCKET_PATH = "/var/tmp/mattermost_local.socket"
)
@@ -250,26 +254,26 @@ var ServerTLSSupportedCiphers = map[string]uint16{
type ServiceSettings struct {
SiteURL *string `access:"environment,authentication,write_restrictable"`
WebsocketURL *string `access:"write_restrictable"`
LicenseFileLocation *string `access:"write_restrictable"`
ListenAddress *string `access:"environment,write_restrictable"`
ConnectionSecurity *string `access:"environment,write_restrictable"`
TLSCertFile *string `access:"environment,write_restrictable"`
TLSKeyFile *string `access:"environment,write_restrictable"`
TLSMinVer *string `access:"write_restrictable"`
TLSStrictTransport *bool `access:"write_restrictable"`
TLSStrictTransportMaxAge *int64 `access:"write_restrictable"`
TLSOverwriteCiphers []string `access:"write_restrictable"`
UseLetsEncrypt *bool `access:"environment,write_restrictable"`
LetsEncryptCertificateCacheFile *string `access:"environment,write_restrictable"`
Forward80To443 *bool `access:"environment,write_restrictable"`
TrustedProxyIPHeader []string `access:"write_restrictable"`
ReadTimeout *int `access:"environment,write_restrictable"`
WriteTimeout *int `access:"environment,write_restrictable"`
IdleTimeout *int `access:"write_restrictable"`
MaximumLoginAttempts *int `access:"authentication,write_restrictable"`
GoroutineHealthThreshold *int `access:"write_restrictable"`
GoogleDeveloperKey *string `access:"site,write_restrictable"`
WebsocketURL *string `access:"write_restrictable,cloud_restrictable"`
LicenseFileLocation *string `access:"write_restrictable,cloud_restrictable"`
ListenAddress *string `access:"environment,write_restrictable,cloud_restrictable"`
ConnectionSecurity *string `access:"environment,write_restrictable,cloud_restrictable"`
TLSCertFile *string `access:"environment,write_restrictable,cloud_restrictable"`
TLSKeyFile *string `access:"environment,write_restrictable,cloud_restrictable"`
TLSMinVer *string `access:"write_restrictable,cloud_restrictable"`
TLSStrictTransport *bool `access:"write_restrictable,cloud_restrictable"`
TLSStrictTransportMaxAge *int64 `access:"write_restrictable,cloud_restrictable"`
TLSOverwriteCiphers []string `access:"write_restrictable,cloud_restrictable"`
UseLetsEncrypt *bool `access:"environment,write_restrictable,cloud_restrictable"`
LetsEncryptCertificateCacheFile *string `access:"environment,write_restrictable,cloud_restrictable"`
Forward80To443 *bool `access:"environment,write_restrictable,cloud_restrictable"`
TrustedProxyIPHeader []string `access:"write_restrictable,cloud_restrictable"`
ReadTimeout *int `access:"environment,write_restrictable,cloud_restrictable"`
WriteTimeout *int `access:"environment,write_restrictable,cloud_restrictable"`
IdleTimeout *int `access:"write_restrictable,cloud_restrictable"`
MaximumLoginAttempts *int `access:"authentication,write_restrictable,cloud_restrictable"`
GoroutineHealthThreshold *int `access:"write_restrictable,cloud_restrictable"`
GoogleDeveloperKey *string `access:"site,write_restrictable,cloud_restrictable"`
EnableOAuthServiceProvider *bool `access:"integrations"`
EnableIncomingWebhooks *bool `access:"integrations"`
EnableOutgoingWebhooks *bool `access:"integrations"`
@@ -278,29 +282,29 @@ type ServiceSettings struct {
EnablePostUsernameOverride *bool `access:"integrations"`
EnablePostIconOverride *bool `access:"integrations"`
EnableLinkPreviews *bool `access:"site"`
EnableTesting *bool `access:"environment,write_restrictable"`
EnableDeveloper *bool `access:"environment,write_restrictable"`
EnableOpenTracing *bool `access:"write_restrictable"`
EnableSecurityFixAlert *bool `access:"environment,write_restrictable"`
EnableInsecureOutgoingConnections *bool `access:"environment,write_restrictable"`
AllowedUntrustedInternalConnections *string `access:"environment,write_restrictable"`
EnableTesting *bool `access:"environment,write_restrictable,cloud_restrictable"`
EnableDeveloper *bool `access:"environment,write_restrictable,cloud_restrictable"`
EnableOpenTracing *bool `access:"write_restrictable,cloud_restrictable"`
EnableSecurityFixAlert *bool `access:"environment,write_restrictable,cloud_restrictable"`
EnableInsecureOutgoingConnections *bool `access:"environment,write_restrictable,cloud_restrictable"`
AllowedUntrustedInternalConnections *string `access:"environment,write_restrictable,cloud_restrictable"`
EnableMultifactorAuthentication *bool `access:"authentication"`
EnforceMultifactorAuthentication *bool `access:"authentication"`
EnableUserAccessTokens *bool `access:"integrations"`
AllowCorsFrom *string `access:"integrations,write_restrictable"`
CorsExposedHeaders *string `access:"integrations,write_restrictable"`
CorsAllowCredentials *bool `access:"integrations,write_restrictable"`
CorsDebug *bool `access:"integrations,write_restrictable"`
AllowCookiesForSubdomains *bool `access:"write_restrictable"`
ExtendSessionLengthWithActivity *bool `access:"environment,write_restrictable"`
SessionLengthWebInDays *int `access:"environment,write_restrictable"`
SessionLengthMobileInDays *int `access:"environment,write_restrictable"`
SessionLengthSSOInDays *int `access:"environment,write_restrictable"`
SessionCacheInMinutes *int `access:"environment,write_restrictable"`
SessionIdleTimeoutInMinutes *int `access:"environment,write_restrictable"`
WebsocketSecurePort *int `access:"write_restrictable"`
WebsocketPort *int `access:"write_restrictable"`
WebserverMode *string `access:"environment,write_restrictable"`
AllowCorsFrom *string `access:"integrations,write_restrictable,cloud_restrictable"`
CorsExposedHeaders *string `access:"integrations,write_restrictable,cloud_restrictable"`
CorsAllowCredentials *bool `access:"integrations,write_restrictable,cloud_restrictable"`
CorsDebug *bool `access:"integrations,write_restrictable,cloud_restrictable"`
AllowCookiesForSubdomains *bool `access:"write_restrictable,cloud_restrictable"`
ExtendSessionLengthWithActivity *bool `access:"environment,write_restrictable,cloud_restrictable"`
SessionLengthWebInDays *int `access:"environment,write_restrictable,cloud_restrictable"`
SessionLengthMobileInDays *int `access:"environment,write_restrictable,cloud_restrictable"`
SessionLengthSSOInDays *int `access:"environment,write_restrictable,cloud_restrictable"`
SessionCacheInMinutes *int `access:"environment,write_restrictable,cloud_restrictable"`
SessionIdleTimeoutInMinutes *int `access:"environment,write_restrictable,cloud_restrictable"`
WebsocketSecurePort *int `access:"write_restrictable,cloud_restrictable"`
WebsocketPort *int `access:"write_restrictable,cloud_restrictable"`
WebserverMode *string `access:"environment,write_restrictable,cloud_restrictable"`
EnableCustomEmoji *bool `access:"site"`
EnableEmojiPicker *bool `access:"site"`
EnableGifPicker *bool `access:"integrations"`
@@ -310,14 +314,14 @@ type ServiceSettings struct {
DEPRECATED_DO_NOT_USE_RestrictPostDelete *string `json:"RestrictPostDelete" mapstructure:"RestrictPostDelete"` // This field is deprecated and must not be used.
DEPRECATED_DO_NOT_USE_AllowEditPost *string `json:"AllowEditPost" mapstructure:"AllowEditPost"` // This field is deprecated and must not be used.
PostEditTimeLimit *int `access:"user_management_permissions"`
TimeBetweenUserTypingUpdatesMilliseconds *int64 `access:"experimental,write_restrictable"`
EnablePostSearch *bool `access:"write_restrictable"`
MinimumHashtagLength *int `access:"environment,write_restrictable"`
EnableUserTypingMessages *bool `access:"experimental,write_restrictable"`
EnableChannelViewedMessages *bool `access:"experimental,write_restrictable"`
EnableUserStatuses *bool `access:"write_restrictable"`
ExperimentalEnableAuthenticationTransfer *bool `access:"experimental,write_restrictable"`
ClusterLogTimeoutMilliseconds *int `access:"write_restrictable"`
TimeBetweenUserTypingUpdatesMilliseconds *int64 `access:"experimental,write_restrictable,cloud_restrictable"`
EnablePostSearch *bool `access:"write_restrictable,cloud_restrictable"`
MinimumHashtagLength *int `access:"environment,write_restrictable,cloud_restrictable"`
EnableUserTypingMessages *bool `access:"experimental,write_restrictable,cloud_restrictable"`
EnableChannelViewedMessages *bool `access:"experimental,write_restrictable,cloud_restrictable"`
EnableUserStatuses *bool `access:"write_restrictable,cloud_restrictable"`
ExperimentalEnableAuthenticationTransfer *bool `access:"experimental,write_restrictable,cloud_restrictable"`
ClusterLogTimeoutMilliseconds *int `access:"write_restrictable,cloud_restrictable"`
CloseUnusedDirectMessages *bool `access:"experimental"`
EnablePreviewFeatures *bool `access:"experimental"`
EnableTutorial *bool `access:"experimental"`
@@ -332,10 +336,10 @@ type ServiceSettings struct {
EnableAPITeamDeletion *bool
EnableAPIUserDeletion *bool
ExperimentalEnableHardenedMode *bool `access:"experimental"`
DisableLegacyMFA *bool `access:"write_restrictable"`
ExperimentalStrictCSRFEnforcement *bool `access:"experimental,write_restrictable"`
DisableLegacyMFA *bool `access:"write_restrictable,cloud_restrictable"`
ExperimentalStrictCSRFEnforcement *bool `access:"experimental,write_restrictable,cloud_restrictable"`
EnableEmailInvitations *bool `access:"authentication"`
DisableBotsWhenOwnerIsDeactivated *bool `access:"integrations,write_restrictable"`
DisableBotsWhenOwnerIsDeactivated *bool `access:"integrations,write_restrictable,cloud_restrictable"`
EnableBotAccountCreation *bool `access:"integrations"`
EnableSVGs *bool `access:"site"`
EnableLatex *bool `access:"site"`
@@ -343,6 +347,9 @@ type ServiceSettings struct {
EnableLocalMode *bool
LocalModeSocketLocation *string
EnableAWSMetering *bool
SplitKey *string `access:"environment,write_restrictable"`
FeatureFlagSyncIntervalSeconds *int `access:"environment,write_restrictable"`
DebugSplit *bool `access:"environment,write_restrictable"`
ThreadAutoFollow *bool `access:"experimental"`
ManagedResourcePaths *string `access:"environment,write_restrictable,cloud_restrictable"`
}
@@ -763,6 +770,18 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) {
s.EnableAWSMetering = NewBool(false)
}
if s.SplitKey == nil {
s.SplitKey = NewString("")
}
if s.FeatureFlagSyncIntervalSeconds == nil {
s.FeatureFlagSyncIntervalSeconds = NewInt(30)
}
if s.DebugSplit == nil {
s.DebugSplit = NewBool(false)
}
if s.ThreadAutoFollow == nil {
s.ThreadAutoFollow = NewBool(true)
}
@@ -774,20 +793,20 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) {
type ClusterSettings struct {
Enable *bool `access:"environment,write_restrictable"`
ClusterName *string `access:"environment,write_restrictable"`
OverrideHostname *string `access:"environment,write_restrictable"`
NetworkInterface *string `access:"environment,write_restrictable"`
BindAddress *string `access:"environment,write_restrictable"`
AdvertiseAddress *string `access:"environment,write_restrictable"`
UseIpAddress *bool `access:"environment,write_restrictable"`
UseExperimentalGossip *bool `access:"environment,write_restrictable"`
EnableExperimentalGossipEncryption *bool `access:"environment,write_restrictable"`
ReadOnlyConfig *bool `access:"environment,write_restrictable"`
GossipPort *int `access:"environment,write_restrictable"`
StreamingPort *int `access:"environment,write_restrictable"`
MaxIdleConns *int `access:"environment,write_restrictable"`
MaxIdleConnsPerHost *int `access:"environment,write_restrictable"`
IdleConnTimeoutMilliseconds *int `access:"environment,write_restrictable"`
ClusterName *string `access:"environment,write_restrictable,cloud_restrictable"`
OverrideHostname *string `access:"environment,write_restrictable,cloud_restrictable"`
NetworkInterface *string `access:"environment,write_restrictable,cloud_restrictable"`
BindAddress *string `access:"environment,write_restrictable,cloud_restrictable"`
AdvertiseAddress *string `access:"environment,write_restrictable,cloud_restrictable"`
UseIpAddress *bool `access:"environment,write_restrictable,cloud_restrictable"`
UseExperimentalGossip *bool `access:"environment,write_restrictable,cloud_restrictable"`
EnableExperimentalGossipEncryption *bool `access:"environment,write_restrictable,cloud_restrictable"`
ReadOnlyConfig *bool `access:"environment,write_restrictable,cloud_restrictable"`
GossipPort *int `access:"environment,write_restrictable,cloud_restrictable"`
StreamingPort *int `access:"environment,write_restrictable,cloud_restrictable"`
MaxIdleConns *int `access:"environment,write_restrictable,cloud_restrictable"`
MaxIdleConnsPerHost *int `access:"environment,write_restrictable,cloud_restrictable"`
IdleConnTimeoutMilliseconds *int `access:"environment,write_restrictable,cloud_restrictable"`
}
func (s *ClusterSettings) SetDefaults() {
@@ -853,9 +872,9 @@ func (s *ClusterSettings) SetDefaults() {
}
type MetricsSettings struct {
Enable *bool `access:"environment,write_restrictable"`
BlockProfileRate *int `access:"environment,write_restrictable"`
ListenAddress *string `access:"environment,write_restrictable"`
Enable *bool `access:"environment,write_restrictable,cloud_restrictable"`
BlockProfileRate *int `access:"environment,write_restrictable,cloud_restrictable"`
ListenAddress *string `access:"environment,write_restrictable,cloud_restrictable"`
}
func (s *MetricsSettings) SetDefaults() {
@@ -873,14 +892,15 @@ func (s *MetricsSettings) SetDefaults() {
}
type ExperimentalSettings struct {
ClientSideCertEnable *bool `access:"experimental"`
ClientSideCertCheck *string `access:"experimental"`
EnableClickToReply *bool `access:"experimental,write_restrictable"`
LinkMetadataTimeoutMilliseconds *int64 `access:"experimental,write_restrictable"`
ClientSideCertEnable *bool `access:"experimental,cloud_restrictable"`
ClientSideCertCheck *string `access:"experimental,cloud_restrictable"`
EnableClickToReply *bool `access:"experimental,write_restrictable,cloud_restrictable"`
LinkMetadataTimeoutMilliseconds *int64 `access:"experimental,write_restrictable,cloud_restrictable"`
RestrictSystemAdmin *bool `access:"experimental,write_restrictable"`
UseNewSAMLLibrary *bool `access:"experimental"`
UseNewSAMLLibrary *bool `access:"experimental,cloud_restrictable"`
CloudUserLimit *int64 `access:"experimental,write_restrictable"`
CloudBilling *bool `access:"experimental,write_restrictable"`
EnableSharedChannels *bool `access:"experimental"`
}
func (s *ExperimentalSettings) SetDefaults() {
@@ -916,10 +936,14 @@ func (s *ExperimentalSettings) SetDefaults() {
if s.UseNewSAMLLibrary == nil {
s.UseNewSAMLLibrary = NewBool(false)
}
if s.EnableSharedChannels == nil {
s.EnableSharedChannels = NewBool(false)
}
}
type AnalyticsSettings struct {
MaxUsersForStatistics *int `access:"write_restrictable"`
MaxUsersForStatistics *int `access:"write_restrictable,cloud_restrictable"`
}
func (s *AnalyticsSettings) SetDefaults() {
@@ -1026,22 +1050,22 @@ func (s *Office365Settings) SSOSettings() *SSOSettings {
}
type SqlSettings struct {
DriverName *string `access:"environment,write_restrictable"`
DataSource *string `access:"environment,write_restrictable"`
DataSourceReplicas []string `access:"environment,write_restrictable"`
DataSourceSearchReplicas []string `access:"environment,write_restrictable"`
MaxIdleConns *int `access:"environment,write_restrictable"`
ConnMaxLifetimeMilliseconds *int `access:"environment,write_restrictable"`
MaxOpenConns *int `access:"environment,write_restrictable"`
Trace *bool `access:"environment,write_restrictable"`
AtRestEncryptKey *string `access:"environment,write_restrictable"`
QueryTimeout *int `access:"environment,write_restrictable"`
DisableDatabaseSearch *bool `access:"environment,write_restrictable"`
DriverName *string `access:"environment,write_restrictable,cloud_restrictable"`
DataSource *string `access:"environment,write_restrictable,cloud_restrictable"`
DataSourceReplicas []string `access:"environment,write_restrictable,cloud_restrictable"`
DataSourceSearchReplicas []string `access:"environment,write_restrictable,cloud_restrictable"`
MaxIdleConns *int `access:"environment,write_restrictable,cloud_restrictable"`
ConnMaxLifetimeMilliseconds *int `access:"environment,write_restrictable,cloud_restrictable"`
MaxOpenConns *int `access:"environment,write_restrictable,cloud_restrictable"`
Trace *bool `access:"environment,write_restrictable,cloud_restrictable"`
AtRestEncryptKey *string `access:"environment,write_restrictable,cloud_restrictable"`
QueryTimeout *int `access:"environment,write_restrictable,cloud_restrictable"`
DisableDatabaseSearch *bool `access:"environment,write_restrictable,cloud_restrictable"`
}
func (s *SqlSettings) SetDefaults(isUpdate bool) {
if s.DriverName == nil {
s.DriverName = NewString(DATABASE_DRIVER_MYSQL)
s.DriverName = NewString(DATABASE_DRIVER_POSTGRES)
}
if s.DataSource == nil {
@@ -1092,17 +1116,17 @@ func (s *SqlSettings) SetDefaults(isUpdate bool) {
}
type LogSettings struct {
EnableConsole *bool `access:"environment,write_restrictable"`
ConsoleLevel *string `access:"environment,write_restrictable"`
ConsoleJson *bool `access:"environment,write_restrictable"`
EnableFile *bool `access:"environment,write_restrictable"`
FileLevel *string `access:"environment,write_restrictable"`
FileJson *bool `access:"environment,write_restrictable"`
FileLocation *string `access:"environment,write_restrictable"`
EnableWebhookDebugging *bool `access:"environment,write_restrictable"`
EnableDiagnostics *bool `access:"environment,write_restrictable"`
EnableSentry *bool `access:"environment,write_restrictable"`
AdvancedLoggingConfig *string `access:"environment,write_restrictable"`
EnableConsole *bool `access:"environment,write_restrictable,cloud_restrictable"`
ConsoleLevel *string `access:"environment,write_restrictable,cloud_restrictable"`
ConsoleJson *bool `access:"environment,write_restrictable,cloud_restrictable"`
EnableFile *bool `access:"environment,write_restrictable,cloud_restrictable"`
FileLevel *string `access:"environment,write_restrictable,cloud_restrictable"`
FileJson *bool `access:"environment,write_restrictable,cloud_restrictable"`
FileLocation *string `access:"environment,write_restrictable,cloud_restrictable"`
EnableWebhookDebugging *bool `access:"environment,write_restrictable,cloud_restrictable"`
EnableDiagnostics *bool `access:"environment,write_restrictable,cloud_restrictable"`
EnableSentry *bool `access:"environment,write_restrictable,cloud_restrictable"`
AdvancedLoggingConfig *string `access:"environment,write_restrictable,cloud_restrictable"`
}
func (s *LogSettings) SetDefaults() {
@@ -1152,14 +1176,14 @@ func (s *LogSettings) SetDefaults() {
}
type ExperimentalAuditSettings struct {
FileEnabled *bool `access:"experimental,write_restrictable"`
FileName *string `access:"experimental,write_restrictable"`
FileMaxSizeMB *int `access:"experimental,write_restrictable"`
FileMaxAgeDays *int `access:"experimental,write_restrictable"`
FileMaxBackups *int `access:"experimental,write_restrictable"`
FileCompress *bool `access:"experimental,write_restrictable"`
FileMaxQueueSize *int `access:"experimental,write_restrictable"`
AdvancedLoggingConfig *string `access:"experimental,write_restrictable"`
FileEnabled *bool `access:"experimental,write_restrictable,cloud_restrictable"`
FileName *string `access:"experimental,write_restrictable,cloud_restrictable"`
FileMaxSizeMB *int `access:"experimental,write_restrictable,cloud_restrictable"`
FileMaxAgeDays *int `access:"experimental,write_restrictable,cloud_restrictable"`
FileMaxBackups *int `access:"experimental,write_restrictable,cloud_restrictable"`
FileCompress *bool `access:"experimental,write_restrictable,cloud_restrictable"`
FileMaxQueueSize *int `access:"experimental,write_restrictable,cloud_restrictable"`
AdvancedLoggingConfig *string `access:"experimental,write_restrictable,cloud_restrictable"`
}
func (s *ExperimentalAuditSettings) SetDefaults() {
@@ -1197,14 +1221,14 @@ func (s *ExperimentalAuditSettings) SetDefaults() {
}
type NotificationLogSettings struct {
EnableConsole *bool `access:"write_restrictable"`
ConsoleLevel *string `access:"write_restrictable"`
ConsoleJson *bool `access:"write_restrictable"`
EnableFile *bool `access:"write_restrictable"`
FileLevel *string `access:"write_restrictable"`
FileJson *bool `access:"write_restrictable"`
FileLocation *string `access:"write_restrictable"`
AdvancedLoggingConfig *string `access:"write_restrictable"`
EnableConsole *bool `access:"write_restrictable,cloud_restrictable"`
ConsoleLevel *string `access:"write_restrictable,cloud_restrictable"`
ConsoleJson *bool `access:"write_restrictable,cloud_restrictable"`
EnableFile *bool `access:"write_restrictable,cloud_restrictable"`
FileLevel *string `access:"write_restrictable,cloud_restrictable"`
FileJson *bool `access:"write_restrictable,cloud_restrictable"`
FileLocation *string `access:"write_restrictable,cloud_restrictable"`
AdvancedLoggingConfig *string `access:"write_restrictable,cloud_restrictable"`
}
func (s *NotificationLogSettings) SetDefaults() {
@@ -1272,25 +1296,25 @@ func (s *PasswordSettings) SetDefaults() {
}
type FileSettings struct {
EnableFileAttachments *bool `access:"site"`
EnableMobileUpload *bool `access:"site"`
EnableMobileDownload *bool `access:"site"`
MaxFileSize *int64 `access:"environment"`
DriverName *string `access:"environment,write_restrictable"`
Directory *string `access:"environment,write_restrictable"`
EnablePublicLink *bool `access:"site"`
PublicLinkSalt *string `access:"site"`
InitialFont *string `access:"environment"`
AmazonS3AccessKeyId *string `access:"environment,write_restrictable"`
AmazonS3SecretAccessKey *string `access:"environment,write_restrictable"`
AmazonS3Bucket *string `access:"environment,write_restrictable"`
AmazonS3PathPrefix *string `access:"environment,write_restrictable"`
AmazonS3Region *string `access:"environment,write_restrictable"`
AmazonS3Endpoint *string `access:"environment,write_restrictable"`
AmazonS3SSL *bool `access:"environment,write_restrictable"`
AmazonS3SignV2 *bool `access:"environment,write_restrictable"`
AmazonS3SSE *bool `access:"environment,write_restrictable"`
AmazonS3Trace *bool `access:"environment,write_restrictable"`
EnableFileAttachments *bool `access:"site,cloud_restrictable"`
EnableMobileUpload *bool `access:"site,cloud_restrictable"`
EnableMobileDownload *bool `access:"site,cloud_restrictable"`
MaxFileSize *int64 `access:"environment,cloud_restrictable"`
DriverName *string `access:"environment,write_restrictable,cloud_restrictable"`
Directory *string `access:"environment,write_restrictable,cloud_restrictable"`
EnablePublicLink *bool `access:"site,cloud_restrictable"`
PublicLinkSalt *string `access:"site,cloud_restrictable"`
InitialFont *string `access:"environment,cloud_restrictable"`
AmazonS3AccessKeyId *string `access:"environment,write_restrictable,cloud_restrictable"`
AmazonS3SecretAccessKey *string `access:"environment,write_restrictable,cloud_restrictable"`
AmazonS3Bucket *string `access:"environment,write_restrictable,cloud_restrictable"`
AmazonS3PathPrefix *string `access:"environment,write_restrictable,cloud_restrictable"`
AmazonS3Region *string `access:"environment,write_restrictable,cloud_restrictable"`
AmazonS3Endpoint *string `access:"environment,write_restrictable,cloud_restrictable"`
AmazonS3SSL *bool `access:"environment,write_restrictable,cloud_restrictable"`
AmazonS3SignV2 *bool `access:"environment,write_restrictable,cloud_restrictable"`
AmazonS3SSE *bool `access:"environment,write_restrictable,cloud_restrictable"`
AmazonS3Trace *bool `access:"environment,write_restrictable,cloud_restrictable"`
}
func (s *FileSettings) SetDefaults(isUpdate bool) {
@@ -1388,16 +1412,16 @@ type EmailSettings struct {
UseChannelInEmailNotifications *bool `access:"experimental"`
RequireEmailVerification *bool `access:"authentication"`
FeedbackName *string `access:"site"`
FeedbackEmail *string `access:"site"`
ReplyToAddress *string `access:"site"`
FeedbackEmail *string `access:"site,cloud_restrictable"`
ReplyToAddress *string `access:"site,cloud_restrictable"`
FeedbackOrganization *string `access:"site"`
EnableSMTPAuth *bool `access:"environment,write_restrictable"`
SMTPUsername *string `access:"environment,write_restrictable"`
SMTPPassword *string `access:"environment,write_restrictable"`
SMTPServer *string `access:"environment,write_restrictable"`
SMTPPort *string `access:"environment,write_restrictable"`
SMTPServerTimeout *int
ConnectionSecurity *string `access:"environment,write_restrictable"`
EnableSMTPAuth *bool `access:"environment,write_restrictable,cloud_restrictable"`
SMTPUsername *string `access:"environment,write_restrictable,cloud_restrictable"`
SMTPPassword *string `access:"environment,write_restrictable,cloud_restrictable"`
SMTPServer *string `access:"environment,write_restrictable,cloud_restrictable"`
SMTPPort *string `access:"environment,write_restrictable,cloud_restrictable"`
SMTPServerTimeout *int `access:"cloud_restrictable"`
ConnectionSecurity *string `access:"environment,write_restrictable,cloud_restrictable"`
SendPushNotifications *bool `access:"environment"`
PushNotificationServer *string `access:"environment"`
PushNotificationContents *string `access:"site"`
@@ -1406,7 +1430,7 @@ type EmailSettings struct {
EmailBatchingBufferSize *int `access:"experimental"`
EmailBatchingInterval *int `access:"experimental"`
EnablePreviewModeBanner *bool `access:"site"`
SkipServerCertificateVerification *bool `access:"environment,write_restrictable"`
SkipServerCertificateVerification *bool `access:"environment,write_restrictable,cloud_restrictable"`
EmailNotificationContentsType *string `access:"site"`
LoginButtonColor *string `access:"experimental"`
LoginButtonBorderColor *string `access:"experimental"`
@@ -1556,13 +1580,13 @@ func (s *EmailSettings) SetDefaults(isUpdate bool) {
}
type RateLimitSettings struct {
Enable *bool `access:"environment,write_restrictable"`
PerSec *int `access:"environment,write_restrictable"`
MaxBurst *int `access:"environment,write_restrictable"`
MemoryStoreSize *int `access:"environment,write_restrictable"`
VaryByRemoteAddr *bool `access:"environment,write_restrictable"`
VaryByUser *bool `access:"environment,write_restrictable"`
VaryByHeader string `access:"environment,write_restrictable"`
Enable *bool `access:"environment,write_restrictable,cloud_restrictable"`
PerSec *int `access:"environment,write_restrictable,cloud_restrictable"`
MaxBurst *int `access:"environment,write_restrictable,cloud_restrictable"`
MemoryStoreSize *int `access:"environment,write_restrictable,cloud_restrictable"`
VaryByRemoteAddr *bool `access:"environment,write_restrictable,cloud_restrictable"`
VaryByUser *bool `access:"environment,write_restrictable,cloud_restrictable"`
VaryByHeader string `access:"environment,write_restrictable,cloud_restrictable"`
}
func (s *RateLimitSettings) SetDefaults() {
@@ -1607,11 +1631,11 @@ func (s *PrivacySettings) setDefaults() {
}
type SupportSettings struct {
TermsOfServiceLink *string `access:"site,write_restrictable"`
PrivacyPolicyLink *string `access:"site,write_restrictable"`
AboutLink *string `access:"site,write_restrictable"`
HelpLink *string `access:"site,write_restrictable"`
ReportAProblemLink *string `access:"site,write_restrictable"`
TermsOfServiceLink *string `access:"site,write_restrictable,cloud_restrictable"`
PrivacyPolicyLink *string `access:"site,write_restrictable,cloud_restrictable"`
AboutLink *string `access:"site,write_restrictable,cloud_restrictable"`
HelpLink *string `access:"site,write_restrictable,cloud_restrictable"`
ReportAProblemLink *string `access:"site,write_restrictable,cloud_restrictable"`
SupportEmail *string `access:"site"`
CustomTermsOfServiceEnabled *bool `access:"compliance"`
CustomTermsOfServiceReAcceptancePeriod *int `access:"compliance"`
@@ -1938,12 +1962,12 @@ func (s *TeamSettings) SetDefaults() {
}
type ClientRequirements struct {
AndroidLatestVersion string `access:"write_restrictable"`
AndroidMinVersion string `access:"write_restrictable"`
DesktopLatestVersion string `access:"write_restrictable"`
DesktopMinVersion string `access:"write_restrictable"`
IosLatestVersion string `access:"write_restrictable"`
IosMinVersion string `access:"write_restrictable"`
AndroidLatestVersion string `access:"write_restrictable,cloud_restrictable"`
AndroidMinVersion string `access:"write_restrictable,cloud_restrictable"`
DesktopLatestVersion string `access:"write_restrictable,cloud_restrictable"`
DesktopMinVersion string `access:"write_restrictable,cloud_restrictable"`
IosLatestVersion string `access:"write_restrictable,cloud_restrictable"`
IosMinVersion string `access:"write_restrictable,cloud_restrictable"`
}
type LdapSettings struct {
@@ -2189,6 +2213,7 @@ type SamlSettings struct {
Enable *bool `access:"authentication"`
EnableSyncWithLdap *bool `access:"authentication"`
EnableSyncWithLdapIncludeAuth *bool `access:"authentication"`
IgnoreGuestsLdapSync *bool `access:"authentication"`
Verify *bool `access:"authentication"`
Encrypt *bool `access:"authentication"`
@@ -2243,6 +2268,10 @@ func (s *SamlSettings) SetDefaults() {
s.EnableSyncWithLdapIncludeAuth = NewBool(false)
}
if s.IgnoreGuestsLdapSync == nil {
s.IgnoreGuestsLdapSync = NewBool(false)
}
if s.EnableAdminAttribute == nil {
s.EnableAdminAttribute = NewBool(false)
}
@@ -2367,9 +2396,9 @@ func (s *SamlSettings) SetDefaults() {
}
type NativeAppSettings struct {
AppDownloadLink *string `access:"site,write_restrictable"`
AndroidAppDownloadLink *string `access:"site,write_restrictable"`
IosAppDownloadLink *string `access:"site,write_restrictable"`
AppDownloadLink *string `access:"site,write_restrictable,cloud_restrictable"`
AndroidAppDownloadLink *string `access:"site,write_restrictable,cloud_restrictable"`
IosAppDownloadLink *string `access:"site,write_restrictable,cloud_restrictable"`
}
func (s *NativeAppSettings) SetDefaults() {
@@ -2387,27 +2416,27 @@ func (s *NativeAppSettings) SetDefaults() {
}
type ElasticsearchSettings struct {
ConnectionUrl *string `access:"environment,write_restrictable"`
Username *string `access:"environment,write_restrictable"`
Password *string `access:"environment,write_restrictable"`
EnableIndexing *bool `access:"environment,write_restrictable"`
EnableSearching *bool `access:"environment,write_restrictable"`
EnableAutocomplete *bool `access:"environment,write_restrictable"`
Sniff *bool `access:"environment,write_restrictable"`
PostIndexReplicas *int `access:"environment,write_restrictable"`
PostIndexShards *int `access:"environment,write_restrictable"`
ChannelIndexReplicas *int `access:"environment,write_restrictable"`
ChannelIndexShards *int `access:"environment,write_restrictable"`
UserIndexReplicas *int `access:"environment,write_restrictable"`
UserIndexShards *int `access:"environment,write_restrictable"`
AggregatePostsAfterDays *int `access:"environment,write_restrictable"`
PostsAggregatorJobStartTime *string `access:"environment,write_restrictable"`
IndexPrefix *string `access:"environment,write_restrictable"`
LiveIndexingBatchSize *int `access:"environment,write_restrictable"`
BulkIndexingTimeWindowSeconds *int `access:"environment,write_restrictable"`
RequestTimeoutSeconds *int `access:"environment,write_restrictable"`
SkipTLSVerification *bool `access:"environment,write_restrictable"`
Trace *string `access:"environment,write_restrictable"`
ConnectionUrl *string `access:"environment,write_restrictable,cloud_restrictable"`
Username *string `access:"environment,write_restrictable,cloud_restrictable"`
Password *string `access:"environment,write_restrictable,cloud_restrictable"`
EnableIndexing *bool `access:"environment,write_restrictable,cloud_restrictable"`
EnableSearching *bool `access:"environment,write_restrictable,cloud_restrictable"`
EnableAutocomplete *bool `access:"environment,write_restrictable,cloud_restrictable"`
Sniff *bool `access:"environment,write_restrictable,cloud_restrictable"`
PostIndexReplicas *int `access:"environment,write_restrictable,cloud_restrictable"`
PostIndexShards *int `access:"environment,write_restrictable,cloud_restrictable"`
ChannelIndexReplicas *int `access:"environment,write_restrictable,cloud_restrictable"`
ChannelIndexShards *int `access:"environment,write_restrictable,cloud_restrictable"`
UserIndexReplicas *int `access:"environment,write_restrictable,cloud_restrictable"`
UserIndexShards *int `access:"environment,write_restrictable,cloud_restrictable"`
AggregatePostsAfterDays *int `access:"environment,write_restrictable,cloud_restrictable"`
PostsAggregatorJobStartTime *string `access:"environment,write_restrictable,cloud_restrictable"`
IndexPrefix *string `access:"environment,write_restrictable,cloud_restrictable"`
LiveIndexingBatchSize *int `access:"environment,write_restrictable,cloud_restrictable"`
BulkIndexingTimeWindowSeconds *int `access:"environment,write_restrictable,cloud_restrictable"`
RequestTimeoutSeconds *int `access:"environment,write_restrictable,cloud_restrictable"`
SkipTLSVerification *bool `access:"environment,write_restrictable,cloud_restrictable"`
Trace *string `access:"environment,write_restrictable,cloud_restrictable"`
}
func (s *ElasticsearchSettings) SetDefaults() {
@@ -2557,8 +2586,8 @@ func (s *DataRetentionSettings) SetDefaults() {
}
type JobSettings struct {
RunJobs *bool `access:"write_restrictable"`
RunScheduler *bool `access:"write_restrictable"`
RunJobs *bool `access:"write_restrictable,cloud_restrictable"`
RunScheduler *bool `access:"write_restrictable,cloud_restrictable"`
}
func (s *JobSettings) SetDefaults() {
@@ -2571,25 +2600,35 @@ func (s *JobSettings) SetDefaults() {
}
}
type CloudSettings struct {
CWSUrl *string `access:"environment,write_restrictable"`
}
func (s *CloudSettings) SetDefaults() {
if s.CWSUrl == nil {
s.CWSUrl = NewString(CLOUD_SETTINGS_DEFAULT_CWS_URL)
}
}
type PluginState struct {
Enable bool
}
type PluginSettings struct {
Enable *bool `access:"plugins"`
EnableUploads *bool `access:"plugins,write_restrictable"`
AllowInsecureDownloadUrl *bool `access:"plugins,write_restrictable"`
EnableHealthCheck *bool `access:"plugins,write_restrictable"`
Directory *string `access:"plugins,write_restrictable"`
ClientDirectory *string `access:"plugins,write_restrictable"`
Enable *bool `access:"plugins,write_restrictable"`
EnableUploads *bool `access:"plugins,write_restrictable,cloud_restrictable"`
AllowInsecureDownloadUrl *bool `access:"plugins,write_restrictable,cloud_restrictable"`
EnableHealthCheck *bool `access:"plugins,write_restrictable,cloud_restrictable"`
Directory *string `access:"plugins,write_restrictable,cloud_restrictable"`
ClientDirectory *string `access:"plugins,write_restrictable,cloud_restrictable"`
Plugins map[string]map[string]interface{} `access:"plugins"`
PluginStates map[string]*PluginState `access:"plugins"`
EnableMarketplace *bool `access:"plugins"`
EnableRemoteMarketplace *bool `access:"plugins"`
AutomaticPrepackagedPlugins *bool `access:"plugins"`
RequirePluginSignature *bool `access:"plugins"`
MarketplaceUrl *string `access:"plugins"`
SignaturePublicKeyFiles []string `access:"plugins"`
EnableMarketplace *bool `access:"plugins,write_restrictable,cloud_restrictable"`
EnableRemoteMarketplace *bool `access:"plugins,write_restrictable,cloud_restrictable"`
AutomaticPrepackagedPlugins *bool `access:"plugins,write_restrictable,cloud_restrictable"`
RequirePluginSignature *bool `access:"plugins,write_restrictable,cloud_restrictable"`
MarketplaceUrl *string `access:"plugins,write_restrictable,cloud_restrictable"`
SignaturePublicKeyFiles []string `access:"plugins,write_restrictable,cloud_restrictable"`
}
func (s *PluginSettings) SetDefaults(ls LogSettings) {
@@ -2630,6 +2669,16 @@ func (s *PluginSettings) SetDefaults(ls LogSettings) {
s.PluginStates["com.mattermost.nps"] = &PluginState{Enable: ls.EnableDiagnostics == nil || *ls.EnableDiagnostics}
}
if s.PluginStates["com.mattermost.plugin-incident-management"] == nil && BuildEnterpriseReady == "true" {
// Enable the incident management plugin by default
s.PluginStates["com.mattermost.plugin-incident-management"] = &PluginState{Enable: true}
}
if s.PluginStates["com.mattermost.plugin-channel-export"] == nil && BuildEnterpriseReady == "true" {
// Enable the channel export plugin by default
s.PluginStates["com.mattermost.plugin-channel-export"] = &PluginState{Enable: true}
}
if s.EnableMarketplace == nil {
s.EnableMarketplace = NewBool(PLUGIN_SETTINGS_DEFAULT_ENABLE_MARKETPLACE)
}
@@ -2808,7 +2857,9 @@ func (s *ImageProxySettings) SetDefaults(ss ServiceSettings) {
type ConfigFunc func() *Config
const ConfigAccessTagType = "access"
const ConfigAccessTagWriteRestrictable = "write_restrictable"
const ConfigAccessTagCloudRestrictable = "cloud_restrictable"
// Config fields support the 'access' tag with the following values corresponding to the suffix of the associated
// PERMISSION_SYSCONSOLE_*_* permission Id: 'about', 'reporting', 'user_management_users',
@@ -2822,6 +2873,9 @@ const ConfigAccessTagWriteRestrictable = "write_restrictable"
//
// PERMISSION_MANAGE_SYSTEM always grants read access.
//
// Config values with the access tag 'cloud_restrictable' mean that are marked to be filtered when it's used in a cloud licensed
// environment with ExperimentalSettings.RestrictedSystemAdmin set to true.
//
// Example:
// type HairSettings struct {
// // Colour is writeable with either PERMISSION_SYSCONSOLE_WRITE_REPORTING or PERMISSION_SYSCONSOLE_WRITE_USER_MANAGEMENT_GROUPS.
@@ -2874,6 +2928,8 @@ type Config struct {
DisplaySettings DisplaySettings
GuestAccountsSettings GuestAccountsSettings
ImageProxySettings ImageProxySettings
CloudSettings CloudSettings
FeatureFlags *FeatureFlags `json:",omitempty"`
}
func (o *Config) Clone() *Config {
@@ -2889,6 +2945,18 @@ func (o *Config) ToJson() string {
return string(b)
}
func (o *Config) ToJsonFiltered(tagType, tagValue string) string {
filteredConfigMap := structToMapFilteredByTag(*o, tagType, tagValue)
for key, value := range filteredConfigMap {
v, ok := value.(map[string]interface{})
if ok && len(v) == 0 {
delete(filteredConfigMap, key)
}
}
b, _ := json.Marshal(filteredConfigMap)
return string(b)
}
func (o *Config) GetSSOService(service string) *SSOSettings {
switch service {
case SERVICE_GITLAB:
@@ -2960,6 +3028,11 @@ func (o *Config) SetDefaults() {
o.DisplaySettings.SetDefaults()
o.GuestAccountsSettings.SetDefaults()
o.ImageProxySettings.SetDefaults(o.ServiceSettings)
o.CloudSettings.SetDefaults()
if o.FeatureFlags == nil {
o.FeatureFlags = &FeatureFlags{}
o.FeatureFlags.SetDefaults()
}
}
func (o *Config) IsValid() *AppError {
@@ -3588,4 +3661,66 @@ func (o *Config) Sanitize() {
if o.ServiceSettings.GfycatApiSecret != nil && len(*o.ServiceSettings.GfycatApiSecret) > 0 {
*o.ServiceSettings.GfycatApiSecret = FAKE_SETTING
}
*o.ServiceSettings.SplitKey = FAKE_SETTING
}
// structToMapFilteredByTag converts a struct into a map removing those fields that has the tag passed
// as argument
func structToMapFilteredByTag(t interface{}, typeOfTag, filterTag string) map[string]interface{} {
defer func() {
if r := recover(); r != nil {
mlog.Error("Panicked in structToMapFilteredByTag. This should never happen.", mlog.Any("recover", r))
}
}()
val := reflect.ValueOf(t)
elemField := reflect.TypeOf(t)
if val.Kind() != reflect.Struct {
return nil
}
out := map[string]interface{}{}
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
structField := elemField.Field(i)
tagPermissions := strings.Split(structField.Tag.Get(typeOfTag), ",")
if isTagPresent(filterTag, tagPermissions) {
continue
}
var value interface{}
switch field.Kind() {
case reflect.Struct:
value = structToMapFilteredByTag(field.Interface(), typeOfTag, filterTag)
case reflect.Ptr:
indirectType := field.Elem()
if indirectType.Kind() == reflect.Struct {
value = structToMapFilteredByTag(indirectType.Interface(), typeOfTag, filterTag)
} else if indirectType.Kind() != reflect.Invalid {
value = indirectType.Interface()
}
default:
value = field.Interface()
}
out[val.Type().Field(i).Name] = value
}
return out
}
func isTagPresent(tag string, tags []string) bool {
for _, val := range tags {
tagValue := strings.TrimSpace(val)
if tagValue != "" && tagValue == tag {
return true
}
}
return false
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
type FeatureFlags struct {
// Exists only for unit and manual testing.
// When set to a value, will be returned by the ping endpoint.
TestFeature string
// Toggle on and off scheduled jobs for cloud user limit emails see MM-29999
CloudDelinquentEmailJobsEnabled bool
}
func (f *FeatureFlags) SetDefaults() {
f.TestFeature = "off"
f.CloudDelinquentEmailJobsEnabled = false
}

View File

@@ -4,14 +4,19 @@
package model
import (
"bytes"
"encoding/json"
"image"
"image/gif"
"image/jpeg"
"io"
"mime"
"net/http"
"path/filepath"
"strings"
"github.com/disintegration/imaging"
"github.com/mattermost/mattermost-server/v5/mlog"
)
const (
@@ -53,6 +58,7 @@ type FileInfo struct {
Height int `json:"height,omitempty"`
HasPreviewImage bool `json:"has_preview_image,omitempty"`
MiniPreview *[]byte `json:"mini_preview"` // declared as *[]byte to avoid postgres/mysql differences in deserialization
Content string `json:"-"`
}
func (fi *FileInfo) ToJson() string {
@@ -151,6 +157,19 @@ func NewInfo(name string) *FileInfo {
return info
}
func GenerateMiniPreviewImage(img image.Image) *[]byte {
preview := imaging.Resize(img, 16, 16, imaging.Lanczos)
buf := new(bytes.Buffer)
if err := jpeg.Encode(buf, preview, &jpeg.Options{Quality: 90}); err != nil {
mlog.Error("Unable to encode image as mini preview jpg", mlog.Err(err))
return nil
}
data := buf.Bytes()
return &data
}
func GetInfoForBytes(name string, data io.ReadSeeker, size int) (*FileInfo, *AppError) {
info := &FileInfo{
Name: name,

View File

@@ -157,7 +157,7 @@ func (group *Group) requiresRemoteId() bool {
func (group *Group) IsValidForUpdate() *AppError {
if !IsValidId(group.Id) {
return NewAppError("Group.IsValidForUpdate", "model.group.id.app_error", nil, "", http.StatusBadRequest)
return NewAppError("Group.IsValidForUpdate", "app.group.id.app_error", nil, "", http.StatusBadRequest)
}
if group.CreateAt == 0 {
return NewAppError("Group.IsValidForUpdate", "model.group.create_at.app_error", nil, "", http.StatusBadRequest)

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