Compare commits

...

54 Commits

Author SHA1 Message Date
Wim
248d88c849 Release v1.2.0 2017-09-11 23:41:13 +02:00
Wim
d19535fa21 Update vendor (nlopes/slack) 2017-09-11 23:33:58 +02:00
Wim
49204cafcc Update vendor (bwmarrin/discordgo) apiv6 2017-09-11 23:23:54 +02:00
Wim
812db2d267 Bump version 2017-09-11 23:17:33 +02:00
Wim
14490bea9f Add partial support for deleted messages (telegram) 2017-09-11 23:12:33 +02:00
Wim
0352970051 Update vendor (go-telegram-bot-api/telegram-bot-api) 2017-09-11 23:11:48 +02:00
Wim
ed01820722 Add support for deleting messages across bridges.
Currently fully support mattermost,slack and discord.
Message deleted on the bridge or received from other bridges will be
deleted.

Partially support for Gitter.
Gitter bridge will delete messages received from other bridges.
But if you delete a message on gitter, this deletion will not be sent to
other bridges (this is a gitter API limitation, it doesn't propogate edits
or deletes via the API)
2017-09-11 22:45:15 +02:00
Wim
90a61f15cc Do not break messages on newline (slack). Closes #258 2017-09-10 18:19:33 +02:00
Wim
86cd7f1ba6 Add UpdateUserNick 2017-09-10 16:33:29 +02:00
Wim
d6ee55e35f Release v1.1.2 2017-09-09 17:06:40 +02:00
Wim
aef64eec32 Update changelog 2017-09-09 17:04:01 +02:00
Wim
c4193d5ccd Add 4.2 support (mattermost) 2017-09-09 17:00:26 +02:00
Wim
0c94186818 Add darwin-amd64 to nightly builds 2017-09-09 14:42:45 +02:00
Wim
9039720013 Send images when text is empty regression. (mattermost). Closes #254 2017-09-08 00:16:17 +02:00
Wim
a3470f8aec Send first message after connect (slack). Closes #252 2017-09-07 23:47:23 +02:00
Wim
01badde21d Add message debugging (gitter) 2017-09-07 20:35:12 +02:00
Ryan Mulligan
a37b232dd9 remove comment about useAPI in sample configuration (#251) 2017-09-04 15:16:58 +02:00
Ryan Mulligan
579ee48385 remove useAPI from sample configuration (#250) 2017-09-04 15:16:29 +02:00
Wim
dd985d1dad Fix sending direct messages with APIv4 2017-09-04 14:24:22 +02:00
Wim
d2caea70a2 Release v1.1.1 2017-09-04 13:43:30 +02:00
Wim
21143cf5ee Fix public links (mattermost) 2017-09-04 12:50:42 +02:00
Wim
dc2aed698d Release v1.1.0 2017-09-01 20:20:46 +02:00
Wim
37c350f19f Convert utf-8 back to charset (irc). #247 2017-08-30 20:59:54 +02:00
Wim
9e03fcf162 Fix private channel joining bug (mattermost). Closes #248 2017-08-30 14:01:17 +02:00
Wim
8d4521c1df Update changelog 2017-08-29 23:45:39 +02:00
Wim
9226252336 Replace mentions from other bridges. (slack). Closes #233 2017-08-29 23:34:50 +02:00
Wim
f4fb83e787 Use the detected charset (irc) 2017-08-29 21:35:36 +02:00
Wim
e7fcb25107 Add a charset option (irc). Closes #247 2017-08-29 21:31:03 +02:00
Wim
5a85258f74 Update travis to go 1.9 2017-08-29 20:34:32 +02:00
Wim
2f7df2df43 Do not add messages without ID to cache 2017-08-29 20:28:44 +02:00
Wim
ad3a753718 Remove debug message 2017-08-28 23:07:13 +02:00
Wim
e45c551880 Add support for editing messages. Remove ZWSP as loopcheck (gitter) 2017-08-28 23:07:12 +02:00
Wim
e59d338d4e Use github.com/42wim/go-gitter for now 2017-08-28 23:07:11 +02:00
Wim
7a86044f7a Add support for editing messages (telegram) 2017-08-28 23:07:03 +02:00
Wim
8b98f605bc Add support for editing messages (slack) 2017-08-28 20:29:02 +02:00
Wim
7c773ebae0 Add support for editing messages across bridges. Currently mattermost/discord.
Our Message type has an extra ID field which contains the message ID of the specific bridge.
The Send() function has been modified to return a msg ID (after the message to that specific
bridge has been created).

There is a lru cache of 5000 entries (message IDs). All in memory, so editing messages
will only work for messages the bot has seen.

Currently we go out from the idea that every message ID is unique, so we don't keep
the ID separate for each bridge. (we do for each gateway though)

If there's a new message from a bridge, we put that message ID in the LRU cache as key
and the []*BrMsgID as value (this slice contains the message ID's of each bridge that
received the new message)

If there's a new message and this message ID already exists in the cache, it must be
an updated message. The value from the cache gets checked for each bridge and if there
is a message ID for this bridge, the ID will be added to the Message{} sent to that
bridge. If the bridge sees that the ID isn't empty, it'll know it has to update the
message with that specific ID instead of creating a new message.
2017-08-28 00:33:17 +02:00
Wim
e84417430d Update PostMessage to also return and error. Add EditMessage function 2017-08-28 00:32:56 +02:00
Wim
5a8d7b5f6d Modify Send() to return also a message id 2017-08-27 22:59:37 +02:00
Wim
cfb8107138 Relay notices (matrix). Closes #243 2017-08-27 01:01:35 +02:00
Wim
43bd779fb7 Handle leave/join events (slack). Closes #246 2017-08-27 00:00:02 +02:00
Wim
7f9a400776 Add support for personal access tokens (mattermost)
* https://docs.mattermost.com/developer/personal-access-tokens.html
2017-08-23 22:49:42 +02:00
Wim
ce1c5873ac Make megacheck happy 2017-08-17 00:00:41 +02:00
Wim
85ff1995fd Use mattermost v4 api (drops support for mattermost < 3.8) 2017-08-16 23:41:35 +02:00
Wim
b963f83c6a Update mattermost vendor (3.7 => 4.1) 2017-08-16 23:37:37 +02:00
Wim
f6297ebbb0 Bump version 2017-08-16 23:28:11 +02:00
Wim
a5259f56c5 Release v1.0.1 2017-08-16 22:25:44 +02:00
Wim
3f75ed9c18 Add 4.1 support (mattermost) 2017-08-16 22:02:13 +02:00
Thracky
49ece51167 Add new file_ids parameter for Mattermost outgoing webhook (#240)
* Added file_id parameter for outgoing webhook

* Typo in the new fileids field name
2017-08-16 21:27:17 +02:00
Wim
e77c3eb20a Swap token/id. Also check for default webhookURL in isWebhookID (discord) 2017-08-12 16:30:00 +02:00
Wim
59b2a5f8d0 Bump version 2017-08-12 14:54:19 +02:00
Wim
28710d0bc7 Allow a webhookurl per channel (discord). #239 2017-08-12 14:51:41 +02:00
Wim
ad4d461606 Release v1.0.0 2017-08-05 15:50:21 +02:00
anon724
67905089ba Add UseUserName option (discord) (#234) 2017-08-01 18:18:55 +02:00
Wim
f2483af561 Do not modify username in action (discord) 2017-07-31 21:37:19 +02:00
193 changed files with 12023 additions and 1674 deletions

View File

@@ -1,7 +1,7 @@
language: go language: go
go: go:
#- 1.7.x #- 1.7.x
- 1.8.x - 1.9.x
# - tip # - tip
# we have everything vendored # we have everything vendored

View File

@@ -36,7 +36,7 @@ Has a REST API.
# Requirements # Requirements
Accounts to one of the supported bridges Accounts to one of the supported bridges
* [Mattermost](https://github.com/mattermost/platform/) 3.5.x - 3.10.x, 4.0.x * [Mattermost](https://github.com/mattermost/platform/) 3.8.x - 3.10.x, 4.0.x - 4.2.x
* [IRC](http://www.mirc.com/servers.html) * [IRC](http://www.mirc.com/servers.html)
* [XMPP](https://jabber.org) * [XMPP](https://jabber.org)
* [Gitter](https://gitter.im) * [Gitter](https://gitter.im)
@@ -53,7 +53,7 @@ See https://github.com/42wim/matterbridge/wiki
# Installing # Installing
## Binaries ## Binaries
* Latest stable release [v1.0.0-rc1](https://github.com/42wim/matterbridge/releases/latest) * Latest stable release [v1.2.0](https://github.com/42wim/matterbridge/releases/latest)
* Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/) * Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/)
## Building ## Building

View File

@@ -61,16 +61,20 @@ func (b *Api) Disconnect() error {
return nil return nil
} }
func (b *Api) JoinChannel(channel string) error { func (b *Api) JoinChannel(channel config.ChannelInfo) error {
return nil return nil
} }
func (b *Api) Send(msg config.Message) error { func (b *Api) Send(msg config.Message) (string, error) {
b.Lock() b.Lock()
defer b.Unlock() defer b.Unlock()
// ignore delete messages
if msg.Event == config.EVENT_MSG_DELETE {
return "", nil
}
b.Messages.Enqueue(&msg) b.Messages.Enqueue(&msg)
return nil return "", nil
} }
func (b *Api) handlePostMessage(c echo.Context) error { func (b *Api) handlePostMessage(c echo.Context) error {

View File

@@ -19,9 +19,9 @@ import (
) )
type Bridger interface { type Bridger interface {
Send(msg config.Message) error Send(msg config.Message) (string, error)
Connect() error Connect() error
JoinChannel(channel string) error JoinChannel(channel config.ChannelInfo) error
Disconnect() error Disconnect() error
} }
@@ -92,16 +92,10 @@ func (b *Bridge) JoinChannels() error {
} }
func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map[string]bool) error { func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map[string]bool) error {
mychannel := ""
for ID, channel := range channels { for ID, channel := range channels {
if !exists[ID] { if !exists[ID] {
mychannel = channel.Name
log.Infof("%s: joining %s (%s)", b.Account, channel.Name, ID) log.Infof("%s: joining %s (%s)", b.Account, channel.Name, ID)
if b.Protocol == "irc" && channel.Options.Key != "" { err := b.JoinChannel(channel)
log.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name)
mychannel = mychannel + " " + channel.Options.Key
}
err := b.JoinChannel(mychannel)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -14,6 +14,7 @@ const (
EVENT_FAILURE = "failure" EVENT_FAILURE = "failure"
EVENT_REJOIN_CHANNELS = "rejoin_channels" EVENT_REJOIN_CHANNELS = "rejoin_channels"
EVENT_USER_ACTION = "user_action" EVENT_USER_ACTION = "user_action"
EVENT_MSG_DELETE = "msg_delete"
) )
type Message struct { type Message struct {
@@ -27,6 +28,7 @@ type Message struct {
Protocol string `json:"protocol"` Protocol string `json:"protocol"`
Gateway string `json:"gateway"` Gateway string `json:"gateway"`
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
ID string `json:"id"`
} }
type ChannelInfo struct { type ChannelInfo struct {
@@ -42,6 +44,7 @@ type Protocol struct {
AuthCode string // steam AuthCode string // steam
BindAddress string // mattermost, slack // DEPRECATED BindAddress string // mattermost, slack // DEPRECATED
Buffer int // api Buffer int // api
Charset string // irc
EditSuffix string // mattermost, slack, discord, telegram, gitter EditSuffix string // mattermost, slack, discord, telegram, gitter
EditDisable bool // mattermost, slack, discord, telegram, gitter EditDisable bool // mattermost, slack, discord, telegram, gitter
IconURL string // mattermost, slack IconURL string // mattermost, slack
@@ -77,6 +80,7 @@ type Protocol struct {
UseSASL bool // IRC UseSASL bool // IRC
UseTLS bool // IRC UseTLS bool // IRC
UseFirstName bool // telegram UseFirstName bool // telegram
UseUserName bool // discord
UseInsecureURL bool // telegram UseInsecureURL bool // telegram
WebhookBindAddress string // mattermost, slack WebhookBindAddress string // mattermost, slack
WebhookURL string // mattermost, slack WebhookURL string // mattermost, slack
@@ -84,7 +88,8 @@ type Protocol struct {
} }
type ChannelOptions struct { type ChannelOptions struct {
Key string // irc Key string // irc
WebhookURL string // discord
} }
type Bridge struct { type Bridge struct {

View File

@@ -10,17 +10,18 @@ import (
) )
type bdiscord struct { type bdiscord struct {
c *discordgo.Session c *discordgo.Session
Config *config.Protocol Config *config.Protocol
Remote chan config.Message Remote chan config.Message
Account string Account string
Channels []*discordgo.Channel Channels []*discordgo.Channel
Nick string Nick string
UseChannelID bool UseChannelID bool
userMemberMap map[string]*discordgo.Member userMemberMap map[string]*discordgo.Member
guildID string guildID string
webhookID string webhookID string
webhookToken string webhookToken string
channelInfoMap map[string]*config.ChannelInfo
sync.RWMutex sync.RWMutex
} }
@@ -37,11 +38,10 @@ func New(cfg config.Protocol, account string, c chan config.Message) *bdiscord {
b.Remote = c b.Remote = c
b.Account = account b.Account = account
b.userMemberMap = make(map[string]*discordgo.Member) b.userMemberMap = make(map[string]*discordgo.Member)
b.channelInfoMap = make(map[string]*config.ChannelInfo)
if b.Config.WebhookURL != "" { if b.Config.WebhookURL != "" {
flog.Debug("Configuring Discord Incoming Webhook") flog.Debug("Configuring Discord Incoming Webhook")
webhookURLSplit := strings.Split(b.Config.WebhookURL, "/") b.webhookID, b.webhookToken = b.splitURL(b.Config.WebhookURL)
b.webhookToken = webhookURLSplit[len(webhookURLSplit)-1]
b.webhookID = webhookURLSplit[len(webhookURLSplit)-2]
} }
return b return b
} }
@@ -66,6 +66,7 @@ func (b *bdiscord) Connect() error {
b.c.AddHandler(b.messageCreate) b.c.AddHandler(b.messageCreate)
b.c.AddHandler(b.memberUpdate) b.c.AddHandler(b.memberUpdate)
b.c.AddHandler(b.messageUpdate) b.c.AddHandler(b.messageUpdate)
b.c.AddHandler(b.messageDelete)
err = b.c.Open() err = b.c.Open()
if err != nil { if err != nil {
flog.Debugf("%#v", err) flog.Debugf("%#v", err)
@@ -99,44 +100,75 @@ func (b *bdiscord) Disconnect() error {
return nil return nil
} }
func (b *bdiscord) JoinChannel(channel string) error { func (b *bdiscord) JoinChannel(channel config.ChannelInfo) error {
idcheck := strings.Split(channel, "ID:") b.channelInfoMap[channel.ID] = &channel
idcheck := strings.Split(channel.Name, "ID:")
if len(idcheck) > 1 { if len(idcheck) > 1 {
b.UseChannelID = true b.UseChannelID = true
} }
return nil return nil
} }
func (b *bdiscord) Send(msg config.Message) error { func (b *bdiscord) Send(msg config.Message) (string, error) {
flog.Debugf("Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
channelID := b.getChannelID(msg.Channel) channelID := b.getChannelID(msg.Channel)
if channelID == "" { if channelID == "" {
flog.Errorf("Could not find channelID for %v", msg.Channel) flog.Errorf("Could not find channelID for %v", msg.Channel)
return nil return "", nil
} }
if b.Config.WebhookURL == "" { if msg.Event == config.EVENT_USER_ACTION {
msg.Text = "_" + msg.Text + "_"
}
wID := b.webhookID
wToken := b.webhookToken
if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok {
if ci.Options.WebhookURL != "" {
wID, wToken = b.splitURL(ci.Options.WebhookURL)
}
}
if wID == "" {
flog.Debugf("Broadcasting using token (API)") flog.Debugf("Broadcasting using token (API)")
if msg.Event == config.EVENT_USER_ACTION { if msg.Event == config.EVENT_MSG_DELETE {
msg.Username = "_" + msg.Username if msg.ID == "" {
msg.Text = msg.Text + "_" return "", nil
}
err := b.c.ChannelMessageDelete(channelID, msg.ID)
return "", err
} }
b.c.ChannelMessageSend(channelID, msg.Username+msg.Text) if msg.ID != "" {
} else { _, err := b.c.ChannelMessageEdit(channelID, msg.ID, msg.Username+msg.Text)
flog.Debugf("Broadcasting using Webhook") return msg.ID, err
if msg.Event == config.EVENT_USER_ACTION {
msg.Text = "_" + msg.Text + "_"
} }
b.c.WebhookExecute( res, err := b.c.ChannelMessageSend(channelID, msg.Username+msg.Text)
b.webhookID, if err != nil {
b.webhookToken, return "", err
true, }
&discordgo.WebhookParams{ return res.ID, err
Content: msg.Text,
Username: msg.Username,
AvatarURL: msg.Avatar,
})
} }
return nil flog.Debugf("Broadcasting using Webhook")
err := b.c.WebhookExecute(
wID,
wToken,
true,
&discordgo.WebhookParams{
Content: msg.Text,
Username: msg.Username,
AvatarURL: msg.Avatar,
})
return "", err
}
func (b *bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) {
rmsg := config.Message{Account: b.Account, ID: m.ID, Event: config.EVENT_MSG_DELETE, Text: config.EVENT_MSG_DELETE}
rmsg.Channel = b.getChannelName(m.ChannelID)
if b.UseChannelID {
rmsg.Channel = "ID:" + m.ChannelID
}
flog.Debugf("Sending message from %s to gateway", b.Account)
flog.Debugf("Message is %#v", rmsg)
b.Remote <- rmsg
} }
func (b *bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) { func (b *bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) {
@@ -157,7 +189,7 @@ func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
return return
} }
// if using webhooks, do not relay if it's ours // if using webhooks, do not relay if it's ours
if b.Config.WebhookURL != "" && m.Author.Bot && m.Author.ID == b.webhookID { if b.useWebhook() && m.Author.Bot && b.isWebhookID(m.Author.ID) {
return return
} }
@@ -179,13 +211,18 @@ func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
} }
rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg", rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg",
UserID: m.Author.ID} UserID: m.Author.ID, ID: m.ID}
rmsg.Channel = b.getChannelName(m.ChannelID) rmsg.Channel = b.getChannelName(m.ChannelID)
if b.UseChannelID { if b.UseChannelID {
rmsg.Channel = "ID:" + m.ChannelID rmsg.Channel = "ID:" + m.ChannelID
} }
rmsg.Username = b.getNick(m.Author)
if !b.Config.UseUserName {
rmsg.Username = b.getNick(m.Author)
} else {
rmsg.Username = m.Author.Username
}
if b.Config.ShowEmbeds && m.Message.Embeds != nil { if b.Config.ShowEmbeds && m.Message.Embeds != nil {
for _, embed := range m.Message.Embeds { for _, embed := range m.Message.Embeds {
@@ -205,6 +242,7 @@ func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
rmsg.Text = text rmsg.Text = text
flog.Debugf("Sending message from %s on %s to gateway", m.Author.Username, b.Account) flog.Debugf("Sending message from %s on %s to gateway", m.Author.Username, b.Account)
flog.Debugf("Message is %#v", rmsg)
b.Remote <- rmsg b.Remote <- rmsg
} }
@@ -309,3 +347,41 @@ func (b *bdiscord) stripCustomoji(text string) string {
re := regexp.MustCompile("<(:.*?:)[0-9]+>") re := regexp.MustCompile("<(:.*?:)[0-9]+>")
return re.ReplaceAllString(text, `$1`) return re.ReplaceAllString(text, `$1`)
} }
// splitURL splits a webhookURL and returns the id and token
func (b *bdiscord) splitURL(url string) (string, string) {
webhookURLSplit := strings.Split(url, "/")
return webhookURLSplit[len(webhookURLSplit)-2], webhookURLSplit[len(webhookURLSplit)-1]
}
// useWebhook returns true if we have a webhook defined somewhere
func (b *bdiscord) useWebhook() bool {
if b.Config.WebhookURL != "" {
return true
}
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.Config.WebhookURL != "" {
wID, _ := b.splitURL(b.Config.WebhookURL)
if wID == id {
return true
}
}
for _, channel := range b.channelInfoMap {
if channel.Options.WebhookURL != "" {
wID, _ := b.splitURL(channel.Options.WebhookURL)
if wID == id {
return true
}
}
}
return false
}

View File

@@ -2,9 +2,9 @@ package bgitter
import ( import (
"fmt" "fmt"
"github.com/42wim/go-gitter"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/sromku/go-gitter"
"strings" "strings"
) )
@@ -13,6 +13,7 @@ type Bgitter struct {
Config *config.Protocol Config *config.Protocol
Remote chan config.Message Remote chan config.Message
Account string Account string
User *gitter.User
Users []gitter.User Users []gitter.User
Rooms []gitter.Room Rooms []gitter.Room
} }
@@ -36,7 +37,7 @@ func (b *Bgitter) Connect() error {
var err error var err error
flog.Info("Connecting") flog.Info("Connecting")
b.c = gitter.New(b.Config.Token) b.c = gitter.New(b.Config.Token)
_, err = b.c.GetUser() b.User, err = b.c.GetUser()
if err != nil { if err != nil {
flog.Debugf("%#v", err) flog.Debugf("%#v", err)
return err return err
@@ -51,10 +52,10 @@ func (b *Bgitter) Disconnect() error {
} }
func (b *Bgitter) JoinChannel(channel string) error { func (b *Bgitter) JoinChannel(channel config.ChannelInfo) error {
roomID, err := b.c.GetRoomId(channel) roomID, err := b.c.GetRoomId(channel.Name)
if err != nil { if err != nil {
return fmt.Errorf("Could not find roomID for %v. Please create the room on gitter.im", channel) return fmt.Errorf("Could not find roomID for %v. Please create the room on gitter.im", channel.Name)
} }
room, err := b.c.GetRoom(roomID) room, err := b.c.GetRoom(roomID)
if err != nil { if err != nil {
@@ -78,15 +79,16 @@ func (b *Bgitter) JoinChannel(channel string) error {
for event := range stream.Event { for event := range stream.Event {
switch ev := event.Data.(type) { switch ev := event.Data.(type) {
case *gitter.MessageReceived: case *gitter.MessageReceived:
// check for ZWSP to see if it's not an echo if ev.Message.From.ID != b.User.ID {
if !strings.HasSuffix(ev.Message.Text, "") {
flog.Debugf("Sending message from %s on %s to gateway", ev.Message.From.Username, b.Account) flog.Debugf("Sending message from %s on %s to gateway", ev.Message.From.Username, b.Account)
rmsg := config.Message{Username: ev.Message.From.Username, Text: ev.Message.Text, Channel: room, rmsg := config.Message{Username: ev.Message.From.Username, Text: ev.Message.Text, Channel: room,
Account: b.Account, Avatar: b.getAvatar(ev.Message.From.Username), UserID: ev.Message.From.ID} Account: b.Account, Avatar: b.getAvatar(ev.Message.From.Username), UserID: ev.Message.From.ID,
ID: ev.Message.ID}
if strings.HasPrefix(ev.Message.Text, "@"+ev.Message.From.Username) { if strings.HasPrefix(ev.Message.Text, "@"+ev.Message.From.Username) {
rmsg.Event = config.EVENT_USER_ACTION rmsg.Event = config.EVENT_USER_ACTION
rmsg.Text = strings.Replace(rmsg.Text, "@"+ev.Message.From.Username+" ", "", -1) rmsg.Text = strings.Replace(rmsg.Text, "@"+ev.Message.From.Username+" ", "", -1)
} }
flog.Debugf("Message is %#v", rmsg)
b.Remote <- rmsg b.Remote <- rmsg
} }
case *gitter.GitterConnectionClosed: case *gitter.GitterConnectionClosed:
@@ -97,15 +99,37 @@ func (b *Bgitter) JoinChannel(channel string) error {
return nil return nil
} }
func (b *Bgitter) Send(msg config.Message) error { func (b *Bgitter) Send(msg config.Message) (string, error) {
flog.Debugf("Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
roomID := b.getRoomID(msg.Channel) roomID := b.getRoomID(msg.Channel)
if roomID == "" { if roomID == "" {
flog.Errorf("Could not find roomID for %v", msg.Channel) flog.Errorf("Could not find roomID for %v", msg.Channel)
return nil return "", nil
} }
// add ZWSP because gitter echoes our own messages if msg.Event == config.EVENT_MSG_DELETE {
return b.c.SendMessage(roomID, msg.Username+msg.Text+" ") if msg.ID == "" {
return "", nil
}
// gitter has no delete message api
_, err := b.c.UpdateMessage(roomID, msg.ID, "")
if err != nil {
return "", err
}
return "", nil
}
if msg.ID != "" {
flog.Debugf("updating message with id %s", msg.ID)
_, err := b.c.UpdateMessage(roomID, msg.ID, msg.Username+msg.Text)
if err != nil {
return "", err
}
return "", nil
}
resp, err := b.c.SendMessage(roomID, msg.Username+msg.Text)
if err != nil {
return "", err
}
return resp.ID, nil
} }
func (b *Bgitter) getRoomID(channel string) string { func (b *Bgitter) getRoomID(channel string) string {

View File

@@ -1,6 +1,7 @@
package birc package birc
import ( import (
"bytes"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"github.com/42wim/go-ircevent" "github.com/42wim/go-ircevent"
@@ -117,16 +118,38 @@ func (b *Birc) Disconnect() error {
return nil return nil
} }
func (b *Birc) JoinChannel(channel string) error { func (b *Birc) JoinChannel(channel config.ChannelInfo) error {
b.i.Join(channel) if channel.Options.Key != "" {
flog.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name)
b.i.Join(channel.Name + " " + channel.Options.Key)
} else {
b.i.Join(channel.Name)
}
return nil return nil
} }
func (b *Birc) Send(msg config.Message) error { func (b *Birc) Send(msg config.Message) (string, error) {
// ignore delete messages
if msg.Event == config.EVENT_MSG_DELETE {
return "", nil
}
flog.Debugf("Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
if strings.HasPrefix(msg.Text, "!") { if strings.HasPrefix(msg.Text, "!") {
b.Command(&msg) b.Command(&msg)
} }
if b.Config.Charset != "" {
buf := new(bytes.Buffer)
w, err := charset.NewWriter(b.Config.Charset, buf)
if err != nil {
flog.Errorf("charset from utf-8 conversion failed: %s", err)
return "", err
}
fmt.Fprintf(w, msg.Text)
w.Close()
msg.Text = buf.String()
}
for _, text := range strings.Split(msg.Text, "\n") { for _, text := range strings.Split(msg.Text, "\n") {
if len(text) > b.Config.MessageLength { if len(text) > b.Config.MessageLength {
text = text[:b.Config.MessageLength] + " <message clipped>" text = text[:b.Config.MessageLength] + " <message clipped>"
@@ -140,7 +163,7 @@ func (b *Birc) Send(msg config.Message) error {
flog.Debugf("flooding, dropping message (queue at %d)", len(b.Local)) flog.Debugf("flooding, dropping message (queue at %d)", len(b.Local))
} }
} }
return nil return "", nil
} }
func (b *Birc) doSend() { func (b *Birc) doSend() {
@@ -260,20 +283,25 @@ func (b *Birc) handlePrivMsg(event *irc.Event) {
re := regexp.MustCompile(`[[:cntrl:]](\d+,|)\d+`) re := regexp.MustCompile(`[[:cntrl:]](\d+,|)\d+`)
msg = re.ReplaceAllString(msg, "") msg = re.ReplaceAllString(msg, "")
// detect what were sending so that we convert it to utf-8
detector := chardet.NewTextDetector()
result, err := detector.DetectBest([]byte(msg))
if err != nil {
flog.Infof("detection failed for msg: %#v", msg)
return
}
flog.Debugf("detected %s confidence %#v", result.Charset, result.Confidence)
var r io.Reader var r io.Reader
r, err = charset.NewReader(result.Charset, strings.NewReader(msg)) var err error
// if we're not sure, just pick ISO-8859-1 mycharset := b.Config.Charset
if result.Confidence < 80 { if mycharset == "" {
r, err = charset.NewReader("ISO-8859-1", strings.NewReader(msg)) // detect what were sending so that we convert it to utf-8
detector := chardet.NewTextDetector()
result, err := detector.DetectBest([]byte(msg))
if err != nil {
flog.Infof("detection failed for msg: %#v", msg)
return
}
flog.Debugf("detected %s confidence %#v", result.Charset, result.Confidence)
mycharset = result.Charset
// if we're not sure, just pick ISO-8859-1
if result.Confidence < 80 {
mycharset = "ISO-8859-1"
}
} }
r, err = charset.NewReader(mycharset, strings.NewReader(msg))
if err != nil { if err != nil {
flog.Errorf("charset to utf-8 conversion failed: %s", err) flog.Errorf("charset to utf-8 conversion failed: %s", err)
return return

View File

@@ -63,28 +63,32 @@ func (b *Bmatrix) Disconnect() error {
return nil return nil
} }
func (b *Bmatrix) JoinChannel(channel string) error { func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error {
resp, err := b.mc.JoinRoom(channel, "", nil) resp, err := b.mc.JoinRoom(channel.Name, "", nil)
if err != nil { if err != nil {
return err return err
} }
b.Lock() b.Lock()
b.RoomMap[resp.RoomID] = channel b.RoomMap[resp.RoomID] = channel.Name
b.Unlock() b.Unlock()
return err return err
} }
func (b *Bmatrix) Send(msg config.Message) error { func (b *Bmatrix) Send(msg config.Message) (string, error) {
flog.Debugf("Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
// ignore delete messages
if msg.Event == config.EVENT_MSG_DELETE {
return "", nil
}
channel := b.getRoomID(msg.Channel) channel := b.getRoomID(msg.Channel)
flog.Debugf("Sending to channel %s", channel) flog.Debugf("Sending to channel %s", channel)
if msg.Event == config.EVENT_USER_ACTION { if msg.Event == config.EVENT_USER_ACTION {
b.mc.SendMessageEvent(channel, "m.room.message", b.mc.SendMessageEvent(channel, "m.room.message",
matrix.TextMessage{"m.emote", msg.Username + msg.Text}) matrix.TextMessage{"m.emote", msg.Username + msg.Text})
return nil return "", nil
} }
b.mc.SendText(channel, msg.Username+msg.Text) b.mc.SendText(channel, msg.Username+msg.Text)
return nil return "", nil
} }
func (b *Bmatrix) getRoomID(channel string) string { func (b *Bmatrix) getRoomID(channel string) string {
@@ -100,7 +104,7 @@ func (b *Bmatrix) getRoomID(channel string) string {
func (b *Bmatrix) handlematrix() error { func (b *Bmatrix) handlematrix() error {
syncer := b.mc.Syncer.(*matrix.DefaultSyncer) syncer := b.mc.Syncer.(*matrix.DefaultSyncer)
syncer.OnEventType("m.room.message", func(ev *matrix.Event) { syncer.OnEventType("m.room.message", func(ev *matrix.Event) {
if (ev.Content["msgtype"].(string) == "m.text" || ev.Content["msgtype"].(string) == "m.emote") && ev.Sender != b.UserID { if (ev.Content["msgtype"].(string) == "m.text" || ev.Content["msgtype"].(string) == "m.notice" || ev.Content["msgtype"].(string) == "m.emote") && ev.Sender != b.UserID {
b.RLock() b.RLock()
channel, ok := b.RoomMap[ev.RoomID] channel, ok := b.RoomMap[ev.RoomID]
b.RUnlock() b.RUnlock()

View File

@@ -2,6 +2,7 @@ package bmattermost
import ( import (
"errors" "errors"
"fmt"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/matterclient" "github.com/42wim/matterbridge/matterclient"
"github.com/42wim/matterbridge/matterhook" "github.com/42wim/matterbridge/matterhook"
@@ -23,6 +24,8 @@ type MMMessage struct {
Channel string Channel string
Username string Username string
UserID string UserID string
ID string
Event string
} }
type Bmattermost struct { type Bmattermost struct {
@@ -61,6 +64,12 @@ func (b *Bmattermost) Connect() error {
b.mh = matterhook.New(b.Config.WebhookURL, b.mh = matterhook.New(b.Config.WebhookURL,
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify, matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
BindAddress: b.Config.WebhookBindAddress}) BindAddress: b.Config.WebhookBindAddress})
} else if b.Config.Token != "" {
flog.Info("Connecting using token (sending)")
err := b.apiLogin()
if err != nil {
return err
}
} else if b.Config.Login != "" { } else if b.Config.Login != "" {
flog.Info("Connecting using login/password (sending)") flog.Info("Connecting using login/password (sending)")
err := b.apiLogin() err := b.apiLogin()
@@ -81,7 +90,14 @@ func (b *Bmattermost) Connect() error {
b.mh = matterhook.New(b.Config.WebhookURL, b.mh = matterhook.New(b.Config.WebhookURL,
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify, matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
DisableServer: true}) DisableServer: true})
if b.Config.Login != "" { if b.Config.Token != "" {
flog.Info("Connecting using token (receiving)")
err := b.apiLogin()
if err != nil {
return err
}
go b.handleMatter()
} else if b.Config.Login != "" {
flog.Info("Connecting using login/password (receiving)") flog.Info("Connecting using login/password (receiving)")
err := b.apiLogin() err := b.apiLogin()
if err != nil { if err != nil {
@@ -90,6 +106,13 @@ func (b *Bmattermost) Connect() error {
go b.handleMatter() go b.handleMatter()
} }
return nil return nil
} else if b.Config.Token != "" {
flog.Info("Connecting using token (sending and receiving)")
err := b.apiLogin()
if err != nil {
return err
}
go b.handleMatter()
} else if b.Config.Login != "" { } else if b.Config.Login != "" {
flog.Info("Connecting using login/password (sending and receiving)") flog.Info("Connecting using login/password (sending and receiving)")
err := b.apiLogin() err := b.apiLogin()
@@ -98,8 +121,8 @@ func (b *Bmattermost) Connect() error {
} }
go b.handleMatter() go b.handleMatter()
} }
if b.Config.WebhookBindAddress == "" && b.Config.WebhookURL == "" && b.Config.Login == "" { if b.Config.WebhookBindAddress == "" && b.Config.WebhookURL == "" && b.Config.Login == "" && b.Config.Token == "" {
return errors.New("No connection method found. See that you have WebhookBindAddress, WebhookURL or Login/Password/Server/Team configured.") return errors.New("No connection method found. See that you have WebhookBindAddress, WebhookURL or Token/Login/Password/Server/Team configured.")
} }
return nil return nil
} }
@@ -108,15 +131,19 @@ func (b *Bmattermost) Disconnect() error {
return nil return nil
} }
func (b *Bmattermost) JoinChannel(channel string) error { func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error {
// we can only join channels using the API // we can only join channels using the API
if b.Config.WebhookURL == "" && b.Config.WebhookBindAddress == "" { if b.Config.WebhookURL == "" && b.Config.WebhookBindAddress == "" {
return b.mc.JoinChannel(b.mc.GetChannelId(channel, "")) id := b.mc.GetChannelId(channel.Name, "")
if id == "" {
return fmt.Errorf("Could not find channel ID for channel %s", channel.Name)
}
return b.mc.JoinChannel(id)
} }
return nil return nil
} }
func (b *Bmattermost) Send(msg config.Message) error { func (b *Bmattermost) Send(msg config.Message) (string, error) {
flog.Debugf("Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
if msg.Event == config.EVENT_USER_ACTION { if msg.Event == config.EVENT_USER_ACTION {
msg.Text = "*" + msg.Text + "*" msg.Text = "*" + msg.Text + "*"
@@ -138,12 +165,20 @@ func (b *Bmattermost) Send(msg config.Message) error {
err := b.mh.Send(matterMessage) err := b.mh.Send(matterMessage)
if err != nil { if err != nil {
flog.Info(err) flog.Info(err)
return err return "", err
} }
return nil return "", nil
} }
b.mc.PostMessage(b.mc.GetChannelId(channel, ""), message) if msg.Event == config.EVENT_MSG_DELETE {
return nil if msg.ID == "" {
return "", nil
}
return msg.ID, b.mc.DeleteMessage(msg.ID)
}
if msg.ID != "" {
return b.mc.EditMessage(msg.ID, message)
}
return b.mc.PostMessage(b.mc.GetChannelId(channel, ""), message)
} }
func (b *Bmattermost) handleMatter() { func (b *Bmattermost) handleMatter() {
@@ -152,17 +187,22 @@ func (b *Bmattermost) handleMatter() {
flog.Debugf("Choosing webhooks based receiving") flog.Debugf("Choosing webhooks based receiving")
go b.handleMatterHook(mchan) go b.handleMatterHook(mchan)
} else { } else {
flog.Debugf("Choosing login/password based receiving") if b.Config.Token != "" {
flog.Debugf("Choosing token based receiving")
} else {
flog.Debugf("Choosing login/password based receiving")
}
go b.handleMatterClient(mchan) go b.handleMatterClient(mchan)
} }
for message := range mchan { for message := range mchan {
rmsg := config.Message{Username: message.Username, Channel: message.Channel, Account: b.Account, UserID: message.UserID} rmsg := config.Message{Username: message.Username, Channel: message.Channel, Account: b.Account, UserID: message.UserID, ID: message.ID, Event: message.Event}
text, ok := b.replaceAction(message.Text) text, ok := b.replaceAction(message.Text)
if ok { if ok {
rmsg.Event = config.EVENT_USER_ACTION rmsg.Event = config.EVENT_USER_ACTION
} }
rmsg.Text = text rmsg.Text = text
flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account) flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account)
flog.Debugf("Message is %#v", rmsg)
b.Remote <- rmsg b.Remote <- rmsg
} }
} }
@@ -182,7 +222,7 @@ func (b *Bmattermost) handleMatterClient(mchan chan *MMMessage) {
} }
// do not post our own messages back to irc // do not post our own messages back to irc
// only listen to message from our team // only listen to message from our team
if (message.Raw.Event == "posted" || message.Raw.Event == "post_edited") && if (message.Raw.Event == "posted" || message.Raw.Event == "post_edited" || message.Raw.Event == "post_deleted") &&
b.mc.User.Username != message.Username && message.Raw.Data["team_id"].(string) == b.TeamId { b.mc.User.Username != message.Username && message.Raw.Data["team_id"].(string) == b.TeamId {
// if the message has reactions don't repost it (for now, until we can correlate reaction with message) // if the message has reactions don't repost it (for now, until we can correlate reaction with message)
if message.Post.HasReactions { if message.Post.HasReactions {
@@ -194,9 +234,13 @@ func (b *Bmattermost) handleMatterClient(mchan chan *MMMessage) {
m.Username = message.Username m.Username = message.Username
m.Channel = message.Channel m.Channel = message.Channel
m.Text = message.Text m.Text = message.Text
m.ID = message.Post.Id
if message.Raw.Event == "post_edited" && !b.Config.EditDisable { if message.Raw.Event == "post_edited" && !b.Config.EditDisable {
m.Text = message.Text + b.Config.EditSuffix m.Text = message.Text + b.Config.EditSuffix
} }
if message.Raw.Event == "post_deleted" {
m.Event = config.EVENT_MSG_DELETE
}
if len(message.Post.FileIds) > 0 { if len(message.Post.FileIds) > 0 {
for _, link := range b.mc.GetFileLinks(message.Post.FileIds) { for _, link := range b.mc.GetFileLinks(message.Post.FileIds) {
m.Text = m.Text + "\n" + link m.Text = m.Text + "\n" + link
@@ -221,7 +265,12 @@ func (b *Bmattermost) handleMatterHook(mchan chan *MMMessage) {
} }
func (b *Bmattermost) apiLogin() error { func (b *Bmattermost) apiLogin() error {
b.mc = matterclient.New(b.Config.Login, b.Config.Password, password := b.Config.Password
if b.Config.Token != "" {
password = "MMAUTHTOKEN=" + b.Config.Token
}
b.mc = matterclient.New(b.Config.Login, password,
b.Config.Team, b.Config.Server) b.Config.Team, b.Config.Server)
b.mc.SkipTLSVerify = b.Config.SkipTLSVerify b.mc.SkipTLSVerify = b.Config.SkipTLSVerify
b.mc.NoTLS = b.Config.NoTLS b.mc.NoTLS = b.Config.NoTLS

View File

@@ -53,11 +53,15 @@ func (b *Brocketchat) Disconnect() error {
} }
func (b *Brocketchat) JoinChannel(channel string) error { func (b *Brocketchat) JoinChannel(channel config.ChannelInfo) error {
return nil return nil
} }
func (b *Brocketchat) Send(msg config.Message) error { func (b *Brocketchat) Send(msg config.Message) (string, error) {
// ignore delete messages
if msg.Event == config.EVENT_MSG_DELETE {
return "", nil
}
flog.Debugf("Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL} matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
matterMessage.Channel = msg.Channel matterMessage.Channel = msg.Channel
@@ -67,9 +71,9 @@ func (b *Brocketchat) Send(msg config.Message) error {
err := b.mh.Send(matterMessage) err := b.mh.Send(matterMessage)
if err != nil { if err != nil {
flog.Info(err) flog.Info(err)
return err return "", err
} }
return nil return "", nil
} }
func (b *Brocketchat) handleRocketHook() { func (b *Brocketchat) handleRocketHook() {

View File

@@ -108,14 +108,14 @@ func (b *Bslack) Disconnect() error {
} }
func (b *Bslack) JoinChannel(channel string) error { func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
// we can only join channels using the API // we can only join channels using the API
if b.Config.WebhookURL == "" && b.Config.WebhookBindAddress == "" { if b.Config.WebhookURL == "" && b.Config.WebhookBindAddress == "" {
if strings.HasPrefix(b.Config.Token, "xoxb") { if strings.HasPrefix(b.Config.Token, "xoxb") {
// TODO check if bot has already joined channel // TODO check if bot has already joined channel
return nil return nil
} }
_, err := b.sc.JoinChannel(channel) _, err := b.sc.JoinChannel(channel.Name)
if err != nil { if err != nil {
if err.Error() != "name_taken" { if err.Error() != "name_taken" {
return err return err
@@ -125,7 +125,7 @@ func (b *Bslack) JoinChannel(channel string) error {
return nil return nil
} }
func (b *Bslack) Send(msg config.Message) error { func (b *Bslack) Send(msg config.Message) (string, error) {
flog.Debugf("Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
if msg.Event == config.EVENT_USER_ACTION { if msg.Event == config.EVENT_USER_ACTION {
msg.Text = "_" + msg.Text + "_" msg.Text = "_" + msg.Text + "_"
@@ -145,13 +145,13 @@ func (b *Bslack) Send(msg config.Message) error {
err := b.mh.Send(matterMessage) err := b.mh.Send(matterMessage)
if err != nil { if err != nil {
flog.Info(err) flog.Info(err)
return err return "", err
} }
return nil return "", nil
} }
schannel, err := b.getChannelByName(channel) schannel, err := b.getChannelByName(channel)
if err != nil { if err != nil {
return err return "", err
} }
np := slack.NewPostMessageParameters() np := slack.NewPostMessageParameters()
if b.Config.PrefixMessagesWithNick { if b.Config.PrefixMessagesWithNick {
@@ -163,14 +163,30 @@ func (b *Bslack) Send(msg config.Message) error {
np.IconURL = msg.Avatar np.IconURL = msg.Avatar
} }
np.Attachments = append(np.Attachments, slack.Attachment{CallbackID: "matterbridge"}) np.Attachments = append(np.Attachments, slack.Attachment{CallbackID: "matterbridge"})
b.sc.PostMessage(schannel.ID, message, np) // replace mentions
np.LinkNames = 1
/* if msg.Event == config.EVENT_MSG_DELETE {
newmsg := b.rtm.NewOutgoingMessage(message, schannel.ID) // some protocols echo deletes, but with empty ID
b.rtm.SendMessage(newmsg) if msg.ID == "" {
*/ return "", nil
}
return nil // we get a "slack <ID>", split it
ts := strings.Fields(msg.ID)
b.sc.DeleteMessage(schannel.ID, ts[1])
return "", nil
}
// if we have no ID it means we're creating a new message, not updating an existing one
if msg.ID != "" {
ts := strings.Fields(msg.ID)
b.sc.UpdateMessage(schannel.ID, ts[1], message)
return "", nil
}
_, id, err := b.sc.PostMessage(schannel.ID, message, np)
if err != nil {
return "", err
}
return "slack " + id, nil
} }
func (b *Bslack) getAvatar(user string) string { func (b *Bslack) getAvatar(user string) string {
@@ -225,83 +241,90 @@ func (b *Bslack) handleSlack() {
if b.Config.WebhookURL == "" && b.Config.WebhookBindAddress == "" && message.Username == b.si.User.Name { if b.Config.WebhookURL == "" && b.Config.WebhookBindAddress == "" && message.Username == b.si.User.Name {
continue continue
} }
if message.Text == "" || message.Username == "" { if (message.Text == "" || message.Username == "") && message.Raw.SubType != "message_deleted" {
continue continue
} }
texts := strings.Split(message.Text, "\n") text := message.Text
for _, text := range texts { text = b.replaceURL(text)
text = b.replaceURL(text) text = html.UnescapeString(text)
text = html.UnescapeString(text) flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account)
flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account) msg := config.Message{Text: text, Username: message.Username, Channel: message.Channel, Account: b.Account, Avatar: b.getAvatar(message.Username), UserID: message.UserID, ID: "slack " + message.Raw.Timestamp}
msg := config.Message{Text: text, Username: message.Username, Channel: message.Channel, Account: b.Account, Avatar: b.getAvatar(message.Username), UserID: message.UserID} if message.Raw.SubType == "me_message" {
if message.Raw.SubType == "me_message" { msg.Event = config.EVENT_USER_ACTION
msg.Event = config.EVENT_USER_ACTION
}
b.Remote <- msg
} }
if message.Raw.SubType == "channel_leave" || message.Raw.SubType == "channel_join" {
msg.Username = "system"
msg.Event = config.EVENT_JOIN_LEAVE
}
// edited messages have a submessage, use this timestamp
if message.Raw.SubMessage != nil {
msg.ID = "slack " + message.Raw.SubMessage.Timestamp
}
if message.Raw.SubType == "message_deleted" {
msg.Text = config.EVENT_MSG_DELETE
msg.Event = config.EVENT_MSG_DELETE
msg.ID = "slack " + message.Raw.DeletedTimestamp
}
flog.Debugf("Message is %#v", msg)
b.Remote <- msg
} }
} }
func (b *Bslack) handleSlackClient(mchan chan *MMMessage) { func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
count := 0
for msg := range b.rtm.IncomingEvents { for msg := range b.rtm.IncomingEvents {
switch ev := msg.Data.(type) { switch ev := msg.Data.(type) {
case *slack.MessageEvent: case *slack.MessageEvent:
// ignore first message flog.Debugf("Receiving from slackclient %#v", ev)
if count > 0 { if len(ev.Attachments) > 0 {
flog.Debugf("Receiving from slackclient %#v", ev) // skip messages we made ourselves
if len(ev.Attachments) > 0 { if ev.Attachments[0].CallbackID == "matterbridge" {
// skip messages we made ourselves continue
if ev.Attachments[0].CallbackID == "matterbridge" {
continue
}
} }
if !b.Config.EditDisable && ev.SubMessage != nil && ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp { }
flog.Debugf("SubMessage %#v", ev.SubMessage) if !b.Config.EditDisable && ev.SubMessage != nil && ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp {
ev.User = ev.SubMessage.User flog.Debugf("SubMessage %#v", ev.SubMessage)
ev.Text = ev.SubMessage.Text + b.Config.EditSuffix ev.User = ev.SubMessage.User
} ev.Text = ev.SubMessage.Text + b.Config.EditSuffix
// use our own func because rtm.GetChannelInfo doesn't work for private channels }
channel, err := b.getChannelByID(ev.Channel) // use our own func because rtm.GetChannelInfo doesn't work for private channels
channel, err := b.getChannelByID(ev.Channel)
if err != nil {
continue
}
m := &MMMessage{}
if ev.BotID == "" && ev.SubType != "message_deleted" {
user, err := b.rtm.GetUserInfo(ev.User)
if err != nil { if err != nil {
continue continue
} }
m := &MMMessage{} m.UserID = user.ID
if ev.BotID == "" { m.Username = user.Name
user, err := b.rtm.GetUserInfo(ev.User)
if err != nil {
continue
}
m.UserID = user.ID
m.Username = user.Name
}
m.Channel = channel.Name
m.Text = ev.Text
if m.Text == "" {
for _, attach := range ev.Attachments {
if attach.Text != "" {
m.Text = attach.Text
} else {
m.Text = attach.Fallback
}
}
}
m.Raw = ev
m.Text = b.replaceMention(m.Text)
// when using webhookURL we can't check if it's our webhook or not for now
if ev.BotID != "" && b.Config.WebhookURL == "" {
bot, err := b.rtm.GetBotInfo(ev.BotID)
if err != nil {
continue
}
if bot.Name != "" {
m.Username = bot.Name
m.UserID = bot.ID
}
}
mchan <- m
} }
count++ m.Channel = channel.Name
m.Text = ev.Text
if m.Text == "" {
for _, attach := range ev.Attachments {
if attach.Text != "" {
m.Text = attach.Text
} else {
m.Text = attach.Fallback
}
}
}
m.Raw = ev
m.Text = b.replaceMention(m.Text)
// when using webhookURL we can't check if it's our webhook or not for now
if ev.BotID != "" && b.Config.WebhookURL == "" {
bot, err := b.rtm.GetBotInfo(ev.BotID)
if err != nil {
continue
}
if bot.Name != "" {
m.Username = bot.Name
m.UserID = bot.ID
}
}
mchan <- m
case *slack.OutgoingErrorEvent: case *slack.OutgoingErrorEvent:
flog.Debugf("%#v", ev.Error()) flog.Debugf("%#v", ev.Error())
case *slack.ChannelJoinedEvent: case *slack.ChannelJoinedEvent:

View File

@@ -60,8 +60,8 @@ func (b *Bsteam) Disconnect() error {
} }
func (b *Bsteam) JoinChannel(channel string) error { func (b *Bsteam) JoinChannel(channel config.ChannelInfo) error {
id, err := steamid.NewId(channel) id, err := steamid.NewId(channel.Name)
if err != nil { if err != nil {
return err return err
} }
@@ -69,13 +69,17 @@ func (b *Bsteam) JoinChannel(channel string) error {
return nil return nil
} }
func (b *Bsteam) Send(msg config.Message) error { func (b *Bsteam) Send(msg config.Message) (string, error) {
// ignore delete messages
if msg.Event == config.EVENT_MSG_DELETE {
return "", nil
}
id, err := steamid.NewId(msg.Channel) id, err := steamid.NewId(msg.Channel)
if err != nil { if err != nil {
return err return "", err
} }
b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text) b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text)
return nil return "", nil
} }
func (b *Bsteam) getNick(id steamid.SteamId) string { func (b *Bsteam) getNick(id steamid.SteamId) string {

View File

@@ -53,26 +53,57 @@ func (b *Btelegram) Disconnect() error {
} }
func (b *Btelegram) JoinChannel(channel string) error { func (b *Btelegram) JoinChannel(channel config.ChannelInfo) error {
return nil return nil
} }
func (b *Btelegram) Send(msg config.Message) error { func (b *Btelegram) Send(msg config.Message) (string, error) {
flog.Debugf("Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
chatid, err := strconv.ParseInt(msg.Channel, 10, 64) chatid, err := strconv.ParseInt(msg.Channel, 10, 64)
if err != nil { if err != nil {
return err return "", err
} }
if b.Config.MessageFormat == "HTML" { if b.Config.MessageFormat == "HTML" {
msg.Text = makeHTML(msg.Text) msg.Text = makeHTML(msg.Text)
} }
if msg.Event == config.EVENT_MSG_DELETE {
if msg.ID == "" {
return "", nil
}
msgid, err := strconv.Atoi(msg.ID)
if err != nil {
return "", err
}
_, err = b.c.DeleteMessage(tgbotapi.DeleteMessageConfig{ChatID: chatid, MessageID: msgid})
return "", err
}
// edit the message if we have a msg ID
if msg.ID != "" {
msgid, err := strconv.Atoi(msg.ID)
if err != nil {
return "", err
}
m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text)
_, err = b.c.Send(m)
if err != nil {
return "", err
}
return "", nil
}
m := tgbotapi.NewMessage(chatid, msg.Username+msg.Text) m := tgbotapi.NewMessage(chatid, msg.Username+msg.Text)
if b.Config.MessageFormat == "HTML" { if b.Config.MessageFormat == "HTML" {
m.ParseMode = tgbotapi.ModeHTML m.ParseMode = tgbotapi.ModeHTML
} }
_, err = b.c.Send(m) res, err := b.c.Send(m)
return err if err != nil {
return "", err
}
return strconv.Itoa(res.MessageID), nil
} }
func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) { func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
@@ -131,7 +162,7 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
} }
if text != "" { if text != "" {
flog.Debugf("Sending message from %s on %s to gateway", username, b.Account) flog.Debugf("Sending message from %s on %s to gateway", username, b.Account)
b.Remote <- config.Message{Username: username, Text: text, Channel: channel, Account: b.Account, UserID: strconv.Itoa(message.From.ID)} b.Remote <- config.Message{Username: username, Text: text, Channel: channel, Account: b.Account, UserID: strconv.Itoa(message.From.ID), ID: strconv.Itoa(message.MessageID)}
} }
} }
} }

View File

@@ -74,15 +74,19 @@ func (b *Bxmpp) Disconnect() error {
return nil return nil
} }
func (b *Bxmpp) JoinChannel(channel string) error { func (b *Bxmpp) JoinChannel(channel config.ChannelInfo) error {
b.xc.JoinMUCNoHistory(channel+"@"+b.Config.Muc, b.Config.Nick) b.xc.JoinMUCNoHistory(channel.Name+"@"+b.Config.Muc, b.Config.Nick)
return nil return nil
} }
func (b *Bxmpp) Send(msg config.Message) error { func (b *Bxmpp) Send(msg config.Message) (string, error) {
// ignore delete messages
if msg.Event == config.EVENT_MSG_DELETE {
return "", nil
}
flog.Debugf("Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.Config.Muc, Text: msg.Username + msg.Text}) b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.Config.Muc, Text: msg.Username + msg.Text})
return nil return "", nil
} }
func (b *Bxmpp) createXMPP() (*xmpp.Client, error) { func (b *Bxmpp) createXMPP() (*xmpp.Client, error) {

View File

@@ -1,3 +1,56 @@
# v1.2.0
## Breaking changes
* If you're running a discord bridge, update to this release before 16 october otherwise
it will stop working. (see https://discordapp.com/developers/docs/reference)
## New features
* general: Add delete support. (actually delete the messages on bridges that support it)
(mattermost,discord,gitter,slack,telegram)
## Bugfix
* Do not break messages on newline (slack). Closes #258
* Update telegram library
* Update discord library (supports v6 API now). Old API is deprecated on 16 October
# v1.1.2
## New features
* general: also build darwin binaries
* mattermost: add support for mattermost 4.2.x
## Bugfix
* mattermost: Send images when text is empty regression. (mattermost). Closes #254
* slack: also send the first messsage after connect. #252
# v1.1.1
## Bugfix
* mattermost: fix public links
# v1.1.0
## New features
* general: Add better editing support. (actually edit the messages on bridges that support it)
(mattermost,discord,gitter,slack,telegram)
* mattermost: use API v4 (removes support for mattermost < 3.8)
* mattermost: add support for personal access tokens (since mattermost 4.1)
Use ```Token="yourtoken"``` in mattermost config
See https://docs.mattermost.com/developer/personal-access-tokens.html for more info
* matrix: Relay notices (matrix). Closes #243
* irc: Add a charset option. Closes #247
## Bugfix
* slack: Handle leave/join events (slack). Closes #246
* slack: Replace mentions from other bridges. (slack). Closes #233
* gitter: remove ZWSP after messages
# v1.0.1
## New features
* mattermost: add support for mattermost 4.1.x
* discord: allow a webhookURL per channel #239
# v1.0.0
## New features
* general: Add action support for slack,mattermost,irc,gitter,matrix,xmpp,discord. #199
* discord: Shows the username instead of the server nickname #234
# v1.0.0-rc1 # v1.0.0-rc1
## New features ## New features
* general: Add action support for slack,mattermost,irc,gitter,matrix,xmpp,discord. #199 * general: Add action support for slack,mattermost,irc,gitter,matrix,xmpp,discord. #199

View File

@@ -1,10 +1,11 @@
#!/bin/bash #!/bin/bash
go version |grep go1.8 || exit go version |grep go1.9 || exit
VERSION=$(git describe --tags) VERSION=$(git describe --tags)
mkdir ci/binaries mkdir ci/binaries
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-win64.exe GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-windows-amd64.exe
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-linux64 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-linux-amd64
GOOS=linux GOARCH=arm go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-linux-arm GOOS=linux GOARCH=arm go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-linux-arm
GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-darwin-amd64
cd ci cd ci
cat > deploy.json <<EOF cat > deploy.json <<EOF
{ {

View File

@@ -6,6 +6,7 @@ import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
// "github.com/davecgh/go-spew/spew" // "github.com/davecgh/go-spew/spew"
"github.com/hashicorp/golang-lru"
"github.com/peterhellberg/emojilib" "github.com/peterhellberg/emojilib"
"regexp" "regexp"
"strings" "strings"
@@ -21,11 +22,19 @@ type Gateway struct {
ChannelOptions map[string]config.ChannelOptions ChannelOptions map[string]config.ChannelOptions
Message chan config.Message Message chan config.Message
Name string Name string
Messages *lru.Cache
}
type BrMsgID struct {
br *bridge.Bridge
ID string
} }
func New(cfg config.Gateway, r *Router) *Gateway { func New(cfg config.Gateway, r *Router) *Gateway {
gw := &Gateway{Channels: make(map[string]*config.ChannelInfo), Message: r.Message, gw := &Gateway{Channels: make(map[string]*config.ChannelInfo), Message: r.Message,
Router: r, Bridges: make(map[string]*bridge.Bridge), Config: r.Config} Router: r, Bridges: make(map[string]*bridge.Bridge), Config: r.Config}
cache, _ := lru.New(5000)
gw.Messages = cache
gw.AddConfig(&cfg) gw.AddConfig(&cfg)
return gw return gw
} }
@@ -136,15 +145,16 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []con
return channels return channels
} }
func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) { func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrMsgID {
var brMsgIDs []*BrMsgID
// only relay join/part when configged // only relay join/part when configged
if msg.Event == config.EVENT_JOIN_LEAVE && !gw.Bridges[dest.Account].Config.ShowJoinPart { if msg.Event == config.EVENT_JOIN_LEAVE && !gw.Bridges[dest.Account].Config.ShowJoinPart {
return return brMsgIDs
} }
// broadcast to every out channel (irc QUIT) // broadcast to every out channel (irc QUIT)
if msg.Channel == "" && msg.Event != config.EVENT_JOIN_LEAVE { if msg.Channel == "" && msg.Event != config.EVENT_JOIN_LEAVE {
log.Debug("empty channel") log.Debug("empty channel")
return return brMsgIDs
} }
originchannel := msg.Channel originchannel := msg.Channel
origmsg := msg origmsg := msg
@@ -158,15 +168,29 @@ func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) {
msg.Channel = channel.Name msg.Channel = channel.Name
msg.Avatar = gw.modifyAvatar(origmsg, dest) msg.Avatar = gw.modifyAvatar(origmsg, dest)
msg.Username = gw.modifyUsername(origmsg, dest) msg.Username = gw.modifyUsername(origmsg, dest)
msg.ID = ""
if res, ok := gw.Messages.Get(origmsg.ID); ok {
IDs := res.([]*BrMsgID)
for _, id := range IDs {
if dest.Protocol == id.br.Protocol {
msg.ID = id.ID
}
}
}
// for api we need originchannel as channel // for api we need originchannel as channel
if dest.Protocol == "api" { if dest.Protocol == "api" {
msg.Channel = originchannel msg.Channel = originchannel
} }
err := dest.Send(msg) mID, err := dest.Send(msg)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
// append the message ID (mID) from this bridge (dest) to our brMsgIDs slice
if mID != "" {
brMsgIDs = append(brMsgIDs, &BrMsgID{dest, mID})
}
} }
return brMsgIDs
} }
func (gw *Gateway) ignoreMessage(msg *config.Message) bool { func (gw *Gateway) ignoreMessage(msg *config.Message) bool {

View File

@@ -94,11 +94,17 @@ func (r *Router) handleReceive() {
} }
} }
for _, gw := range r.Gateways { for _, gw := range r.Gateways {
// record all the message ID's of the different bridges
var msgIDs []*BrMsgID
if !gw.ignoreMessage(&msg) { if !gw.ignoreMessage(&msg) {
msg.Timestamp = time.Now() msg.Timestamp = time.Now()
gw.modifyMessage(&msg) gw.modifyMessage(&msg)
for _, br := range gw.Bridges { for _, br := range gw.Bridges {
gw.handleMessage(msg, br) msgIDs = append(msgIDs, gw.handleMessage(msg, br)...)
}
// only add the message ID if it doesn't already exists
if _, ok := gw.Messages.Get(msg.ID); !ok && msg.ID != "" {
gw.Messages.Add(msg.ID, msgIDs)
} }
} }
} }

View File

@@ -5,14 +5,13 @@ import (
"fmt" "fmt"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/gateway" "github.com/42wim/matterbridge/gateway"
//"github.com/42wim/matterbridge/gateway/samechannel"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/google/gops/agent" "github.com/google/gops/agent"
"strings" "strings"
) )
var ( var (
version = "1.0.0-rc1" version = "1.2.0"
githash string githash string
) )

View File

@@ -32,6 +32,23 @@ UseSASL=false
#OPTIONAL (default false) #OPTIONAL (default false)
SkipTLSVerify=true SkipTLSVerify=true
#If you know your charset, you can specify it manually.
#Otherwise it tries to detect this automatically. Select one below
# "iso-8859-2:1987", "iso-8859-9:1989", "866", "latin9", "iso-8859-10:1992", "iso-ir-109", "hebrew",
# "cp932", "iso-8859-15", "cp437", "utf-16be", "iso-8859-3:1988", "windows-1251", "utf16", "latin6",
# "latin3", "iso-8859-1:1987", "iso-8859-9", "utf-16le", "big5", "cp819", "asmo-708", "utf-8",
# "ibm437", "iso-ir-157", "iso-ir-144", "latin4", "850", "iso-8859-5", "iso-8859-5:1988", "l3",
# "windows-31j", "utf8", "iso-8859-3", "437", "greek", "iso-8859-8", "l6", "l9-iso-8859-15",
# "iso-8859-2", "latin2", "iso-ir-100", "iso-8859-6", "arabic", "iso-ir-148", "us-ascii", "x-sjis",
# "utf16be", "iso-8859-8:1988", "utf16le", "l4", "utf-16", "iso-ir-138", "iso-8859-7", "iso-8859-7:1987",
# "windows-1252", "l2", "koi8-r", "iso8859-1", "latin1", "ecma-114", "iso-ir-110", "elot-928",
# "iso-ir-126", "iso-8859-1", "iso-ir-127", "cp850", "cyrillic", "greek8", "windows-1250", "iso-latin-1",
# "l5", "ibm866", "cp866", "ms-kanji", "ibm850", "ecma-118", "iso-ir-101", "ibm819", "l1", "iso-8859-6:1987",
# "latin5", "ascii", "sjis", "iso-8859-10", "iso-8859-4", "iso-8859-4:1988", "shift-jis
# The select charset will be converted to utf-8 when sent to other bridges.
#OPTIONAL (default "")
Charset=""
#Your nick on irc. #Your nick on irc.
#REQUIRED #REQUIRED
Nick="matterbot" Nick="matterbot"
@@ -213,6 +230,11 @@ Team="yourteam"
Login="yourlogin" Login="yourlogin"
Password="yourpass" Password="yourpass"
#personal access token of the bot.
#new feature since mattermost 4.1. See https://docs.mattermost.com/developer/personal-access-tokens.html
#OPTIONAL (you can use token instead of login/password)
#Token="abcdefghijklm"
#Enable this to make a http connection (instead of https) to your mattermost. #Enable this to make a http connection (instead of https) to your mattermost.
#OPTIONAL (default false) #OPTIONAL (default false)
NoTLS=false NoTLS=false
@@ -361,7 +383,6 @@ WebhookURL="https://hooks.slack.com/services/yourhook"
#AND DEDICATED BOT USER WHEN POSSIBLE! #AND DEDICATED BOT USER WHEN POSSIBLE!
#Address to listen on for outgoing webhook requests from slack #Address to listen on for outgoing webhook requests from slack
#See account settings - integrations - outgoing webhooks on slack #See account settings - integrations - outgoing webhooks on slack
#This setting will not be used when useAPI is eanbled
#webhooks #webhooks
#OPTIONAL #OPTIONAL
WebhookBindAddress="0.0.0.0:9999" WebhookBindAddress="0.0.0.0:9999"
@@ -443,7 +464,12 @@ Server="yourservername"
#OPTIONAL (default false) #OPTIONAL (default false)
ShowEmbeds=false ShowEmbeds=false
#Shows the username (minus the discriminator) instead of the server nickname
#OPTIONAL (default false)
UseUserName=false
#Specify WebhookURL. If given, will relay messages using the Webhook, which gives a better look to messages. #Specify WebhookURL. If given, will relay messages using the Webhook, which gives a better look to messages.
#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
#OPTIONAL (default empty) #OPTIONAL (default empty)
WebhookURL="Yourwebhooktokenhere" WebhookURL="Yourwebhooktokenhere"
@@ -829,6 +855,14 @@ enable=true
#OPTIONAL - your irc channel key #OPTIONAL - your irc channel key
key="yourkey" key="yourkey"
[[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"
#API example #API example
#[[gateway.inout]] #[[gateway.inout]]
#account="api.local" #account="api.local"

View File

@@ -6,7 +6,6 @@
[mattermost] [mattermost]
[mattermost.work] [mattermost.work]
useAPI=true
#do not prefix it wit http:// or https:// #do not prefix it wit http:// or https://
Server="yourmattermostserver.domain" Server="yourmattermostserver.domain"
Team="yourteam" Team="yourteam"

View File

@@ -9,7 +9,6 @@ import (
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"net/url" "net/url"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -45,8 +44,8 @@ type Message struct {
type Team struct { type Team struct {
Team *model.Team Team *model.Team
Id string Id string
Channels *model.ChannelList Channels []*model.Channel
MoreChannels *model.ChannelList MoreChannels []*model.Channel
Users map[string]*model.User Users map[string]*model.User
} }
@@ -55,7 +54,7 @@ type MMClient struct {
*Credentials *Credentials
Team *Team Team *Team
OtherTeams []*Team OtherTeams []*Team
Client *model.Client Client *model.Client4
User *model.User User *model.User
Users map[string]*model.User Users map[string]*model.User
MessageChan chan *Message MessageChan chan *Message
@@ -109,21 +108,21 @@ func (m *MMClient) Login() error {
uriScheme = "http://" uriScheme = "http://"
} }
// login to mattermost // login to mattermost
m.Client = model.NewClient(uriScheme + m.Credentials.Server) m.Client = model.NewAPIv4Client(uriScheme + m.Credentials.Server)
m.Client.HttpClient.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, Proxy: http.ProxyFromEnvironment} m.Client.HttpClient.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, Proxy: http.ProxyFromEnvironment}
m.Client.HttpClient.Timeout = time.Second * 10 m.Client.HttpClient.Timeout = time.Second * 10
for { for {
d := b.Duration() d := b.Duration()
// bogus call to get the serverversion // bogus call to get the serverversion
_, err := m.Client.GetClientProperties() _, resp := m.Client.Logout()
if err != nil { if resp.Error != nil {
return fmt.Errorf("%#v", err.Error()) return fmt.Errorf("%#v", resp.Error.Error())
} }
if firstConnection && !supportedVersion(m.Client.ServerVersion) { if firstConnection && !supportedVersion(resp.ServerVersion) {
return fmt.Errorf("unsupported mattermost version: %s", m.Client.ServerVersion) return fmt.Errorf("unsupported mattermost version: %s", resp.ServerVersion)
} }
m.ServerVersion = m.Client.ServerVersion m.ServerVersion = resp.ServerVersion
if m.ServerVersion == "" { if m.ServerVersion == "" {
m.log.Debugf("Server not up yet, reconnecting in %s", d) m.log.Debugf("Server not up yet, reconnecting in %s", d)
time.Sleep(d) time.Sleep(d)
@@ -134,30 +133,33 @@ func (m *MMClient) Login() error {
} }
b.Reset() b.Reset()
var myinfo *model.Result var resp *model.Response
//var myinfo *model.Result
var appErr *model.AppError var appErr *model.AppError
var logmsg = "trying login" var logmsg = "trying login"
for { for {
m.log.Debugf("%s %s %s %s", logmsg, m.Credentials.Team, m.Credentials.Login, m.Credentials.Server) m.log.Debugf("%s %s %s %s", logmsg, m.Credentials.Team, m.Credentials.Login, m.Credentials.Server)
if strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN) { if strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN) {
m.log.Debugf(logmsg+" with %s", model.SESSION_COOKIE_TOKEN) m.log.Debugf(logmsg + " with token")
token := strings.Split(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN+"=") token := strings.Split(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN+"=")
if len(token) != 2 { if len(token) != 2 {
return errors.New("incorrect MMAUTHTOKEN. valid input is MMAUTHTOKEN=yourtoken") return errors.New("incorrect MMAUTHTOKEN. valid input is MMAUTHTOKEN=yourtoken")
} }
m.Client.HttpClient.Jar = m.createCookieJar(token[1]) m.Client.HttpClient.Jar = m.createCookieJar(token[1])
m.Client.MockSession(token[1]) m.Client.AuthToken = token[1]
myinfo, appErr = m.Client.GetMe("") m.Client.AuthType = model.HEADER_BEARER
if appErr != nil { m.User, resp = m.Client.GetMe("")
return errors.New(appErr.DetailedError) if resp.Error != nil {
return resp.Error
} }
if myinfo.Data.(*model.User) == nil { if m.User == nil {
m.log.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass) m.log.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass)
return errors.New("invalid " + model.SESSION_COOKIE_TOKEN) return errors.New("invalid " + model.SESSION_COOKIE_TOKEN)
} }
} else { } else {
_, appErr = m.Client.Login(m.Credentials.Login, m.Credentials.Pass) m.User, resp = m.Client.Login(m.Credentials.Login, m.Credentials.Pass)
} }
appErr = resp.Error
if appErr != nil { if appErr != nil {
d := b.Duration() d := b.Duration()
m.log.Debug(appErr.DetailedError) m.log.Debug(appErr.DetailedError)
@@ -185,8 +187,6 @@ func (m *MMClient) Login() error {
if m.Team == nil { if m.Team == nil {
return errors.New("team not found") return errors.New("team not found")
} }
// set our team id as default route
m.Client.SetTeamId(m.Team.Id)
m.wsConnect() m.wsConnect()
@@ -207,7 +207,7 @@ func (m *MMClient) wsConnect() {
} }
// setup websocket connection // setup websocket connection
wsurl := wsScheme + m.Credentials.Server + model.API_URL_SUFFIX_V3 + "/users/websocket" wsurl := wsScheme + m.Credentials.Server + model.API_URL_SUFFIX_V4 + "/websocket"
header := http.Header{} header := http.Header{}
header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken) header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken)
@@ -241,9 +241,9 @@ func (m *MMClient) Logout() error {
m.log.Debug("Not invalidating session in logout, credential is a token") m.log.Debug("Not invalidating session in logout, credential is a token")
return nil return nil
} }
_, err := m.Client.Logout() _, resp := m.Client.Logout()
if err != nil { if resp.Error != nil {
return err return resp.Error
} }
return nil return nil
} }
@@ -277,6 +277,13 @@ func (m *MMClient) WsReceiver() {
// check if we didn't empty the message // check if we didn't empty the message
if msg.Text != "" { if msg.Text != "" {
m.MessageChan <- msg m.MessageChan <- msg
continue
}
// if we have file attached but the message is empty, also send it
if msg.Post != nil {
if msg.Text != "" || len(msg.Post.FileIds) > 0 {
m.MessageChan <- msg
}
} }
continue continue
} }
@@ -292,7 +299,7 @@ func (m *MMClient) WsReceiver() {
func (m *MMClient) parseMessage(rmsg *Message) { func (m *MMClient) parseMessage(rmsg *Message) {
switch rmsg.Raw.Event { switch rmsg.Raw.Event {
case model.WEBSOCKET_EVENT_POSTED, model.WEBSOCKET_EVENT_POST_EDITED: case model.WEBSOCKET_EVENT_POSTED, model.WEBSOCKET_EVENT_POST_EDITED, model.WEBSOCKET_EVENT_POST_DELETED:
m.parseActionPost(rmsg) m.parseActionPost(rmsg)
/* /*
case model.ACTION_USER_REMOVED: case model.ACTION_USER_REMOVED:
@@ -349,33 +356,34 @@ func (m *MMClient) parseActionPost(rmsg *Message) {
} }
func (m *MMClient) UpdateUsers() error { func (m *MMClient) UpdateUsers() error {
mmusers, err := m.Client.GetProfiles(0, 50000, "") mmusers, resp := m.Client.GetUsers(0, 50000, "")
if err != nil { if resp.Error != nil {
return errors.New(err.DetailedError) return errors.New(resp.Error.DetailedError)
} }
m.Lock() m.Lock()
m.Users = mmusers.Data.(map[string]*model.User) for _, user := range mmusers {
m.Users[user.Id] = user
}
m.Unlock() m.Unlock()
return nil return nil
} }
func (m *MMClient) UpdateChannels() error { func (m *MMClient) UpdateChannels() error {
mmchannels, err := m.Client.GetChannels("") mmchannels, resp := m.Client.GetChannelsForTeamForUser(m.Team.Id, m.User.Id, "")
if err != nil { if resp.Error != nil {
return errors.New(err.DetailedError) return errors.New(resp.Error.DetailedError)
}
var mmchannels2 *model.Result
if m.mmVersion() >= 3.08 {
mmchannels2, err = m.Client.GetMoreChannelsPage(0, 5000)
} else {
mmchannels2, err = m.Client.GetMoreChannels("")
}
if err != nil {
return errors.New(err.DetailedError)
} }
m.Lock() m.Lock()
m.Team.Channels = mmchannels.Data.(*model.ChannelList) m.Team.Channels = mmchannels
m.Team.MoreChannels = mmchannels2.Data.(*model.ChannelList) m.Unlock()
mmchannels, resp = m.Client.GetPublicChannelsForTeam(m.Team.Id, 0, 5000, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
m.Lock()
m.Team.MoreChannels = mmchannels
m.Unlock() m.Unlock()
return nil return nil
} }
@@ -388,14 +396,14 @@ func (m *MMClient) GetChannelName(channelId string) string {
continue continue
} }
if t.Channels != nil { if t.Channels != nil {
for _, channel := range *t.Channels { for _, channel := range t.Channels {
if channel.Id == channelId { if channel.Id == channelId {
return channel.Name return channel.Name
} }
} }
} }
if t.MoreChannels != nil { if t.MoreChannels != nil {
for _, channel := range *t.MoreChannels { for _, channel := range t.MoreChannels {
if channel.Id == channelId { if channel.Id == channelId {
return channel.Name return channel.Name
} }
@@ -413,7 +421,7 @@ func (m *MMClient) GetChannelId(name string, teamId string) string {
} }
for _, t := range m.OtherTeams { for _, t := range m.OtherTeams {
if t.Id == teamId { if t.Id == teamId {
for _, channel := range append(*t.Channels, *t.MoreChannels...) { for _, channel := range append(t.Channels, t.MoreChannels...) {
if channel.Name == name { if channel.Name == name {
return channel.Id return channel.Id
} }
@@ -427,7 +435,7 @@ func (m *MMClient) GetChannelTeamId(id string) string {
m.RLock() m.RLock()
defer m.RUnlock() defer m.RUnlock()
for _, t := range append(m.OtherTeams, m.Team) { for _, t := range append(m.OtherTeams, m.Team) {
for _, channel := range append(*t.Channels, *t.MoreChannels...) { for _, channel := range append(t.Channels, t.MoreChannels...) {
if channel.Id == id { if channel.Id == id {
return channel.TeamId return channel.TeamId
} }
@@ -440,7 +448,7 @@ func (m *MMClient) GetChannelHeader(channelId string) string {
m.RLock() m.RLock()
defer m.RUnlock() defer m.RUnlock()
for _, t := range m.OtherTeams { for _, t := range m.OtherTeams {
for _, channel := range append(*t.Channels, *t.MoreChannels...) { for _, channel := range append(t.Channels, t.MoreChannels...) {
if channel.Id == channelId { if channel.Id == channelId {
return channel.Header return channel.Header
} }
@@ -450,55 +458,76 @@ func (m *MMClient) GetChannelHeader(channelId string) string {
return "" return ""
} }
func (m *MMClient) PostMessage(channelId string, text string) { func (m *MMClient) PostMessage(channelId string, text string) (string, error) {
post := &model.Post{ChannelId: channelId, Message: text} post := &model.Post{ChannelId: channelId, Message: text}
m.Client.CreatePost(post) res, resp := m.Client.CreatePost(post)
if resp.Error != nil {
return "", resp.Error
}
return res.Id, nil
}
func (m *MMClient) EditMessage(postId string, text string) (string, error) {
post := &model.Post{Message: text}
res, resp := m.Client.UpdatePost(postId, post)
if resp.Error != nil {
return "", resp.Error
}
return res.Id, nil
}
func (m *MMClient) DeleteMessage(postId string) error {
_, resp := m.Client.DeletePost(postId)
if resp.Error != nil {
return resp.Error
}
return nil
} }
func (m *MMClient) JoinChannel(channelId string) error { func (m *MMClient) JoinChannel(channelId string) error {
m.RLock() m.RLock()
defer m.RUnlock() defer m.RUnlock()
for _, c := range *m.Team.Channels { for _, c := range m.Team.Channels {
if c.Id == channelId { if c.Id == channelId {
m.log.Debug("Not joining ", channelId, " already joined.") m.log.Debug("Not joining ", channelId, " already joined.")
return nil return nil
} }
} }
m.log.Debug("Joining ", channelId) m.log.Debug("Joining ", channelId)
_, err := m.Client.JoinChannel(channelId) _, resp := m.Client.AddChannelMember(channelId, m.User.Id)
if err != nil { if resp.Error != nil {
return errors.New("failed to join") return resp.Error
} }
return nil return nil
} }
func (m *MMClient) GetPostsSince(channelId string, time int64) *model.PostList { func (m *MMClient) GetPostsSince(channelId string, time int64) *model.PostList {
res, err := m.Client.GetPostsSince(channelId, time) res, resp := m.Client.GetPostsSince(channelId, time)
if err != nil { if resp.Error != nil {
return nil return nil
} }
return res.Data.(*model.PostList) return res
} }
func (m *MMClient) SearchPosts(query string) *model.PostList { func (m *MMClient) SearchPosts(query string) *model.PostList {
res, err := m.Client.SearchPosts(query, false) res, resp := m.Client.SearchPosts(m.Team.Id, query, false)
if err != nil { if resp.Error != nil {
return nil return nil
} }
return res.Data.(*model.PostList) return res
} }
func (m *MMClient) GetPosts(channelId string, limit int) *model.PostList { func (m *MMClient) GetPosts(channelId string, limit int) *model.PostList {
res, err := m.Client.GetPosts(channelId, 0, limit, "") res, resp := m.Client.GetPostsForChannel(channelId, 0, limit, "")
if err != nil { if resp.Error != nil {
return nil return nil
} }
return res.Data.(*model.PostList) return res
} }
func (m *MMClient) GetPublicLink(filename string) string { func (m *MMClient) GetPublicLink(filename string) string {
res, err := m.Client.GetPublicLink(filename) res, resp := m.Client.GetFileLink(filename)
if err != nil { if resp.Error != nil {
return "" return ""
} }
return res return res
@@ -507,8 +536,8 @@ func (m *MMClient) GetPublicLink(filename string) string {
func (m *MMClient) GetPublicLinks(filenames []string) []string { func (m *MMClient) GetPublicLinks(filenames []string) []string {
var output []string var output []string
for _, f := range filenames { for _, f := range filenames {
res, err := m.Client.GetPublicLink(f) res, resp := m.Client.GetFileLink(f)
if err != nil { if resp.Error != nil {
continue continue
} }
output = append(output, res) output = append(output, res)
@@ -524,8 +553,8 @@ func (m *MMClient) GetFileLinks(filenames []string) []string {
var output []string var output []string
for _, f := range filenames { for _, f := range filenames {
res, err := m.Client.GetPublicLink(f) res, resp := m.Client.GetFileLink(f)
if err != nil { if resp.Error != nil {
// public links is probably disabled, create the link ourselves // public links is probably disabled, create the link ourselves
output = append(output, uriScheme+m.Credentials.Server+model.API_URL_SUFFIX_V3+"/files/"+f+"/get") output = append(output, uriScheme+m.Credentials.Server+model.API_URL_SUFFIX_V3+"/files/"+f+"/get")
continue continue
@@ -536,42 +565,43 @@ func (m *MMClient) GetFileLinks(filenames []string) []string {
} }
func (m *MMClient) UpdateChannelHeader(channelId string, header string) { func (m *MMClient) UpdateChannelHeader(channelId string, header string) {
data := make(map[string]string) channel := &model.Channel{Id: channelId, Header: header}
data["channel_id"] = channelId
data["channel_header"] = header
m.log.Debugf("updating channelheader %#v, %#v", channelId, header) m.log.Debugf("updating channelheader %#v, %#v", channelId, header)
_, err := m.Client.UpdateChannelHeader(data) _, resp := m.Client.UpdateChannel(channel)
if err != nil { if resp.Error != nil {
log.Error(err) log.Error(resp.Error)
} }
} }
func (m *MMClient) UpdateLastViewed(channelId string) { func (m *MMClient) UpdateLastViewed(channelId string) {
m.log.Debugf("posting lastview %#v", channelId) m.log.Debugf("posting lastview %#v", channelId)
if m.mmVersion() >= 3.08 { view := &model.ChannelView{ChannelId: channelId}
view := model.ChannelView{ChannelId: channelId} res, _ := m.Client.ViewChannel(m.User.Id, view)
res, _ := m.Client.ViewChannel(view) if !res {
if !res { m.log.Errorf("ChannelView update for %s failed", channelId)
m.log.Errorf("ChannelView update for %s failed", channelId)
}
return
}
_, err := m.Client.UpdateLastViewedAt(channelId, true)
if err != nil {
m.log.Error(err)
} }
} }
func (m *MMClient) UpdateUserNick(nick string) error {
user := m.User
user.Nickname = nick
_, resp := m.Client.UpdateUser(user)
if resp.Error != nil {
return resp.Error
}
return nil
}
func (m *MMClient) UsernamesInChannel(channelId string) []string { func (m *MMClient) UsernamesInChannel(channelId string) []string {
res, err := m.Client.GetProfilesInChannel(channelId, 0, 50000, "") res, resp := m.Client.GetChannelMembers(channelId, 0, 50000, "")
if err != nil { if resp.Error != nil {
m.log.Errorf("UsernamesInChannel(%s) failed: %s", channelId, err) m.log.Errorf("UsernamesInChannel(%s) failed: %s", channelId, resp.Error)
return []string{} return []string{}
} }
members := res.Data.(map[string]*model.User) allusers := m.GetUsers()
result := []string{} result := []string{}
for _, member := range members { for _, member := range *res {
result = append(result, member.Nickname) result = append(result, allusers[member.UserId].Nickname)
} }
return result return result
} }
@@ -595,22 +625,15 @@ func (m *MMClient) createCookieJar(token string) *cookiejar.Jar {
func (m *MMClient) SendDirectMessage(toUserId string, msg string) { func (m *MMClient) SendDirectMessage(toUserId string, msg string) {
m.log.Debugf("SendDirectMessage to %s, msg %s", toUserId, msg) m.log.Debugf("SendDirectMessage to %s, msg %s", toUserId, msg)
// create DM channel (only happens on first message) // create DM channel (only happens on first message)
_, err := m.Client.CreateDirectChannel(toUserId) _, resp := m.Client.CreateDirectChannel(m.User.Id, toUserId)
if err != nil { if resp.Error != nil {
m.log.Debugf("SendDirectMessage to %#v failed: %s", toUserId, err) m.log.Debugf("SendDirectMessage to %#v failed: %s", toUserId, resp.Error)
return return
} }
channelName := model.GetDMNameFromIds(toUserId, m.User.Id) channelName := model.GetDMNameFromIds(toUserId, m.User.Id)
// update our channels // update our channels
mmchannels, err := m.Client.GetChannels("") m.UpdateChannels()
if err != nil {
m.log.Debug("SendDirectMessage: Couldn't update channels")
return
}
m.Lock()
m.Team.Channels = mmchannels.Data.(*model.ChannelList)
m.Unlock()
// build & send the message // build & send the message
msg = strings.Replace(msg, "\r", "", -1) msg = strings.Replace(msg, "\r", "", -1)
@@ -636,10 +659,10 @@ func (m *MMClient) GetChannels() []*model.Channel {
defer m.RUnlock() defer m.RUnlock()
var channels []*model.Channel var channels []*model.Channel
// our primary team channels first // our primary team channels first
channels = append(channels, *m.Team.Channels...) channels = append(channels, m.Team.Channels...)
for _, t := range m.OtherTeams { for _, t := range m.OtherTeams {
if t.Id != m.Team.Id { if t.Id != m.Team.Id {
channels = append(channels, *t.Channels...) channels = append(channels, t.Channels...)
} }
} }
return channels return channels
@@ -651,7 +674,7 @@ func (m *MMClient) GetMoreChannels() []*model.Channel {
defer m.RUnlock() defer m.RUnlock()
var channels []*model.Channel var channels []*model.Channel
for _, t := range m.OtherTeams { for _, t := range m.OtherTeams {
channels = append(channels, *t.MoreChannels...) channels = append(channels, t.MoreChannels...)
} }
return channels return channels
} }
@@ -662,9 +685,9 @@ func (m *MMClient) GetTeamFromChannel(channelId string) string {
defer m.RUnlock() defer m.RUnlock()
var channels []*model.Channel var channels []*model.Channel
for _, t := range m.OtherTeams { for _, t := range m.OtherTeams {
channels = append(channels, *t.Channels...) channels = append(channels, t.Channels...)
if t.MoreChannels != nil { if t.MoreChannels != nil {
channels = append(channels, *t.MoreChannels...) channels = append(channels, t.MoreChannels...)
} }
for _, c := range channels { for _, c := range channels {
if c.Id == channelId { if c.Id == channelId {
@@ -678,12 +701,11 @@ func (m *MMClient) GetTeamFromChannel(channelId string) string {
func (m *MMClient) GetLastViewedAt(channelId string) int64 { func (m *MMClient) GetLastViewedAt(channelId string) int64 {
m.RLock() m.RLock()
defer m.RUnlock() defer m.RUnlock()
res, err := m.Client.GetChannel(channelId, "") res, resp := m.Client.GetChannelMember(channelId, m.User.Id, "")
if err != nil { if resp.Error != nil {
return model.GetMillis() return model.GetMillis()
} }
data := res.Data.(*model.ChannelData) return res.LastViewedAt
return data.Member.LastViewedAt
} }
func (m *MMClient) GetUsers() map[string]*model.User { func (m *MMClient) GetUsers() map[string]*model.User {
@@ -701,12 +723,11 @@ func (m *MMClient) GetUser(userId string) *model.User {
defer m.Unlock() defer m.Unlock()
_, ok := m.Users[userId] _, ok := m.Users[userId]
if !ok { if !ok {
res, err := m.Client.GetProfilesByIds([]string{userId}) res, resp := m.Client.GetUser(userId, "")
if err != nil { if resp.Error != nil {
return nil return nil
} }
u := res.Data.(map[string]*model.User)[userId] m.Users[userId] = res
m.Users[userId] = u
} }
return m.Users[userId] return m.Users[userId]
} }
@@ -720,36 +741,36 @@ func (m *MMClient) GetUserName(userId string) string {
} }
func (m *MMClient) GetStatus(userId string) string { func (m *MMClient) GetStatus(userId string) string {
res, err := m.Client.GetStatusesByIds([]string{userId}) res, resp := m.Client.GetUserStatus(userId, "")
if err != nil { if resp.Error != nil {
return "" return ""
} }
status := res.Data.(map[string]string) if res.Status == model.STATUS_AWAY {
if status[userId] == model.STATUS_AWAY {
return "away" return "away"
} }
if status[userId] == model.STATUS_ONLINE { if res.Status == model.STATUS_ONLINE {
return "online" return "online"
} }
return "offline" return "offline"
} }
func (m *MMClient) GetStatuses() map[string]string { func (m *MMClient) GetStatuses() map[string]string {
var ok bool var ids []string
statuses := make(map[string]string) statuses := make(map[string]string)
res, err := m.Client.GetStatuses() for id := range m.Users {
if err != nil { ids = append(ids, id)
}
res, resp := m.Client.GetUsersStatusesByIds(ids)
if resp.Error != nil {
return statuses return statuses
} }
if statuses, ok = res.Data.(map[string]string); ok { for _, status := range res {
for userId, status := range statuses { statuses[status.UserId] = "offline"
statuses[userId] = "offline" if status.Status == model.STATUS_AWAY {
if status == model.STATUS_AWAY { statuses[status.UserId] = "away"
statuses[userId] = "away" }
} if status.Status == model.STATUS_ONLINE {
if status == model.STATUS_ONLINE { statuses[status.UserId] = "online"
statuses[userId] = "online"
}
} }
} }
return statuses return statuses
@@ -800,40 +821,39 @@ func (m *MMClient) StatusLoop() {
func (m *MMClient) initUser() error { func (m *MMClient) initUser() error {
m.Lock() m.Lock()
defer m.Unlock() defer m.Unlock()
initLoad, err := m.Client.GetInitialLoad()
if err != nil {
return err
}
initData := initLoad.Data.(*model.InitialLoad)
m.User = initData.User
// we only load all team data on initial login. // we only load all team data on initial login.
// all other updates are for channels from our (primary) team only. // all other updates are for channels from our (primary) team only.
//m.log.Debug("initUser(): loading all team data") //m.log.Debug("initUser(): loading all team data")
for _, v := range initData.Teams { teams, resp := m.Client.GetTeamsForUser(m.User.Id, "")
m.Client.SetTeamId(v.Id) if resp.Error != nil {
mmusers, err := m.Client.GetProfiles(0, 50000, "") return resp.Error
if err != nil { }
return errors.New(err.DetailedError) for _, team := range teams {
mmusers, resp := m.Client.GetUsersInTeam(team.Id, 0, 50000, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
} }
t := &Team{Team: v, Users: mmusers.Data.(map[string]*model.User), Id: v.Id} usermap := make(map[string]*model.User)
mmchannels, err := m.Client.GetChannels("") for _, user := range mmusers {
if err != nil { usermap[user.Id] = user
return errors.New(err.DetailedError)
} }
t.Channels = mmchannels.Data.(*model.ChannelList)
if m.mmVersion() >= 3.08 { t := &Team{Team: team, Users: usermap, Id: team.Id}
mmchannels, err = m.Client.GetMoreChannelsPage(0, 5000)
} else { mmchannels, resp := m.Client.GetChannelsForTeamForUser(team.Id, m.User.Id, "")
mmchannels, err = m.Client.GetMoreChannels("") if resp.Error != nil {
return resp.Error
} }
if err != nil { t.Channels = mmchannels
return errors.New(err.DetailedError) mmchannels, resp = m.Client.GetPublicChannelsForTeam(team.Id, 0, 5000, "")
if resp.Error != nil {
return resp.Error
} }
t.MoreChannels = mmchannels.Data.(*model.ChannelList) t.MoreChannels = mmchannels
m.OtherTeams = append(m.OtherTeams, t) m.OtherTeams = append(m.OtherTeams, t)
if v.Name == m.Credentials.Team { if team.Name == m.Credentials.Team {
m.Team = t m.Team = t
m.log.Debugf("initUser(): found our team %s (id: %s)", v.Name, v.Id) m.log.Debugf("initUser(): found our team %s (id: %s)", team.Name, team.Id)
} }
// add all users // add all users
for k, v := range t.Users { for k, v := range t.Users {
@@ -854,22 +874,13 @@ func (m *MMClient) sendWSRequest(action string, data map[string]interface{}) err
return nil return nil
} }
func (m *MMClient) mmVersion() float64 {
v, _ := strconv.ParseFloat(string(m.ServerVersion[0:2])+"0"+string(m.ServerVersion[2]), 64)
if string(m.ServerVersion[4]) == "." {
v, _ = strconv.ParseFloat(m.ServerVersion[0:4], 64)
}
return v
}
func supportedVersion(version string) bool { func supportedVersion(version string) bool {
if strings.HasPrefix(version, "3.5.0") || if strings.HasPrefix(version, "3.8.0") ||
strings.HasPrefix(version, "3.6.0") ||
strings.HasPrefix(version, "3.7.0") ||
strings.HasPrefix(version, "3.8.0") ||
strings.HasPrefix(version, "3.9.0") || strings.HasPrefix(version, "3.9.0") ||
strings.HasPrefix(version, "3.10.0") || strings.HasPrefix(version, "3.10.0") ||
strings.HasPrefix(version, "4.0") { strings.HasPrefix(version, "4.0") ||
strings.HasPrefix(version, "4.1") ||
strings.HasPrefix(version, "4.2") {
return true return true
} }
return false return false

View File

@@ -43,6 +43,7 @@ type IMessage struct {
ServiceId string `schema:"service_id"` ServiceId string `schema:"service_id"`
Text string `schema:"text"` Text string `schema:"text"`
TriggerWord string `schema:"trigger_word"` TriggerWord string `schema:"trigger_word"`
FileIDs string `schema:"file_ids"`
} }
// Client for Mattermost. // Client for Mattermost.

View File

@@ -205,17 +205,43 @@ func (gitter *Gitter) GetMessage(roomID, messageID string) (*Message, error) {
} }
// SendMessage sends a message to a room // SendMessage sends a message to a room
func (gitter *Gitter) SendMessage(roomID, text string) error { func (gitter *Gitter) SendMessage(roomID, text string) (*Message, error) {
message := Message{Text: text} message := Message{Text: text}
body, _ := json.Marshal(message) body, _ := json.Marshal(message)
_, err := gitter.post(gitter.config.apiBaseURL+"rooms/"+roomID+"/chatMessages", body) response, err := gitter.post(gitter.config.apiBaseURL+"rooms/"+roomID+"/chatMessages", body)
if err != nil { if err != nil {
gitter.log(err) gitter.log(err)
return err return nil, err
} }
return nil err = json.Unmarshal(response, &message)
if err != nil {
gitter.log(err)
return nil, err
}
return &message, nil
}
// UpdateMessage updates a message in a room
func (gitter *Gitter) UpdateMessage(roomID, msgID, text string) (*Message, error) {
message := Message{Text: text}
body, _ := json.Marshal(message)
response, err := gitter.put(gitter.config.apiBaseURL+"rooms/"+roomID+"/chatMessages/"+msgID, body)
if err != nil {
gitter.log(err)
return nil, err
}
err = json.Unmarshal(response, &message)
if err != nil {
gitter.log(err)
return nil, err
}
return &message, nil
} }
// JoinRoom joins a room // JoinRoom joins a room
@@ -265,7 +291,7 @@ func (gitter *Gitter) SearchRooms(room string) ([]Room, error) {
Results []Room `json:"results"` Results []Room `json:"results"`
} }
response, err := gitter.get(gitter.config.apiBaseURL + "rooms?q=" + room ) response, err := gitter.get(gitter.config.apiBaseURL + "rooms?q=" + room)
if err != nil { if err != nil {
gitter.log(err) gitter.log(err)
@@ -414,6 +440,39 @@ func (gitter *Gitter) post(url string, body []byte) ([]byte, error) {
return result, nil return result, nil
} }
func (gitter *Gitter) put(url string, body []byte) ([]byte, error) {
r, err := http.NewRequest("PUT", url, bytes.NewBuffer(body))
if err != nil {
gitter.log(err)
return nil, err
}
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Accept", "application/json")
r.Header.Set("Authorization", "Bearer "+gitter.config.token)
resp, err := gitter.config.client.Do(r)
if err != nil {
gitter.log(err)
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err = APIError{What: fmt.Sprintf("Status code: %v", resp.StatusCode)}
gitter.log(err)
return nil, err
}
result, err := ioutil.ReadAll(resp.Body)
if err != nil {
gitter.log(err)
return nil, err
}
return result, nil
}
func (gitter *Gitter) delete(url string) ([]byte, error) { func (gitter *Gitter) delete(url string) ([]byte, error) {
r, err := http.NewRequest("delete", url, nil) r, err := http.NewRequest("delete", url, nil)
if err != nil { if err != nil {

View File

@@ -21,7 +21,7 @@ import (
) )
// VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/) // VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/)
const VERSION = "0.16.0" const VERSION = "0.17.0"
// ErrMFA will be risen by New when the user has 2FA. // ErrMFA will be risen by New when the user has 2FA.
var ErrMFA = errors.New("account has 2FA enabled") var ErrMFA = errors.New("account has 2FA enabled")
@@ -59,6 +59,7 @@ func New(args ...interface{}) (s *Session, err error) {
MaxRestRetries: 3, MaxRestRetries: 3,
Client: &http.Client{Timeout: (20 * time.Second)}, Client: &http.Client{Timeout: (20 * time.Second)},
sequence: new(int64), sequence: new(int64),
LastHeartbeatAck: time.Now().UTC(),
} }
// If no arguments are passed return the empty Session interface. // If no arguments are passed return the empty Session interface.

View File

@@ -11,6 +11,9 @@
package discordgo package discordgo
// APIVersion is the Discord API version used for the REST and Websocket API.
var APIVersion = "6"
// Known Discord API Endpoints. // Known Discord API Endpoints.
var ( var (
EndpointStatus = "https://status.discordapp.com/api/v2/" EndpointStatus = "https://status.discordapp.com/api/v2/"
@@ -18,13 +21,14 @@ var (
EndpointSmActive = EndpointSm + "active.json" EndpointSmActive = EndpointSm + "active.json"
EndpointSmUpcoming = EndpointSm + "upcoming.json" EndpointSmUpcoming = EndpointSm + "upcoming.json"
EndpointDiscord = "https://discordapp.com/" EndpointDiscord = "https://discordapp.com/"
EndpointAPI = EndpointDiscord + "api/" EndpointAPI = EndpointDiscord + "api/v" + APIVersion + "/"
EndpointGuilds = EndpointAPI + "guilds/" EndpointGuilds = EndpointAPI + "guilds/"
EndpointChannels = EndpointAPI + "channels/" EndpointChannels = EndpointAPI + "channels/"
EndpointUsers = EndpointAPI + "users/" EndpointUsers = EndpointAPI + "users/"
EndpointGateway = EndpointAPI + "gateway" EndpointGateway = EndpointAPI + "gateway"
EndpointWebhooks = EndpointAPI + "webhooks/" EndpointGatewayBot = EndpointGateway + "/bot"
EndpointWebhooks = EndpointAPI + "webhooks/"
EndpointCDN = "https://cdn.discordapp.com/" EndpointCDN = "https://cdn.discordapp.com/"
EndpointCDNAttachments = EndpointCDN + "attachments/" EndpointCDNAttachments = EndpointCDN + "attachments/"
@@ -54,16 +58,17 @@ var (
EndpointReport = EndpointAPI + "report" EndpointReport = EndpointAPI + "report"
EndpointIntegrations = EndpointAPI + "integrations" EndpointIntegrations = EndpointAPI + "integrations"
EndpointUser = func(uID string) string { return EndpointUsers + uID } EndpointUser = func(uID string) string { return EndpointUsers + uID }
EndpointUserAvatar = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".png" } EndpointUserAvatar = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".png" }
EndpointUserSettings = func(uID string) string { return EndpointUsers + uID + "/settings" } EndpointUserAvatarAnimated = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".gif" }
EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" } EndpointUserSettings = func(uID string) string { return EndpointUsers + uID + "/settings" }
EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID } EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" }
EndpointUserGuildSettings = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID + "/settings" } EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID }
EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" } EndpointUserGuildSettings = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID + "/settings" }
EndpointUserDevices = func(uID string) string { return EndpointUsers + uID + "/devices" } EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" }
EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" } EndpointUserDevices = func(uID string) string { return EndpointUsers + uID + "/devices" }
EndpointUserNotes = func(uID string) string { return EndpointUsers + "@me/notes/" + uID } EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" }
EndpointUserNotes = func(uID string) string { return EndpointUsers + "@me/notes/" + uID }
EndpointGuild = func(gID string) string { return EndpointGuilds + gID } EndpointGuild = func(gID string) string { return EndpointGuilds + gID }
EndpointGuildInivtes = func(gID string) string { return EndpointGuilds + gID + "/invites" } EndpointGuildInivtes = func(gID string) string { return EndpointGuilds + gID + "/invites" }
@@ -103,6 +108,9 @@ var (
EndpointWebhook = func(wID string) string { return EndpointWebhooks + wID } EndpointWebhook = func(wID string) string { return EndpointWebhooks + wID }
EndpointWebhookToken = func(wID, token string) string { return EndpointWebhooks + wID + "/" + token } EndpointWebhookToken = func(wID, token string) string { return EndpointWebhooks + wID + "/" + token }
EndpointMessageReactionsAll = func(cID, mID string) string {
return EndpointChannelMessage(cID, mID) + "/reactions"
}
EndpointMessageReactions = func(cID, mID, eID string) string { EndpointMessageReactions = func(cID, mID, eID string) string {
return EndpointChannelMessage(cID, mID) + "/reactions/" + eID return EndpointChannelMessage(cID, mID) + "/reactions/" + eID
} }

View File

@@ -156,12 +156,20 @@ func (s *Session) removeEventHandlerInstance(t string, ehi *eventHandlerInstance
// Handles calling permanent and once handlers for an event type. // Handles calling permanent and once handlers for an event type.
func (s *Session) handle(t string, i interface{}) { func (s *Session) handle(t string, i interface{}) {
for _, eh := range s.handlers[t] { for _, eh := range s.handlers[t] {
go eh.eventHandler.Handle(s, i) if s.SyncEvents {
eh.eventHandler.Handle(s, i)
} else {
go eh.eventHandler.Handle(s, i)
}
} }
if len(s.onceHandlers[t]) > 0 { if len(s.onceHandlers[t]) > 0 {
for _, eh := range s.onceHandlers[t] { for _, eh := range s.onceHandlers[t] {
go eh.eventHandler.Handle(s, i) if s.SyncEvents {
eh.eventHandler.Handle(s, i)
} else {
go eh.eventHandler.Handle(s, i)
}
} }
s.onceHandlers[t] = nil s.onceHandlers[t] = nil
} }
@@ -216,7 +224,7 @@ func (s *Session) onInterface(i interface{}) {
case *VoiceStateUpdate: case *VoiceStateUpdate:
go s.onVoiceStateUpdate(t) go s.onVoiceStateUpdate(t)
} }
err := s.State.onInterface(s, i) err := s.State.OnInterface(s, i)
if err != nil { if err != nil {
s.log(LogDebug, "error dispatching internal event, %s", err) s.log(LogDebug, "error dispatching internal event, %s", err)
} }

View File

@@ -10,9 +10,24 @@
package discordgo package discordgo
import ( import (
"fmt"
"io" "io"
"regexp" "regexp"
"strings"
)
// MessageType is the type of Message
type MessageType int
// Block contains the valid known MessageType values
const (
MessageTypeDefault MessageType = iota
MessageTypeRecipientAdd
MessageTypeRecipientRemove
MessageTypeCall
MessageTypeChannelNameChange
MessageTypeChannelIconChange
MessageTypeChannelPinnedMessage
MessageTypeGuildMemberJoin
) )
// A Message stores all data related to a specific Discord message. // A Message stores all data related to a specific Discord message.
@@ -30,12 +45,14 @@ type Message struct {
Embeds []*MessageEmbed `json:"embeds"` Embeds []*MessageEmbed `json:"embeds"`
Mentions []*User `json:"mentions"` Mentions []*User `json:"mentions"`
Reactions []*MessageReactions `json:"reactions"` Reactions []*MessageReactions `json:"reactions"`
Type MessageType `json:"type"`
} }
// File stores info about files you e.g. send in messages. // File stores info about files you e.g. send in messages.
type File struct { type File struct {
Name string Name string
Reader io.Reader ContentType string
Reader io.Reader
} }
// MessageSend stores all parameters you can send with ChannelMessageSendComplex. // MessageSend stores all parameters you can send with ChannelMessageSendComplex.
@@ -43,7 +60,10 @@ type MessageSend struct {
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
Embed *MessageEmbed `json:"embed,omitempty"` Embed *MessageEmbed `json:"embed,omitempty"`
Tts bool `json:"tts"` Tts bool `json:"tts"`
File *File `json:"file"` Files []*File `json:"-"`
// TODO: Remove this when compatibility is not required.
File *File `json:"-"`
} }
// MessageEdit is used to chain parameters via ChannelMessageEditComplex, which // MessageEdit is used to chain parameters via ChannelMessageEditComplex, which
@@ -168,13 +188,65 @@ type MessageReactions struct {
// ContentWithMentionsReplaced will replace all @<id> mentions with the // ContentWithMentionsReplaced will replace all @<id> mentions with the
// username of the mention. // username of the mention.
func (m *Message) ContentWithMentionsReplaced() string { func (m *Message) ContentWithMentionsReplaced() (content string) {
if m.Mentions == nil { content = m.Content
return m.Content
}
content := m.Content
for _, user := range m.Mentions { for _, user := range m.Mentions {
content = regexp.MustCompile(fmt.Sprintf("<@!?(%s)>", user.ID)).ReplaceAllString(content, "@"+user.Username) content = strings.NewReplacer(
"<@"+user.ID+">", "@"+user.Username,
"<@!"+user.ID+">", "@"+user.Username,
).Replace(content)
} }
return content return
}
var patternChannels = regexp.MustCompile("<#[^>]*>")
// ContentWithMoreMentionsReplaced will replace all @<id> mentions with the
// username of the mention, but also role IDs and more.
func (m *Message) ContentWithMoreMentionsReplaced(s *Session) (content string, err error) {
content = m.Content
if !s.StateEnabled {
content = m.ContentWithMentionsReplaced()
return
}
channel, err := s.State.Channel(m.ChannelID)
if err != nil {
content = m.ContentWithMentionsReplaced()
return
}
for _, user := range m.Mentions {
nick := user.Username
member, err := s.State.Member(channel.GuildID, user.ID)
if err == nil && member.Nick != "" {
nick = member.Nick
}
content = strings.NewReplacer(
"<@"+user.ID+">", "@"+user.Username,
"<@!"+user.ID+">", "@"+nick,
).Replace(content)
}
for _, roleID := range m.MentionRoles {
role, err := s.State.Role(channel.GuildID, roleID)
if err != nil || !role.Mentionable {
continue
}
content = strings.Replace(content, "<&"+role.ID+">", "@"+role.Name, -1)
}
content = patternChannels.ReplaceAllStringFunc(content, func(mention string) string {
channel, err := s.State.Channel(mention[2 : len(mention)-1])
if err != nil || channel.Type == ChannelTypeGuildVoice {
return mention
}
return "#" + channel.Name
})
return
} }

View File

@@ -3,17 +3,26 @@ package discordgo
import ( import (
"net/http" "net/http"
"strconv" "strconv"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
) )
// customRateLimit holds information for defining a custom rate limit
type customRateLimit struct {
suffix string
requests int
reset time.Duration
}
// RateLimiter holds all ratelimit buckets // RateLimiter holds all ratelimit buckets
type RateLimiter struct { type RateLimiter struct {
sync.Mutex sync.Mutex
global *int64 global *int64
buckets map[string]*Bucket buckets map[string]*Bucket
globalRateLimit time.Duration globalRateLimit time.Duration
customRateLimits []*customRateLimit
} }
// NewRatelimiter returns a new RateLimiter // NewRatelimiter returns a new RateLimiter
@@ -22,6 +31,13 @@ func NewRatelimiter() *RateLimiter {
return &RateLimiter{ return &RateLimiter{
buckets: make(map[string]*Bucket), buckets: make(map[string]*Bucket),
global: new(int64), global: new(int64),
customRateLimits: []*customRateLimit{
&customRateLimit{
suffix: "//reactions//",
requests: 1,
reset: 200 * time.Millisecond,
},
},
} }
} }
@@ -40,6 +56,14 @@ func (r *RateLimiter) getBucket(key string) *Bucket {
global: r.global, global: r.global,
} }
// Check if there is a custom ratelimit set for this bucket ID.
for _, rl := range r.customRateLimits {
if strings.HasSuffix(b.Key, rl.suffix) {
b.customRateLimit = rl
break
}
}
r.buckets[key] = b r.buckets[key] = b
return b return b
} }
@@ -76,13 +100,28 @@ type Bucket struct {
limit int limit int
reset time.Time reset time.Time
global *int64 global *int64
lastReset time.Time
customRateLimit *customRateLimit
} }
// Release unlocks the bucket and reads the headers to update the buckets ratelimit info // Release unlocks the bucket and reads the headers to update the buckets ratelimit info
// and locks up the whole thing in case if there's a global ratelimit. // and locks up the whole thing in case if there's a global ratelimit.
func (b *Bucket) Release(headers http.Header) error { func (b *Bucket) Release(headers http.Header) error {
defer b.Unlock() defer b.Unlock()
// Check if the bucket uses a custom ratelimiter
if rl := b.customRateLimit; rl != nil {
if time.Now().Sub(b.lastReset) >= rl.reset {
b.remaining = rl.requests - 1
b.lastReset = time.Now()
}
if b.remaining < 1 {
b.reset = time.Now().Add(rl.reset)
}
return nil
}
if headers == nil { if headers == nil {
return nil return nil
} }

View File

@@ -23,6 +23,7 @@ import (
"log" "log"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/textproto"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
@@ -309,8 +310,8 @@ func (s *Session) UserUpdate(email, password, username, avatar, newPassword stri
// If left blank, avatar will be set to null/blank // If left blank, avatar will be set to null/blank
data := struct { data := struct {
Email string `json:"email"` Email string `json:"email,omitempty"`
Password string `json:"password"` Password string `json:"password,omitempty"`
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`
Avatar string `json:"avatar,omitempty"` Avatar string `json:"avatar,omitempty"`
NewPassword string `json:"new_password,omitempty"` NewPassword string `json:"new_password,omitempty"`
@@ -763,7 +764,21 @@ func (s *Session) GuildMember(guildID, userID string) (st *Member, err error) {
// userID : The ID of a User // userID : The ID of a User
func (s *Session) GuildMemberDelete(guildID, userID string) (err error) { func (s *Session) GuildMemberDelete(guildID, userID string) (err error) {
_, err = s.RequestWithBucketID("DELETE", EndpointGuildMember(guildID, userID), nil, EndpointGuildMember(guildID, "")) return s.GuildMemberDeleteWithReason(guildID, userID, "")
}
// GuildMemberDeleteWithReason removes the given user from the given guild.
// guildID : The ID of a Guild.
// userID : The ID of a User
// reason : The reason for the kick
func (s *Session) GuildMemberDeleteWithReason(guildID, userID, reason string) (err error) {
uri := EndpointGuildMember(guildID, userID)
if reason != "" {
uri += "?reason=" + url.QueryEscape(reason)
}
_, err = s.RequestWithBucketID("DELETE", uri, nil, EndpointGuildMember(guildID, ""))
return return
} }
@@ -1316,6 +1331,8 @@ func (s *Session) ChannelMessageSend(channelID string, content string) (*Message
}) })
} }
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
// ChannelMessageSendComplex sends a message to the given channel. // ChannelMessageSendComplex sends a message to the given channel.
// channelID : The ID of a Channel. // channelID : The ID of a Channel.
// data : The message struct to send. // data : The message struct to send.
@@ -1326,48 +1343,62 @@ func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend)
endpoint := EndpointChannelMessages(channelID) endpoint := EndpointChannelMessages(channelID)
var response []byte // TODO: Remove this when compatibility is not required.
files := data.Files
if data.File != nil { if data.File != nil {
if files == nil {
files = []*File{data.File}
} else {
err = fmt.Errorf("cannot specify both File and Files")
return
}
}
var response []byte
if len(files) > 0 {
body := &bytes.Buffer{} body := &bytes.Buffer{}
bodywriter := multipart.NewWriter(body) bodywriter := multipart.NewWriter(body)
// What's a better way of doing this? Reflect? Generator? I'm open to suggestions var payload []byte
payload, err = json.Marshal(data)
if data.Content != "" {
if err = bodywriter.WriteField("content", data.Content); err != nil {
return
}
}
if data.Embed != nil {
var embed []byte
embed, err = json.Marshal(data.Embed)
if err != nil {
return
}
err = bodywriter.WriteField("embed", string(embed))
if err != nil {
return
}
}
if data.Tts {
if err = bodywriter.WriteField("tts", "true"); err != nil {
return
}
}
var writer io.Writer
writer, err = bodywriter.CreateFormFile("file", data.File.Name)
if err != nil { if err != nil {
return return
} }
_, err = io.Copy(writer, data.File.Reader) var p io.Writer
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", `form-data; name="payload_json"`)
h.Set("Content-Type", "application/json")
p, err = bodywriter.CreatePart(h)
if err != nil { if err != nil {
return return
} }
if _, err = p.Write(payload); err != nil {
return
}
for i, file := range files {
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file%d"; filename="%s"`, i, quoteEscaper.Replace(file.Name)))
contentType := file.ContentType
if contentType == "" {
contentType = "application/octet-stream"
}
h.Set("Content-Type", contentType)
p, err = bodywriter.CreatePart(h)
if err != nil {
return
}
if _, err = io.Copy(p, file.Reader); err != nil {
return
}
}
err = bodywriter.Close() err = bodywriter.Close()
if err != nil { if err != nil {
return return
@@ -1685,6 +1716,28 @@ func (s *Session) Gateway() (gateway string, err error) {
return return
} }
// GatewayBot returns the websocket Gateway address and the recommended number of shards
func (s *Session) GatewayBot() (st *GatewayBotResponse, err error) {
response, err := s.RequestWithBucketID("GET", EndpointGatewayBot, nil, EndpointGatewayBot)
if err != nil {
return
}
err = unmarshal(response, &st)
if err != nil {
return
}
// Ensure the gateway always has a trailing slash.
// MacOS will fail to connect if we add query params without a trailing slash on the base domain.
if !strings.HasSuffix(st.URL, "/") {
st.URL += "/"
}
return
}
// Functions specific to Webhooks // Functions specific to Webhooks
// WebhookCreate returns a new Webhook. // WebhookCreate returns a new Webhook.
@@ -1810,14 +1863,9 @@ func (s *Session) WebhookEditWithToken(webhookID, token, name, avatar string) (s
// WebhookDelete deletes a webhook for a given ID // WebhookDelete deletes a webhook for a given ID
// webhookID: The ID of a webhook. // webhookID: The ID of a webhook.
func (s *Session) WebhookDelete(webhookID string) (st *Webhook, err error) { func (s *Session) WebhookDelete(webhookID string) (err error) {
body, err := s.RequestWithBucketID("DELETE", EndpointWebhook(webhookID), nil, EndpointWebhooks) _, err = s.RequestWithBucketID("DELETE", EndpointWebhook(webhookID), nil, EndpointWebhooks)
if err != nil {
return
}
err = unmarshal(body, &st)
return return
} }
@@ -1875,6 +1923,16 @@ func (s *Session) MessageReactionRemove(channelID, messageID, emojiID, userID st
return err return err
} }
// MessageReactionsRemoveAll deletes all reactions from a message
// channelID : The channel ID
// messageID : The message ID.
func (s *Session) MessageReactionsRemoveAll(channelID, messageID string) error {
_, err := s.RequestWithBucketID("DELETE", EndpointMessageReactionsAll(channelID, messageID), nil, EndpointMessageReactionsAll(channelID, messageID))
return err
}
// MessageReactions gets all the users reactions for a specific emoji. // MessageReactions gets all the users reactions for a specific emoji.
// channelID : The channel ID. // channelID : The channel ID.
// messageID : The message ID. // messageID : The message ID.

View File

@@ -42,6 +42,7 @@ type State struct {
guildMap map[string]*Guild guildMap map[string]*Guild
channelMap map[string]*Channel channelMap map[string]*Channel
memberMap map[string]map[string]*Member
} }
// NewState creates an empty state. // NewState creates an empty state.
@@ -59,9 +60,18 @@ func NewState() *State {
TrackPresences: true, TrackPresences: true,
guildMap: make(map[string]*Guild), guildMap: make(map[string]*Guild),
channelMap: make(map[string]*Channel), channelMap: make(map[string]*Channel),
memberMap: make(map[string]map[string]*Member),
} }
} }
func (s *State) createMemberMap(guild *Guild) {
members := make(map[string]*Member)
for _, m := range guild.Members {
members[m.User.ID] = m
}
s.memberMap[guild.ID] = members
}
// GuildAdd adds a guild to the current world state, or // GuildAdd adds a guild to the current world state, or
// updates it if it already exists. // updates it if it already exists.
func (s *State) GuildAdd(guild *Guild) error { func (s *State) GuildAdd(guild *Guild) error {
@@ -77,6 +87,14 @@ func (s *State) GuildAdd(guild *Guild) error {
s.channelMap[c.ID] = c s.channelMap[c.ID] = c
} }
// If this guild contains a new member slice, we must regenerate the member map so the pointers stay valid
if guild.Members != nil {
s.createMemberMap(guild)
} else if _, ok := s.memberMap[guild.ID]; !ok {
// Even if we have no new member slice, we still initialize the member map for this guild if it doesn't exist
s.memberMap[guild.ID] = make(map[string]*Member)
}
if g, ok := s.guildMap[guild.ID]; ok { if g, ok := s.guildMap[guild.ID]; ok {
// We are about to replace `g` in the state with `guild`, but first we need to // We are about to replace `g` in the state with `guild`, but first we need to
// make sure we preserve any fields that the `guild` doesn't contain from `g`. // make sure we preserve any fields that the `guild` doesn't contain from `g`.
@@ -271,14 +289,19 @@ func (s *State) MemberAdd(member *Member) error {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
for i, m := range guild.Members { members, ok := s.memberMap[member.GuildID]
if m.User.ID == member.User.ID { if !ok {
guild.Members[i] = member return ErrStateNotFound
return nil }
}
m, ok := members[member.User.ID]
if !ok {
members[member.User.ID] = member
guild.Members = append(guild.Members, member)
} else {
*m = *member // Update the actual data, which will also update the member pointer in the slice
} }
guild.Members = append(guild.Members, member)
return nil return nil
} }
@@ -296,6 +319,17 @@ func (s *State) MemberRemove(member *Member) error {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
members, ok := s.memberMap[member.GuildID]
if !ok {
return ErrStateNotFound
}
_, ok = members[member.User.ID]
if !ok {
return ErrStateNotFound
}
delete(members, member.User.ID)
for i, m := range guild.Members { for i, m := range guild.Members {
if m.User.ID == member.User.ID { if m.User.ID == member.User.ID {
guild.Members = append(guild.Members[:i], guild.Members[i+1:]...) guild.Members = append(guild.Members[:i], guild.Members[i+1:]...)
@@ -312,18 +346,17 @@ func (s *State) Member(guildID, userID string) (*Member, error) {
return nil, ErrNilState return nil, ErrNilState
} }
guild, err := s.Guild(guildID)
if err != nil {
return nil, err
}
s.RLock() s.RLock()
defer s.RUnlock() defer s.RUnlock()
for _, m := range guild.Members { members, ok := s.memberMap[guildID]
if m.User.ID == userID { if !ok {
return m, nil return nil, ErrStateNotFound
} }
m, ok := members[userID]
if ok {
return m, nil
} }
return nil, ErrStateNotFound return nil, ErrStateNotFound
@@ -427,7 +460,7 @@ func (s *State) ChannelAdd(channel *Channel) error {
return nil return nil
} }
if channel.IsPrivate { if channel.Type == ChannelTypeDM || channel.Type == ChannelTypeGroupDM {
s.PrivateChannels = append(s.PrivateChannels, channel) s.PrivateChannels = append(s.PrivateChannels, channel)
} else { } else {
guild, ok := s.guildMap[channel.GuildID] guild, ok := s.guildMap[channel.GuildID]
@@ -454,7 +487,7 @@ func (s *State) ChannelRemove(channel *Channel) error {
return err return err
} }
if channel.IsPrivate { if channel.Type == ChannelTypeDM || channel.Type == ChannelTypeGroupDM {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
@@ -735,6 +768,7 @@ func (s *State) onReady(se *Session, r *Ready) (err error) {
for _, g := range s.Guilds { for _, g := range s.Guilds {
s.guildMap[g.ID] = g s.guildMap[g.ID] = g
s.createMemberMap(g)
for _, c := range g.Channels { for _, c := range g.Channels {
s.channelMap[c.ID] = c s.channelMap[c.ID] = c
@@ -748,8 +782,8 @@ func (s *State) onReady(se *Session, r *Ready) (err error) {
return nil return nil
} }
// onInterface handles all events related to states. // OnInterface handles all events related to states.
func (s *State) onInterface(se *Session, i interface{}) (err error) { func (s *State) OnInterface(se *Session, i interface{}) (err error) {
if s == nil { if s == nil {
return ErrNilState return ErrNilState
} }

View File

@@ -50,6 +50,10 @@ type Session struct {
// active guilds and the members of the guilds. // active guilds and the members of the guilds.
StateEnabled bool StateEnabled bool
// Whether or not to call event handlers synchronously.
// e.g false = launch event handlers in their own goroutines.
SyncEvents bool
// Exposed but should not be modified by User. // Exposed but should not be modified by User.
// Whether the Data Websocket is ready // Whether the Data Websocket is ready
@@ -78,6 +82,9 @@ type Session struct {
// The http client used for REST requests // The http client used for REST requests
Client *http.Client Client *http.Client
// Stores the last HeartbeatAck that was recieved (in UTC)
LastHeartbeatAck time.Time
// Event handlers // Event handlers
handlersMu sync.RWMutex handlersMu sync.RWMutex
handlers map[string][]*eventHandlerInstance handlers map[string][]*eventHandlerInstance
@@ -141,18 +148,30 @@ type Invite struct {
Temporary bool `json:"temporary"` Temporary bool `json:"temporary"`
} }
// ChannelType is the type of a Channel
type ChannelType int
// Block contains known ChannelType values
const (
ChannelTypeGuildText ChannelType = iota
ChannelTypeDM
ChannelTypeGuildVoice
ChannelTypeGroupDM
ChannelTypeGuildCategory
)
// A Channel holds all data related to an individual Discord channel. // A Channel holds all data related to an individual Discord channel.
type Channel struct { type Channel struct {
ID string `json:"id"` ID string `json:"id"`
GuildID string `json:"guild_id"` GuildID string `json:"guild_id"`
Name string `json:"name"` Name string `json:"name"`
Topic string `json:"topic"` Topic string `json:"topic"`
Type string `json:"type"` Type ChannelType `json:"type"`
LastMessageID string `json:"last_message_id"` LastMessageID string `json:"last_message_id"`
NSFW bool `json:"nsfw"`
Position int `json:"position"` Position int `json:"position"`
Bitrate int `json:"bitrate"` Bitrate int `json:"bitrate"`
IsPrivate bool `json:"is_private"` Recipients []*User `json:"recipient"`
Recipient *User `json:"recipient"`
Messages []*Message `json:"-"` Messages []*Message `json:"-"`
PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"` PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"`
} }
@@ -292,13 +311,14 @@ type Presence struct {
Game *Game `json:"game"` Game *Game `json:"game"`
Nick string `json:"nick"` Nick string `json:"nick"`
Roles []string `json:"roles"` Roles []string `json:"roles"`
Since *int `json:"since"`
} }
// A Game struct holds the name of the "playing .." game for a user // A Game struct holds the name of the "playing .." game for a user
type Game struct { type Game struct {
Name string `json:"name"` Name string `json:"name"`
Type int `json:"type"` Type int `json:"type"`
URL string `json:"url"` URL string `json:"url,omitempty"`
} }
// UnmarshalJSON unmarshals json to Game struct // UnmarshalJSON unmarshals json to Game struct
@@ -509,6 +529,12 @@ type MessageReaction struct {
ChannelID string `json:"channel_id"` ChannelID string `json:"channel_id"`
} }
// GatewayBotResponse stores the data for the gateway/bot response
type GatewayBotResponse struct {
URL string `json:"url"`
Shards int `json:"shards"`
}
// Constants for the different bit offsets of text channel permissions // Constants for the different bit offsets of text channel permissions
const ( const (
PermissionReadMessages = 1 << (iota + 10) PermissionReadMessages = 1 << (iota + 10)
@@ -579,3 +605,56 @@ const (
PermissionManageServer | PermissionManageServer |
PermissionAdministrator PermissionAdministrator
) )
// Block contains Discord JSON Error Response codes
const (
ErrCodeUnknownAccount = 10001
ErrCodeUnknownApplication = 10002
ErrCodeUnknownChannel = 10003
ErrCodeUnknownGuild = 10004
ErrCodeUnknownIntegration = 10005
ErrCodeUnknownInvite = 10006
ErrCodeUnknownMember = 10007
ErrCodeUnknownMessage = 10008
ErrCodeUnknownOverwrite = 10009
ErrCodeUnknownProvider = 10010
ErrCodeUnknownRole = 10011
ErrCodeUnknownToken = 10012
ErrCodeUnknownUser = 10013
ErrCodeUnknownEmoji = 10014
ErrCodeBotsCannotUseEndpoint = 20001
ErrCodeOnlyBotsCanUseEndpoint = 20002
ErrCodeMaximumGuildsReached = 30001
ErrCodeMaximumFriendsReached = 30002
ErrCodeMaximumPinsReached = 30003
ErrCodeMaximumGuildRolesReached = 30005
ErrCodeTooManyReactions = 30010
ErrCodeUnauthorized = 40001
ErrCodeMissingAccess = 50001
ErrCodeInvalidAccountType = 50002
ErrCodeCannotExecuteActionOnDMChannel = 50003
ErrCodeEmbedCisabled = 50004
ErrCodeCannotEditFromAnotherUser = 50005
ErrCodeCannotSendEmptyMessage = 50006
ErrCodeCannotSendMessagesToThisUser = 50007
ErrCodeCannotSendMessagesInVoiceChannel = 50008
ErrCodeChannelVerificationLevelTooHigh = 50009
ErrCodeOAuth2ApplicationDoesNotHaveBot = 50010
ErrCodeOAuth2ApplicationLimitReached = 50011
ErrCodeInvalidOAuthState = 50012
ErrCodeMissingPermissions = 50013
ErrCodeInvalidAuthenticationToken = 50014
ErrCodeNoteTooLong = 50015
ErrCodeTooFewOrTooManyMessagesToDelete = 50016
ErrCodeCanOnlyPinMessageToOriginatingChannel = 50019
ErrCodeCannotExecuteActionOnSystemMessage = 50021
ErrCodeMessageProvidedTooOldForBulkDelete = 50034
ErrCodeInvalidFormBody = 50035
ErrCodeInviteAcceptedToGuildApplicationsBotNotIn = 50036
ErrCodeReactionBlocked = 90001
)

View File

@@ -1,6 +1,9 @@
package discordgo package discordgo
import "fmt" import (
"fmt"
"strings"
)
// A User stores all data for an individual Discord user. // A User stores all data for an individual Discord user.
type User struct { type User struct {
@@ -24,3 +27,16 @@ func (u *User) String() string {
func (u *User) Mention() string { func (u *User) Mention() string {
return fmt.Sprintf("<@%s>", u.ID) return fmt.Sprintf("<@%s>", u.ID)
} }
// AvatarURL returns a URL to the user's avatar.
// size: The size of the user's avatar as a power of two
func (u *User) AvatarURL(size string) string {
var URL string
if strings.HasPrefix(u.Avatar, "a_") {
URL = EndpointUserAvatarAnimated(u.ID, u.Avatar)
} else {
URL = EndpointUserAvatar(u.ID, u.Avatar)
}
return URL + "?size=" + size
}

View File

@@ -796,7 +796,7 @@ func (v *VoiceConnection) opusReceiver(udpConn *net.UDPConn, close <-chan struct
} }
// For now, skip anything except audio. // For now, skip anything except audio.
if rlen < 12 || recvbuf[0] != 0x80 { if rlen < 12 || (recvbuf[0] != 0x80 && recvbuf[0] != 0x90) {
continue continue
} }
@@ -810,8 +810,17 @@ func (v *VoiceConnection) opusReceiver(udpConn *net.UDPConn, close <-chan struct
copy(nonce[:], recvbuf[0:12]) copy(nonce[:], recvbuf[0:12])
p.Opus, _ = secretbox.Open(nil, recvbuf[12:rlen], &nonce, &v.op4.SecretKey) p.Opus, _ = secretbox.Open(nil, recvbuf[12:rlen], &nonce, &v.op4.SecretKey)
if len(p.Opus) > 8 && recvbuf[0] == 0x90 {
// Extension bit is set, first 8 bytes is the extended header
p.Opus = p.Opus[8:]
}
if c != nil { if c != nil {
c <- &p select {
case c <- &p:
case <-close:
return
}
} }
} }
} }

View File

@@ -15,7 +15,6 @@ import (
"compress/zlib" "compress/zlib"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io" "io"
"net/http" "net/http"
"runtime" "runtime"
@@ -87,7 +86,7 @@ func (s *Session) Open() (err error) {
} }
// Add the version and encoding to the URL // Add the version and encoding to the URL
s.gateway = fmt.Sprintf("%s?v=5&encoding=json", s.gateway) s.gateway = s.gateway + "?v=" + APIVersion + "&encoding=json"
} }
header := http.Header{} header := http.Header{}
@@ -131,6 +130,7 @@ func (s *Session) Open() (err error) {
// lock. // lock.
s.listening = make(chan interface{}) s.listening = make(chan interface{})
go s.listen(s.wsConn, s.listening) go s.listen(s.wsConn, s.listening)
s.LastHeartbeatAck = time.Now().UTC()
s.Unlock() s.Unlock()
@@ -199,10 +199,13 @@ type helloOp struct {
Trace []string `json:"_trace"` Trace []string `json:"_trace"`
} }
// FailedHeartbeatAcks is the Number of heartbeat intervals to wait until forcing a connection restart.
const FailedHeartbeatAcks time.Duration = 5 * time.Millisecond
// heartbeat sends regular heartbeats to Discord so it knows the client // heartbeat sends regular heartbeats to Discord so it knows the client
// is still connected. If you do not send these heartbeats Discord will // is still connected. If you do not send these heartbeats Discord will
// disconnect the websocket connection after a few seconds. // disconnect the websocket connection after a few seconds.
func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}, i time.Duration) { func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}, heartbeatIntervalMsec time.Duration) {
s.log(LogInformational, "called") s.log(LogInformational, "called")
@@ -211,20 +214,26 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}
} }
var err error var err error
ticker := time.NewTicker(i * time.Millisecond) ticker := time.NewTicker(heartbeatIntervalMsec * time.Millisecond)
defer ticker.Stop() defer ticker.Stop()
for { for {
s.RLock()
last := s.LastHeartbeatAck
s.RUnlock()
sequence := atomic.LoadInt64(s.sequence) sequence := atomic.LoadInt64(s.sequence)
s.log(LogInformational, "sending gateway websocket heartbeat seq %d", sequence) s.log(LogInformational, "sending gateway websocket heartbeat seq %d", sequence)
s.wsMutex.Lock() s.wsMutex.Lock()
err = wsConn.WriteJSON(heartbeatOp{1, sequence}) err = wsConn.WriteJSON(heartbeatOp{1, sequence})
s.wsMutex.Unlock() s.wsMutex.Unlock()
if err != nil { if err != nil || time.Now().UTC().Sub(last) > (heartbeatIntervalMsec*FailedHeartbeatAcks) {
s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err) if err != nil {
s.Lock() s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err)
s.DataReady = false } else {
s.Unlock() s.log(LogError, "haven't gotten a heartbeat ACK in %v, triggering a reconnection", time.Now().UTC().Sub(last))
}
s.Close()
s.reconnect()
return return
} }
s.Lock() s.Lock()
@@ -241,8 +250,10 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}
} }
type updateStatusData struct { type updateStatusData struct {
IdleSince *int `json:"idle_since"` IdleSince *int `json:"since"`
Game *Game `json:"game"` Game *Game `json:"game"`
AFK bool `json:"afk"`
Status string `json:"status"`
} }
type updateStatusOp struct { type updateStatusOp struct {
@@ -265,7 +276,10 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err
return ErrWSNotFound return ErrWSNotFound
} }
var usd updateStatusData usd := updateStatusData{
Status: "online",
}
if idle > 0 { if idle > 0 {
usd.IdleSince = &idle usd.IdleSince = &idle
} }
@@ -398,7 +412,10 @@ func (s *Session) onEvent(messageType int, message []byte) {
// Reconnect // Reconnect
// Must immediately disconnect from gateway and reconnect to new gateway. // Must immediately disconnect from gateway and reconnect to new gateway.
if e.Operation == 7 { if e.Operation == 7 {
// TODO s.log(LogInformational, "Closing and reconnecting in response to Op7")
s.Close()
s.reconnect()
return
} }
// Invalid Session // Invalid Session
@@ -426,6 +443,14 @@ func (s *Session) onEvent(messageType int, message []byte) {
return return
} }
if e.Operation == 11 {
s.Lock()
s.LastHeartbeatAck = time.Now().UTC()
s.Unlock()
s.log(LogInformational, "got heartbeat ACK")
return
}
// Do not try to Dispatch a non-Dispatch Message // Do not try to Dispatch a non-Dispatch Message
if e.Operation != 0 { if e.Operation != 0 {
// But we probably should be doing something with them. // But we probably should be doing something with them.
@@ -688,6 +713,13 @@ func (s *Session) reconnect() {
return return
} }
// Certain race conditions can call reconnect() twice. If this happens, we
// just break out of the reconnect loop
if err == ErrWSAlreadyOpen {
s.log(LogInformational, "Websocket already exists, no need to reconnect")
return
}
s.log(LogError, "error reconnecting to gateway, %s", err) s.log(LogError, "error reconnecting to gateway, %s", err)
<-time.After(wait * time.Second) <-time.After(wait * time.Second)

View File

@@ -191,7 +191,11 @@ func (bot *BotAPI) UploadFile(endpoint string, params map[string]string, fieldna
} }
var apiResp APIResponse var apiResp APIResponse
json.Unmarshal(bytes, &apiResp)
err = json.Unmarshal(bytes, &apiResp)
if err != nil {
return APIResponse{}, err
}
if !apiResp.Ok { if !apiResp.Ok {
return APIResponse{}, errors.New(apiResp.Description) return APIResponse{}, errors.New(apiResp.Description)
@@ -438,14 +442,7 @@ func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) {
return APIResponse{}, err return APIResponse{}, err
} }
var apiResp APIResponse return resp, nil
json.Unmarshal(resp.Result, &apiResp)
if bot.Debug {
log.Printf("setWebhook resp: %+v\n", apiResp)
}
return apiResp, nil
} }
// GetWebhookInfo allows you to fetch information about a webhook and if // GetWebhookInfo allows you to fetch information about a webhook and if
@@ -550,7 +547,7 @@ func (bot *BotAPI) AnswerCallbackQuery(config CallbackConfig) (APIResponse, erro
// KickChatMember kicks a user from a chat. Note that this only will work // KickChatMember kicks a user from a chat. Note that this only will work
// in supergroups, and requires the bot to be an admin. Also note they // in supergroups, and requires the bot to be an admin. Also note they
// will be unable to rejoin until they are unbanned. // will be unable to rejoin until they are unbanned.
func (bot *BotAPI) KickChatMember(config ChatMemberConfig) (APIResponse, error) { func (bot *BotAPI) KickChatMember(config KickChatMemberConfig) (APIResponse, error) {
v := url.Values{} v := url.Values{}
if config.SuperGroupUsername == "" { if config.SuperGroupUsername == "" {
@@ -560,6 +557,10 @@ func (bot *BotAPI) KickChatMember(config ChatMemberConfig) (APIResponse, error)
} }
v.Add("user_id", strconv.Itoa(config.UserID)) v.Add("user_id", strconv.Itoa(config.UserID))
if config.UntilDate != 0 {
v.Add("until_date", strconv.FormatInt(config.UntilDate, 10))
}
bot.debugLog("kickChatMember", v, nil) bot.debugLog("kickChatMember", v, nil)
return bot.MakeRequest("kickChatMember", v) return bot.MakeRequest("kickChatMember", v)
@@ -677,14 +678,16 @@ func (bot *BotAPI) GetChatMember(config ChatConfigWithUser) (ChatMember, error)
} }
// UnbanChatMember unbans a user from a chat. Note that this only will work // UnbanChatMember unbans a user from a chat. Note that this only will work
// in supergroups, and requires the bot to be an admin. // in supergroups and channels, and requires the bot to be an admin.
func (bot *BotAPI) UnbanChatMember(config ChatMemberConfig) (APIResponse, error) { func (bot *BotAPI) UnbanChatMember(config ChatMemberConfig) (APIResponse, error) {
v := url.Values{} v := url.Values{}
if config.SuperGroupUsername == "" { if config.SuperGroupUsername != "" {
v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
} else {
v.Add("chat_id", config.SuperGroupUsername) v.Add("chat_id", config.SuperGroupUsername)
} else if config.ChannelUsername != "" {
v.Add("chat_id", config.ChannelUsername)
} else {
v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
} }
v.Add("user_id", strconv.Itoa(config.UserID)) v.Add("user_id", strconv.Itoa(config.UserID))
@@ -693,6 +696,82 @@ func (bot *BotAPI) UnbanChatMember(config ChatMemberConfig) (APIResponse, error)
return bot.MakeRequest("unbanChatMember", v) return bot.MakeRequest("unbanChatMember", v)
} }
// RestrictChatMember to restrict a user in a supergroup. The bot must be an
//administrator in the supergroup for this to work and must have the
//appropriate admin rights. Pass True for all boolean parameters to lift
//restrictions from a user. Returns True on success.
func (bot *BotAPI) RestrictChatMember(config RestrictChatMemberConfig) (APIResponse, error) {
v := url.Values{}
if config.SuperGroupUsername != "" {
v.Add("chat_id", config.SuperGroupUsername)
} else if config.ChannelUsername != "" {
v.Add("chat_id", config.ChannelUsername)
} else {
v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
}
v.Add("user_id", strconv.Itoa(config.UserID))
if &config.CanSendMessages != nil {
v.Add("can_send_messages", strconv.FormatBool(*config.CanSendMessages))
}
if &config.CanSendMediaMessages != nil {
v.Add("can_send_media_messages", strconv.FormatBool(*config.CanSendMediaMessages))
}
if &config.CanSendOtherMessages != nil {
v.Add("can_send_other_messages", strconv.FormatBool(*config.CanSendOtherMessages))
}
if &config.CanAddWebPagePreviews != nil {
v.Add("can_add_web_page_previews", strconv.FormatBool(*config.CanAddWebPagePreviews))
}
bot.debugLog("restrictChatMember", v, nil)
return bot.MakeRequest("restrictChatMember", v)
}
func (bot *BotAPI) PromoteChatMember(config PromoteChatMemberConfig) (APIResponse, error) {
v := url.Values{}
if config.SuperGroupUsername != "" {
v.Add("chat_id", config.SuperGroupUsername)
} else if config.ChannelUsername != "" {
v.Add("chat_id", config.ChannelUsername)
} else {
v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
}
v.Add("user_id", strconv.Itoa(config.UserID))
if &config.CanChangeInfo != nil {
v.Add("can_change_info", strconv.FormatBool(*config.CanChangeInfo))
}
if &config.CanPostMessages != nil {
v.Add("can_post_messages", strconv.FormatBool(*config.CanPostMessages))
}
if &config.CanEditMessages != nil {
v.Add("can_edit_messages", strconv.FormatBool(*config.CanEditMessages))
}
if &config.CanDeleteMessages != nil {
v.Add("can_delete_messages", strconv.FormatBool(*config.CanDeleteMessages))
}
if &config.CanInviteUsers != nil {
v.Add("can_invite_users", strconv.FormatBool(*config.CanInviteUsers))
}
if &config.CanRestrictMembers != nil {
v.Add("can_restrict_members", strconv.FormatBool(*config.CanRestrictMembers))
}
if &config.CanPinMessages != nil {
v.Add("can_pin_messages", strconv.FormatBool(*config.CanPinMessages))
}
if &config.CanPromoteMembers != nil {
v.Add("can_promote_members", strconv.FormatBool(*config.CanPromoteMembers))
}
bot.debugLog("promoteChatMember", v, nil)
return bot.MakeRequest("promoteChatMember", v)
}
// GetGameHighScores allows you to get the high scores for a game. // GetGameHighScores allows you to get the high scores for a game.
func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) { func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
v, _ := config.values() v, _ := config.values()
@@ -707,3 +786,93 @@ func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHigh
return highScores, err return highScores, err
} }
// AnswerShippingQuery allows you to reply to Update with shipping_query parameter.
func (bot *BotAPI) AnswerShippingQuery(config ShippingConfig) (APIResponse, error) {
v := url.Values{}
v.Add("shipping_query_id", config.ShippingQueryID)
v.Add("ok", strconv.FormatBool(config.OK))
if config.OK == true {
data, err := json.Marshal(config.ShippingOptions)
if err != nil {
return APIResponse{}, err
}
v.Add("shipping_options", string(data))
} else {
v.Add("error_message", config.ErrorMessage)
}
bot.debugLog("answerShippingQuery", v, nil)
return bot.MakeRequest("answerShippingQuery", v)
}
// AnswerPreCheckoutQuery allows you to reply to Update with pre_checkout_query.
func (bot *BotAPI) AnswerPreCheckoutQuery(config PreCheckoutConfig) (APIResponse, error) {
v := url.Values{}
v.Add("pre_checkout_query_id", config.PreCheckoutQueryID)
v.Add("ok", strconv.FormatBool(config.OK))
if config.OK != true {
v.Add("error", config.ErrorMessage)
}
bot.debugLog("answerPreCheckoutQuery", v, nil)
return bot.MakeRequest("answerPreCheckoutQuery", v)
}
// DeleteMessage deletes a message in a chat
func (bot *BotAPI) DeleteMessage(config DeleteMessageConfig) (APIResponse, error) {
v, err := config.values()
if err != nil {
return APIResponse{}, err
}
bot.debugLog(config.method(), v, nil)
return bot.MakeRequest(config.method(), v)
}
// GetInviteLink get InviteLink for a chat
func (bot *BotAPI) GetInviteLink(config ChatConfig) (string, error) {
v := url.Values{}
if config.SuperGroupUsername == "" {
v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
} else {
v.Add("chat_id", config.SuperGroupUsername)
}
resp, err := bot.MakeRequest("exportChatInviteLink", v)
var inviteLink string
err = json.Unmarshal(resp.Result, &inviteLink)
return inviteLink, err
}
// Pin message in supergroup
func (bot *BotAPI) PinChatMessage(config PinChatMessageConfig) (APIResponse, error) {
v, err := config.values()
if err != nil {
return APIResponse{}, err
}
bot.debugLog(config.method(), v, nil)
return bot.MakeRequest(config.method(), v)
}
// Unpin message in supergroup
func (bot *BotAPI) UnpinChatMessage(config UnpinChatMessageConfig) (APIResponse, error) {
v, err := config.values()
if err != nil {
return APIResponse{}, err
}
bot.debugLog(config.method(), v, nil)
return bot.MakeRequest(config.method(), v)
}

View File

@@ -349,6 +349,7 @@ func (config AudioConfig) method() string {
// DocumentConfig contains information about a SendDocument request. // DocumentConfig contains information about a SendDocument request.
type DocumentConfig struct { type DocumentConfig struct {
BaseFile BaseFile
Caption string
} }
// values returns a url.Values representation of DocumentConfig. // values returns a url.Values representation of DocumentConfig.
@@ -359,6 +360,9 @@ func (config DocumentConfig) values() (url.Values, error) {
} }
v.Add(config.name(), config.FileID) v.Add(config.name(), config.FileID)
if config.Caption != "" {
v.Add("caption", config.Caption)
}
return v, nil return v, nil
} }
@@ -367,6 +371,10 @@ func (config DocumentConfig) values() (url.Values, error) {
func (config DocumentConfig) params() (map[string]string, error) { func (config DocumentConfig) params() (map[string]string, error) {
params, _ := config.BaseFile.params() params, _ := config.BaseFile.params()
if config.Caption != "" {
params["caption"] = config.Caption
}
return params, nil return params, nil
} }
@@ -443,6 +451,10 @@ func (config VideoConfig) values() (url.Values, error) {
func (config VideoConfig) params() (map[string]string, error) { func (config VideoConfig) params() (map[string]string, error) {
params, _ := config.BaseFile.params() params, _ := config.BaseFile.params()
if config.Caption != "" {
params["caption"] = config.Caption
}
return params, nil return params, nil
} }
@@ -456,6 +468,57 @@ func (config VideoConfig) method() string {
return "sendVideo" return "sendVideo"
} }
// VideoNoteConfig contains information about a SendVideoNote request.
type VideoNoteConfig struct {
BaseFile
Duration int
Length int
}
// values returns a url.Values representation of VideoNoteConfig.
func (config VideoNoteConfig) values() (url.Values, error) {
v, err := config.BaseChat.values()
if err != nil {
return v, err
}
v.Add(config.name(), config.FileID)
if config.Duration != 0 {
v.Add("duration", strconv.Itoa(config.Duration))
}
// Telegram API seems to have a bug, if no length is provided or it is 0, it will send an error response
if config.Length != 0 {
v.Add("length", strconv.Itoa(config.Length))
}
return v, nil
}
// params returns a map[string]string representation of VideoNoteConfig.
func (config VideoNoteConfig) params() (map[string]string, error) {
params, _ := config.BaseFile.params()
if config.Length != 0 {
params["length"] = strconv.Itoa(config.Length)
}
if config.Duration != 0 {
params["duration"] = strconv.Itoa(config.Duration)
}
return params, nil
}
// name returns the field name for the VideoNote.
func (config VideoNoteConfig) name() string {
return "video_note"
}
// method returns Telegram API method name for sending VideoNote.
func (config VideoNoteConfig) method() string {
return "sendVideoNote"
}
// VoiceConfig contains information about a SendVoice request. // VoiceConfig contains information about a SendVoice request.
type VoiceConfig struct { type VoiceConfig struct {
BaseFile BaseFile
@@ -474,6 +537,9 @@ func (config VoiceConfig) values() (url.Values, error) {
if config.Duration != 0 { if config.Duration != 0 {
v.Add("duration", strconv.Itoa(config.Duration)) v.Add("duration", strconv.Itoa(config.Duration))
} }
if config.Caption != "" {
v.Add("caption", config.Caption)
}
return v, nil return v, nil
} }
@@ -485,6 +551,9 @@ func (config VoiceConfig) params() (map[string]string, error) {
if config.Duration != 0 { if config.Duration != 0 {
params["duration"] = strconv.Itoa(config.Duration) params["duration"] = strconv.Itoa(config.Duration)
} }
if config.Caption != "" {
params["caption"] = config.Caption
}
return params, nil return params, nil
} }
@@ -814,9 +883,39 @@ type CallbackConfig struct {
type ChatMemberConfig struct { type ChatMemberConfig struct {
ChatID int64 ChatID int64
SuperGroupUsername string SuperGroupUsername string
ChannelUsername string
UserID int UserID int
} }
// KickChatMemberConfig contains extra fields to kick user
type KickChatMemberConfig struct {
ChatMemberConfig
UntilDate int64
}
// RestrictChatMemberConfig contains fields to restrict members of chat
type RestrictChatMemberConfig struct {
ChatMemberConfig
UntilDate int64
CanSendMessages *bool
CanSendMediaMessages *bool
CanSendOtherMessages *bool
CanAddWebPagePreviews *bool
}
// PromoteChatMemberConfig contains fields to promote members of chat
type PromoteChatMemberConfig struct {
ChatMemberConfig
CanChangeInfo *bool
CanPostMessages *bool
CanEditMessages *bool
CanDeleteMessages *bool
CanInviteUsers *bool
CanRestrictMembers *bool
CanPinMessages *bool
CanPromoteMembers *bool
}
// ChatConfig contains information about getting information on a chat. // ChatConfig contains information about getting information on a chat.
type ChatConfig struct { type ChatConfig struct {
ChatID int64 ChatID int64
@@ -830,3 +929,147 @@ type ChatConfigWithUser struct {
SuperGroupUsername string SuperGroupUsername string
UserID int UserID int
} }
// InvoiceConfig contains information for sendInvoice request.
type InvoiceConfig struct {
BaseChat
Title string // required
Description string // required
Payload string // required
ProviderToken string // required
StartParameter string // required
Currency string // required
Prices *[]LabeledPrice // required
PhotoURL string
PhotoSize int
PhotoWidth int
PhotoHeight int
NeedName bool
NeedPhoneNumber bool
NeedEmail bool
NeedShippingAddress bool
IsFlexible bool
}
func (config InvoiceConfig) values() (url.Values, error) {
v, err := config.BaseChat.values()
if err != nil {
return v, err
}
v.Add("title", config.Title)
v.Add("description", config.Description)
v.Add("payload", config.Payload)
v.Add("provider_token", config.ProviderToken)
v.Add("start_parameter", config.StartParameter)
v.Add("currency", config.Currency)
data, err := json.Marshal(config.Prices)
if err != nil {
return v, err
}
v.Add("prices", string(data))
if config.PhotoURL != "" {
v.Add("photo_url", config.PhotoURL)
}
if config.PhotoSize != 0 {
v.Add("photo_size", strconv.Itoa(config.PhotoSize))
}
if config.PhotoWidth != 0 {
v.Add("photo_width", strconv.Itoa(config.PhotoWidth))
}
if config.PhotoHeight != 0 {
v.Add("photo_height", strconv.Itoa(config.PhotoHeight))
}
if config.NeedName != false {
v.Add("need_name", strconv.FormatBool(config.NeedName))
}
if config.NeedPhoneNumber != false {
v.Add("need_phone_number", strconv.FormatBool(config.NeedPhoneNumber))
}
if config.NeedEmail != false {
v.Add("need_email", strconv.FormatBool(config.NeedEmail))
}
if config.NeedShippingAddress != false {
v.Add("need_shipping_address", strconv.FormatBool(config.NeedShippingAddress))
}
if config.IsFlexible != false {
v.Add("is_flexible", strconv.FormatBool(config.IsFlexible))
}
return v, nil
}
func (config InvoiceConfig) method() string {
return "sendInvoice"
}
// ShippingConfig contains information for answerShippingQuery request.
type ShippingConfig struct {
ShippingQueryID string // required
OK bool // required
ShippingOptions *[]ShippingOption
ErrorMessage string
}
// PreCheckoutConfig conatins information for answerPreCheckoutQuery request.
type PreCheckoutConfig struct {
PreCheckoutQueryID string // required
OK bool // required
ErrorMessage string
}
// DeleteMessageConfig contains information of a message in a chat to delete.
type DeleteMessageConfig struct {
ChatID int64
MessageID int
}
func (config DeleteMessageConfig) method() string {
return "deleteMessage"
}
func (config DeleteMessageConfig) values() (url.Values, error) {
v := url.Values{}
v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
v.Add("message_id", strconv.Itoa(config.MessageID))
return v, nil
}
// PinChatMessageConfig contains information of a message in a chat to pin.
type PinChatMessageConfig struct {
ChatID int64
MessageID int
DisableNotification bool
}
func (config PinChatMessageConfig) method() string {
return "pinChatMessage"
}
func (config PinChatMessageConfig) values() (url.Values, error) {
v := url.Values{}
v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
v.Add("message_id", strconv.Itoa(config.MessageID))
v.Add("disable_notification", strconv.FormatBool(config.DisableNotification))
return v, nil
}
// UnpinChatMessageConfig contains information of chat to unpin.
type UnpinChatMessageConfig struct {
ChatID int64
}
func (config UnpinChatMessageConfig) method() string {
return "unpinChatMessage"
}
func (config UnpinChatMessageConfig) values() (url.Values, error) {
v := url.Values{}
v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
return v, nil
}

View File

@@ -194,6 +194,37 @@ func NewVideoShare(chatID int64, fileID string) VideoConfig {
} }
} }
// NewVideoNoteUpload creates a new video note uploader.
//
// chatID is where to send it, file is a string path to the file,
// FileReader, or FileBytes.
func NewVideoNoteUpload(chatID int64, length int, file interface{}) VideoNoteConfig {
return VideoNoteConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
File: file,
UseExisting: false,
},
Length: length,
}
}
// NewVideoNoteShare shares an existing video.
// You may use this to reshare an existing video without reuploading it.
//
// chatID is where to send it, fileID is the ID of the video
// already uploaded.
func NewVideoNoteShare(chatID int64, length int, fileID string) VideoNoteConfig {
return VideoNoteConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
FileID: fileID,
UseExisting: true,
},
Length: length,
}
}
// NewVoiceUpload creates a new voice uploader. // NewVoiceUpload creates a new voice uploader.
// //
// chatID is where to send it, file is a string path to the file, // chatID is where to send it, file is a string path to the file,
@@ -609,3 +640,16 @@ func NewCallbackWithAlert(id, text string) CallbackConfig {
ShowAlert: true, ShowAlert: true,
} }
} }
// NewInvoice created a new Invoice request to the user.
func NewInvoice(chatID int64, title, description, payload, providerToken, startParameter, currency string, prices *[]LabeledPrice) InvoiceConfig {
return InvoiceConfig{
BaseChat: BaseChat{ChatID: chatID},
Title: title,
Description: description,
Payload: payload,
ProviderToken: providerToken,
StartParameter: startParameter,
Currency: currency,
Prices: prices}
}

View File

@@ -35,6 +35,8 @@ type Update struct {
InlineQuery *InlineQuery `json:"inline_query"` InlineQuery *InlineQuery `json:"inline_query"`
ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result"` ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result"`
CallbackQuery *CallbackQuery `json:"callback_query"` CallbackQuery *CallbackQuery `json:"callback_query"`
ShippingQuery *ShippingQuery `json:"shipping_query"`
PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query"`
} }
// UpdatesChannel is the channel for getting updates. // UpdatesChannel is the channel for getting updates.
@@ -49,10 +51,11 @@ func (ch UpdatesChannel) Clear() {
// User is a user on Telegram. // User is a user on Telegram.
type User struct { type User struct {
ID int `json:"id"` ID int `json:"id"`
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` // optional LastName string `json:"last_name"` // optional
UserName string `json:"username"` // optional UserName string `json:"username"` // optional
LanguageCode string `json:"language_code"` // optional
} }
// String displays a simple text version of a user. // String displays a simple text version of a user.
@@ -78,15 +81,24 @@ type GroupChat struct {
Title string `json:"title"` Title string `json:"title"`
} }
// ChatPhoto represents a chat photo.
type ChatPhoto struct {
SmallFileID string `json:"small_file_id"`
BigFileID string `json:"big_file_id"`
}
// Chat contains information about the place a message was sent. // Chat contains information about the place a message was sent.
type Chat struct { type Chat struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Type string `json:"type"` Type string `json:"type"`
Title string `json:"title"` // optional Title string `json:"title"` // optional
UserName string `json:"username"` // optional UserName string `json:"username"` // optional
FirstName string `json:"first_name"` // optional FirstName string `json:"first_name"` // optional
LastName string `json:"last_name"` // optional LastName string `json:"last_name"` // optional
AllMembersAreAdmins bool `json:"all_members_are_administrators"` // optional AllMembersAreAdmins bool `json:"all_members_are_administrators"` // optional
Photo *ChatPhoto `json:"photo"`
Description string `json:"description,omitempty"` // optional
InviteLink string `json:"invite_link,omitempty"` // optional
} }
// IsPrivate returns if the Chat is a private conversation. // IsPrivate returns if the Chat is a private conversation.
@@ -117,40 +129,43 @@ func (c Chat) ChatConfig() ChatConfig {
// Message is returned by almost every request, and contains data about // Message is returned by almost every request, and contains data about
// almost anything. // almost anything.
type Message struct { type Message struct {
MessageID int `json:"message_id"` MessageID int `json:"message_id"`
From *User `json:"from"` // optional From *User `json:"from"` // optional
Date int `json:"date"` Date int `json:"date"`
Chat *Chat `json:"chat"` Chat *Chat `json:"chat"`
ForwardFrom *User `json:"forward_from"` // optional ForwardFrom *User `json:"forward_from"` // optional
ForwardFromChat *Chat `json:"forward_from_chat"` // optional ForwardFromChat *Chat `json:"forward_from_chat"` // optional
ForwardFromMessageID int `json:"forward_from_message_id"` // optional ForwardFromMessageID int `json:"forward_from_message_id"` // optional
ForwardDate int `json:"forward_date"` // optional ForwardDate int `json:"forward_date"` // optional
ReplyToMessage *Message `json:"reply_to_message"` // optional ReplyToMessage *Message `json:"reply_to_message"` // optional
EditDate int `json:"edit_date"` // optional EditDate int `json:"edit_date"` // optional
Text string `json:"text"` // optional Text string `json:"text"` // optional
Entities *[]MessageEntity `json:"entities"` // optional Entities *[]MessageEntity `json:"entities"` // optional
Audio *Audio `json:"audio"` // optional Audio *Audio `json:"audio"` // optional
Document *Document `json:"document"` // optional Document *Document `json:"document"` // optional
Game *Game `json:"game"` // optional Game *Game `json:"game"` // optional
Photo *[]PhotoSize `json:"photo"` // optional Photo *[]PhotoSize `json:"photo"` // optional
Sticker *Sticker `json:"sticker"` // optional Sticker *Sticker `json:"sticker"` // optional
Video *Video `json:"video"` // optional Video *Video `json:"video"` // optional
Voice *Voice `json:"voice"` // optional VideoNote *VideoNote `json:"video_note"` // optional
Caption string `json:"caption"` // optional Voice *Voice `json:"voice"` // optional
Contact *Contact `json:"contact"` // optional Caption string `json:"caption"` // optional
Location *Location `json:"location"` // optional Contact *Contact `json:"contact"` // optional
Venue *Venue `json:"venue"` // optional Location *Location `json:"location"` // optional
NewChatMember *User `json:"new_chat_member"` // optional Venue *Venue `json:"venue"` // optional
LeftChatMember *User `json:"left_chat_member"` // optional NewChatMembers *[]User `json:"new_chat_members"` // optional
NewChatTitle string `json:"new_chat_title"` // optional LeftChatMember *User `json:"left_chat_member"` // optional
NewChatPhoto *[]PhotoSize `json:"new_chat_photo"` // optional NewChatTitle string `json:"new_chat_title"` // optional
DeleteChatPhoto bool `json:"delete_chat_photo"` // optional NewChatPhoto *[]PhotoSize `json:"new_chat_photo"` // optional
GroupChatCreated bool `json:"group_chat_created"` // optional DeleteChatPhoto bool `json:"delete_chat_photo"` // optional
SuperGroupChatCreated bool `json:"supergroup_chat_created"` // optional GroupChatCreated bool `json:"group_chat_created"` // optional
ChannelChatCreated bool `json:"channel_chat_created"` // optional SuperGroupChatCreated bool `json:"supergroup_chat_created"` // optional
MigrateToChatID int64 `json:"migrate_to_chat_id"` // optional ChannelChatCreated bool `json:"channel_chat_created"` // optional
MigrateFromChatID int64 `json:"migrate_from_chat_id"` // optional MigrateToChatID int64 `json:"migrate_to_chat_id"` // optional
PinnedMessage *Message `json:"pinned_message"` // optional MigrateFromChatID int64 `json:"migrate_from_chat_id"` // optional
PinnedMessage *Message `json:"pinned_message"` // optional
Invoice *Invoice `json:"invoice"` // optional
SuccessfulPayment *SuccessfulPayment `json:"successful_payment"` // optional
} }
// Time converts the message timestamp into a Time. // Time converts the message timestamp into a Time.
@@ -263,6 +278,15 @@ type Video struct {
FileSize int `json:"file_size"` // optional FileSize int `json:"file_size"` // optional
} }
// VideoNote contains information about a video.
type VideoNote struct {
FileID string `json:"file_id"`
Length int `json:"length"`
Duration int `json:"duration"`
Thumbnail *PhotoSize `json:"thumb"` // optional
FileSize int `json:"file_size"` // optional
}
// Voice contains information about a voice. // Voice contains information about a voice.
type Voice struct { type Voice struct {
FileID string `json:"file_id"` FileID string `json:"file_id"`
@@ -361,6 +385,7 @@ type InlineKeyboardButton struct {
SwitchInlineQuery *string `json:"switch_inline_query,omitempty"` // optional SwitchInlineQuery *string `json:"switch_inline_query,omitempty"` // optional
SwitchInlineQueryCurrentChat *string `json:"switch_inline_query_current_chat,omitempty"` // optional SwitchInlineQueryCurrentChat *string `json:"switch_inline_query_current_chat,omitempty"` // optional
CallbackGame *CallbackGame `json:"callback_game,omitempty"` // optional CallbackGame *CallbackGame `json:"callback_game,omitempty"` // optional
Pay bool `json:"pay,omitempty"` // optional
} }
// CallbackQuery is data sent when a keyboard button with callback data // CallbackQuery is data sent when a keyboard button with callback data
@@ -384,8 +409,22 @@ type ForceReply struct {
// ChatMember is information about a member in a chat. // ChatMember is information about a member in a chat.
type ChatMember struct { type ChatMember struct {
User *User `json:"user"` User *User `json:"user"`
Status string `json:"status"` Status string `json:"status"`
UntilDate int64 `json:"until_date,omitempty"` // optional
CanBeEdited bool `json:"can_be_edited,omitempty"` // optional
CanChangeInfo bool `json:"can_change_info,omitempty"` // optional
CanPostMessages bool `json:"can_post_messages,omitempty"` // optional
CanEditMessages bool `json:"can_edit_messages,omitempty"` // optional
CanDeleteMessages bool `json:"can_delete_messages,omitempty"` // optional
CanInviteUsers bool `json:"can_invite_users,omitempty"` // optional
CanRestrictMembers bool `json:"can_restrict_members,omitempty"` // optional
CanPinMessages bool `json:"can_pin_messages,omitempty"` // optional
CanPromoteMembers bool `json:"can_promote_members,omitempty"` // optional
CanSendMessages bool `json:"can_send_messages,omitempty"` // optional
CanSendMediaMessages bool `json:"can_send_media_messages,omitempty"` // optional
CanSendOtherMessages bool `json:"can_send_other_messages,omitempty"` // optional
CanAddWebPagePreviews bool `json:"can_add_web_page_previews,omitempty"` // optional
} }
// IsCreator returns if the ChatMember was the creator of the chat. // IsCreator returns if the ChatMember was the creator of the chat.
@@ -493,6 +532,7 @@ type InlineQueryResultGIF struct {
URL string `json:"gif_url"` // required URL string `json:"gif_url"` // required
Width int `json:"gif_width"` Width int `json:"gif_width"`
Height int `json:"gif_height"` Height int `json:"gif_height"`
Duration int `json:"gif_duration"`
ThumbURL string `json:"thumb_url"` ThumbURL string `json:"thumb_url"`
Title string `json:"title"` Title string `json:"title"`
Caption string `json:"caption"` Caption string `json:"caption"`
@@ -507,6 +547,7 @@ type InlineQueryResultMPEG4GIF struct {
URL string `json:"mpeg4_url"` // required URL string `json:"mpeg4_url"` // required
Width int `json:"mpeg4_width"` Width int `json:"mpeg4_width"`
Height int `json:"mpeg4_height"` Height int `json:"mpeg4_height"`
Duration int `json:"mpeg4_duration"`
ThumbURL string `json:"thumb_url"` ThumbURL string `json:"thumb_url"`
Title string `json:"title"` Title string `json:"title"`
Caption string `json:"caption"` Caption string `json:"caption"`
@@ -635,3 +676,73 @@ type InputContactMessageContent struct {
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
} }
// Invoice contains basic information about an invoice.
type Invoice struct {
Title string `json:"title"`
Description string `json:"description"`
StartParameter string `json:"start_parameter"`
Currency string `json:"currency"`
TotalAmount int `json:"total_amount"`
}
// LabeledPrice represents a portion of the price for goods or services.
type LabeledPrice struct {
Label string `json:"label"`
Amount int `json:"amount"`
}
// ShippingAddress represents a shipping address.
type ShippingAddress struct {
CountryCode string `json:"country_code"`
State string `json:"state"`
City string `json:"city"`
StreetLine1 string `json:"street_line1"`
StreetLine2 string `json:"street_line2"`
PostCode string `json:"post_code"`
}
// OrderInfo represents information about an order.
type OrderInfo struct {
Name string `json:"name,omitempty"`
PhoneNumber string `json:"phone_number,omitempty"`
Email string `json:"email,omitempty"`
ShippingAddress *ShippingAddress `json:"shipping_address,omitempty"`
}
// ShippingOption represents one shipping option.
type ShippingOption struct {
ID string `json:"id"`
Title string `json:"title"`
Prices *[]LabeledPrice `json:"prices"`
}
// SuccessfulPayment contains basic information about a successful payment.
type SuccessfulPayment struct {
Currency string `json:"currency"`
TotalAmount int `json:"total_amount"`
InvoicePayload string `json:"invoice_payload"`
ShippingOptionID string `json:"shipping_option_id,omitempty"`
OrderInfo *OrderInfo `json:"order_info,omitempty"`
TelegramPaymentChargeID string `json:"telegram_payment_charge_id"`
ProviderPaymentChargeID string `json:"provider_payment_charge_id"`
}
// ShippingQuery contains information about an incoming shipping query.
type ShippingQuery struct {
ID string `json:"id"`
From *User `json:"from"`
InvoicePayload string `json:"invoice_payload"`
ShippingAddress *ShippingAddress `json:"shipping_address"`
}
// PreCheckoutQuery contains information about an incoming pre-checkout query.
type PreCheckoutQuery struct {
ID string `json:"id"`
From *User `json:"from"`
Currency string `json:"currency"`
TotalAmount int `json:"total_amount"`
InvoicePayload string `json:"invoice_payload"`
ShippingOptionID string `json:"shipping_option_id,omitempty"`
OrderInfo *OrderInfo `json:"order_info,omitempty"`
}

View File

@@ -1,32 +1,32 @@
Mattermost Licensing Mattermost Licensing
SOFTWARE LICENSING SOFTWARE LICENSING
You are licensed to use compiled versions of the Mattermost platform produced by Mattermost, Inc. under an MIT LICENSE You are licensed to use compiled versions of the Mattermost platform produced by Mattermost, Inc. under an MIT LICENSE
- See MIT-COMPILED-LICENSE.md included in compiled versions for details - See MIT-COMPILED-LICENSE.md included in compiled versions for details
You may be licensed to use source code to create compiled versions not produced by Mattermost, Inc. in one of two ways: You may be licensed to use source code to create compiled versions not produced by Mattermost, Inc. in one of two ways:
1. Under the Free Software Foundations GNU AGPL v.3.0, subject to the exceptions outlined in this policy; or 1. Under the Free Software Foundations GNU AGPL v.3.0, subject to the exceptions outlined in this policy; or
2. Under a commercial license available from Mattermost, Inc. by contacting commercial@mattermost.com 2. Under a commercial license available from Mattermost, Inc. by contacting commercial@mattermost.com
You are licensed to use the source code in Admin Tools and Configuration Files (templates/, config/, model/, You are licensed to use the source code in Admin Tools and Configuration Files (templates/, config/, model/,
webapp/client, webapp/fonts, webapp/i18n, webapp/images and all subdirectories thereof) under the Apache License v2.0. webapp/client, webapp/fonts, webapp/i18n, webapp/images and all subdirectories thereof) under the Apache License v2.0.
We promise that we will not enforce the copyleft provisions in AGPL v3.0 against you if your application (a) does not We promise that we will not enforce the copyleft provisions in AGPL v3.0 against you if your application (a) does not
link to the Mattermost Platform directly, but exclusively uses the Mattermost Admin Tools and Configuration Files, and link to the Mattermost Platform directly, but exclusively uses the Mattermost Admin Tools and Configuration Files, and
(b) you have not modified, added to or adapted the source code of Mattermost in a way that results in the creation of (b) you have not modified, added to or adapted the source code of Mattermost in a way that results in the creation of
a “modified version” or “work based on” Mattermost as these terms are defined in the AGPL v3.0 license. a “modified version” or “work based on” Mattermost as these terms are defined in the AGPL v3.0 license.
MATTERMOST TRADEMARK GUIDELINES MATTERMOST TRADEMARK GUIDELINES
Your use of the mark Mattermost is subject to Mattermost, Inc's prior written approval and our organizations Trademark Your use of the mark Mattermost is subject to Mattermost, Inc's prior written approval and our organizations Trademark
Standards of Use at http://www.mattermost.org/trademark-standards-of-use/. For trademark approval or any questions Standards of Use at http://www.mattermost.org/trademark-standards-of-use/. For trademark approval or any questions
you have about using these trademarks, please email trademark@mattermost.com you have about using these trademarks, please email trademark@mattermost.com
------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------
Apache License Apache License
Version 2.0, January 2004 Version 2.0, January 2004
http://www.apache.org/licenses/ http://www.apache.org/licenses/

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package einterfaces package einterfaces
@@ -6,7 +6,7 @@ package einterfaces
import "github.com/mattermost/platform/model" import "github.com/mattermost/platform/model"
type AccountMigrationInterface interface { type AccountMigrationInterface interface {
MigrateToLdap(fromAuthService string, forignUserFieldNameToMatch string) *model.AppError MigrateToLdap(fromAuthService string, forignUserFieldNameToMatch string, force bool) *model.AppError
} }
var theAccountMigrationInterface AccountMigrationInterface var theAccountMigrationInterface AccountMigrationInterface

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package einterfaces package einterfaces

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package einterfaces package einterfaces
@@ -7,26 +7,19 @@ import (
"github.com/mattermost/platform/model" "github.com/mattermost/platform/model"
) )
type ClusterMessageHandler func(msg *model.ClusterMessage)
type ClusterInterface interface { type ClusterInterface interface {
StartInterNodeCommunication() StartInterNodeCommunication()
StopInterNodeCommunication() StopInterNodeCommunication()
GetClusterInfos() []*model.ClusterInfo RegisterClusterMessageHandler(event string, crm ClusterMessageHandler)
GetClusterStats() ([]*model.ClusterStats, *model.AppError)
ClearSessionCacheForUser(userId string)
InvalidateCacheForUser(userId string)
InvalidateCacheForChannel(channelId string)
InvalidateCacheForChannelByName(teamId, name string)
InvalidateCacheForChannelMembers(channelId string)
InvalidateCacheForChannelMembersNotifyProps(channelId string)
InvalidateCacheForChannelPosts(channelId string)
InvalidateCacheForWebhook(webhookId string)
InvalidateCacheForReactions(postId string)
Publish(event *model.WebSocketEvent)
UpdateStatus(status *model.Status)
GetLogs() ([]string, *model.AppError)
GetClusterId() string GetClusterId() string
GetClusterInfos() []*model.ClusterInfo
SendClusterMessage(cluster *model.ClusterMessage)
NotifyMsg(buf []byte)
GetClusterStats() ([]*model.ClusterStats, *model.AppError)
GetLogs(page, perPage int) ([]string, *model.AppError)
ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError
InvalidateAllCaches() *model.AppError
} }
var theClusterInterface ClusterInterface var theClusterInterface ClusterInterface

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package einterfaces package einterfaces

View File

@@ -0,0 +1,25 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package einterfaces
import "github.com/mattermost/platform/model"
type ElasticsearchInterface interface {
Start() *model.AppError
IndexPost(post *model.Post, teamId string) *model.AppError
SearchPosts(channels *model.ChannelList, searchParams []*model.SearchParams) ([]string, *model.AppError)
DeletePost(post *model.Post) *model.AppError
TestConfig(cfg *model.Config) *model.AppError
PurgeIndexes() *model.AppError
}
var theElasticsearchInterface ElasticsearchInterface
func RegisterElasticsearchInterface(newInterface ElasticsearchInterface) {
theElasticsearchInterface = newInterface
}
func GetElasticsearchInterface() ElasticsearchInterface {
return theElasticsearchInterface
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package einterfaces package einterfaces

View File

@@ -0,0 +1,23 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package jobs
import (
"github.com/mattermost/platform/model"
)
type DataRetentionInterface interface {
MakeWorker() model.Worker
MakeScheduler() model.Scheduler
}
var theDataRetentionInterface DataRetentionInterface
func RegisterDataRetentionInterface(newInterface DataRetentionInterface) {
theDataRetentionInterface = newInterface
}
func GetDataRetentionInterface() DataRetentionInterface {
return theDataRetentionInterface
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package jobs
import (
"github.com/mattermost/platform/model"
)
type ElasticsearchIndexerInterface interface {
MakeWorker() model.Worker
}
var theElasticsearchIndexerInterface ElasticsearchIndexerInterface
func RegisterElasticsearchIndexerInterface(newInterface ElasticsearchIndexerInterface) {
theElasticsearchIndexerInterface = newInterface
}
func GetElasticsearchIndexerInterface() ElasticsearchIndexerInterface {
return theElasticsearchIndexerInterface
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package einterfaces package einterfaces

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package einterfaces package einterfaces
@@ -33,6 +33,7 @@ type MetricsInterface interface {
IncrementMemCacheHitCounterSession() IncrementMemCacheHitCounterSession()
IncrementWebsocketEvent(eventType string) IncrementWebsocketEvent(eventType string)
IncrementWebSocketBroadcast(eventType string)
AddMemCacheHitCounter(cacheName string, amount float64) AddMemCacheHitCounter(cacheName string, amount float64)
AddMemCacheMissCounter(cacheName string, amount float64) AddMemCacheMissCounter(cacheName string, amount float64)

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package einterfaces package einterfaces

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package einterfaces package einterfaces

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package einterfaces package einterfaces
@@ -10,7 +10,7 @@ import (
type SamlInterface interface { type SamlInterface interface {
ConfigureSP() *model.AppError ConfigureSP() *model.AppError
BuildRequest(relayState string) (*model.SamlAuthRequest, *model.AppError) BuildRequest(relayState string) (*model.SamlAuthRequest, *model.AppError)
DoLogin(encodedXML string, relayState map[string]string, siteURL string) (*model.User, *model.AppError) DoLogin(encodedXML string, relayState map[string]string) (*model.User, *model.AppError)
GetMetadata() (string, *model.AppError) GetMetadata() (string, *model.AppError)
} }

View File

@@ -1,32 +1,32 @@
Mattermost Licensing Mattermost Licensing
SOFTWARE LICENSING SOFTWARE LICENSING
You are licensed to use compiled versions of the Mattermost platform produced by Mattermost, Inc. under an MIT LICENSE You are licensed to use compiled versions of the Mattermost platform produced by Mattermost, Inc. under an MIT LICENSE
- See MIT-COMPILED-LICENSE.md included in compiled versions for details - See MIT-COMPILED-LICENSE.md included in compiled versions for details
You may be licensed to use source code to create compiled versions not produced by Mattermost, Inc. in one of two ways: You may be licensed to use source code to create compiled versions not produced by Mattermost, Inc. in one of two ways:
1. Under the Free Software Foundations GNU AGPL v.3.0, subject to the exceptions outlined in this policy; or 1. Under the Free Software Foundations GNU AGPL v.3.0, subject to the exceptions outlined in this policy; or
2. Under a commercial license available from Mattermost, Inc. by contacting commercial@mattermost.com 2. Under a commercial license available from Mattermost, Inc. by contacting commercial@mattermost.com
You are licensed to use the source code in Admin Tools and Configuration Files (templates/, config/, model/, You are licensed to use the source code in Admin Tools and Configuration Files (templates/, config/, model/,
webapp/client, webapp/fonts, webapp/i18n, webapp/images and all subdirectories thereof) under the Apache License v2.0. webapp/client, webapp/fonts, webapp/i18n, webapp/images and all subdirectories thereof) under the Apache License v2.0.
We promise that we will not enforce the copyleft provisions in AGPL v3.0 against you if your application (a) does not We promise that we will not enforce the copyleft provisions in AGPL v3.0 against you if your application (a) does not
link to the Mattermost Platform directly, but exclusively uses the Mattermost Admin Tools and Configuration Files, and link to the Mattermost Platform directly, but exclusively uses the Mattermost Admin Tools and Configuration Files, and
(b) you have not modified, added to or adapted the source code of Mattermost in a way that results in the creation of (b) you have not modified, added to or adapted the source code of Mattermost in a way that results in the creation of
a “modified version” or “work based on” Mattermost as these terms are defined in the AGPL v3.0 license. a “modified version” or “work based on” Mattermost as these terms are defined in the AGPL v3.0 license.
MATTERMOST TRADEMARK GUIDELINES MATTERMOST TRADEMARK GUIDELINES
Your use of the mark Mattermost is subject to Mattermost, Inc's prior written approval and our organizations Trademark Your use of the mark Mattermost is subject to Mattermost, Inc's prior written approval and our organizations Trademark
Standards of Use at http://www.mattermost.org/trademark-standards-of-use/. For trademark approval or any questions Standards of Use at http://www.mattermost.org/trademark-standards-of-use/. For trademark approval or any questions
you have about using these trademarks, please email trademark@mattermost.com you have about using these trademarks, please email trademark@mattermost.com
------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------
Apache License Apache License
Version 2.0, January 2004 Version 2.0, January 2004
http://www.apache.org/licenses/ http://www.apache.org/licenses/

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model
@@ -21,6 +21,7 @@ type AccessData struct {
RefreshToken string `json:"refresh_token"` RefreshToken string `json:"refresh_token"`
RedirectUri string `json:"redirect_uri"` RedirectUri string `json:"redirect_uri"`
ExpiresAt int64 `json:"expires_at"` ExpiresAt int64 `json:"expires_at"`
Scope string `json:"scope"`
} }
type AccessResponse struct { type AccessResponse struct {
@@ -51,7 +52,7 @@ func (ad *AccessData) IsValid() *AppError {
return NewLocAppError("AccessData.IsValid", "model.access.is_valid.refresh_token.app_error", nil, "") return NewLocAppError("AccessData.IsValid", "model.access.is_valid.refresh_token.app_error", nil, "")
} }
if len(ad.RedirectUri) > 256 { if len(ad.RedirectUri) == 0 || len(ad.RedirectUri) > 256 || !IsValidHttpUrl(ad.RedirectUri) {
return NewLocAppError("AccessData.IsValid", "model.access.is_valid.redirect_uri.app_error", nil, "") return NewLocAppError("AccessData.IsValid", "model.access.is_valid.redirect_uri.app_error", nil, "")
} }

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model
@@ -39,6 +39,7 @@ var PERMISSION_DELETE_PUBLIC_CHANNEL *Permission
var PERMISSION_DELETE_PRIVATE_CHANNEL *Permission var PERMISSION_DELETE_PRIVATE_CHANNEL *Permission
var PERMISSION_EDIT_OTHER_USERS *Permission var PERMISSION_EDIT_OTHER_USERS *Permission
var PERMISSION_READ_CHANNEL *Permission var PERMISSION_READ_CHANNEL *Permission
var PERMISSION_READ_PUBLIC_CHANNEL *Permission
var PERMISSION_PERMANENT_DELETE_USER *Permission var PERMISSION_PERMANENT_DELETE_USER *Permission
var PERMISSION_UPLOAD_FILE *Permission var PERMISSION_UPLOAD_FILE *Permission
var PERMISSION_GET_PUBLIC_LINK *Permission var PERMISSION_GET_PUBLIC_LINK *Permission
@@ -47,6 +48,7 @@ var PERMISSION_MANAGE_OTHERS_WEBHOOKS *Permission
var PERMISSION_MANAGE_OAUTH *Permission var PERMISSION_MANAGE_OAUTH *Permission
var PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH *Permission var PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH *Permission
var PERMISSION_CREATE_POST *Permission var PERMISSION_CREATE_POST *Permission
var PERMISSION_CREATE_POST_PUBLIC *Permission
var PERMISSION_EDIT_POST *Permission var PERMISSION_EDIT_POST *Permission
var PERMISSION_EDIT_OTHERS_POSTS *Permission var PERMISSION_EDIT_OTHERS_POSTS *Permission
var PERMISSION_DELETE_POST *Permission var PERMISSION_DELETE_POST *Permission
@@ -56,6 +58,11 @@ var PERMISSION_CREATE_TEAM *Permission
var PERMISSION_MANAGE_TEAM *Permission var PERMISSION_MANAGE_TEAM *Permission
var PERMISSION_IMPORT_TEAM *Permission var PERMISSION_IMPORT_TEAM *Permission
var PERMISSION_VIEW_TEAM *Permission var PERMISSION_VIEW_TEAM *Permission
var PERMISSION_LIST_USERS_WITHOUT_TEAM *Permission
var PERMISSION_MANAGE_JOBS *Permission
var PERMISSION_CREATE_USER_ACCESS_TOKEN *Permission
var PERMISSION_READ_USER_ACCESS_TOKEN *Permission
var PERMISSION_REVOKE_USER_ACCESS_TOKEN *Permission
// General permission that encompases all system admin functions // General permission that encompases all system admin functions
// in the future this could be broken up to allow access to some // in the future this could be broken up to allow access to some
@@ -64,9 +71,14 @@ var PERMISSION_MANAGE_SYSTEM *Permission
var ROLE_SYSTEM_USER *Role var ROLE_SYSTEM_USER *Role
var ROLE_SYSTEM_ADMIN *Role var ROLE_SYSTEM_ADMIN *Role
var ROLE_SYSTEM_POST_ALL *Role
var ROLE_SYSTEM_POST_ALL_PUBLIC *Role
var ROLE_SYSTEM_USER_ACCESS_TOKEN *Role
var ROLE_TEAM_USER *Role var ROLE_TEAM_USER *Role
var ROLE_TEAM_ADMIN *Role var ROLE_TEAM_ADMIN *Role
var ROLE_TEAM_POST_ALL *Role
var ROLE_TEAM_POST_ALL_PUBLIC *Role
var ROLE_CHANNEL_USER *Role var ROLE_CHANNEL_USER *Role
var ROLE_CHANNEL_ADMIN *Role var ROLE_CHANNEL_ADMIN *Role
@@ -195,6 +207,11 @@ func InitalizePermissions() {
"authentication.permissions.read_channel.name", "authentication.permissions.read_channel.name",
"authentication.permissions.read_channel.description", "authentication.permissions.read_channel.description",
} }
PERMISSION_READ_PUBLIC_CHANNEL = &Permission{
"read_public_channel",
"authentication.permissions.read_public_channel.name",
"authentication.permissions.read_public_channel.description",
}
PERMISSION_PERMANENT_DELETE_USER = &Permission{ PERMISSION_PERMANENT_DELETE_USER = &Permission{
"permanent_delete_user", "permanent_delete_user",
"authentication.permissions.permanent_delete_user.name", "authentication.permissions.permanent_delete_user.name",
@@ -235,6 +252,11 @@ func InitalizePermissions() {
"authentication.permissions.create_post.name", "authentication.permissions.create_post.name",
"authentication.permissions.create_post.description", "authentication.permissions.create_post.description",
} }
PERMISSION_CREATE_POST_PUBLIC = &Permission{
"create_post_public",
"authentication.permissions.create_post_public.name",
"authentication.permissions.create_post_public.description",
}
PERMISSION_EDIT_POST = &Permission{ PERMISSION_EDIT_POST = &Permission{
"edit_post", "edit_post",
"authentication.permissions.edit_post.name", "authentication.permissions.edit_post.name",
@@ -280,6 +302,31 @@ func InitalizePermissions() {
"authentication.permissions.view_team.name", "authentication.permissions.view_team.name",
"authentication.permissions.view_team.description", "authentication.permissions.view_team.description",
} }
PERMISSION_LIST_USERS_WITHOUT_TEAM = &Permission{
"list_users_without_team",
"authentication.permissions.list_users_without_team.name",
"authentication.permissions.list_users_without_team.description",
}
PERMISSION_CREATE_USER_ACCESS_TOKEN = &Permission{
"create_user_access_token",
"authentication.permissions.create_user_access_token.name",
"authentication.permissions.create_user_access_token.description",
}
PERMISSION_READ_USER_ACCESS_TOKEN = &Permission{
"read_user_access_token",
"authentication.permissions.read_user_access_token.name",
"authentication.permissions.read_user_access_token.description",
}
PERMISSION_REVOKE_USER_ACCESS_TOKEN = &Permission{
"revoke_user_access_token",
"authentication.permissions.revoke_user_access_token.name",
"authentication.permissions.revoke_user_access_token.description",
}
PERMISSION_MANAGE_JOBS = &Permission{
"manage_jobs",
"authentication.permisssions.manage_jobs.name",
"authentication.permisssions.manage_jobs.description",
}
} }
func InitalizeRoles() { func InitalizeRoles() {
@@ -293,7 +340,6 @@ func InitalizeRoles() {
[]string{ []string{
PERMISSION_READ_CHANNEL.Id, PERMISSION_READ_CHANNEL.Id,
PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id, PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id,
PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id,
PERMISSION_UPLOAD_FILE.Id, PERMISSION_UPLOAD_FILE.Id,
PERMISSION_GET_PUBLIC_LINK.Id, PERMISSION_GET_PUBLIC_LINK.Id,
PERMISSION_CREATE_POST.Id, PERMISSION_CREATE_POST.Id,
@@ -326,17 +372,38 @@ func InitalizeRoles() {
[]string{ []string{
PERMISSION_LIST_TEAM_CHANNELS.Id, PERMISSION_LIST_TEAM_CHANNELS.Id,
PERMISSION_JOIN_PUBLIC_CHANNELS.Id, PERMISSION_JOIN_PUBLIC_CHANNELS.Id,
PERMISSION_READ_PUBLIC_CHANNEL.Id,
PERMISSION_VIEW_TEAM.Id, PERMISSION_VIEW_TEAM.Id,
}, },
} }
BuiltInRoles[ROLE_TEAM_USER.Id] = ROLE_TEAM_USER BuiltInRoles[ROLE_TEAM_USER.Id] = ROLE_TEAM_USER
ROLE_TEAM_POST_ALL = &Role{
"team_post_all",
"authentication.roles.team_post_all.name",
"authentication.roles.team_post_all.description",
[]string{
PERMISSION_CREATE_POST.Id,
},
}
BuiltInRoles[ROLE_TEAM_POST_ALL.Id] = ROLE_TEAM_POST_ALL
ROLE_TEAM_POST_ALL_PUBLIC = &Role{
"team_post_all_public",
"authentication.roles.team_post_all_public.name",
"authentication.roles.team_post_all_public.description",
[]string{
PERMISSION_CREATE_POST_PUBLIC.Id,
},
}
BuiltInRoles[ROLE_TEAM_POST_ALL_PUBLIC.Id] = ROLE_TEAM_POST_ALL_PUBLIC
ROLE_TEAM_ADMIN = &Role{ ROLE_TEAM_ADMIN = &Role{
"team_admin", "team_admin",
"authentication.roles.team_admin.name", "authentication.roles.team_admin.name",
"authentication.roles.team_admin.description", "authentication.roles.team_admin.description",
[]string{ []string{
PERMISSION_EDIT_OTHERS_POSTS.Id, PERMISSION_EDIT_OTHERS_POSTS.Id,
PERMISSION_ADD_USER_TO_TEAM.Id,
PERMISSION_REMOVE_USER_FROM_TEAM.Id, PERMISSION_REMOVE_USER_FROM_TEAM.Id,
PERMISSION_MANAGE_TEAM.Id, PERMISSION_MANAGE_TEAM.Id,
PERMISSION_IMPORT_TEAM.Id, PERMISSION_IMPORT_TEAM.Id,
@@ -358,10 +425,42 @@ func InitalizeRoles() {
PERMISSION_CREATE_DIRECT_CHANNEL.Id, PERMISSION_CREATE_DIRECT_CHANNEL.Id,
PERMISSION_CREATE_GROUP_CHANNEL.Id, PERMISSION_CREATE_GROUP_CHANNEL.Id,
PERMISSION_PERMANENT_DELETE_USER.Id, PERMISSION_PERMANENT_DELETE_USER.Id,
PERMISSION_MANAGE_OAUTH.Id,
}, },
} }
BuiltInRoles[ROLE_SYSTEM_USER.Id] = ROLE_SYSTEM_USER BuiltInRoles[ROLE_SYSTEM_USER.Id] = ROLE_SYSTEM_USER
ROLE_SYSTEM_POST_ALL = &Role{
"system_post_all",
"authentication.roles.system_post_all.name",
"authentication.roles.system_post_all.description",
[]string{
PERMISSION_CREATE_POST.Id,
},
}
BuiltInRoles[ROLE_SYSTEM_POST_ALL.Id] = ROLE_SYSTEM_POST_ALL
ROLE_SYSTEM_POST_ALL_PUBLIC = &Role{
"system_post_all_public",
"authentication.roles.system_post_all_public.name",
"authentication.roles.system_post_all_public.description",
[]string{
PERMISSION_CREATE_POST_PUBLIC.Id,
},
}
BuiltInRoles[ROLE_SYSTEM_POST_ALL_PUBLIC.Id] = ROLE_SYSTEM_POST_ALL_PUBLIC
ROLE_SYSTEM_USER_ACCESS_TOKEN = &Role{
"system_user_access_token",
"authentication.roles.system_user_access_token.name",
"authentication.roles.system_user_access_token.description",
[]string{
PERMISSION_CREATE_USER_ACCESS_TOKEN.Id,
PERMISSION_READ_USER_ACCESS_TOKEN.Id,
PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id,
},
}
BuiltInRoles[ROLE_SYSTEM_USER_ACCESS_TOKEN.Id] = ROLE_SYSTEM_USER_ACCESS_TOKEN
ROLE_SYSTEM_ADMIN = &Role{ ROLE_SYSTEM_ADMIN = &Role{
"system_admin", "system_admin",
"authentication.roles.global_admin.name", "authentication.roles.global_admin.name",
@@ -378,6 +477,8 @@ func InitalizeRoles() {
PERMISSION_MANAGE_SYSTEM.Id, PERMISSION_MANAGE_SYSTEM.Id,
PERMISSION_MANAGE_ROLES.Id, PERMISSION_MANAGE_ROLES.Id,
PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES.Id, PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES.Id,
PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id,
PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id,
PERMISSION_DELETE_PUBLIC_CHANNEL.Id, PERMISSION_DELETE_PUBLIC_CHANNEL.Id,
PERMISSION_CREATE_PUBLIC_CHANNEL.Id, PERMISSION_CREATE_PUBLIC_CHANNEL.Id,
PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES.Id, PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES.Id,
@@ -391,6 +492,13 @@ func InitalizeRoles() {
PERMISSION_DELETE_POST.Id, PERMISSION_DELETE_POST.Id,
PERMISSION_DELETE_OTHERS_POSTS.Id, PERMISSION_DELETE_OTHERS_POSTS.Id,
PERMISSION_CREATE_TEAM.Id, PERMISSION_CREATE_TEAM.Id,
PERMISSION_ADD_USER_TO_TEAM.Id,
PERMISSION_LIST_USERS_WITHOUT_TEAM.Id,
PERMISSION_MANAGE_JOBS.Id,
PERMISSION_CREATE_POST_PUBLIC.Id,
PERMISSION_CREATE_USER_ACCESS_TOKEN.Id,
PERMISSION_READ_USER_ACCESS_TOKEN.Id,
PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id,
}, },
ROLE_TEAM_USER.Permissions..., ROLE_TEAM_USER.Permissions...,
), ),

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model
@@ -6,6 +6,7 @@ package model
import ( import (
"encoding/json" "encoding/json"
"io" "io"
"net/http"
) )
const ( const (
@@ -25,6 +26,14 @@ type AuthData struct {
Scope string `json:"scope"` Scope string `json:"scope"`
} }
type AuthorizeRequest struct {
ResponseType string `json:"response_type"`
ClientId string `json:"client_id"`
RedirectUri string `json:"redirect_uri"`
Scope string `json:"scope"`
State string `json:"state"`
}
// IsValid validates the AuthData and returns an error if it isn't configured // IsValid validates the AuthData and returns an error if it isn't configured
// correctly. // correctly.
func (ad *AuthData) IsValid() *AppError { func (ad *AuthData) IsValid() *AppError {
@@ -49,7 +58,7 @@ func (ad *AuthData) IsValid() *AppError {
return NewLocAppError("AuthData.IsValid", "model.authorize.is_valid.create_at.app_error", nil, "client_id="+ad.ClientId) return NewLocAppError("AuthData.IsValid", "model.authorize.is_valid.create_at.app_error", nil, "client_id="+ad.ClientId)
} }
if len(ad.RedirectUri) > 256 { if len(ad.RedirectUri) == 0 || len(ad.RedirectUri) > 256 || !IsValidHttpUrl(ad.RedirectUri) {
return NewLocAppError("AuthData.IsValid", "model.authorize.is_valid.redirect_uri.app_error", nil, "client_id="+ad.ClientId) return NewLocAppError("AuthData.IsValid", "model.authorize.is_valid.redirect_uri.app_error", nil, "client_id="+ad.ClientId)
} }
@@ -64,6 +73,33 @@ func (ad *AuthData) IsValid() *AppError {
return nil return nil
} }
// IsValid validates the AuthorizeRequest and returns an error if it isn't configured
// correctly.
func (ar *AuthorizeRequest) IsValid() *AppError {
if len(ar.ClientId) != 26 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.client_id.app_error", nil, "", http.StatusBadRequest)
}
if len(ar.ResponseType) == 0 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.response_type.app_error", nil, "", http.StatusBadRequest)
}
if len(ar.RedirectUri) == 0 || len(ar.RedirectUri) > 256 || !IsValidHttpUrl(ar.RedirectUri) {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.redirect_uri.app_error", nil, "client_id="+ar.ClientId, http.StatusBadRequest)
}
if len(ar.State) > 128 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.state.app_error", nil, "client_id="+ar.ClientId, http.StatusBadRequest)
}
if len(ar.Scope) > 128 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.scope.app_error", nil, "client_id="+ar.ClientId, http.StatusBadRequest)
}
return nil
}
func (ad *AuthData) PreSave() { func (ad *AuthData) PreSave() {
if ad.ExpiresIn == 0 { if ad.ExpiresIn == 0 {
ad.ExpiresIn = AUTHCODE_EXPIRE_TIME ad.ExpiresIn = AUTHCODE_EXPIRE_TIME
@@ -98,6 +134,26 @@ func AuthDataFromJson(data io.Reader) *AuthData {
} }
} }
func (ar *AuthorizeRequest) ToJson() string {
b, err := json.Marshal(ar)
if err != nil {
return ""
} else {
return string(b)
}
}
func AuthorizeRequestFromJson(data io.Reader) *AuthorizeRequest {
decoder := json.NewDecoder(data)
var ar AuthorizeRequest
err := decoder.Decode(&ar)
if err == nil {
return &ar
} else {
return nil
}
}
func (ad *AuthData) IsExpired() bool { func (ad *AuthData) IsExpired() bool {
if GetMillis() > ad.CreateAt+int64(ad.ExpiresIn*1000) { if GetMillis() > ad.CreateAt+int64(ad.ExpiresIn*1000) {

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model
@@ -46,6 +46,13 @@ type Channel struct {
CreatorId string `json:"creator_id"` CreatorId string `json:"creator_id"`
} }
type ChannelPatch struct {
DisplayName *string `json:"display_name"`
Name *string `json:"name"`
Header *string `json:"header"`
Purpose *string `json:"purpose"`
}
func (o *Channel) ToJson() string { func (o *Channel) ToJson() string {
b, err := json.Marshal(o) b, err := json.Marshal(o)
if err != nil { if err != nil {
@@ -55,6 +62,15 @@ func (o *Channel) ToJson() string {
} }
} }
func (o *ChannelPatch) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func ChannelFromJson(data io.Reader) *Channel { func ChannelFromJson(data io.Reader) *Channel {
decoder := json.NewDecoder(data) decoder := json.NewDecoder(data)
var o Channel var o Channel
@@ -66,6 +82,17 @@ func ChannelFromJson(data io.Reader) *Channel {
} }
} }
func ChannelPatchFromJson(data io.Reader) *ChannelPatch {
decoder := json.NewDecoder(data)
var o ChannelPatch
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}
func (o *Channel) Etag() string { func (o *Channel) Etag() string {
return Etag(o.Id, o.UpdateAt) return Etag(o.Id, o.UpdateAt)
} }
@@ -137,6 +164,24 @@ func (o *Channel) IsGroupOrDirect() bool {
return o.Type == CHANNEL_DIRECT || o.Type == CHANNEL_GROUP return o.Type == CHANNEL_DIRECT || o.Type == CHANNEL_GROUP
} }
func (o *Channel) Patch(patch *ChannelPatch) {
if patch.DisplayName != nil {
o.DisplayName = *patch.DisplayName
}
if patch.Name != nil {
o.Name = *patch.Name
}
if patch.Header != nil {
o.Header = *patch.Header
}
if patch.Purpose != nil {
o.Purpose = *patch.Purpose
}
}
func GetDMNameFromIds(userId1, userId2 string) string { func GetDMNameFromIds(userId1, userId2 string) string {
if userId1 > userId2 { if userId1 > userId2 {
return userId2 + "__" + userId1 return userId2 + "__" + userId1

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model
@@ -50,3 +50,14 @@ func ChannelListFromJson(data io.Reader) *ChannelList {
return nil return nil
} }
} }
func ChannelSliceFromJson(data io.Reader) []*Channel {
decoder := json.NewDecoder(data)
var o []*Channel
err := decoder.Decode(&o)
if err == nil {
return o
} else {
return nil
}
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model
@@ -19,11 +19,11 @@ const (
) )
type ChannelUnread struct { type ChannelUnread struct {
TeamId string TeamId string `json:"team_id"`
TotalMsgCount int64 ChannelId string `json:"channel_id"`
MsgCount int64 MsgCount int64 `json:"msg_count"`
MentionCount int64 MentionCount int64 `json:"mention_count"`
NotifyProps StringMap NotifyProps StringMap `json:"-"`
} }
type ChannelMember struct { type ChannelMember struct {
@@ -47,6 +47,15 @@ func (o *ChannelMembers) ToJson() string {
} }
} }
func (o *ChannelUnread) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func ChannelMembersFromJson(data io.Reader) *ChannelMembers { func ChannelMembersFromJson(data io.Reader) *ChannelMembers {
decoder := json.NewDecoder(data) decoder := json.NewDecoder(data)
var o ChannelMembers var o ChannelMembers
@@ -58,6 +67,17 @@ func ChannelMembersFromJson(data io.Reader) *ChannelMembers {
} }
} }
func ChannelUnreadFromJson(data io.Reader) *ChannelUnread {
decoder := json.NewDecoder(data)
var o ChannelUnread
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}
func (o *ChannelMember) ToJson() string { func (o *ChannelMember) ToJson() string {
b, err := json.Marshal(o) b, err := json.Marshal(o)
if err != nil { if err != nil {

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model
@@ -1191,17 +1191,6 @@ func (c *Client) GetChannel(id, etag string) (*Result, *AppError) {
} }
} }
// SCHEDULED FOR DEPRECATION IN 3.7 - use GetMoreChannelsPage instead
func (c *Client) GetMoreChannels(etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/more", "", etag); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ChannelListFromJson(r.Body)}, nil
}
}
// GetMoreChannelsPage will return a page of open channels the user is not in based on // GetMoreChannelsPage will return a page of open channels the user is not in based on
// the provided offset and limit. Must be authenticated. // the provided offset and limit. Must be authenticated.
func (c *Client) GetMoreChannelsPage(offset int, limit int) (*Result, *AppError) { func (c *Client) GetMoreChannelsPage(offset int, limit int) (*Result, *AppError) {
@@ -1333,22 +1322,6 @@ func (c *Client) RemoveChannelMember(id, user_id string) (*Result, *AppError) {
} }
} }
// UpdateLastViewedAt will mark a channel as read.
// The channelId indicates the channel to mark as read. If active is true, push notifications
// will be cleared if there are unread messages. The default for active is true.
// SCHEDULED FOR DEPRECATION IN 3.8 - use ViewChannel instead
func (c *Client) UpdateLastViewedAt(channelId string, active bool) (*Result, *AppError) {
data := make(map[string]interface{})
data["active"] = active
if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/update_last_viewed_at", StringInterfaceToJson(data)); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), nil}, nil
}
}
// ViewChannel performs all the actions related to viewing a channel. This includes marking // ViewChannel performs all the actions related to viewing a channel. This includes marking
// the channel and the previous one as read, and marking the channel as being actively viewed. // the channel and the previous one as read, and marking the channel as being actively viewed.
// ChannelId is required but may be blank to indicate no channel is being viewed. // ChannelId is required but may be blank to indicate no channel is being viewed.
@@ -1533,6 +1506,16 @@ func (c *Client) GetFlaggedPosts(offset int, limit int) (*Result, *AppError) {
} }
} }
func (c *Client) GetPinnedPosts(channelId string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/pinned", "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil
}
}
func (c *Client) UploadProfileFile(data []byte, contentType string) (*Result, *AppError) { func (c *Client) UploadProfileFile(data []byte, contentType string) (*Result, *AppError) {
return c.uploadFile(c.ApiUrl+"/users/newimage", data, contentType) return c.uploadFile(c.ApiUrl+"/users/newimage", data, contentType)
} }
@@ -1782,22 +1765,6 @@ func (c *Client) GetStatusesByIds(userIds []string) (*Result, *AppError) {
} }
} }
// SetActiveChannel sets the the channel id the user is currently viewing.
// The channelId key is required but the value can be blank. Returns standard
// response.
// SCHEDULED FOR DEPRECATION IN 3.8 - use ViewChannel instead
func (c *Client) SetActiveChannel(channelId string) (*Result, *AppError) {
data := map[string]string{}
data["channel_id"] = channelId
if r, err := c.DoApiPost("/users/status/set_active_channel", MapToJson(data)); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
}
}
func (c *Client) GetMyTeam(etag string) (*Result, *AppError) { func (c *Client) GetMyTeam(etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetTeamRoute()+"/me", "", etag); err != nil { if r, err := c.DoApiGet(c.GetTeamRoute()+"/me", "", etag); err != nil {
return nil, err return nil, err
@@ -1874,15 +1841,14 @@ func (c *Client) GetTeamStats(teamId string) (*Result, *AppError) {
} }
} }
// GetTeamStats will return a team stats object containing the number of users on the team // GetTeamByName will return a team object based on the team name provided. Must be authenticated.
// based on the team id provided. Must be authenticated.
func (c *Client) GetTeamByName(teamName string) (*Result, *AppError) { func (c *Client) GetTeamByName(teamName string) (*Result, *AppError) {
if r, err := c.DoApiGet(fmt.Sprintf("/teams/name/%v", teamName), "", ""); err != nil { if r, err := c.DoApiGet(fmt.Sprintf("/teams/name/%v", teamName), "", ""); err != nil {
return nil, err return nil, err
} else { } else {
defer closeBody(r) defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID), return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), TeamStatsFromJson(r.Body)}, nil r.Header.Get(HEADER_ETAG_SERVER), TeamFromJson(r.Body)}, nil
} }
} }
@@ -2389,3 +2355,23 @@ func (c *Client) UpdateChannelRoles(channelId string, userId string, roles strin
} }
} }
} }
func (c *Client) PinPost(channelId string, postId string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/posts/"+postId+"/pin", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), PostFromJson(r.Body)}, nil
}
}
func (c *Client) UnpinPost(channelId string, postId string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/posts/"+postId+"/unpin", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), PostFromJson(r.Body)}, nil
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,132 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
"os"
)
const (
CDS_OFFLINE_AFTER_MILLIS = 1000 * 60 * 30 // 30 minutes
CDS_TYPE_APP = "mattermost_app"
)
type ClusterDiscovery struct {
Id string `json:"id"`
Type string `json:"type"`
ClusterName string `json:"cluster_name"`
Hostname string `json:"hostname"`
GossipPort int32 `json:"gossip_port"`
Port int32 `json:"port"`
CreateAt int64 `json:"create_at"`
LastPingAt int64 `json:"last_ping_at"`
}
func (o *ClusterDiscovery) PreSave() {
if o.Id == "" {
o.Id = NewId()
}
if o.CreateAt == 0 {
o.CreateAt = GetMillis()
o.LastPingAt = o.CreateAt
}
}
func (o *ClusterDiscovery) AutoFillHostname() {
// attempt to set the hostname from the OS
if len(o.Hostname) == 0 {
if hn, err := os.Hostname(); err == nil {
o.Hostname = hn
}
}
}
func (o *ClusterDiscovery) AutoFillIpAddress() {
// attempt to set the hostname to the first non-local IP address
if len(o.Hostname) == 0 {
o.Hostname = GetServerIpAddress()
}
}
func (o *ClusterDiscovery) IsEqual(in *ClusterDiscovery) bool {
if in == nil {
return false
}
if o.Type != in.Type {
return false
}
if o.ClusterName != in.ClusterName {
return false
}
if o.Hostname != in.Hostname {
return false
}
return true
}
func FilterClusterDiscovery(vs []*ClusterDiscovery, f func(*ClusterDiscovery) bool) []*ClusterDiscovery {
copy := make([]*ClusterDiscovery, 0)
for _, v := range vs {
if f(v) {
copy = append(copy, v)
}
}
return copy
}
func (o *ClusterDiscovery) IsValid() *AppError {
if len(o.Id) != 26 {
return NewLocAppError("Channel.IsValid", "model.channel.is_valid.id.app_error", nil, "")
}
if len(o.ClusterName) == 0 {
return NewLocAppError("ClusterDiscovery.IsValid", "ClusterName must be set", nil, "")
}
if len(o.Type) == 0 {
return NewLocAppError("ClusterDiscovery.IsValid", "Type must be set", nil, "")
}
if len(o.Hostname) == 0 {
return NewLocAppError("ClusterDiscovery.IsValid", "Hostname must be set", nil, "")
}
if o.CreateAt == 0 {
return NewLocAppError("ClusterDiscovery.IsValid", "CreateAt must be set", nil, "")
}
if o.LastPingAt == 0 {
return NewLocAppError("ClusterDiscovery.IsValid", "LastPingAt must be set", nil, "")
}
return nil
}
func (o *ClusterDiscovery) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
}
return string(b)
}
func ClusterDiscoveryFromJson(data io.Reader) *ClusterDiscovery {
decoder := json.NewDecoder(data)
var me ClusterDiscovery
err := decoder.Decode(&me)
if err == nil {
return &me
}
return nil
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model
@@ -6,16 +6,14 @@ package model
import ( import (
"encoding/json" "encoding/json"
"io" "io"
"strings"
) )
type ClusterInfo struct { type ClusterInfo struct {
Id string `json:"id"` Version string `json:"version"`
Version string `json:"version"` ConfigHash string `json:"config_hash"`
ConfigHash string `json:"config_hash"` IpAddress string `json:"ipaddress"`
InterNodeUrl string `json:"internode_url"` Hostname string `json:"hostname"`
Hostname string `json:"hostname"`
LastSuccessfulPing int64 `json:"last_ping"`
IsAlive bool `json:"is_alive"`
} }
func (me *ClusterInfo) ToJson() string { func (me *ClusterInfo) ToJson() string {
@@ -27,6 +25,11 @@ func (me *ClusterInfo) ToJson() string {
} }
} }
func (me *ClusterInfo) Copy() *ClusterInfo {
json := me.ToJson()
return ClusterInfoFromJson(strings.NewReader(json))
}
func ClusterInfoFromJson(data io.Reader) *ClusterInfo { func ClusterInfoFromJson(data io.Reader) *ClusterInfo {
decoder := json.NewDecoder(data) decoder := json.NewDecoder(data)
var me ClusterInfo var me ClusterInfo
@@ -38,14 +41,6 @@ func ClusterInfoFromJson(data io.Reader) *ClusterInfo {
} }
} }
func (me *ClusterInfo) HaveEstablishedInitialContact() bool {
if me.Id != "" {
return true
}
return false
}
func ClusterInfosToJson(objmap []*ClusterInfo) string { func ClusterInfosToJson(objmap []*ClusterInfo) string {
if b, err := json.Marshal(objmap); err != nil { if b, err := json.Marshal(objmap); err != nil {
return "" return ""

View File

@@ -0,0 +1,55 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
const (
CLUSTER_EVENT_PUBLISH = "publish"
CLUSTER_EVENT_UPDATE_STATUS = "update_status"
CLUSTER_EVENT_INVALIDATE_ALL_CACHES = "inv_all_caches"
CLUSTER_EVENT_INVALIDATE_CACHE_FOR_REACTIONS = "inv_reactions"
CLUSTER_EVENT_INVALIDATE_CACHE_FOR_WEBHOOK = "inv_webhook"
CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL_POSTS = "inv_channel_posts"
CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL_MEMBERS_NOTIFY_PROPS = "inv_channel_members_notify_props"
CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL_MEMBERS = "inv_channel_members"
CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL_BY_NAME = "inv_channel_name"
CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL = "inv_channel"
CLUSTER_EVENT_INVALIDATE_CACHE_FOR_USER = "inv_user"
CLUSTER_EVENT_CLEAR_SESSION_CACHE_FOR_USER = "clear_session_user"
CLUSTER_SEND_BEST_EFFORT = "best_effort"
CLUSTER_SEND_RELIABLE = "reliable"
)
type ClusterMessage struct {
Event string `json:"event"`
SendType string `json:"-"`
WaitForAllToSend bool `json:"-"`
Data string `json:"data,omitempty"`
Props map[string]string `json:"props,omitempty"`
}
func (o *ClusterMessage) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func ClusterMessageFromJson(data io.Reader) *ClusterMessage {
decoder := json.NewDecoder(data)
var o ClusterMessage
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model
@@ -6,13 +6,20 @@ package model
import ( import (
"encoding/json" "encoding/json"
"io" "io"
goi18n "github.com/nicksnyder/go-i18n/i18n"
) )
type CommandArgs struct { type CommandArgs struct {
ChannelId string `json:"channel_id"` UserId string `json:"user_id"`
RootId string `json:"root_id"` ChannelId string `json:"channel_id"`
ParentId string `json:"parent_id"` TeamId string `json:"team_id"`
Command string `json:"command"` RootId string `json:"root_id"`
ParentId string `json:"parent_id"`
Command string `json:"command"`
SiteURL string `json:"-"`
T goi18n.TranslateFunc `json:"-"`
Session Session `json:"-"`
} }
func (o *CommandArgs) ToJson() string { func (o *CommandArgs) ToJson() string {

View File

@@ -1,11 +1,10 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io" "io"
) )
@@ -40,14 +39,8 @@ func CommandResponseFromJson(data io.Reader) *CommandResponse {
return nil return nil
} }
// Ensure attachment fields are stored as strings o.Text = ExpandAnnouncement(o.Text)
for _, attachment := range o.Attachments { o.Attachments = ProcessSlackAttachments(o.Attachments)
for _, field := range attachment.Fields {
if field.Value != nil {
field.Value = fmt.Sprintf("%v", field.Value)
}
}
}
return &o return &o
} }

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model

View File

@@ -1,9 +1,10 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model
import ( import (
"regexp"
"time" "time"
) )
@@ -64,6 +65,15 @@ func CompliancePostHeader() []string {
} }
} }
func cleanComplianceStrings(in string) string {
if matched, _ := regexp.MatchString("^\\s*(=|\\+|\\-)", in); matched {
return "'" + in
} else {
return in
}
}
func (me *CompliancePost) Row() []string { func (me *CompliancePost) Row() []string {
postDeleteAt := "" postDeleteAt := ""
@@ -77,15 +87,15 @@ func (me *CompliancePost) Row() []string {
} }
return []string{ return []string{
me.TeamName, cleanComplianceStrings(me.TeamName),
me.TeamDisplayName, cleanComplianceStrings(me.TeamDisplayName),
me.ChannelName, cleanComplianceStrings(me.ChannelName),
me.ChannelDisplayName, cleanComplianceStrings(me.ChannelDisplayName),
me.UserUsername, cleanComplianceStrings(me.UserUsername),
me.UserEmail, cleanComplianceStrings(me.UserEmail),
me.UserNickname, cleanComplianceStrings(me.UserNickname),
me.PostId, me.PostId,
time.Unix(0, me.PostCreateAt*int64(1000*1000)).Format(time.RFC3339), time.Unix(0, me.PostCreateAt*int64(1000*1000)).Format(time.RFC3339),
@@ -95,7 +105,7 @@ func (me *CompliancePost) Row() []string {
me.PostRootId, me.PostRootId,
me.PostParentId, me.PostParentId,
me.PostOriginalId, me.PostOriginalId,
me.PostMessage, cleanComplianceStrings(me.PostMessage),
me.PostType, me.PostType,
me.PostProps, me.PostProps,
me.PostHashtags, me.PostHashtags,

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model
@@ -6,6 +6,7 @@ package model
import ( import (
"encoding/json" "encoding/json"
"io" "io"
"net/http"
"net/url" "net/url"
) )
@@ -32,12 +33,17 @@ const (
WEBSERVER_MODE_GZIP = "gzip" WEBSERVER_MODE_GZIP = "gzip"
WEBSERVER_MODE_DISABLED = "disabled" WEBSERVER_MODE_DISABLED = "disabled"
GENERIC_NOTIFICATION = "generic" GENERIC_NO_CHANNEL_NOTIFICATION = "generic_no_channel"
FULL_NOTIFICATION = "full" GENERIC_NOTIFICATION = "generic"
FULL_NOTIFICATION = "full"
DIRECT_MESSAGE_ANY = "any" DIRECT_MESSAGE_ANY = "any"
DIRECT_MESSAGE_TEAM = "team" DIRECT_MESSAGE_TEAM = "team"
SHOW_USERNAME = "username"
SHOW_NICKNAME_FULLNAME = "nickname_full_name"
SHOW_FULLNAME = "full_name"
PERMISSIONS_ALL = "all" PERMISSIONS_ALL = "all"
PERMISSIONS_CHANNEL_ADMIN = "channel_admin" PERMISSIONS_CHANNEL_ADMIN = "channel_admin"
PERMISSIONS_TEAM_ADMIN = "team_admin" PERMISSIONS_TEAM_ADMIN = "team_admin"
@@ -60,6 +66,9 @@ const (
EMAIL_BATCHING_BUFFER_SIZE = 256 EMAIL_BATCHING_BUFFER_SIZE = 256
EMAIL_BATCHING_INTERVAL = 30 EMAIL_BATCHING_INTERVAL = 30
EMAIL_NOTIFICATION_CONTENTS_FULL = "full"
EMAIL_NOTIFICATION_CONTENTS_GENERIC = "generic"
SITENAME_MAX_LENGTH = 30 SITENAME_MAX_LENGTH = 30
SERVICE_SETTINGS_DEFAULT_SITE_URL = "" SERVICE_SETTINGS_DEFAULT_SITE_URL = ""
@@ -75,12 +84,15 @@ const (
EMAIL_SETTINGS_DEFAULT_FEEDBACK_ORGANIZATION = "" EMAIL_SETTINGS_DEFAULT_FEEDBACK_ORGANIZATION = ""
SUPPORT_SETTINGS_DEFAULT_TERMS_OF_SERVICE_LINK = "https://about.mattermost.com/default-terms/" SUPPORT_SETTINGS_DEFAULT_TERMS_OF_SERVICE_LINK = "https://about.mattermost.com/default-terms/"
SUPPORT_SETTINGS_DEFAULT_PRIVACY_POLICY_LINK = "https://about.mattermost.com/default-privacy-policy/" SUPPORT_SETTINGS_DEFAULT_PRIVACY_POLICY_LINK = "https://about.mattermost.com/default-privacy-policy/"
SUPPORT_SETTINGS_DEFAULT_ABOUT_LINK = "https://about.mattermost.com/default-about/" SUPPORT_SETTINGS_DEFAULT_ABOUT_LINK = "https://about.mattermost.com/default-about/"
SUPPORT_SETTINGS_DEFAULT_HELP_LINK = "https://about.mattermost.com/default-help/" SUPPORT_SETTINGS_DEFAULT_HELP_LINK = "https://about.mattermost.com/default-help/"
SUPPORT_SETTINGS_DEFAULT_REPORT_A_PROBLEM_LINK = "https://about.mattermost.com/default-report-a-problem/" SUPPORT_SETTINGS_DEFAULT_REPORT_A_PROBLEM_LINK = "https://about.mattermost.com/default-report-a-problem/"
SUPPORT_SETTINGS_DEFAULT_SUPPORT_EMAIL = "feedback@mattermost.com" SUPPORT_SETTINGS_DEFAULT_ADMINISTRATORS_GUIDE_LINK = "https://about.mattermost.com/administrators-guide/"
SUPPORT_SETTINGS_DEFAULT_TROUBLESHOOTING_FORUM_LINK = "https://about.mattermost.com/troubleshooting-forum/"
SUPPORT_SETTINGS_DEFAULT_COMMERCIAL_SUPPORT_LINK = "https://about.mattermost.com/commercial-support/"
SUPPORT_SETTINGS_DEFAULT_SUPPORT_EMAIL = "feedback@mattermost.com"
LDAP_SETTINGS_DEFAULT_FIRST_NAME_ATTRIBUTE = "" LDAP_SETTINGS_DEFAULT_FIRST_NAME_ATTRIBUTE = ""
LDAP_SETTINGS_DEFAULT_LAST_NAME_ATTRIBUTE = "" LDAP_SETTINGS_DEFAULT_LAST_NAME_ATTRIBUTE = ""
@@ -107,10 +119,20 @@ const (
WEBRTC_SETTINGS_DEFAULT_TURN_URI = "" WEBRTC_SETTINGS_DEFAULT_TURN_URI = ""
ANALYTICS_SETTINGS_DEFAULT_MAX_USERS_FOR_STATISTICS = 2500 ANALYTICS_SETTINGS_DEFAULT_MAX_USERS_FOR_STATISTICS = 2500
ANNOUNCEMENT_SETTINGS_DEFAULT_BANNER_COLOR = "#f2a93b"
ANNOUNCEMENT_SETTINGS_DEFAULT_BANNER_TEXT_COLOR = "#333333"
ELASTICSEARCH_SETTINGS_DEFAULT_CONNECTION_URL = ""
ELASTICSEARCH_SETTINGS_DEFAULT_USERNAME = ""
ELASTICSEARCH_SETTINGS_DEFAULT_PASSWORD = ""
ELASTICSEARCH_SETTINGS_DEFAULT_POST_INDEX_REPLICAS = 1
ELASTICSEARCH_SETTINGS_DEFAULT_POST_INDEX_SHARDS = 1
) )
type ServiceSettings struct { type ServiceSettings struct {
SiteURL *string SiteURL *string
LicenseFileLocation *string
ListenAddress string ListenAddress string
ConnectionSecurity *string ConnectionSecurity *string
TLSCertFile *string TLSCertFile *string
@@ -121,6 +143,7 @@ type ServiceSettings struct {
ReadTimeout *int ReadTimeout *int
WriteTimeout *int WriteTimeout *int
MaximumLoginAttempts int MaximumLoginAttempts int
GoroutineHealthThreshold *int
GoogleDeveloperKey string GoogleDeveloperKey string
EnableOAuthServiceProvider bool EnableOAuthServiceProvider bool
EnableIncomingWebhooks bool EnableIncomingWebhooks bool
@@ -129,6 +152,7 @@ type ServiceSettings struct {
EnableOnlyAdminIntegrations *bool EnableOnlyAdminIntegrations *bool
EnablePostUsernameOverride bool EnablePostUsernameOverride bool
EnablePostIconOverride bool EnablePostIconOverride bool
EnableAPIv3 *bool
EnableLinkPreviews *bool EnableLinkPreviews *bool
EnableTesting bool EnableTesting bool
EnableDeveloper *bool EnableDeveloper *bool
@@ -136,6 +160,7 @@ type ServiceSettings struct {
EnableInsecureOutgoingConnections *bool EnableInsecureOutgoingConnections *bool
EnableMultifactorAuthentication *bool EnableMultifactorAuthentication *bool
EnforceMultifactorAuthentication *bool EnforceMultifactorAuthentication *bool
EnableUserAccessTokens *bool
AllowCorsFrom *string AllowCorsFrom *string
SessionLengthWebInDays *int SessionLengthWebInDays *int
SessionLengthMobileInDays *int SessionLengthMobileInDays *int
@@ -145,19 +170,28 @@ type ServiceSettings struct {
WebsocketPort *int WebsocketPort *int
WebserverMode *string WebserverMode *string
EnableCustomEmoji *bool EnableCustomEmoji *bool
EnableEmojiPicker *bool
RestrictCustomEmojiCreation *string RestrictCustomEmojiCreation *string
RestrictPostDelete *string RestrictPostDelete *string
AllowEditPost *string AllowEditPost *string
PostEditTimeLimit *int PostEditTimeLimit *int
TimeBetweenUserTypingUpdatesMilliseconds *int64 TimeBetweenUserTypingUpdatesMilliseconds *int64
EnablePostSearch *bool
EnableUserTypingMessages *bool EnableUserTypingMessages *bool
EnableChannelViewedMessages *bool
EnableUserStatuses *bool
ClusterLogTimeoutMilliseconds *int ClusterLogTimeoutMilliseconds *int
} }
type ClusterSettings struct { type ClusterSettings struct {
Enable *bool Enable *bool
InterNodeListenAddress *string ClusterName *string
InterNodeUrls []string OverrideHostname *string
UseIpAddress *bool
UseExperimentalGossip *bool
ReadOnlyConfig *bool
GossipPort *int
StreamingPort *int
} }
type MetricsSettings struct { type MetricsSettings struct {
@@ -181,13 +215,15 @@ type SSOSettings struct {
} }
type SqlSettings struct { type SqlSettings struct {
DriverName string DriverName string
DataSource string DataSource string
DataSourceReplicas []string DataSourceReplicas []string
MaxIdleConns int DataSourceSearchReplicas []string
MaxOpenConns int MaxIdleConns int
Trace bool MaxOpenConns int
AtRestEncryptKey string Trace bool
AtRestEncryptKey string
QueryTimeout *int
} }
type LogSettings struct { type LogSettings struct {
@@ -210,17 +246,14 @@ type PasswordSettings struct {
} }
type FileSettings struct { type FileSettings struct {
EnableFileAttachments *bool
EnableMobileUpload *bool
EnableMobileDownload *bool
MaxFileSize *int64 MaxFileSize *int64
DriverName string DriverName string
Directory string Directory string
EnablePublicLink bool EnablePublicLink bool
PublicLinkSalt *string PublicLinkSalt *string
ThumbnailWidth int
ThumbnailHeight int
PreviewWidth int
PreviewHeight int
ProfileWidth int
ProfileHeight int
InitialFont string InitialFont string
AmazonS3AccessKeyId string AmazonS3AccessKeyId string
AmazonS3SecretAccessKey string AmazonS3SecretAccessKey string
@@ -228,30 +261,34 @@ type FileSettings struct {
AmazonS3Region string AmazonS3Region string
AmazonS3Endpoint string AmazonS3Endpoint string
AmazonS3SSL *bool AmazonS3SSL *bool
AmazonS3SignV2 *bool
AmazonS3SSE *bool
} }
type EmailSettings struct { type EmailSettings struct {
EnableSignUpWithEmail bool EnableSignUpWithEmail bool
EnableSignInWithEmail *bool EnableSignInWithEmail *bool
EnableSignInWithUsername *bool EnableSignInWithUsername *bool
SendEmailNotifications bool SendEmailNotifications bool
RequireEmailVerification bool RequireEmailVerification bool
FeedbackName string FeedbackName string
FeedbackEmail string FeedbackEmail string
FeedbackOrganization *string FeedbackOrganization *string
SMTPUsername string EnableSMTPAuth *bool
SMTPPassword string SMTPUsername string
SMTPServer string SMTPPassword string
SMTPPort string SMTPServer string
ConnectionSecurity string SMTPPort string
InviteSalt string ConnectionSecurity string
PasswordResetSalt string InviteSalt string
SendPushNotifications *bool SendPushNotifications *bool
PushNotificationServer *string PushNotificationServer *string
PushNotificationContents *string PushNotificationContents *string
EnableEmailBatching *bool EnableEmailBatching *bool
EmailBatchingBufferSize *int EmailBatchingBufferSize *int
EmailBatchingInterval *int EmailBatchingInterval *int
SkipServerCertificateVerification *bool
EmailNotificationContentsType *string
} }
type RateLimitSettings struct { type RateLimitSettings struct {
@@ -269,35 +306,48 @@ type PrivacySettings struct {
} }
type SupportSettings struct { type SupportSettings struct {
TermsOfServiceLink *string TermsOfServiceLink *string
PrivacyPolicyLink *string PrivacyPolicyLink *string
AboutLink *string AboutLink *string
HelpLink *string HelpLink *string
ReportAProblemLink *string ReportAProblemLink *string
SupportEmail *string AdministratorsGuideLink *string
TroubleshootingForumLink *string
CommercialSupportLink *string
SupportEmail *string
}
type AnnouncementSettings struct {
EnableBanner *bool
BannerText *string
BannerColor *string
BannerTextColor *string
AllowBannerDismissal *bool
} }
type TeamSettings struct { type TeamSettings struct {
SiteName string SiteName string
MaxUsersPerTeam int MaxUsersPerTeam int
EnableTeamCreation bool EnableTeamCreation bool
EnableUserCreation bool EnableUserCreation bool
EnableOpenServer *bool EnableOpenServer *bool
RestrictCreationToDomains string RestrictCreationToDomains string
EnableCustomBrand *bool EnableCustomBrand *bool
CustomBrandText *string CustomBrandText *string
CustomDescriptionText *string CustomDescriptionText *string
RestrictDirectMessage *string RestrictDirectMessage *string
RestrictTeamInvite *string RestrictTeamInvite *string
RestrictPublicChannelManagement *string RestrictPublicChannelManagement *string
RestrictPrivateChannelManagement *string RestrictPrivateChannelManagement *string
RestrictPublicChannelCreation *string RestrictPublicChannelCreation *string
RestrictPrivateChannelCreation *string RestrictPrivateChannelCreation *string
RestrictPublicChannelDeletion *string RestrictPublicChannelDeletion *string
RestrictPrivateChannelDeletion *string RestrictPrivateChannelDeletion *string
UserStatusAwayTimeout *int64 RestrictPrivateChannelManageMembers *string
MaxChannelsPerTeam *int64 UserStatusAwayTimeout *int64
MaxNotificationsPerChannel *int64 MaxChannelsPerTeam *int64
MaxNotificationsPerChannel *int64
TeammateNameDisplay *string
} }
type LdapSettings struct { type LdapSettings struct {
@@ -389,29 +439,58 @@ type WebrtcSettings struct {
TurnSharedKey *string TurnSharedKey *string
} }
type ElasticsearchSettings struct {
ConnectionUrl *string
Username *string
Password *string
EnableIndexing *bool
EnableSearching *bool
Sniff *bool
PostIndexReplicas *int
PostIndexShards *int
}
type DataRetentionSettings struct {
Enable *bool
}
type JobSettings struct {
RunJobs *bool
RunScheduler *bool
}
type PluginSettings struct {
Plugins map[string]interface{}
}
type Config struct { type Config struct {
ServiceSettings ServiceSettings ServiceSettings ServiceSettings
TeamSettings TeamSettings TeamSettings TeamSettings
SqlSettings SqlSettings SqlSettings SqlSettings
LogSettings LogSettings LogSettings LogSettings
PasswordSettings PasswordSettings PasswordSettings PasswordSettings
FileSettings FileSettings FileSettings FileSettings
EmailSettings EmailSettings EmailSettings EmailSettings
RateLimitSettings RateLimitSettings RateLimitSettings RateLimitSettings
PrivacySettings PrivacySettings PrivacySettings PrivacySettings
SupportSettings SupportSettings SupportSettings SupportSettings
GitLabSettings SSOSettings AnnouncementSettings AnnouncementSettings
GoogleSettings SSOSettings GitLabSettings SSOSettings
Office365Settings SSOSettings GoogleSettings SSOSettings
LdapSettings LdapSettings Office365Settings SSOSettings
ComplianceSettings ComplianceSettings LdapSettings LdapSettings
LocalizationSettings LocalizationSettings ComplianceSettings ComplianceSettings
SamlSettings SamlSettings LocalizationSettings LocalizationSettings
NativeAppSettings NativeAppSettings SamlSettings SamlSettings
ClusterSettings ClusterSettings NativeAppSettings NativeAppSettings
MetricsSettings MetricsSettings ClusterSettings ClusterSettings
AnalyticsSettings AnalyticsSettings MetricsSettings MetricsSettings
WebrtcSettings WebrtcSettings AnalyticsSettings AnalyticsSettings
WebrtcSettings WebrtcSettings
ElasticsearchSettings ElasticsearchSettings
DataRetentionSettings DataRetentionSettings
JobSettings JobSettings
PluginSettings PluginSettings
} }
func (o *Config) ToJson() string { func (o *Config) ToJson() string {
@@ -453,27 +532,52 @@ func (o *Config) SetDefaults() {
o.SqlSettings.AtRestEncryptKey = NewRandomString(32) o.SqlSettings.AtRestEncryptKey = NewRandomString(32)
} }
if o.SqlSettings.QueryTimeout == nil {
o.SqlSettings.QueryTimeout = new(int)
*o.SqlSettings.QueryTimeout = 30
}
if o.FileSettings.AmazonS3Endpoint == "" { if o.FileSettings.AmazonS3Endpoint == "" {
// Defaults to "s3.amazonaws.com" // Defaults to "s3.amazonaws.com"
o.FileSettings.AmazonS3Endpoint = "s3.amazonaws.com" o.FileSettings.AmazonS3Endpoint = "s3.amazonaws.com"
} }
if o.FileSettings.AmazonS3Region == "" {
// Defaults to "us-east-1" region.
o.FileSettings.AmazonS3Region = "us-east-1"
}
if o.FileSettings.AmazonS3SSL == nil { if o.FileSettings.AmazonS3SSL == nil {
o.FileSettings.AmazonS3SSL = new(bool) o.FileSettings.AmazonS3SSL = new(bool)
*o.FileSettings.AmazonS3SSL = true // Secure by default. *o.FileSettings.AmazonS3SSL = true // Secure by default.
} }
if o.FileSettings.AmazonS3SignV2 == nil {
o.FileSettings.AmazonS3SignV2 = new(bool)
// Signature v2 is not enabled by default.
}
if o.FileSettings.AmazonS3SSE == nil {
o.FileSettings.AmazonS3SSE = new(bool)
*o.FileSettings.AmazonS3SSE = false // Not Encrypted by default.
}
if o.FileSettings.EnableFileAttachments == nil {
o.FileSettings.EnableFileAttachments = new(bool)
*o.FileSettings.EnableFileAttachments = true
}
if o.FileSettings.EnableMobileUpload == nil {
o.FileSettings.EnableMobileUpload = new(bool)
*o.FileSettings.EnableMobileUpload = true
}
if o.FileSettings.EnableMobileDownload == nil {
o.FileSettings.EnableMobileDownload = new(bool)
*o.FileSettings.EnableMobileDownload = true
}
if o.FileSettings.MaxFileSize == nil { if o.FileSettings.MaxFileSize == nil {
o.FileSettings.MaxFileSize = new(int64) o.FileSettings.MaxFileSize = new(int64)
*o.FileSettings.MaxFileSize = 52428800 // 50 MB *o.FileSettings.MaxFileSize = 52428800 // 50 MB
} }
if len(*o.FileSettings.PublicLinkSalt) == 0 { if o.FileSettings.PublicLinkSalt == nil || len(*o.FileSettings.PublicLinkSalt) == 0 {
o.FileSettings.PublicLinkSalt = new(string) o.FileSettings.PublicLinkSalt = new(string)
*o.FileSettings.PublicLinkSalt = NewRandomString(32) *o.FileSettings.PublicLinkSalt = NewRandomString(32)
} }
@@ -483,12 +587,12 @@ func (o *Config) SetDefaults() {
o.FileSettings.InitialFont = "luximbi.ttf" o.FileSettings.InitialFont = "luximbi.ttf"
} }
if len(o.EmailSettings.InviteSalt) == 0 { if o.FileSettings.Directory == "" {
o.EmailSettings.InviteSalt = NewRandomString(32) o.FileSettings.Directory = "./data/"
} }
if len(o.EmailSettings.PasswordResetSalt) == 0 { if len(o.EmailSettings.InviteSalt) == 0 {
o.EmailSettings.PasswordResetSalt = NewRandomString(32) o.EmailSettings.InviteSalt = NewRandomString(32)
} }
if o.ServiceSettings.SiteURL == nil { if o.ServiceSettings.SiteURL == nil {
@@ -496,6 +600,15 @@ func (o *Config) SetDefaults() {
*o.ServiceSettings.SiteURL = SERVICE_SETTINGS_DEFAULT_SITE_URL *o.ServiceSettings.SiteURL = SERVICE_SETTINGS_DEFAULT_SITE_URL
} }
if o.ServiceSettings.LicenseFileLocation == nil {
o.ServiceSettings.LicenseFileLocation = new(string)
}
if o.ServiceSettings.EnableAPIv3 == nil {
o.ServiceSettings.EnableAPIv3 = new(bool)
*o.ServiceSettings.EnableAPIv3 = true
}
if o.ServiceSettings.EnableLinkPreviews == nil { if o.ServiceSettings.EnableLinkPreviews == nil {
o.ServiceSettings.EnableLinkPreviews = new(bool) o.ServiceSettings.EnableLinkPreviews = new(bool)
*o.ServiceSettings.EnableLinkPreviews = false *o.ServiceSettings.EnableLinkPreviews = false
@@ -526,6 +639,11 @@ func (o *Config) SetDefaults() {
*o.ServiceSettings.EnforceMultifactorAuthentication = false *o.ServiceSettings.EnforceMultifactorAuthentication = false
} }
if o.ServiceSettings.EnableUserAccessTokens == nil {
o.ServiceSettings.EnableUserAccessTokens = new(bool)
*o.ServiceSettings.EnableUserAccessTokens = false
}
if o.PasswordSettings.MinimumLength == nil { if o.PasswordSettings.MinimumLength == nil {
o.PasswordSettings.MinimumLength = new(int) o.PasswordSettings.MinimumLength = new(int)
*o.PasswordSettings.MinimumLength = PASSWORD_MINIMUM_LENGTH *o.PasswordSettings.MinimumLength = PASSWORD_MINIMUM_LENGTH
@@ -594,13 +712,21 @@ func (o *Config) SetDefaults() {
if o.TeamSettings.RestrictPublicChannelCreation == nil { if o.TeamSettings.RestrictPublicChannelCreation == nil {
o.TeamSettings.RestrictPublicChannelCreation = new(string) o.TeamSettings.RestrictPublicChannelCreation = new(string)
// If this setting does not exist, assume migration from <3.6, so use management setting as default. // If this setting does not exist, assume migration from <3.6, so use management setting as default.
*o.TeamSettings.RestrictPublicChannelCreation = *o.TeamSettings.RestrictPublicChannelManagement if *o.TeamSettings.RestrictPublicChannelManagement == PERMISSIONS_CHANNEL_ADMIN {
*o.TeamSettings.RestrictPublicChannelCreation = PERMISSIONS_TEAM_ADMIN
} else {
*o.TeamSettings.RestrictPublicChannelCreation = *o.TeamSettings.RestrictPublicChannelManagement
}
} }
if o.TeamSettings.RestrictPrivateChannelCreation == nil { if o.TeamSettings.RestrictPrivateChannelCreation == nil {
o.TeamSettings.RestrictPrivateChannelCreation = new(string) o.TeamSettings.RestrictPrivateChannelCreation = new(string)
// If this setting does not exist, assume migration from <3.6, so use management setting as default. // If this setting does not exist, assume migration from <3.6, so use management setting as default.
*o.TeamSettings.RestrictPrivateChannelCreation = *o.TeamSettings.RestrictPrivateChannelManagement if *o.TeamSettings.RestrictPrivateChannelManagement == PERMISSIONS_CHANNEL_ADMIN {
*o.TeamSettings.RestrictPrivateChannelCreation = PERMISSIONS_TEAM_ADMIN
} else {
*o.TeamSettings.RestrictPrivateChannelCreation = *o.TeamSettings.RestrictPrivateChannelManagement
}
} }
if o.TeamSettings.RestrictPublicChannelDeletion == nil { if o.TeamSettings.RestrictPublicChannelDeletion == nil {
@@ -615,6 +741,11 @@ func (o *Config) SetDefaults() {
*o.TeamSettings.RestrictPrivateChannelDeletion = *o.TeamSettings.RestrictPrivateChannelManagement *o.TeamSettings.RestrictPrivateChannelDeletion = *o.TeamSettings.RestrictPrivateChannelManagement
} }
if o.TeamSettings.RestrictPrivateChannelManageMembers == nil {
o.TeamSettings.RestrictPrivateChannelManageMembers = new(string)
*o.TeamSettings.RestrictPrivateChannelManageMembers = PERMISSIONS_ALL
}
if o.TeamSettings.UserStatusAwayTimeout == nil { if o.TeamSettings.UserStatusAwayTimeout == nil {
o.TeamSettings.UserStatusAwayTimeout = new(int64) o.TeamSettings.UserStatusAwayTimeout = new(int64)
*o.TeamSettings.UserStatusAwayTimeout = TEAM_SETTINGS_DEFAULT_USER_STATUS_AWAY_TIMEOUT *o.TeamSettings.UserStatusAwayTimeout = TEAM_SETTINGS_DEFAULT_USER_STATUS_AWAY_TIMEOUT
@@ -680,8 +811,31 @@ func (o *Config) SetDefaults() {
*o.EmailSettings.EmailBatchingInterval = EMAIL_BATCHING_INTERVAL *o.EmailSettings.EmailBatchingInterval = EMAIL_BATCHING_INTERVAL
} }
if o.EmailSettings.EnableSMTPAuth == nil {
o.EmailSettings.EnableSMTPAuth = new(bool)
if o.EmailSettings.ConnectionSecurity == CONN_SECURITY_NONE {
*o.EmailSettings.EnableSMTPAuth = false
} else {
*o.EmailSettings.EnableSMTPAuth = true
}
}
if o.EmailSettings.ConnectionSecurity == CONN_SECURITY_PLAIN {
o.EmailSettings.ConnectionSecurity = CONN_SECURITY_NONE
}
if o.EmailSettings.SkipServerCertificateVerification == nil {
o.EmailSettings.SkipServerCertificateVerification = new(bool)
*o.EmailSettings.SkipServerCertificateVerification = false
}
if o.EmailSettings.EmailNotificationContentsType == nil {
o.EmailSettings.EmailNotificationContentsType = new(string)
*o.EmailSettings.EmailNotificationContentsType = EMAIL_NOTIFICATION_CONTENTS_FULL
}
if !IsSafeLink(o.SupportSettings.TermsOfServiceLink) { if !IsSafeLink(o.SupportSettings.TermsOfServiceLink) {
o.SupportSettings.TermsOfServiceLink = nil *o.SupportSettings.TermsOfServiceLink = SUPPORT_SETTINGS_DEFAULT_TERMS_OF_SERVICE_LINK
} }
if o.SupportSettings.TermsOfServiceLink == nil { if o.SupportSettings.TermsOfServiceLink == nil {
@@ -690,7 +844,7 @@ func (o *Config) SetDefaults() {
} }
if !IsSafeLink(o.SupportSettings.PrivacyPolicyLink) { if !IsSafeLink(o.SupportSettings.PrivacyPolicyLink) {
o.SupportSettings.PrivacyPolicyLink = nil *o.SupportSettings.PrivacyPolicyLink = ""
} }
if o.SupportSettings.PrivacyPolicyLink == nil { if o.SupportSettings.PrivacyPolicyLink == nil {
@@ -699,7 +853,7 @@ func (o *Config) SetDefaults() {
} }
if !IsSafeLink(o.SupportSettings.AboutLink) { if !IsSafeLink(o.SupportSettings.AboutLink) {
o.SupportSettings.AboutLink = nil *o.SupportSettings.AboutLink = ""
} }
if o.SupportSettings.AboutLink == nil { if o.SupportSettings.AboutLink == nil {
@@ -708,7 +862,7 @@ func (o *Config) SetDefaults() {
} }
if !IsSafeLink(o.SupportSettings.HelpLink) { if !IsSafeLink(o.SupportSettings.HelpLink) {
o.SupportSettings.HelpLink = nil *o.SupportSettings.HelpLink = ""
} }
if o.SupportSettings.HelpLink == nil { if o.SupportSettings.HelpLink == nil {
@@ -717,7 +871,7 @@ func (o *Config) SetDefaults() {
} }
if !IsSafeLink(o.SupportSettings.ReportAProblemLink) { if !IsSafeLink(o.SupportSettings.ReportAProblemLink) {
o.SupportSettings.ReportAProblemLink = nil *o.SupportSettings.ReportAProblemLink = ""
} }
if o.SupportSettings.ReportAProblemLink == nil { if o.SupportSettings.ReportAProblemLink == nil {
@@ -725,11 +879,63 @@ func (o *Config) SetDefaults() {
*o.SupportSettings.ReportAProblemLink = SUPPORT_SETTINGS_DEFAULT_REPORT_A_PROBLEM_LINK *o.SupportSettings.ReportAProblemLink = SUPPORT_SETTINGS_DEFAULT_REPORT_A_PROBLEM_LINK
} }
if !IsSafeLink(o.SupportSettings.AdministratorsGuideLink) {
*o.SupportSettings.AdministratorsGuideLink = ""
}
if o.SupportSettings.AdministratorsGuideLink == nil {
o.SupportSettings.AdministratorsGuideLink = new(string)
*o.SupportSettings.AdministratorsGuideLink = SUPPORT_SETTINGS_DEFAULT_ADMINISTRATORS_GUIDE_LINK
}
if !IsSafeLink(o.SupportSettings.TroubleshootingForumLink) {
*o.SupportSettings.TroubleshootingForumLink = ""
}
if o.SupportSettings.TroubleshootingForumLink == nil {
o.SupportSettings.TroubleshootingForumLink = new(string)
*o.SupportSettings.TroubleshootingForumLink = SUPPORT_SETTINGS_DEFAULT_TROUBLESHOOTING_FORUM_LINK
}
if !IsSafeLink(o.SupportSettings.CommercialSupportLink) {
*o.SupportSettings.CommercialSupportLink = ""
}
if o.SupportSettings.CommercialSupportLink == nil {
o.SupportSettings.CommercialSupportLink = new(string)
*o.SupportSettings.CommercialSupportLink = SUPPORT_SETTINGS_DEFAULT_COMMERCIAL_SUPPORT_LINK
}
if o.SupportSettings.SupportEmail == nil { if o.SupportSettings.SupportEmail == nil {
o.SupportSettings.SupportEmail = new(string) o.SupportSettings.SupportEmail = new(string)
*o.SupportSettings.SupportEmail = SUPPORT_SETTINGS_DEFAULT_SUPPORT_EMAIL *o.SupportSettings.SupportEmail = SUPPORT_SETTINGS_DEFAULT_SUPPORT_EMAIL
} }
if o.AnnouncementSettings.EnableBanner == nil {
o.AnnouncementSettings.EnableBanner = new(bool)
*o.AnnouncementSettings.EnableBanner = false
}
if o.AnnouncementSettings.BannerText == nil {
o.AnnouncementSettings.BannerText = new(string)
*o.AnnouncementSettings.BannerText = ""
}
if o.AnnouncementSettings.BannerColor == nil {
o.AnnouncementSettings.BannerColor = new(string)
*o.AnnouncementSettings.BannerColor = ANNOUNCEMENT_SETTINGS_DEFAULT_BANNER_COLOR
}
if o.AnnouncementSettings.BannerTextColor == nil {
o.AnnouncementSettings.BannerTextColor = new(string)
*o.AnnouncementSettings.BannerTextColor = ANNOUNCEMENT_SETTINGS_DEFAULT_BANNER_TEXT_COLOR
}
if o.AnnouncementSettings.AllowBannerDismissal == nil {
o.AnnouncementSettings.AllowBannerDismissal = new(bool)
*o.AnnouncementSettings.AllowBannerDismissal = true
}
if o.LdapSettings.Enable == nil { if o.LdapSettings.Enable == nil {
o.LdapSettings.Enable = new(bool) o.LdapSettings.Enable = new(bool)
*o.LdapSettings.Enable = false *o.LdapSettings.Enable = false
@@ -884,7 +1090,12 @@ func (o *Config) SetDefaults() {
if o.ServiceSettings.EnableCustomEmoji == nil { if o.ServiceSettings.EnableCustomEmoji == nil {
o.ServiceSettings.EnableCustomEmoji = new(bool) o.ServiceSettings.EnableCustomEmoji = new(bool)
*o.ServiceSettings.EnableCustomEmoji = true *o.ServiceSettings.EnableCustomEmoji = false
}
if o.ServiceSettings.EnableEmojiPicker == nil {
o.ServiceSettings.EnableEmojiPicker = new(bool)
*o.ServiceSettings.EnableEmojiPicker = true
} }
if o.ServiceSettings.RestrictCustomEmojiCreation == nil { if o.ServiceSettings.RestrictCustomEmojiCreation == nil {
@@ -907,18 +1118,44 @@ func (o *Config) SetDefaults() {
*o.ServiceSettings.PostEditTimeLimit = 300 *o.ServiceSettings.PostEditTimeLimit = 300
} }
if o.ClusterSettings.InterNodeListenAddress == nil {
o.ClusterSettings.InterNodeListenAddress = new(string)
*o.ClusterSettings.InterNodeListenAddress = ":8075"
}
if o.ClusterSettings.Enable == nil { if o.ClusterSettings.Enable == nil {
o.ClusterSettings.Enable = new(bool) o.ClusterSettings.Enable = new(bool)
*o.ClusterSettings.Enable = false *o.ClusterSettings.Enable = false
} }
if o.ClusterSettings.InterNodeUrls == nil { if o.ClusterSettings.ClusterName == nil {
o.ClusterSettings.InterNodeUrls = []string{} o.ClusterSettings.ClusterName = new(string)
*o.ClusterSettings.ClusterName = ""
}
if o.ClusterSettings.OverrideHostname == nil {
o.ClusterSettings.OverrideHostname = new(string)
*o.ClusterSettings.OverrideHostname = ""
}
if o.ClusterSettings.UseIpAddress == nil {
o.ClusterSettings.UseIpAddress = new(bool)
*o.ClusterSettings.UseIpAddress = true
}
if o.ClusterSettings.UseExperimentalGossip == nil {
o.ClusterSettings.UseExperimentalGossip = new(bool)
*o.ClusterSettings.UseExperimentalGossip = false
}
if o.ClusterSettings.ReadOnlyConfig == nil {
o.ClusterSettings.ReadOnlyConfig = new(bool)
*o.ClusterSettings.ReadOnlyConfig = true
}
if o.ClusterSettings.GossipPort == nil {
o.ClusterSettings.GossipPort = new(int)
*o.ClusterSettings.GossipPort = 8074
}
if o.ClusterSettings.StreamingPort == nil {
o.ClusterSettings.StreamingPort = new(int)
*o.ClusterSettings.StreamingPort = 8075
} }
if o.MetricsSettings.ListenAddress == nil { if o.MetricsSettings.ListenAddress == nil {
@@ -978,12 +1215,12 @@ func (o *Config) SetDefaults() {
if o.SamlSettings.Verify == nil { if o.SamlSettings.Verify == nil {
o.SamlSettings.Verify = new(bool) o.SamlSettings.Verify = new(bool)
*o.SamlSettings.Verify = false *o.SamlSettings.Verify = true
} }
if o.SamlSettings.Encrypt == nil { if o.SamlSettings.Encrypt == nil {
o.SamlSettings.Encrypt = new(bool) o.SamlSettings.Encrypt = new(bool)
*o.SamlSettings.Encrypt = false *o.SamlSettings.Encrypt = true
} }
if o.SamlSettings.IdpUrl == nil { if o.SamlSettings.IdpUrl == nil {
@@ -1056,6 +1293,15 @@ func (o *Config) SetDefaults() {
*o.SamlSettings.LocaleAttribute = SAML_SETTINGS_DEFAULT_LOCALE_ATTRIBUTE *o.SamlSettings.LocaleAttribute = SAML_SETTINGS_DEFAULT_LOCALE_ATTRIBUTE
} }
if o.TeamSettings.TeammateNameDisplay == nil {
o.TeamSettings.TeammateNameDisplay = new(string)
*o.TeamSettings.TeammateNameDisplay = SHOW_USERNAME
if *o.SamlSettings.Enable || *o.LdapSettings.Enable {
*o.TeamSettings.TeammateNameDisplay = SHOW_FULLNAME
}
}
if o.NativeAppSettings.AppDownloadLink == nil { if o.NativeAppSettings.AppDownloadLink == nil {
o.NativeAppSettings.AppDownloadLink = new(string) o.NativeAppSettings.AppDownloadLink = new(string)
*o.NativeAppSettings.AppDownloadLink = NATIVEAPP_SETTINGS_DEFAULT_APP_DOWNLOAD_LINK *o.NativeAppSettings.AppDownloadLink = NATIVEAPP_SETTINGS_DEFAULT_APP_DOWNLOAD_LINK
@@ -1076,6 +1322,11 @@ func (o *Config) SetDefaults() {
*o.RateLimitSettings.Enable = false *o.RateLimitSettings.Enable = false
} }
if o.ServiceSettings.GoroutineHealthThreshold == nil {
o.ServiceSettings.GoroutineHealthThreshold = new(int)
*o.ServiceSettings.GoroutineHealthThreshold = -1
}
if o.RateLimitSettings.MaxBurst == nil { if o.RateLimitSettings.MaxBurst == nil {
o.RateLimitSettings.MaxBurst = new(int) o.RateLimitSettings.MaxBurst = new(int)
*o.RateLimitSettings.MaxBurst = 100 *o.RateLimitSettings.MaxBurst = 100
@@ -1131,16 +1382,90 @@ func (o *Config) SetDefaults() {
*o.ServiceSettings.TimeBetweenUserTypingUpdatesMilliseconds = 5000 *o.ServiceSettings.TimeBetweenUserTypingUpdatesMilliseconds = 5000
} }
if o.ServiceSettings.EnablePostSearch == nil {
o.ServiceSettings.EnablePostSearch = new(bool)
*o.ServiceSettings.EnablePostSearch = true
}
if o.ServiceSettings.EnableUserTypingMessages == nil { if o.ServiceSettings.EnableUserTypingMessages == nil {
o.ServiceSettings.EnableUserTypingMessages = new(bool) o.ServiceSettings.EnableUserTypingMessages = new(bool)
*o.ServiceSettings.EnableUserTypingMessages = true *o.ServiceSettings.EnableUserTypingMessages = true
} }
if o.ServiceSettings.EnableChannelViewedMessages == nil {
o.ServiceSettings.EnableChannelViewedMessages = new(bool)
*o.ServiceSettings.EnableChannelViewedMessages = true
}
if o.ServiceSettings.EnableUserStatuses == nil {
o.ServiceSettings.EnableUserStatuses = new(bool)
*o.ServiceSettings.EnableUserStatuses = true
}
if o.ServiceSettings.ClusterLogTimeoutMilliseconds == nil { if o.ServiceSettings.ClusterLogTimeoutMilliseconds == nil {
o.ServiceSettings.ClusterLogTimeoutMilliseconds = new(int) o.ServiceSettings.ClusterLogTimeoutMilliseconds = new(int)
*o.ServiceSettings.ClusterLogTimeoutMilliseconds = 2000 *o.ServiceSettings.ClusterLogTimeoutMilliseconds = 2000
} }
if o.ElasticsearchSettings.ConnectionUrl == nil {
o.ElasticsearchSettings.ConnectionUrl = new(string)
*o.ElasticsearchSettings.ConnectionUrl = ELASTICSEARCH_SETTINGS_DEFAULT_CONNECTION_URL
}
if o.ElasticsearchSettings.Username == nil {
o.ElasticsearchSettings.Username = new(string)
*o.ElasticsearchSettings.Username = ELASTICSEARCH_SETTINGS_DEFAULT_USERNAME
}
if o.ElasticsearchSettings.Password == nil {
o.ElasticsearchSettings.Password = new(string)
*o.ElasticsearchSettings.Password = ELASTICSEARCH_SETTINGS_DEFAULT_PASSWORD
}
if o.ElasticsearchSettings.EnableIndexing == nil {
o.ElasticsearchSettings.EnableIndexing = new(bool)
*o.ElasticsearchSettings.EnableIndexing = false
}
if o.ElasticsearchSettings.EnableSearching == nil {
o.ElasticsearchSettings.EnableSearching = new(bool)
*o.ElasticsearchSettings.EnableSearching = false
}
if o.ElasticsearchSettings.Sniff == nil {
o.ElasticsearchSettings.Sniff = new(bool)
*o.ElasticsearchSettings.Sniff = true
}
if o.ElasticsearchSettings.PostIndexReplicas == nil {
o.ElasticsearchSettings.PostIndexReplicas = new(int)
*o.ElasticsearchSettings.PostIndexReplicas = ELASTICSEARCH_SETTINGS_DEFAULT_POST_INDEX_REPLICAS
}
if o.ElasticsearchSettings.PostIndexShards == nil {
o.ElasticsearchSettings.PostIndexShards = new(int)
*o.ElasticsearchSettings.PostIndexShards = ELASTICSEARCH_SETTINGS_DEFAULT_POST_INDEX_SHARDS
}
if o.DataRetentionSettings.Enable == nil {
o.DataRetentionSettings.Enable = new(bool)
*o.DataRetentionSettings.Enable = false
}
if o.JobSettings.RunJobs == nil {
o.JobSettings.RunJobs = new(bool)
*o.JobSettings.RunJobs = true
}
if o.JobSettings.RunScheduler == nil {
o.JobSettings.RunScheduler = new(bool)
*o.JobSettings.RunScheduler = true
}
if o.PluginSettings.Plugins == nil {
o.PluginSettings.Plugins = make(map[string]interface{})
}
o.defaultWebrtcSettings() o.defaultWebrtcSettings()
} }
@@ -1184,6 +1509,10 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.restrict_direct_message.app_error", nil, "") return NewLocAppError("Config.IsValid", "model.config.is_valid.restrict_direct_message.app_error", nil, "")
} }
if !(*o.TeamSettings.TeammateNameDisplay == SHOW_FULLNAME || *o.TeamSettings.TeammateNameDisplay == SHOW_NICKNAME_FULLNAME || *o.TeamSettings.TeammateNameDisplay == SHOW_USERNAME) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.teammate_name_display.app_error", nil, "")
}
if len(o.SqlSettings.AtRestEncryptKey) < 32 { if len(o.SqlSettings.AtRestEncryptKey) < 32 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.encrypt_sql.app_error", nil, "") return NewLocAppError("Config.IsValid", "model.config.is_valid.encrypt_sql.app_error", nil, "")
} }
@@ -1196,6 +1525,10 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.sql_idle.app_error", nil, "") return NewLocAppError("Config.IsValid", "model.config.is_valid.sql_idle.app_error", nil, "")
} }
if *o.SqlSettings.QueryTimeout <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.sql_query_timeout.app_error", nil, "", http.StatusBadRequest)
}
if len(o.SqlSettings.DataSource) == 0 { if len(o.SqlSettings.DataSource) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.sql_data_src.app_error", nil, "") return NewLocAppError("Config.IsValid", "model.config.is_valid.sql_data_src.app_error", nil, "")
} }
@@ -1212,30 +1545,6 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.file_driver.app_error", nil, "") return NewLocAppError("Config.IsValid", "model.config.is_valid.file_driver.app_error", nil, "")
} }
if o.FileSettings.PreviewHeight < 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.file_preview_height.app_error", nil, "")
}
if o.FileSettings.PreviewWidth <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.file_preview_width.app_error", nil, "")
}
if o.FileSettings.ProfileHeight <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.file_profile_height.app_error", nil, "")
}
if o.FileSettings.ProfileWidth <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.file_profile_width.app_error", nil, "")
}
if o.FileSettings.ThumbnailHeight <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.file_thumb_height.app_error", nil, "")
}
if o.FileSettings.ThumbnailWidth <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.file_thumb_width.app_error", nil, "")
}
if len(*o.FileSettings.PublicLinkSalt) < 32 { if len(*o.FileSettings.PublicLinkSalt) < 32 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.file_salt.app_error", nil, "") return NewLocAppError("Config.IsValid", "model.config.is_valid.file_salt.app_error", nil, "")
} }
@@ -1248,10 +1557,6 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.email_salt.app_error", nil, "") return NewLocAppError("Config.IsValid", "model.config.is_valid.email_salt.app_error", nil, "")
} }
if len(o.EmailSettings.PasswordResetSalt) < 32 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.email_reset_salt.app_error", nil, "")
}
if *o.EmailSettings.EmailBatchingBufferSize <= 0 { if *o.EmailSettings.EmailBatchingBufferSize <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.email_batching_buffer_size.app_error", nil, "") return NewLocAppError("Config.IsValid", "model.config.is_valid.email_batching_buffer_size.app_error", nil, "")
} }
@@ -1260,6 +1565,10 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.email_batching_interval.app_error", nil, "") return NewLocAppError("Config.IsValid", "model.config.is_valid.email_batching_interval.app_error", nil, "")
} }
if !(*o.EmailSettings.EmailNotificationContentsType == EMAIL_NOTIFICATION_CONTENTS_FULL || *o.EmailSettings.EmailNotificationContentsType == EMAIL_NOTIFICATION_CONTENTS_GENERIC) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.email_notification_contents_type.app_error", nil, "")
}
if o.RateLimitSettings.MemoryStoreSize <= 0 { if o.RateLimitSettings.MemoryStoreSize <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.rate_mem.app_error", nil, "") return NewLocAppError("Config.IsValid", "model.config.is_valid.rate_mem.app_error", nil, "")
} }
@@ -1376,6 +1685,16 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.time_between_user_typing.app_error", nil, "") return NewLocAppError("Config.IsValid", "model.config.is_valid.time_between_user_typing.app_error", nil, "")
} }
if *o.ElasticsearchSettings.EnableIndexing {
if len(*o.ElasticsearchSettings.ConnectionUrl) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.elastic_search.connection_url.app_error", nil, "")
}
}
if *o.ElasticsearchSettings.EnableSearching && !*o.ElasticsearchSettings.EnableIndexing {
return NewLocAppError("Config.IsValid", "model.config.is_valid.elastic_search.enable_searching.app_error", nil, "")
}
return nil return nil
} }
@@ -1398,7 +1717,6 @@ func (o *Config) Sanitize() {
} }
o.EmailSettings.InviteSalt = FAKE_SETTING o.EmailSettings.InviteSalt = FAKE_SETTING
o.EmailSettings.PasswordResetSalt = FAKE_SETTING
if len(o.EmailSettings.SMTPPassword) > 0 { if len(o.EmailSettings.SMTPPassword) > 0 {
o.EmailSettings.SMTPPassword = FAKE_SETTING o.EmailSettings.SMTPPassword = FAKE_SETTING
} }
@@ -1413,6 +1731,12 @@ func (o *Config) Sanitize() {
for i := range o.SqlSettings.DataSourceReplicas { for i := range o.SqlSettings.DataSourceReplicas {
o.SqlSettings.DataSourceReplicas[i] = FAKE_SETTING o.SqlSettings.DataSourceReplicas[i] = FAKE_SETTING
} }
for i := range o.SqlSettings.DataSourceSearchReplicas {
o.SqlSettings.DataSourceSearchReplicas[i] = FAKE_SETTING
}
*o.ElasticsearchSettings.Password = FAKE_SETTING
} }
func (o *Config) defaultWebrtcSettings() { func (o *Config) defaultWebrtcSettings() {

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model
@@ -34,7 +34,7 @@ func (emoji *Emoji) IsValid() *AppError {
return NewLocAppError("Emoji.IsValid", "model.emoji.user_id.app_error", nil, "") return NewLocAppError("Emoji.IsValid", "model.emoji.user_id.app_error", nil, "")
} }
if len(emoji.Name) == 0 || len(emoji.Name) > 64 { if len(emoji.Name) == 0 || len(emoji.Name) > 64 || !IsValidAlphaNumHyphenUnderscore(emoji.Name, false) {
return NewLocAppError("Emoji.IsValid", "model.emoji.name.app_error", nil, "") return NewLocAppError("Emoji.IsValid", "model.emoji.name.app_error", nil, "")
} }

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package oauthgitlab package oauthgitlab

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model
@@ -6,10 +6,9 @@ package model
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net/http"
"regexp" "regexp"
"strings"
) )
const ( const (
@@ -81,35 +80,36 @@ func IncomingWebhookListFromJson(data io.Reader) []*IncomingWebhook {
func (o *IncomingWebhook) IsValid() *AppError { func (o *IncomingWebhook) IsValid() *AppError {
if len(o.Id) != 26 { if len(o.Id) != 26 {
return NewLocAppError("IncomingWebhook.IsValid", "model.incoming_hook.id.app_error", nil, "") return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.id.app_error", nil, "", http.StatusBadRequest)
} }
if o.CreateAt == 0 { if o.CreateAt == 0 {
return NewLocAppError("IncomingWebhook.IsValid", "model.incoming_hook.create_at.app_error", nil, "id="+o.Id) return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
} }
if o.UpdateAt == 0 { if o.UpdateAt == 0 {
return NewLocAppError("IncomingWebhook.IsValid", "model.incoming_hook.update_at.app_error", nil, "id="+o.Id) return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
} }
if len(o.UserId) != 26 { if len(o.UserId) != 26 {
return NewLocAppError("IncomingWebhook.IsValid", "model.incoming_hook.user_id.app_error", nil, "") return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.user_id.app_error", nil, "", http.StatusBadRequest)
} }
if len(o.ChannelId) != 26 { if len(o.ChannelId) != 26 {
return NewLocAppError("IncomingWebhook.IsValid", "model.incoming_hook.channel_id.app_error", nil, "") return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.channel_id.app_error", nil, "", http.StatusBadRequest)
} }
if len(o.TeamId) != 26 { if len(o.TeamId) != 26 {
return NewLocAppError("IncomingWebhook.IsValid", "model.incoming_hook.team_id.app_error", nil, "") return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.team_id.app_error", nil, "", http.StatusBadRequest)
} }
if len(o.DisplayName) > 64 { if len(o.DisplayName) > 64 {
return NewLocAppError("IncomingWebhook.IsValid", "model.incoming_hook.display_name.app_error", nil, "") return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.display_name.app_error", nil, "", http.StatusBadRequest)
} }
if len(o.Description) > 128 { if len(o.Description) > 128 {
return NewLocAppError("IncomingWebhook.IsValid", "model.incoming_hook.description.app_error", nil, "") return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.description.app_error", nil, "", http.StatusBadRequest)
} }
return nil return nil
@@ -193,39 +193,6 @@ func decodeIncomingWebhookRequest(by []byte) (*IncomingWebhookRequest, error) {
} }
} }
// To mention @channel via a webhook in Slack, the message should contain
// <!channel>, as explained at the bottom of this article:
// https://get.slack.help/hc/en-us/articles/202009646-Making-announcements
func expandAnnouncement(text string) string {
c1 := "<!channel>"
c2 := "@channel"
if strings.Contains(text, c1) {
return strings.Replace(text, c1, c2, -1)
}
return text
}
// Expand announcements in incoming webhooks from Slack. Those announcements
// can be found in the text attribute, or in the pretext, text, title and value
// attributes of the attachment structure. The Slack attachment structure is
// documented here: https://api.slack.com/docs/attachments
func expandAnnouncements(i *IncomingWebhookRequest) {
i.Text = expandAnnouncement(i.Text)
for _, attachment := range i.Attachments {
attachment.Pretext = expandAnnouncement(attachment.Pretext)
attachment.Text = expandAnnouncement(attachment.Text)
attachment.Title = expandAnnouncement(attachment.Title)
for _, field := range attachment.Fields {
if field.Value != nil {
// Ensure the value is set to a string if it is set
field.Value = expandAnnouncement(fmt.Sprintf("%v", field.Value))
}
}
}
}
func IncomingWebhookRequestFromJson(data io.Reader) *IncomingWebhookRequest { func IncomingWebhookRequestFromJson(data io.Reader) *IncomingWebhookRequest {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
buf.ReadFrom(data) buf.ReadFrom(data)
@@ -241,7 +208,8 @@ func IncomingWebhookRequestFromJson(data io.Reader) *IncomingWebhookRequest {
} }
} }
expandAnnouncements(o) o.Text = ExpandAnnouncement(o.Text)
o.Attachments = ProcessSlackAttachments(o.Attachments)
return o return o
} }

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model

View File

@@ -1,100 +1,117 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model
import ( import (
"fmt" "encoding/json"
"time" "io"
"net/http"
) )
type TaskFunc func() const (
JOB_TYPE_DATA_RETENTION = "data_retention"
JOB_TYPE_ELASTICSEARCH_POST_INDEXING = "elasticsearch_post_indexing"
type ScheduledTask struct { JOB_STATUS_PENDING = "pending"
Name string `json:"name"` JOB_STATUS_IN_PROGRESS = "in_progress"
Interval time.Duration `json:"interval"` JOB_STATUS_SUCCESS = "success"
Recurring bool `json:"recurring"` JOB_STATUS_ERROR = "error"
function TaskFunc JOB_STATUS_CANCEL_REQUESTED = "cancel_requested"
timer *time.Timer JOB_STATUS_CANCELED = "canceled"
)
type Job struct {
Id string `json:"id"`
Type string `json:"type"`
Priority int64 `json:"priority"`
CreateAt int64 `json:"create_at"`
StartAt int64 `json:"start_at"`
LastActivityAt int64 `json:"last_activity_at"`
Status string `json:"status"`
Progress int64 `json:"progress"`
Data map[string]interface{} `json:"data"`
} }
var tasks = make(map[string]*ScheduledTask) func (j *Job) IsValid() *AppError {
if len(j.Id) != 26 {
func addTask(task *ScheduledTask) { return NewAppError("Job.IsValid", "model.job.is_valid.id.app_error", nil, "id="+j.Id, http.StatusBadRequest)
tasks[task.Name] = task
}
func removeTaskByName(name string) {
delete(tasks, name)
}
func GetTaskByName(name string) *ScheduledTask {
if task, ok := tasks[name]; ok {
return task
} }
if j.CreateAt == 0 {
return NewAppError("Job.IsValid", "model.job.is_valid.create_at.app_error", nil, "id="+j.Id, http.StatusBadRequest)
}
switch j.Type {
case JOB_TYPE_DATA_RETENTION:
case JOB_TYPE_ELASTICSEARCH_POST_INDEXING:
default:
return NewAppError("Job.IsValid", "model.job.is_valid.type.app_error", nil, "id="+j.Id, http.StatusBadRequest)
}
switch j.Status {
case JOB_STATUS_PENDING:
case JOB_STATUS_IN_PROGRESS:
case JOB_STATUS_SUCCESS:
case JOB_STATUS_ERROR:
case JOB_STATUS_CANCEL_REQUESTED:
case JOB_STATUS_CANCELED:
default:
return NewAppError("Job.IsValid", "model.job.is_valid.status.app_error", nil, "id="+j.Id, http.StatusBadRequest)
}
return nil return nil
} }
func GetAllTasks() *map[string]*ScheduledTask { func (js *Job) ToJson() string {
return &tasks if b, err := json.Marshal(js); err != nil {
} return ""
} else {
func CreateTask(name string, function TaskFunc, timeToExecution time.Duration) *ScheduledTask { return string(b)
task := &ScheduledTask{
Name: name,
Interval: timeToExecution,
Recurring: false,
function: function,
} }
}
taskRunner := func() { func JobFromJson(data io.Reader) *Job {
go task.function() var job Job
removeTaskByName(task.Name) if err := json.NewDecoder(data).Decode(&job); err == nil {
return &job
} else {
return nil
} }
task.timer = time.AfterFunc(timeToExecution, taskRunner)
addTask(task)
return task
} }
func CreateRecurringTask(name string, function TaskFunc, interval time.Duration) *ScheduledTask { func JobsToJson(jobs []*Job) string {
task := &ScheduledTask{ if b, err := json.Marshal(jobs); err != nil {
Name: name, return ""
Interval: interval, } else {
Recurring: true, return string(b)
function: function,
} }
}
taskRecurer := func() { func JobsFromJson(data io.Reader) []*Job {
go task.function() var jobs []*Job
task.timer.Reset(task.Interval) if err := json.NewDecoder(data).Decode(&jobs); err == nil {
return jobs
} else {
return nil
} }
task.timer = time.AfterFunc(interval, taskRecurer)
addTask(task)
return task
} }
func (task *ScheduledTask) Cancel() { func (js *Job) DataToJson() string {
task.timer.Stop() if b, err := json.Marshal(js.Data); err != nil {
removeTaskByName(task.Name) return ""
} else {
return string(b)
}
} }
// Executes the task immediatly. A recurring task will be run regularally after interval. type Worker interface {
func (task *ScheduledTask) Execute() { Run()
task.function() Stop()
task.timer.Reset(task.Interval) JobChannel() chan<- Job
} }
func (task *ScheduledTask) String() string { type Scheduler interface {
return fmt.Sprintf( Run()
"%s\nInterval: %s\nRecurring: %t\n", Stop()
task.Name,
task.Interval.String(),
task.Recurring,
)
} }

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model
@@ -37,36 +37,42 @@ type Customer struct {
} }
type Features struct { type Features struct {
Users *int `json:"users"` Users *int `json:"users"`
LDAP *bool `json:"ldap"` LDAP *bool `json:"ldap"`
MFA *bool `json:"mfa"` MFA *bool `json:"mfa"`
GoogleOAuth *bool `json:"google_oauth"` GoogleOAuth *bool `json:"google_oauth"`
Office365OAuth *bool `json:"office365_oauth"` Office365OAuth *bool `json:"office365_oauth"`
Compliance *bool `json:"compliance"` Compliance *bool `json:"compliance"`
Cluster *bool `json:"cluster"` Cluster *bool `json:"cluster"`
Metrics *bool `json:"metrics"` Metrics *bool `json:"metrics"`
CustomBrand *bool `json:"custom_brand"` CustomBrand *bool `json:"custom_brand"`
MHPNS *bool `json:"mhpns"` MHPNS *bool `json:"mhpns"`
SAML *bool `json:"saml"` SAML *bool `json:"saml"`
PasswordRequirements *bool `json:"password_requirements"` PasswordRequirements *bool `json:"password_requirements"`
Elasticsearch *bool `json:"elastic_search"`
Announcement *bool `json:"announcement"`
EmailNotificationContents *bool `json:"email_notification_contents"`
// after we enabled more features for webrtc we'll need to control them with this // after we enabled more features for webrtc we'll need to control them with this
FutureFeatures *bool `json:"future_features"` FutureFeatures *bool `json:"future_features"`
} }
func (f *Features) ToMap() map[string]interface{} { func (f *Features) ToMap() map[string]interface{} {
return map[string]interface{}{ return map[string]interface{}{
"ldap": *f.LDAP, "ldap": *f.LDAP,
"mfa": *f.MFA, "mfa": *f.MFA,
"google": *f.GoogleOAuth, "google": *f.GoogleOAuth,
"office365": *f.Office365OAuth, "office365": *f.Office365OAuth,
"compliance": *f.Compliance, "compliance": *f.Compliance,
"cluster": *f.Cluster, "cluster": *f.Cluster,
"metrics": *f.Metrics, "metrics": *f.Metrics,
"custom_brand": *f.CustomBrand, "custom_brand": *f.CustomBrand,
"mhpns": *f.MHPNS, "mhpns": *f.MHPNS,
"saml": *f.SAML, "saml": *f.SAML,
"password": *f.PasswordRequirements, "password": *f.PasswordRequirements,
"future": *f.FutureFeatures, "elastic_search": *f.Elasticsearch,
"email_notification_contents": *f.EmailNotificationContents,
"future": *f.FutureFeatures,
} }
} }
@@ -135,6 +141,21 @@ func (f *Features) SetDefaults() {
f.PasswordRequirements = new(bool) f.PasswordRequirements = new(bool)
*f.PasswordRequirements = *f.FutureFeatures *f.PasswordRequirements = *f.FutureFeatures
} }
if f.Elasticsearch == nil {
f.Elasticsearch = new(bool)
*f.Elasticsearch = *f.FutureFeatures
}
if f.Announcement == nil {
f.Announcement = new(bool)
*f.Announcement = true
}
if f.EmailNotificationContents == nil {
f.EmailNotificationContents = new(bool)
*f.EmailNotificationContents = *f.FutureFeatures
}
} }
func (l *License) IsExpired() bool { func (l *License) IsExpired() bool {

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type MfaSecret struct {
Secret string `json:"secret"`
QRCode string `json:"qr_code"`
}
func (me *MfaSecret) ToJson() string {
b, err := json.Marshal(me)
if err != nil {
return ""
} else {
return string(b)
}
}
func MfaSecretFromJson(data io.Reader) *MfaSecret {
decoder := json.NewDecoder(data)
var me MfaSecret
err := decoder.Decode(&me)
if err == nil {
return &me
} else {
return nil
}
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model
@@ -7,6 +7,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net/http"
"unicode/utf8" "unicode/utf8"
) )
@@ -15,6 +16,7 @@ const (
OAUTH_ACTION_LOGIN = "login" OAUTH_ACTION_LOGIN = "login"
OAUTH_ACTION_EMAIL_TO_SSO = "email_to_sso" OAUTH_ACTION_EMAIL_TO_SSO = "email_to_sso"
OAUTH_ACTION_SSO_TO_EMAIL = "sso_to_email" OAUTH_ACTION_SSO_TO_EMAIL = "sso_to_email"
OAUTH_ACTION_MOBILE = "mobile"
) )
type OAuthApp struct { type OAuthApp struct {
@@ -36,50 +38,50 @@ type OAuthApp struct {
func (a *OAuthApp) IsValid() *AppError { func (a *OAuthApp) IsValid() *AppError {
if len(a.Id) != 26 { if len(a.Id) != 26 {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.app_id.app_error", nil, "") return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.app_id.app_error", nil, "", http.StatusBadRequest)
} }
if a.CreateAt == 0 { if a.CreateAt == 0 {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.create_at.app_error", nil, "app_id="+a.Id) return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.create_at.app_error", nil, "app_id="+a.Id, http.StatusBadRequest)
} }
if a.UpdateAt == 0 { if a.UpdateAt == 0 {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.update_at.app_error", nil, "app_id="+a.Id) return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.update_at.app_error", nil, "app_id="+a.Id, http.StatusBadRequest)
} }
if len(a.CreatorId) != 26 { if len(a.CreatorId) != 26 {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.creator_id.app_error", nil, "app_id="+a.Id) return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.creator_id.app_error", nil, "app_id="+a.Id, http.StatusBadRequest)
} }
if len(a.ClientSecret) == 0 || len(a.ClientSecret) > 128 { if len(a.ClientSecret) == 0 || len(a.ClientSecret) > 128 {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.client_secret.app_error", nil, "app_id="+a.Id) return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.client_secret.app_error", nil, "app_id="+a.Id, http.StatusBadRequest)
} }
if len(a.Name) == 0 || len(a.Name) > 64 { if len(a.Name) == 0 || len(a.Name) > 64 {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.name.app_error", nil, "app_id="+a.Id) return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.name.app_error", nil, "app_id="+a.Id, http.StatusBadRequest)
} }
if len(a.CallbackUrls) == 0 || len(fmt.Sprintf("%s", a.CallbackUrls)) > 1024 { if len(a.CallbackUrls) == 0 || len(fmt.Sprintf("%s", a.CallbackUrls)) > 1024 {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.callback.app_error", nil, "app_id="+a.Id) return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.callback.app_error", nil, "app_id="+a.Id, http.StatusBadRequest)
} }
for _, callback := range a.CallbackUrls { for _, callback := range a.CallbackUrls {
if !IsValidHttpUrl(callback) { if !IsValidHttpUrl(callback) {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.callback.app_error", nil, "") return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.callback.app_error", nil, "", http.StatusBadRequest)
} }
} }
if len(a.Homepage) == 0 || len(a.Homepage) > 256 || !IsValidHttpUrl(a.Homepage) { if len(a.Homepage) == 0 || len(a.Homepage) > 256 || !IsValidHttpUrl(a.Homepage) {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.homepage.app_error", nil, "app_id="+a.Id) return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.homepage.app_error", nil, "app_id="+a.Id, http.StatusBadRequest)
} }
if utf8.RuneCountInString(a.Description) > 512 { if utf8.RuneCountInString(a.Description) > 512 {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.description.app_error", nil, "app_id="+a.Id) return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.description.app_error", nil, "app_id="+a.Id, http.StatusBadRequest)
} }
if len(a.IconURL) > 0 { if len(a.IconURL) > 0 {
if len(a.IconURL) > 512 || !IsValidHttpUrl(a.IconURL) { if len(a.IconURL) > 512 || !IsValidHttpUrl(a.IconURL) {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.icon_url.app_error", nil, "app_id="+a.Id) return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.icon_url.app_error", nil, "app_id="+a.Id, http.StatusBadRequest)
} }
} }

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
package model package model
@@ -7,6 +7,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
@@ -41,6 +42,7 @@ type OutgoingWebhookPayload struct {
PostId string `json:"post_id"` PostId string `json:"post_id"`
Text string `json:"text"` Text string `json:"text"`
TriggerWord string `json:"trigger_word"` TriggerWord string `json:"trigger_word"`
FileIds string `json:"file_ids"`
} }
func (o *OutgoingWebhookPayload) ToJSON() string { func (o *OutgoingWebhookPayload) ToJSON() string {
@@ -65,6 +67,7 @@ func (o *OutgoingWebhookPayload) ToFormValues() string {
v.Set("post_id", o.PostId) v.Set("post_id", o.PostId)
v.Set("text", o.Text) v.Set("text", o.Text)
v.Set("trigger_word", o.TriggerWord) v.Set("trigger_word", o.TriggerWord)
v.Set("file_ids", o.FileIds)
return v.Encode() return v.Encode()
} }
@@ -112,69 +115,69 @@ func OutgoingWebhookListFromJson(data io.Reader) []*OutgoingWebhook {
func (o *OutgoingWebhook) IsValid() *AppError { func (o *OutgoingWebhook) IsValid() *AppError {
if len(o.Id) != 26 { if len(o.Id) != 26 {
return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.id.app_error", nil, "") return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.id.app_error", nil, "", http.StatusBadRequest)
} }
if len(o.Token) != 26 { if len(o.Token) != 26 {
return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.token.app_error", nil, "") return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.token.app_error", nil, "", http.StatusBadRequest)
} }
if o.CreateAt == 0 { if o.CreateAt == 0 {
return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.create_at.app_error", nil, "id="+o.Id) return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
} }
if o.UpdateAt == 0 { if o.UpdateAt == 0 {
return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.update_at.app_error", nil, "id="+o.Id) return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
} }
if len(o.CreatorId) != 26 { if len(o.CreatorId) != 26 {
return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.user_id.app_error", nil, "") return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
} }
if len(o.ChannelId) != 0 && len(o.ChannelId) != 26 { if len(o.ChannelId) != 0 && len(o.ChannelId) != 26 {
return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.channel_id.app_error", nil, "") return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest)
} }
if len(o.TeamId) != 26 { if len(o.TeamId) != 26 {
return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.team_id.app_error", nil, "") return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.team_id.app_error", nil, "", http.StatusBadRequest)
} }
if len(fmt.Sprintf("%s", o.TriggerWords)) > 1024 { if len(fmt.Sprintf("%s", o.TriggerWords)) > 1024 {
return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.words.app_error", nil, "") return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.words.app_error", nil, "", http.StatusBadRequest)
} }
if len(o.TriggerWords) != 0 { if len(o.TriggerWords) != 0 {
for _, triggerWord := range o.TriggerWords { for _, triggerWord := range o.TriggerWords {
if len(triggerWord) == 0 { if len(triggerWord) == 0 {
return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.trigger_words.app_error", nil, "") return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.trigger_words.app_error", nil, "", http.StatusBadRequest)
} }
} }
} }
if len(o.CallbackURLs) == 0 || len(fmt.Sprintf("%s", o.CallbackURLs)) > 1024 { if len(o.CallbackURLs) == 0 || len(fmt.Sprintf("%s", o.CallbackURLs)) > 1024 {
return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.callback.app_error", nil, "") return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.callback.app_error", nil, "", http.StatusBadRequest)
} }
for _, callback := range o.CallbackURLs { for _, callback := range o.CallbackURLs {
if !IsValidHttpUrl(callback) { if !IsValidHttpUrl(callback) {
return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.url.app_error", nil, "") return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.url.app_error", nil, "", http.StatusBadRequest)
} }
} }
if len(o.DisplayName) > 64 { if len(o.DisplayName) > 64 {
return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.display_name.app_error", nil, "") return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.display_name.app_error", nil, "", http.StatusBadRequest)
} }
if len(o.Description) > 128 { if len(o.Description) > 128 {
return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.description.app_error", nil, "") return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.description.app_error", nil, "", http.StatusBadRequest)
} }
if len(o.ContentType) > 128 { if len(o.ContentType) > 128 {
return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.content_type.app_error", nil, "") return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.content_type.app_error", nil, "", http.StatusBadRequest)
} }
if o.TriggerWhen > 1 { if o.TriggerWhen > 1 {
return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.content_type.app_error", nil, "") return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.content_type.app_error", nil, "", http.StatusBadRequest)
} }
return nil return nil
@@ -197,8 +200,8 @@ func (o *OutgoingWebhook) PreUpdate() {
o.UpdateAt = GetMillis() o.UpdateAt = GetMillis()
} }
func (o *OutgoingWebhook) HasTriggerWord(word string) bool { func (o *OutgoingWebhook) TriggerWordExactMatch(word string) bool {
if len(o.TriggerWords) == 0 || len(word) == 0 { if len(word) == 0 {
return false return false
} }
@@ -212,7 +215,7 @@ func (o *OutgoingWebhook) HasTriggerWord(word string) bool {
} }
func (o *OutgoingWebhook) TriggerWordStartsWith(word string) bool { func (o *OutgoingWebhook) TriggerWordStartsWith(word string) bool {
if len(o.TriggerWords) == 0 || len(word) == 0 { if len(word) == 0 {
return false return false
} }
@@ -224,3 +227,27 @@ func (o *OutgoingWebhook) TriggerWordStartsWith(word string) bool {
return false return false
} }
func (o *OutgoingWebhook) GetTriggerWord(word string, isExactMatch bool) (triggerWord string) {
if len(word) == 0 {
return
}
if isExactMatch {
for _, trigger := range o.TriggerWords {
if trigger == word {
triggerWord = trigger
break
}
}
} else {
for _, trigger := range o.TriggerWords {
if strings.HasPrefix(word, trigger) {
triggerWord = trigger
break
}
}
}
return triggerWord
}

View File

@@ -1,37 +0,0 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
const (
PASSWORD_RECOVERY_CODE_SIZE = 128
PASSWORD_RECOVER_EXPIRY_TIME = 1000 * 60 * 60 // 1 hour
)
type PasswordRecovery struct {
UserId string
Code string
CreateAt int64
}
func (p *PasswordRecovery) IsValid() *AppError {
if len(p.UserId) != 26 {
return NewLocAppError("User.IsValid", "model.password_recovery.is_valid.user_id.app_error", nil, "")
}
if len(p.Code) != PASSWORD_RECOVERY_CODE_SIZE {
return NewLocAppError("User.IsValid", "model.password_recovery.is_valid.code.app_error", nil, "")
}
if p.CreateAt == 0 {
return NewLocAppError("User.IsValid", "model.password_recovery.is_valid.create_at.app_error", nil, "")
}
return nil
}
func (p *PasswordRecovery) PreSave() {
p.Code = NewRandomString(PASSWORD_RECOVERY_CODE_SIZE)
p.CreateAt = GetMillis()
}

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