4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-06-26 20:09:24 +00:00

Compare commits

...

31 Commits

Author SHA1 Message Date
Wim
d81e6bf6ce Release v0.9.0 2016-12-01 22:15:40 +01:00
Wim
70c93d970c Update public links to new API (mattermost) 2016-11-26 15:46:39 +01:00
Wim
4960273832 Do not relay empty or delayed messages (xmpp) 2016-11-26 15:08:41 +01:00
Wim
6c018ee6fe Enable keepalive (xmpp) 2016-11-26 15:04:06 +01:00
Wim
4ef32103ca Update xmpp vendor 2016-11-26 14:44:33 +01:00
Wim
e4ec27c5e2 Add sample documentation for hipchat support via xmpp 2016-11-26 00:40:21 +01:00
Wim
20c04f7977 Fix loop because of closed channel. Fixes #89 2016-11-23 23:51:51 +01:00
Wim
571f50d734 Support mattermost setup with up to 50k users 2016-11-23 21:24:35 +01:00
780ea6f7c0 Create ISSUE_TEMPLATE.md 2016-11-23 20:22:27 +01:00
Wim
4279906f6e Add logo 2016-11-23 00:15:17 +01:00
Wim
2e54b97fc2 Add support for RemoteNickFormat in general configuration (samechannelgateway) 2016-11-20 23:50:12 +01:00
Wim
e1641b2c2e Add support for RemoteNickFormat in general configuration 2016-11-20 23:33:41 +01:00
Wim
e0e1e4be80 Add gateway.inout config for bidirectional bridges. Closes #85 2016-11-20 23:01:44 +01:00
Wim
d5845ce900 Replace id-mentions to usernames (slack). Closes #86 2016-11-20 22:40:09 +01:00
Wim
85f2cde4c3 Update documentation 2016-11-20 18:01:59 +01:00
Wim
cef64e01b3 Remove callbacks after being called. Fixes #88 (irc) 2016-11-20 17:21:15 +01:00
Wim
94ea775232 Merge branch 'telegram'
Add telegram support
2016-11-20 17:02:17 +01:00
Wim
2e4b7fac11 Update documentation and sample (telegram) 2016-11-20 17:01:53 +01:00
Wim
2867ec459a Add missing imports 2016-11-19 15:05:11 +01:00
Wim
cd18d89894 Add initial telegram support 2016-11-15 23:15:57 +01:00
Wim
449ed31e25 Fix ShowJoinPart from irc bridge. Closes #72 2016-11-14 22:53:06 +01:00
Wim
1f36904588 Update sample config. Closes #75 2016-11-14 16:42:32 +01:00
Wim
f7495dd0c3 Add bot tag to api if not specified (discord) 2016-11-14 16:30:43 +01:00
Wim
a11f77835d Fix !users command for irc. Closes #78. 2016-11-14 00:12:48 +01:00
Wim
af1ad82c8e Fix merge issue 2016-11-13 23:12:17 +01:00
Wim
4976338677 Merge branch 'refactor' 2016-11-13 23:09:06 +01:00
Wim
99d130d1ed Refactor 2016-11-13 23:06:37 +01:00
Wim
4fb0544b0e Fix GetLastViewedAt 2016-11-13 16:03:04 +01:00
Wim
0b4ac61435 Update documentation 2016-11-12 22:33:58 +01:00
Wim
1d5cd1d7c4 Sync with mattermost 3.5.0 2016-11-12 22:00:53 +01:00
Wim
14830d9f1c Refactor gateway 2016-11-08 23:44:16 +01:00
59 changed files with 4822 additions and 798 deletions

20
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,20 @@
Please answer the following questions.
### Which version of matterbridge are you using?
run ```matterbridge -version```
### If you're having problems with mattermost please specify mattermost version.
### Please describe the expected behavior.
### Please describe the actual behavior.
#### Use logs from running ```matterbridge -debug``` if possible.
### Any steps to reproduce the behavior?
### Please add your configuration file
#### (be sure to exclude or anonymize private data (tokens/passwords))

View File

@ -1,11 +1,9 @@
# matterbridge
![matterbridge.gif](https://s15.postimg.org/qpjhp6y3f/matterbridge.gif)
:warning: Look at [README-0.6.md] (https://github.com/42wim/matterbridge/blob/master/README-0.6.md) for the documentation of the current stable.
The information below is about the develop version.
Simple bridge between mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram and Hipchat(via xmpp).
Simple bridge between mattermost, IRC, XMPP, Gitter, Slack and Discord
* Relays public channel messages between multiple mattermost, IRC, XMPP, Gitter, Slack and Discord. Pick and mix.
* Relays public channel messages between multiple mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram and Hipchat (via xmpp). Pick and mix.
* Supports multiple channels.
* Matterbridge can also work with private groups on your mattermost.
* Allow for bridging the same bridges, which means you can eg bridge between multiple mattermosts.
@ -14,8 +12,9 @@ Simple bridge between mattermost, IRC, XMPP, Gitter, Slack and Discord
Look at [matterbridge.toml.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for documentation and an example.
Look at [matterbridge.toml.simple] (https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.simple) for a simple example.
## Changelog
Since v0.7.0-dev the configuration has changed. More details in [changelog.md] (https://github.com/42wim/matterbridge/blob/master/changelog.md)
Since v0.7.0 the configuration has changed. More details in [changelog.md] (https://github.com/42wim/matterbridge/blob/master/changelog.md)
## Requirements
Accounts to one of the supported bridges
@ -25,6 +24,8 @@ Accounts to one of the supported bridges
* [Gitter] (https://gitter.im)
* [Slack] (https://slack.com)
* [Discord] (https://discordapp.com)
* [Telegram] (https://telegram.org)
* [Hipchat] (https://www.hipchat.com)
## Docker
Create your matterbridge.toml file locally eg in ```/tmp/matterbridge.toml```
@ -34,12 +35,14 @@ docker run -ti -v /tmp/matterbridge.toml:/matterbridge.toml 42wim/matterbridge
## binaries
Binaries can be found [here] (https://github.com/42wim/matterbridge/releases/)
* For use with mattermost 3.3.0+ [v0.6.1](https://github.com/42wim/matterircd/releases/tag/v0.6.1)
* For use with mattermost 3.0.0-3.2.0 [v0.5.0](https://github.com/42wim/matterircd/releases/tag/v0.5.0)
* For use with mattermost 3.5.0+ [v0.9.0](https://github.com/42wim/matterircd/releases/tag/v0.9.0)
* For use with mattermost 3.3.0 - 3.4.0 [v0.7.1](https://github.com/42wim/matterircd/releases/tag/v0.7.1)
* For use with mattermost 3.0.0 - 3.2.0 [v0.5.0](https://github.com/42wim/matterircd/releases/tag/v0.5.0) (not maintained anymore)
## Compatibility
### Mattermost
* Matterbridge v0.6.1 works with mattermost 3.3.0 and higher [3.3.0 release](https://github.com/mattermost/platform/releases/tag/v3.3.0)
* Matterbridge v0.9.0 works with mattermost 3.5.0+ [3.5.1 release](https://github.com/mattermost/platform/releases/tag/v3.5.1)
* Matterbridge v0.7.1 works with mattermost 3.3.0 - 3.4.0 [3.4.0 release](https://github.com/mattermost/platform/releases/tag/v3.4.0)
* Matterbridge v0.5.0 works with mattermost 3.0.0 - 3.2.0 [3.2.0 release](https://github.com/mattermost/platform/releases/tag/v3.2.0)

View File

@ -7,39 +7,58 @@ import (
"github.com/42wim/matterbridge/bridge/irc"
"github.com/42wim/matterbridge/bridge/mattermost"
"github.com/42wim/matterbridge/bridge/slack"
"github.com/42wim/matterbridge/bridge/telegram"
"github.com/42wim/matterbridge/bridge/xmpp"
"strings"
)
type Bridge interface {
type Bridger interface {
Send(msg config.Message) error
Name() string
Connect() error
FullOrigin() string
Origin() string
Protocol() string
JoinChannel(channel string) error
}
func New(cfg *config.Config, bridge *config.Bridge, c chan config.Message) Bridge {
type Bridge struct {
Config config.Protocol
Bridger
Name string
Account string
Protocol string
}
func New(cfg *config.Config, bridge *config.Bridge, c chan config.Message) *Bridge {
b := new(Bridge)
accInfo := strings.Split(bridge.Account, ".")
protocol := accInfo[0]
name := accInfo[1]
b.Name = name
b.Protocol = protocol
b.Account = bridge.Account
// override config from environment
config.OverrideCfgFromEnv(cfg, protocol, name)
switch protocol {
case "mattermost":
return bmattermost.New(cfg.Mattermost[name], name, c)
b.Config = cfg.Mattermost[name]
b.Bridger = bmattermost.New(cfg.Mattermost[name], bridge.Account, c)
case "irc":
return birc.New(cfg.IRC[name], name, c)
b.Config = cfg.IRC[name]
b.Bridger = birc.New(cfg.IRC[name], bridge.Account, c)
case "gitter":
return bgitter.New(cfg.Gitter[name], name, c)
b.Config = cfg.Gitter[name]
b.Bridger = bgitter.New(cfg.Gitter[name], bridge.Account, c)
case "slack":
return bslack.New(cfg.Slack[name], name, c)
b.Config = cfg.Slack[name]
b.Bridger = bslack.New(cfg.Slack[name], bridge.Account, c)
case "xmpp":
return bxmpp.New(cfg.Xmpp[name], name, c)
b.Config = cfg.Xmpp[name]
b.Bridger = bxmpp.New(cfg.Xmpp[name], bridge.Account, c)
case "discord":
return bdiscord.New(cfg.Discord[name], name, c)
b.Config = cfg.Discord[name]
b.Bridger = bdiscord.New(cfg.Discord[name], bridge.Account, c)
case "telegram":
b.Config = cfg.Telegram[name]
b.Bridger = btelegram.New(cfg.Telegram[name], bridge.Account, c)
}
return nil
return b
}

View File

@ -8,14 +8,17 @@ import (
"strings"
)
const (
EVENT_JOIN_LEAVE = "join_leave"
)
type Message struct {
Text string
Channel string
Username string
Origin string
FullOrigin string
Protocol string
Avatar string
Text string
Channel string
Username string
Avatar string
Account string
Event string
}
type Protocol struct {
@ -59,6 +62,7 @@ type Gateway struct {
Enable bool
In []Bridge
Out []Bridge
InOut []Bridge
}
type SameChannelGateway struct {
@ -75,6 +79,8 @@ type Config struct {
Gitter map[string]Protocol
Xmpp map[string]Protocol
Discord map[string]Protocol
Telegram map[string]Protocol
General Protocol
Gateway []Gateway
SameChannelGateway []SameChannelGateway
}
@ -126,16 +132,11 @@ func OverrideCfgFromEnv(cfg *Config, protocol string, account string) {
func GetIconURL(msg *Message, cfg *Protocol) string {
iconURL := cfg.IconURL
info := strings.Split(msg.Account, ".")
protocol := info[0]
name := info[1]
iconURL = strings.Replace(iconURL, "{NICK}", msg.Username, -1)
iconURL = strings.Replace(iconURL, "{BRIDGE}", msg.Origin, -1)
iconURL = strings.Replace(iconURL, "{PROTOCOL}", msg.Protocol, -1)
iconURL = strings.Replace(iconURL, "{BRIDGE}", name, -1)
iconURL = strings.Replace(iconURL, "{PROTOCOL}", protocol, -1)
return iconURL
}
func GetNick(msg *Message, cfg *Protocol) string {
nick := cfg.RemoteNickFormat
nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
nick = strings.Replace(nick, "{BRIDGE}", msg.Origin, -1)
nick = strings.Replace(nick, "{PROTOCOL}", msg.Protocol, -1)
return nick
}

View File

@ -11,8 +11,7 @@ type bdiscord struct {
c *discordgo.Session
Config *config.Protocol
Remote chan config.Message
protocol string
origin string
Account string
Channels []*discordgo.Channel
Nick string
UseChannelID bool
@ -25,18 +24,20 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, origin string, c chan config.Message) *bdiscord {
func New(cfg config.Protocol, account string, c chan config.Message) *bdiscord {
b := &bdiscord{}
b.Config = &cfg
b.Remote = c
b.protocol = protocol
b.origin = origin
b.Account = account
return b
}
func (b *bdiscord) Connect() error {
var err error
flog.Info("Connecting")
if !strings.HasPrefix(b.Config.Token, "Bot ") {
b.Config.Token = "Bot " + b.Config.Token
}
b.c, err = discordgo.New(b.Config.Token)
if err != nil {
flog.Debugf("%#v", err)
@ -72,10 +73,6 @@ func (b *bdiscord) Connect() error {
return nil
}
func (b *bdiscord) FullOrigin() string {
return b.protocol + "." + b.origin
}
func (b *bdiscord) JoinChannel(channel string) error {
idcheck := strings.Split(channel, "ID:")
if len(idcheck) > 1 {
@ -84,18 +81,6 @@ func (b *bdiscord) JoinChannel(channel string) error {
return nil
}
func (b *bdiscord) Name() string {
return b.protocol + "." + b.origin
}
func (b *bdiscord) Protocol() string {
return b.protocol
}
func (b *bdiscord) Origin() string {
return b.origin
}
func (b *bdiscord) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg)
channelID := b.getChannelID(msg.Channel)
@ -103,8 +88,7 @@ func (b *bdiscord) Send(msg config.Message) error {
flog.Errorf("Could not find channelID for %v", msg.Channel)
return nil
}
nick := config.GetNick(&msg, b.Config)
b.c.ChannelMessageSend(channelID, nick+msg.Text)
b.c.ChannelMessageSend(channelID, msg.Username+msg.Text)
return nil
}
@ -121,13 +105,13 @@ func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
if m.Content == "" {
return
}
flog.Debugf("Sending message from %s on %s to gateway", m.Author.Username, b.FullOrigin())
flog.Debugf("Sending message from %s on %s to gateway", m.Author.Username, b.Account)
channelName := b.getChannelName(m.ChannelID)
if b.UseChannelID {
channelName = "ID:" + m.ChannelID
}
b.Remote <- config.Message{Username: m.Author.Username, Text: m.ContentWithMentionsReplaced(), Channel: channelName,
Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin(), Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg"}
Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg"}
}
func (b *bdiscord) getChannelID(name string) string {

View File

@ -8,13 +8,12 @@ import (
)
type Bgitter struct {
c *gitter.Gitter
Config *config.Protocol
Remote chan config.Message
protocol string
origin string
Users []gitter.User
Rooms []gitter.Room
c *gitter.Gitter
Config *config.Protocol
Remote chan config.Message
Account string
Users []gitter.User
Rooms []gitter.Room
}
var flog *log.Entry
@ -24,12 +23,11 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, origin string, c chan config.Message) *Bgitter {
func New(cfg config.Protocol, account string, c chan config.Message) *Bgitter {
b := &Bgitter{}
b.Config = &cfg
b.Remote = c
b.protocol = protocol
b.origin = origin
b.Account = account
return b
}
@ -47,10 +45,6 @@ func (b *Bgitter) Connect() error {
return nil
}
func (b *Bgitter) FullOrigin() string {
return b.protocol + "." + b.origin
}
func (b *Bgitter) JoinChannel(channel string) error {
room := channel
roomID := b.getRoomID(room)
@ -71,15 +65,14 @@ func (b *Bgitter) JoinChannel(channel string) error {
go b.c.Listen(stream)
go func(stream *gitter.Stream, room string) {
for {
event := <-stream.Event
for event := range stream.Event {
switch ev := event.Data.(type) {
case *gitter.MessageReceived:
// check for ZWSP to see if it's not an echo
if !strings.HasSuffix(ev.Message.Text, "") {
flog.Debugf("Sending message from %s on %s to gateway", ev.Message.From.Username, b.FullOrigin())
flog.Debugf("Sending message from %s on %s to gateway", ev.Message.From.Username, b.Account)
b.Remote <- config.Message{Username: ev.Message.From.Username, Text: ev.Message.Text, Channel: room,
Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin(), Avatar: b.getAvatar(ev.Message.From.Username)}
Account: b.Account, Avatar: b.getAvatar(ev.Message.From.Username)}
}
case *gitter.GitterConnectionClosed:
flog.Errorf("connection with gitter closed for room %s", room)
@ -89,18 +82,6 @@ func (b *Bgitter) JoinChannel(channel string) error {
return nil
}
func (b *Bgitter) Name() string {
return b.protocol + "." + b.origin
}
func (b *Bgitter) Protocol() string {
return b.protocol
}
func (b *Bgitter) Origin() string {
return b.origin
}
func (b *Bgitter) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg)
roomID := b.getRoomID(msg.Channel)
@ -108,9 +89,8 @@ func (b *Bgitter) Send(msg config.Message) error {
flog.Errorf("Could not find roomID for %v", msg.Channel)
return nil
}
nick := config.GetNick(&msg, b.Config)
// add ZWSP because gitter echoes our own messages
return b.c.SendMessage(roomID, nick+msg.Text+" ")
return b.c.SendMessage(roomID, msg.Username+msg.Text+" ")
}
func (b *Bgitter) getRoomID(channel string) string {

View File

@ -19,11 +19,10 @@ type Birc struct {
Nick string
names map[string][]string
Config *config.Protocol
origin string
protocol string
Remote chan config.Message
connected chan struct{}
Local chan config.Message // local queue for flood control
Account string
}
var flog *log.Entry
@ -33,14 +32,13 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, origin string, c chan config.Message) *Birc {
func New(cfg config.Protocol, account string, c chan config.Message) *Birc {
b := &Birc{}
b.Config = &cfg
b.Nick = b.Config.Nick
b.Remote = c
b.names = make(map[string][]string)
b.origin = origin
b.protocol = protocol
b.Account = account
b.connected = make(chan struct{})
if b.Config.MessageDelay == 0 {
b.Config.MessageDelay = 1300
@ -55,9 +53,9 @@ func New(cfg config.Protocol, origin string, c chan config.Message) *Birc {
func (b *Birc) Command(msg *config.Message) string {
switch msg.Text {
case "!users":
b.i.AddCallback(ircm.RPL_NAMREPLY, b.storeNames)
b.i.AddCallback(ircm.RPL_ENDOFNAMES, b.endNames)
b.i.SendRaw("NAMES " + msg.Channel)
b.i.ClearCallback(ircm.RPL_ENDOFNAMES)
}
return ""
}
@ -93,43 +91,26 @@ func (b *Birc) Connect() error {
return nil
}
func (b *Birc) FullOrigin() string {
return b.protocol + "." + b.origin
}
func (b *Birc) JoinChannel(channel string) error {
b.i.Join(channel)
return nil
}
func (b *Birc) Name() string {
return b.protocol + "." + b.origin
}
func (b *Birc) Protocol() string {
return b.protocol
}
func (b *Birc) Origin() string {
return b.origin
}
func (b *Birc) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg)
if msg.FullOrigin == b.FullOrigin() {
if msg.Account == b.Account {
return nil
}
if strings.HasPrefix(msg.Text, "!") {
b.Command(&msg)
return nil
}
nick := config.GetNick(&msg, b.Config)
for _, text := range strings.Split(msg.Text, "\n") {
if len(b.Local) < b.Config.MessageQueue {
if len(b.Local) == b.Config.MessageQueue-1 {
text = text + " <message clipped>"
}
b.Local <- config.Message{Text: text, Username: nick, Channel: msg.Channel}
b.Local <- config.Message{Text: text, Username: msg.Username, Channel: msg.Channel}
} else {
flog.Debugf("flooding, dropping message (queue at %d)", len(b.Local))
}
@ -153,13 +134,15 @@ func (b *Birc) endNames(event *irc.Event) {
continued := false
for len(b.names[channel]) > maxNamesPerPost {
b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost], continued),
Channel: channel, Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin()}
Channel: channel, Account: b.Account}
b.names[channel] = b.names[channel][maxNamesPerPost:]
continued = true
}
b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel], continued), Channel: channel,
Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin()}
b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel], continued),
Channel: channel, Account: b.Account}
b.names[channel] = nil
b.i.ClearCallback(ircm.RPL_NAMREPLY)
b.i.ClearCallback(ircm.RPL_ENDOFNAMES)
}
func (b *Birc) handleNewConnection(event *irc.Event) {
@ -169,18 +152,30 @@ func (b *Birc) handleNewConnection(event *irc.Event) {
i.AddCallback("PRIVMSG", b.handlePrivMsg)
i.AddCallback("CTCP_ACTION", b.handlePrivMsg)
i.AddCallback(ircm.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
i.AddCallback(ircm.RPL_NAMREPLY, b.storeNames)
i.AddCallback(ircm.NOTICE, b.handleNotice)
//i.AddCallback(ircm.RPL_MYINFO, func(e *irc.Event) { flog.Infof("%s: %s", e.Code, strings.Join(e.Arguments[1:], " ")) })
i.AddCallback("PING", func(e *irc.Event) {
i.SendRaw("PONG :" + e.Message())
flog.Debugf("PING/PONG")
})
i.AddCallback("JOIN", b.handleJoinPart)
i.AddCallback("PART", b.handleJoinPart)
i.AddCallback("QUIT", b.handleJoinPart)
i.AddCallback("*", b.handleOther)
// we are now fully connected
b.connected <- struct{}{}
}
func (b *Birc) handleJoinPart(event *irc.Event) {
flog.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
channel := event.Arguments[0]
if event.Code == "QUIT" {
channel = ""
}
b.Remote <- config.Message{Username: "system", Text: event.Nick + " " + strings.ToLower(event.Code) + "s", Channel: channel, Account: b.Account, Event: config.EVENT_JOIN_LEAVE}
flog.Debugf("handle %#v", event)
}
func (b *Birc) handleNotice(event *irc.Event) {
if strings.Contains(event.Message(), "This nickname is registered") && event.Nick == b.Config.NickServNick {
b.i.Privmsg(b.Config.NickServNick, "IDENTIFY "+b.Config.NickServPassword)
@ -215,8 +210,8 @@ func (b *Birc) handlePrivMsg(event *irc.Event) {
// strip IRC colors
re := regexp.MustCompile(`[[:cntrl:]](\d+,|)\d+`)
msg = re.ReplaceAllString(msg, "")
flog.Debugf("Sending message from %s on %s to gateway", event.Arguments[0], b.FullOrigin())
b.Remote <- config.Message{Username: event.Nick, Text: msg, Channel: event.Arguments[0], Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin()}
flog.Debugf("Sending message from %s on %s to gateway", event.Arguments[0], b.Account)
b.Remote <- config.Message{Username: event.Nick, Text: msg, Channel: event.Arguments[0], Account: b.Account}
}
func (b *Birc) handleTopicWhoTime(event *irc.Event) {

View File

@ -26,12 +26,11 @@ type MMMessage struct {
type Bmattermost struct {
MMhook
MMapi
Config *config.Protocol
Remote chan config.Message
name string
origin string
protocol string
TeamId string
Config *config.Protocol
Remote chan config.Message
name string
TeamId string
Account string
}
var flog *log.Entry
@ -41,13 +40,11 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, origin string, c chan config.Message) *Bmattermost {
func New(cfg config.Protocol, account string, c chan config.Message) *Bmattermost {
b := &Bmattermost{}
b.Config = &cfg
b.origin = origin
b.Remote = c
b.protocol = "mattermost"
b.name = cfg.Name
b.Account = account
b.mmMap = make(map[string]string)
return b
}
@ -80,10 +77,6 @@ func (b *Bmattermost) Connect() error {
return nil
}
func (b *Bmattermost) FullOrigin() string {
return b.protocol + "." + b.origin
}
func (b *Bmattermost) JoinChannel(channel string) error {
// we can only join channels using the API
if b.Config.UseAPI {
@ -92,21 +85,9 @@ func (b *Bmattermost) JoinChannel(channel string) error {
return nil
}
func (b *Bmattermost) Name() string {
return b.protocol + "." + b.origin
}
func (b *Bmattermost) Origin() string {
return b.origin
}
func (b *Bmattermost) Protocol() string {
return b.protocol
}
func (b *Bmattermost) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg)
nick := config.GetNick(&msg, b.Config)
nick := msg.Username
message := msg.Text
channel := msg.Channel
@ -144,8 +125,8 @@ func (b *Bmattermost) handleMatter() {
go b.handleMatterHook(mchan)
}
for message := range mchan {
flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.FullOrigin())
b.Remote <- config.Message{Text: message.Text, Username: message.Username, Channel: message.Channel, Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin()}
flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account)
b.Remote <- config.Message{Text: message.Text, Username: message.Username, Channel: message.Channel, Account: b.Account}
}
}
@ -153,14 +134,14 @@ func (b *Bmattermost) handleMatterClient(mchan chan *MMMessage) {
for message := range b.mc.MessageChan {
// do not post our own messages back to irc
// only listen to message from our team
if message.Raw.Event == "posted" && b.mc.User.Username != message.Username && message.Raw.TeamId == b.TeamId {
if message.Raw.Event == "posted" && b.mc.User.Username != message.Username && message.Raw.Data["team_id"].(string) == b.TeamId {
flog.Debugf("Receiving from matterclient %#v", message)
m := &MMMessage{}
m.Username = message.Username
m.Channel = message.Channel
m.Text = message.Text
if len(message.Post.Filenames) > 0 {
for _, link := range b.mc.GetPublicLinks(message.Post.Filenames) {
if len(message.Post.FileIds) > 0 {
for _, link := range b.mc.GetPublicLinks(message.Post.FileIds) {
m.Text = m.Text + "\n" + link
}
}

View File

@ -6,6 +6,7 @@ import (
"github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus"
"github.com/nlopes/slack"
"regexp"
"strings"
"time"
)
@ -25,8 +26,7 @@ type Bslack struct {
Plus bool
Remote chan config.Message
Users []slack.User
protocol string
origin string
Account string
si *slack.Info
channels []slack.Channel
}
@ -38,12 +38,11 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, origin string, c chan config.Message) *Bslack {
func New(cfg config.Protocol, account string, c chan config.Message) *Bslack {
b := &Bslack{}
b.Config = &cfg
b.Remote = c
b.protocol = protocol
b.origin = origin
b.Account = account
return b
}
@ -66,10 +65,6 @@ func (b *Bslack) Connect() error {
return nil
}
func (b *Bslack) FullOrigin() string {
return b.protocol + "." + b.origin
}
func (b *Bslack) JoinChannel(channel string) error {
// we can only join channels using the API
if b.Config.UseAPI {
@ -81,24 +76,12 @@ func (b *Bslack) JoinChannel(channel string) error {
return nil
}
func (b *Bslack) Name() string {
return b.protocol + "." + b.origin
}
func (b *Bslack) Protocol() string {
return b.protocol
}
func (b *Bslack) Origin() string {
return b.origin
}
func (b *Bslack) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg)
if msg.FullOrigin == b.FullOrigin() {
if msg.Account == b.Account {
return nil
}
nick := config.GetNick(&msg, b.Config)
nick := msg.Username
message := msg.Text
channel := msg.Channel
if b.Config.PrefixMessagesWithNick {
@ -154,14 +137,14 @@ func (b *Bslack) getAvatar(user string) string {
func (b *Bslack) getChannelByName(name string) (*slack.Channel, error) {
if b.channels == nil {
return nil, fmt.Errorf("%s: channel %s not found (no channels found)", b.FullOrigin(), name)
return nil, fmt.Errorf("%s: channel %s not found (no channels found)", b.Account, name)
}
for _, channel := range b.channels {
if channel.Name == name {
return &channel, nil
}
}
return nil, fmt.Errorf("%s: channel %s not found", b.FullOrigin(), name)
return nil, fmt.Errorf("%s: channel %s not found", b.Account, name)
}
func (b *Bslack) handleSlack() {
@ -181,8 +164,8 @@ func (b *Bslack) handleSlack() {
}
texts := strings.Split(message.Text, "\n")
for _, text := range texts {
flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.FullOrigin())
b.Remote <- config.Message{Text: text, Username: message.Username, Channel: message.Channel, Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin(), Avatar: b.getAvatar(message.Username)}
flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account)
b.Remote <- config.Message{Text: text, Username: message.Username, Channel: message.Channel, Account: b.Account, Avatar: b.getAvatar(message.Username)}
}
}
}
@ -209,11 +192,14 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
m.Channel = channel.Name
m.Text = ev.Text
m.Raw = ev
m.Text = b.replaceMention(m.Text)
mchan <- m
}
count++
case *slack.OutgoingErrorEvent:
flog.Debugf("%#v", ev.Error())
case *slack.ChannelJoinedEvent:
b.Users, _ = b.sc.GetUsers()
case *slack.ConnectedEvent:
b.channels = ev.Info.Channels
b.si = ev.Info
@ -232,7 +218,26 @@ func (b *Bslack) handleMatterHook(mchan chan *MMMessage) {
m := &MMMessage{}
m.Username = message.UserName
m.Text = message.Text
m.Text = b.replaceMention(m.Text)
m.Channel = message.ChannelName
mchan <- m
}
}
func (b *Bslack) userName(id string) string {
for _, u := range b.Users {
if u.ID == id {
return u.Name
}
}
return ""
}
func (b *Bslack) replaceMention(text string) string {
results := regexp.MustCompile(`<@([a-zA-z0-9]+)>`).FindAllStringSubmatch(text, -1)
for _, r := range results {
text = strings.Replace(text, "<@"+r[1]+">", "@"+b.userName(r[1]), -1)
}
return text
}

View File

@ -0,0 +1,74 @@
package btelegram
import (
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
"github.com/go-telegram-bot-api/telegram-bot-api"
"strconv"
)
type Btelegram struct {
c *tgbotapi.BotAPI
Config *config.Protocol
Remote chan config.Message
Account string
}
var flog *log.Entry
var protocol = "telegram"
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, account string, c chan config.Message) *Btelegram {
b := &Btelegram{}
b.Config = &cfg
b.Remote = c
b.Account = account
return b
}
func (b *Btelegram) Connect() error {
var err error
flog.Info("Connecting")
b.c, err = tgbotapi.NewBotAPI(b.Config.Token)
if err != nil {
flog.Debugf("%#v", err)
return err
}
updates, err := b.c.GetUpdatesChan(tgbotapi.NewUpdate(0))
if err != nil {
flog.Debugf("%#v", err)
return err
}
flog.Info("Connection succeeded")
go b.handleRecv(updates)
return nil
}
func (b *Btelegram) JoinChannel(channel string) error {
return nil
}
func (b *Btelegram) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg)
chatid, err := strconv.ParseInt(msg.Channel, 10, 64)
if err != nil {
return err
}
m := tgbotapi.NewMessage(chatid, msg.Text)
_, err = b.c.Send(m)
return err
}
func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
for update := range updates {
if update.Message == nil {
continue
}
flog.Debugf("Sending message from %s on %s to gateway", update.Message.From.UserName, b.Account)
b.Remote <- config.Message{Username: update.Message.From.UserName, Text: update.Message.Text, Channel: strconv.FormatInt(update.Message.Chat.ID, 10), Account: b.Account}
}
}

View File

@ -10,12 +10,11 @@ import (
)
type Bxmpp struct {
xc *xmpp.Client
xmppMap map[string]string
Config *config.Protocol
origin string
protocol string
Remote chan config.Message
xc *xmpp.Client
xmppMap map[string]string
Config *config.Protocol
Remote chan config.Message
Account string
}
var flog *log.Entry
@ -25,12 +24,11 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, origin string, c chan config.Message) *Bxmpp {
func New(cfg config.Protocol, account string, c chan config.Message) *Bxmpp {
b := &Bxmpp{}
b.xmppMap = make(map[string]string)
b.Config = &cfg
b.protocol = protocol
b.origin = origin
b.Account = account
b.Remote = c
return b
}
@ -48,31 +46,14 @@ func (b *Bxmpp) Connect() error {
return nil
}
func (b *Bxmpp) FullOrigin() string {
return b.protocol + "." + b.origin
}
func (b *Bxmpp) JoinChannel(channel string) error {
b.xc.JoinMUCNoHistory(channel+"@"+b.Config.Muc, b.Config.Nick)
return nil
}
func (b *Bxmpp) Name() string {
return b.protocol + "." + b.origin
}
func (b *Bxmpp) Protocol() string {
return b.protocol
}
func (b *Bxmpp) Origin() string {
return b.origin
}
func (b *Bxmpp) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg)
nick := config.GetNick(&msg, b.Config)
b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.Config.Muc, Text: nick + msg.Text})
b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.Config.Muc, Text: msg.Username + msg.Text})
return nil
}
@ -97,19 +78,27 @@ func (b *Bxmpp) createXMPP() (*xmpp.Client, error) {
return b.xc, err
}
func (b *Bxmpp) xmppKeepAlive() {
func (b *Bxmpp) xmppKeepAlive() chan bool {
done := make(chan bool)
go func() {
ticker := time.NewTicker(90 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
b.xc.Send(xmpp.Chat{})
b.xc.PingC2S("", "")
case <-done:
return
}
}
}()
return done
}
func (b *Bxmpp) handleXmpp() error {
done := b.xmppKeepAlive()
defer close(done)
nodelay := time.Time{}
for {
m, err := b.xc.Recv()
if err != nil {
@ -127,9 +116,9 @@ func (b *Bxmpp) handleXmpp() error {
if len(s) == 2 {
nick = s[1]
}
if nick != b.Config.Nick {
flog.Debugf("Sending message from %s on %s to gateway", nick, b.FullOrigin())
b.Remote <- config.Message{Username: nick, Text: v.Text, Channel: channel, Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin()}
if nick != b.Config.Nick && v.Stamp == nodelay && v.Text != "" {
flog.Debugf("Sending message from %s on %s to gateway", nick, b.Account)
b.Remote <- config.Message{Username: nick, Text: v.Text, Channel: channel, Account: b.Account}
}
}
case xmpp.Presence:

View File

@ -1,4 +1,38 @@
# v0.7-dev
# v0.9.0
## New features
* Telegram: New protocol support added (https://telegram.org)
* Hipchat: Add sample config to connect to hipchat via xmpp
* discord: add "Bot " tag to discord tokens automatically
* slack: Add support for dynamic Iconurl #43
* general: Add ```gateway.inout``` config option for bidirectional bridges #85
* general: Add ```[general]``` section so that ```RemoteNickFormat``` can be set globally
## Bugfix
* general: when using samechannelgateway NickFormat get doubled by the NICK #77
* general: fix ShowJoinPart for messages from irc bridge #72
* gitter: fix high cpu usage #89
* irc: fix !users command #78
* xmpp: fix keepalive
* xmpp: do not relay delayed/empty messages
* slack: Replace id-mentions to usernames #86
* mattermost: fix public links not working (API changes)
# v0.8.1
## Bugfix
* general: when using samechannelgateway NickFormat get doubled by the NICK #77
* irc: fix !users command #78
# v0.8.0
Release because of breaking mattermost API changes
## New features
* Supports mattermost v3.5.0
# v0.7.1
## Bugfix
* general: when using samechannelgateway NickFormat get doubled by the NICK #77
* irc: fix !users command #78
# v0.7.0
## Breaking config changes from 0.6 to 0.7
Matterbridge now uses TOML configuration (https://github.com/toml-lang/toml)
See matterbridge.toml.sample for an example

View File

@ -11,55 +11,68 @@ import (
type Gateway struct {
*config.Config
MyConfig *config.Gateway
Bridges []bridge.Bridge
MyConfig *config.Gateway
//Bridges []*bridge.Bridge
Bridges map[string]*bridge.Bridge
ChannelsOut map[string][]string
ChannelsIn map[string][]string
ignoreNicks map[string][]string
Name string
Message chan config.Message
}
func New(cfg *config.Config, gateway *config.Gateway) error {
c := make(chan config.Message)
func New(cfg *config.Config, gateway *config.Gateway) *Gateway {
gw := &Gateway{}
gw.Name = gateway.Name
gw.Config = cfg
gw.MyConfig = gateway
exists := make(map[string]bool)
for _, br := range append(gateway.In, gateway.Out...) {
if exists[br.Account] {
continue
}
log.Infof("Starting bridge: %s channel: %s", br.Account, br.Channel)
gw.Bridges = append(gw.Bridges, bridge.New(cfg, &br, c))
exists[br.Account] = true
}
gw.mapChannels()
//TODO fix mapIgnores
//gw.mapIgnores()
exists = make(map[string]bool)
gw.Message = make(chan config.Message)
gw.Bridges = make(map[string]*bridge.Bridge)
return gw
}
func (gw *Gateway) AddBridge(cfg *config.Bridge) error {
for _, br := range gw.Bridges {
err := br.Connect()
if err != nil {
log.Fatalf("Bridge %s failed to start: %v", br.FullOrigin(), err)
}
for _, channel := range append(gw.ChannelsOut[br.FullOrigin()], gw.ChannelsIn[br.FullOrigin()]...) {
if exists[br.FullOrigin()+channel] {
continue
}
log.Infof("%s: joining %s", br.FullOrigin(), channel)
br.JoinChannel(channel)
exists[br.FullOrigin()+channel] = true
if br.Account == cfg.Account {
return nil
}
}
log.Infof("Starting bridge: %s ", cfg.Account)
br := bridge.New(gw.Config, cfg, gw.Message)
gw.Bridges[cfg.Account] = br
err := br.Connect()
if err != nil {
return fmt.Errorf("Bridge %s failed to start: %v", br.Account, err)
}
exists := make(map[string]bool)
for _, channel := range append(gw.ChannelsOut[br.Account], gw.ChannelsIn[br.Account]...) {
if !exists[br.Account+channel] {
log.Infof("%s: joining %s", br.Account, channel)
br.JoinChannel(channel)
exists[br.Account+channel] = true
}
}
gw.handleReceive(c)
return nil
}
func (gw *Gateway) handleReceive(c chan config.Message) {
func (gw *Gateway) Start() error {
gw.mapChannels()
for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) {
err := gw.AddBridge(&br)
if err != nil {
return err
}
}
//TODO fix mapIgnores
//gw.mapIgnores()
go gw.handleReceive()
return nil
}
func (gw *Gateway) handleReceive() {
for {
select {
case msg := <-c:
case msg := <-gw.Message:
for _, br := range gw.Bridges {
gw.handleMessage(msg, br)
}
@ -79,6 +92,10 @@ func (gw *Gateway) mapChannels() error {
m[br.Account] = append(m[br.Account], br.Channel)
}
gw.ChannelsIn = m
for _, br := range gw.MyConfig.InOut {
gw.ChannelsIn[br.Account] = append(gw.ChannelsIn[br.Account], br.Channel)
gw.ChannelsOut[br.Account] = append(gw.ChannelsOut[br.Account], br.Channel)
}
return nil
}
@ -92,7 +109,11 @@ func (gw *Gateway) mapIgnores() {
}
func (gw *Gateway) getDestChannel(msg *config.Message, dest string) []string {
channels := gw.ChannelsIn[msg.FullOrigin]
channels := gw.ChannelsIn[msg.Account]
// broadcast to every out channel (irc QUIT)
if msg.Event == config.EVENT_JOIN_LEAVE && msg.Channel == "" {
return gw.ChannelsOut[dest]
}
for _, channel := range channels {
if channel == msg.Channel {
return gw.ChannelsOut[dest]
@ -101,15 +122,19 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest string) []string {
return []string{}
}
func (gw *Gateway) handleMessage(msg config.Message, dest bridge.Bridge) {
func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) {
if gw.ignoreMessage(&msg) {
return
}
// only relay join/part when configged
if msg.Event == config.EVENT_JOIN_LEAVE && !gw.Bridges[dest.Account].Config.ShowJoinPart {
return
}
originchannel := msg.Channel
channels := gw.getDestChannel(&msg, dest.FullOrigin())
channels := gw.getDestChannel(&msg, dest.Account)
for _, channel := range channels {
// do not send the message to the bridge we come from if also the channel is the same
if msg.FullOrigin == dest.FullOrigin() && channel == originchannel {
if msg.Account == dest.Account && channel == originchannel {
continue
}
msg.Channel = channel
@ -117,7 +142,8 @@ func (gw *Gateway) handleMessage(msg config.Message, dest bridge.Bridge) {
log.Debug("empty channel")
return
}
log.Debugf("Sending %#v from %s (%s) to %s (%s)", msg, msg.FullOrigin, originchannel, dest.FullOrigin(), channel)
log.Debugf("Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, originchannel, dest.Account, channel)
gw.modifyUsername(&msg, dest)
err := dest.Send(msg)
if err != nil {
fmt.Println(err)
@ -127,7 +153,7 @@ func (gw *Gateway) handleMessage(msg config.Message, dest bridge.Bridge) {
func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
// should we discard messages ?
for _, entry := range gw.ignoreNicks[msg.FullOrigin] {
for _, entry := range gw.ignoreNicks[msg.Account] {
if msg.Username == entry {
return true
}
@ -135,17 +161,29 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
return false
}
func (gw *Gateway) modifyMessage(msg *config.Message, dest bridge.Bridge) {
func (gw *Gateway) modifyMessage(msg *config.Message, dest *bridge.Bridge) {
val := reflect.ValueOf(gw.Config).Elem()
for i := 0; i < val.NumField(); i++ {
typeField := val.Type().Field(i)
// look for the protocol map (both lowercase)
if strings.ToLower(typeField.Name) == dest.Protocol() {
if strings.ToLower(typeField.Name) == dest.Protocol {
// get the Protocol struct from the map
protoCfg := val.Field(i).MapIndex(reflect.ValueOf(dest.Origin()))
protoCfg := val.Field(i).MapIndex(reflect.ValueOf(dest.Name))
//config.SetNickFormat(msg, protoCfg.Interface().(config.Protocol))
val.Field(i).SetMapIndex(reflect.ValueOf(dest.Origin()), protoCfg)
val.Field(i).SetMapIndex(reflect.ValueOf(dest.Name), protoCfg)
break
}
}
}
func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) {
br := gw.Bridges[msg.Account]
nick := gw.Config.General.RemoteNickFormat
if nick == "" {
nick = dest.Config.RemoteNickFormat
}
nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1)
nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1)
msg.Username = nick
}

View File

@ -10,7 +10,7 @@ import (
type SameChannelGateway struct {
*config.Config
MyConfig *config.SameChannelGateway
Bridges []bridge.Bridge
Bridges map[string]*bridge.Bridge
Channels []string
ignoreNicks map[string][]string
Name string
@ -19,6 +19,7 @@ type SameChannelGateway struct {
func New(cfg *config.Config, gateway *config.SameChannelGateway) error {
c := make(chan config.Message)
gw := &SameChannelGateway{}
gw.Bridges = make(map[string]*bridge.Bridge)
gw.Name = gateway.Name
gw.Config = cfg
gw.MyConfig = gateway
@ -26,15 +27,15 @@ func New(cfg *config.Config, gateway *config.SameChannelGateway) error {
for _, account := range gateway.Accounts {
br := config.Bridge{Account: account}
log.Infof("Starting bridge: %s", account)
gw.Bridges = append(gw.Bridges, bridge.New(cfg, &br, c))
gw.Bridges[account] = bridge.New(cfg, &br, c)
}
for _, br := range gw.Bridges {
err := br.Connect()
if err != nil {
log.Fatalf("Bridge %s failed to start: %v", br.FullOrigin(), err)
log.Fatalf("Bridge %s failed to start: %v", br.Account, err)
}
for _, channel := range gw.Channels {
log.Infof("%s: joining %s", br.FullOrigin(), channel)
log.Infof("%s: joining %s", br.Account, channel)
br.JoinChannel(channel)
}
}
@ -53,44 +54,33 @@ func (gw *SameChannelGateway) handleReceive(c chan config.Message) {
}
}
func (gw *SameChannelGateway) handleMessage(msg config.Message, dest bridge.Bridge) {
func (gw *SameChannelGateway) handleMessage(msg config.Message, dest *bridge.Bridge) {
// is this a configured channel
if !gw.validChannel(msg.Channel) {
return
}
// do not send the message to the bridge we come from if also the channel is the same
if msg.FullOrigin == dest.FullOrigin() {
if msg.Account == dest.Account {
return
}
gw.modifyMessage(&msg, dest)
log.Debugf("Sending %#v from %s (%s) to %s (%s)", msg, msg.FullOrigin, msg.Channel, dest.FullOrigin(), msg.Channel)
gw.modifyUsername(&msg, dest)
log.Debugf("Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, msg.Channel, dest.Account, msg.Channel)
err := dest.Send(msg)
if err != nil {
log.Error(err)
}
}
func setNickFormat(msg *config.Message, format string) {
if format == "" {
msg.Username = msg.Protocol + "." + msg.Origin + "-" + msg.Username + ": "
return
}
msg.Username = strings.Replace(format, "{NICK}", msg.Username, -1)
msg.Username = strings.Replace(msg.Username, "{BRIDGE}", msg.Origin, -1)
msg.Username = strings.Replace(msg.Username, "{PROTOCOL}", msg.Protocol, -1)
}
func (gw *SameChannelGateway) modifyMessage(msg *config.Message, dest bridge.Bridge) {
switch dest.Protocol() {
case "irc":
setNickFormat(msg, gw.Config.IRC[dest.Origin()].RemoteNickFormat)
case "mattermost":
setNickFormat(msg, gw.Config.Mattermost[dest.Origin()].RemoteNickFormat)
case "slack":
setNickFormat(msg, gw.Config.Slack[dest.Origin()].RemoteNickFormat)
case "discord":
setNickFormat(msg, gw.Config.Discord[dest.Origin()].RemoteNickFormat)
func (gw *SameChannelGateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) {
br := gw.Bridges[msg.Account]
nick := gw.Config.General.RemoteNickFormat
if nick == "" {
nick = dest.Config.RemoteNickFormat
}
nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1)
nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1)
msg.Username = nick
}
func (gw *SameChannelGateway) validChannel(channel string) bool {

View File

@ -9,7 +9,7 @@ import (
log "github.com/Sirupsen/logrus"
)
var version = "0.7.0-dev"
var version = "0.9.0"
func init() {
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
@ -49,12 +49,11 @@ func main() {
continue
}
fmt.Printf("starting gateway %#v\n", gw.Name)
go func(gw config.Gateway) {
err := gateway.New(cfg, &gw)
if err != nil {
log.Debugf("starting gateway failed %#v", err)
}
}(gw)
g := gateway.New(cfg, &gw)
err := g.Start()
if err != nil {
log.Debugf("starting gateway failed %#v", err)
}
}
select {}
}

View File

@ -37,18 +37,6 @@ Nick="matterbot"
NickServNick="nickserv"
NickServPassword="secret"
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Nicks you want to ignore.
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
#Flood control
#Delay in milliseconds between each message send to the IRC server
#OPTIONAL (default 1300)
@ -60,6 +48,22 @@ MessageDelay=1300
#OPTIONAL (default 30)
MessageQueue=30
#Nicks you want to ignore.
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges (only from irc-bridge at the moment)
#OPTIONAL (default false)
ShowJoinPart=false
###################################################################
#XMPP section
###################################################################
@ -89,6 +93,65 @@ Muc="conference.jabber.example.com"
#REQUIRED
Nick="xmppbot"
#Nicks you want to ignore.
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges (only from irc-bridge at the moment)
#OPTIONAL (default false)
ShowJoinPart=false
###################################################################
#hipchat section
###################################################################
#Go to https://www.hipchat.com/account/xmpp this will show you the necessary data
#to fill in the section below
[xmpp.hipchat]
#xmpp server to connect to.
#REQUIRED
Server="chat.hipchat.com:5222"
#Jabber ID
#REQUIRED
Jid="12345_12345@chat.hipchat.com"
#Password (your hipchat password)
#REQUIRED
Password="yourpass"
#Conference (MUC) domain
#REQUIRED
Muc="conf.hipchat.com"
#Room nickname
#REQUIRED
Nick="yourlogin"
#Nicks you want to ignore.
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="spammer1 spammer2"
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> "
#Enable to show users joins/parts from other bridges (only from irc-bridge at the moment)
#OPTIONAL (default false)
ShowJoinPart=false
###################################################################
#mattermost section
@ -150,9 +213,13 @@ NoTLS=false
#OPTIONAL (default false)
SkipTLSVerify=true
#Enable to show IRC joins/parts in mattermost.
#OPTIONAL (default false)
ShowJoinPart=false
#how to format the list of IRC nicks when displayed in mattermost.
#Possible options are "table" and "plain"
#OPTIONAL (default plain)
NickFormatter="plain"
#How many nicks to list per row for formatters that support this.
#OPTIONAL (default 4)
NicksPerRow=4
#Whether to prefix messages from other bridges to mattermost with the sender's nick.
#Useful if username overrides for incoming webhooks isn't enabled on the
@ -162,6 +229,11 @@ ShowJoinPart=false
#OPTIONAL (default false)
PrefixMessagesWithNick=false
#Nicks you want to ignore.
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -169,17 +241,9 @@ PrefixMessagesWithNick=false
#OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#how to format the list of IRC nicks when displayed in mattermost.
#Possible options are "table" and "plain"
#OPTIONAL (default plain)
NickFormatter="plain"
#How many nicks to list per row for formatters that support this.
#OPTIONAL (default 4)
NicksPerRow=4
#Nicks you want to ignore. Messages from those users will not be bridged.
#OPTIONAL
IgnoreNicks="mmbot spammer2"
#Enable to show users joins/parts from other bridges (only from irc-bridge at the moment)
#OPTIONAL (default false)
ShowJoinPart=false
###################################################################
#Gitter section
@ -197,9 +261,10 @@ IgnoreNicks="mmbot spammer2"
#REQUIRED
Token="Yourtokenhere"
#Nicks you want to ignore. Messages of those users will not be bridged.
#OPTIONAL
IgnoreNicks="spammer1 spammer2"
#Nicks you want to ignore.
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
@ -208,6 +273,10 @@ IgnoreNicks="spammer1 spammer2"
#OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges (only from irc-bridge at the moment)
#OPTIONAL (default false)
ShowJoinPart=false
###################################################################
#slack section
###################################################################
@ -237,6 +306,8 @@ BindAddress="0.0.0.0:9999"
useAPI=false
#Token to connect with the Slack API
#You'll have to use a test/api-token using a dedicated user and not a bot token.
#See https://github.com/42wim/matterbridge/issues/75 for more info.
#REQUIRED (when useAPI=true)
Token="yourslacktoken"
@ -249,21 +320,6 @@ Token="yourslacktoken"
#OPTIONAL
IconURL="https://robohash.org/{NICK}.png?size=48x48"
#Whether to prefix messages from other bridges to mattermost with RemoteNickFormat
#Useful if username overrides for incoming webhooks isn't enabled on the
#slack server. If you set PrefixMessagesWithNick to true, each message
#from bridge to Slack will by default be prefixed by "bridge-" + nick. You can,
#however, modify how the messages appear, by setting (and modifying) RemoteNickFormat
#OPTIONAL (default false)
PrefixMessagesWithNick=false
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#how to format the list of IRC nicks when displayed in slack
#Possible options are "table" and "plain"
#OPTIONAL (default plain)
@ -272,9 +328,29 @@ NickFormatter="plain"
#OPTIONAL (default 4)
NicksPerRow=4
#Nicks you want to ignore. Messages from those users will not be bridged.
#OPTIONAL
IgnoreNicks="mmbot spammer2"
#Whether to prefix messages from other bridges to mattermost with RemoteNickFormat
#Useful if username overrides for incoming webhooks isn't enabled on the
#slack server. If you set PrefixMessagesWithNick to true, each message
#from bridge to Slack will by default be prefixed by "bridge-" + nick. You can,
#however, modify how the messages appear, by setting (and modifying) RemoteNickFormat
#OPTIONAL (default false)
PrefixMessagesWithNick=false
#Nicks you want to ignore.
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges (only from irc-bridge at the moment)
#OPTIONAL (default false)
ShowJoinPart=false
###################################################################
#discord section
@ -288,15 +364,44 @@ IgnoreNicks="mmbot spammer2"
#Token to connect with Discord API
#You can get your token by following the instructions on
#https://github.com/reactiflux/discord-irc/wiki/Creating-a-discord-bot-&-getting-a-token
#The "Bot" tag needs to be added before the token
#REQUIRED
Token="Bot Yourtokenhere"
Token="Yourtokenhere"
#REQUIRED
Server="yourservername"
#Nicks you want to ignore. Messages of those users will not be bridged.
#OPTIONAL
#Nicks you want to ignore.
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges (only from irc-bridge at the moment)
#OPTIONAL (default false)
ShowJoinPart=false
###################################################################
#telegram section
###################################################################
[telegram]
#You can configure multiple servers "[telegram.name]" or "[telegram.name2]"
#In this example we use [telegram.secure]
#REQUIRED
[telegram.secure]
#Token to connect with telegram API
#REQUIRED
Token="Yourtokenhere"
#Nicks you want to ignore.
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="spammer1 spammer2"
#RemoteNickFormat defines how remote users appear on this bridge
@ -306,6 +411,23 @@ IgnoreNicks="spammer1 spammer2"
#OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges (only from irc-bridge at the moment)
#OPTIONAL (default false)
ShowJoinPart=false
###################################################################
#General configuration
###################################################################
#Settings here override specific settings for each protocol
[general]
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
###################################################################
#Gateway configuration
@ -318,7 +440,7 @@ RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#from [[gateway.in]] to.
#
#Most of the time [[gateway.in]] and [[gateway.out]] are the same if you
#want bidirectional bridging.
#want bidirectional bridging. You can then use [[gateway.inout]]
#
[[gateway]]
@ -346,18 +468,20 @@ enable=true
#discord - channel (without the #)
# - ID:123456789 (where 123456789 is the channel ID)
# (https://github.com/42wim/matterbridge/issues/57)
#telegram - chatid (a large negative number, eg -123456789)
#hipchat - id_channel (see https://www.hipchat.com/account/xmpp for the correct channel)
#REQUIRED
channel="#testing"
[[gateway.in]]
account="mattermost.work"
channel="off-topic"
#[[gateway.out]] specifies the account and channels we will sent messages to.
[[gateway.out]]
account="irc.freenode"
channel="#testing"
[[gateway.out]]
#[[gateway.inout]] can be used when then channel will be used to receive from
#and send messages to
[[gateway.inout]]
account="mattermost.work"
channel="off-topic"

View File

@ -19,14 +19,14 @@ enable=true
account="irc.freenode"
channel="#testing"
[[gateway.in]]
account="mattermost.work"
channel="off-topic"
[[gateway.out]]
account="irc.freenode"
channel="#testing"
[[gateway.in]]
account="mattermost.work"
channel="off-topic"
[[gateway.out]]
account="mattermost.work"
channel="off-topic"

View File

@ -264,7 +264,7 @@ func (m *MMClient) parseActionPost(rmsg *Message) {
}
rmsg.Username = m.GetUser(data.UserId).Username
rmsg.Channel = m.GetChannelName(data.ChannelId)
rmsg.Team = m.GetTeamName(rmsg.Raw.TeamId)
rmsg.Team = m.GetTeamName(rmsg.Raw.Data["team_id"].(string))
// direct message
if rmsg.Raw.Data["channel_type"] == "D" {
rmsg.Channel = m.GetUser(data.UserId).Username
@ -275,7 +275,7 @@ func (m *MMClient) parseActionPost(rmsg *Message) {
}
func (m *MMClient) UpdateUsers() error {
mmusers, err := m.Client.GetProfilesForDirectMessageList(m.Team.Id)
mmusers, err := m.Client.GetProfiles(0, 50000, "")
if err != nil {
return errors.New(err.DetailedError)
}
@ -305,7 +305,7 @@ func (m *MMClient) GetChannelName(channelId string) string {
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
for _, channel := range append(t.Channels.Channels, t.MoreChannels.Channels...) {
for _, channel := range append(*t.Channels, *t.MoreChannels...) {
if channel.Id == channelId {
return channel.Name
}
@ -322,7 +322,7 @@ func (m *MMClient) GetChannelId(name string, teamId string) string {
}
for _, t := range m.OtherTeams {
if t.Id == teamId {
for _, channel := range append(t.Channels.Channels, t.MoreChannels.Channels...) {
for _, channel := range append(*t.Channels, *t.MoreChannels...) {
if channel.Name == name {
return channel.Id
}
@ -336,7 +336,7 @@ func (m *MMClient) GetChannelHeader(channelId string) string {
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
for _, channel := range append(t.Channels.Channels, t.MoreChannels.Channels...) {
for _, channel := range append(*t.Channels, *t.MoreChannels...) {
if channel.Id == channelId {
return channel.Header
}
@ -354,7 +354,7 @@ func (m *MMClient) PostMessage(channelId string, text string) {
func (m *MMClient) JoinChannel(channelId string) error {
m.RLock()
defer m.RUnlock()
for _, c := range m.Team.Channels.Channels {
for _, c := range *m.Team.Channels {
if c.Id == channelId {
m.log.Debug("Not joining ", channelId, " already joined.")
return nil
@ -397,7 +397,7 @@ func (m *MMClient) GetPublicLink(filename string) string {
if err != nil {
return ""
}
return res.Data.(string)
return res
}
func (m *MMClient) GetPublicLinks(filenames []string) []string {
@ -407,7 +407,7 @@ func (m *MMClient) GetPublicLinks(filenames []string) []string {
if err != nil {
continue
}
output = append(output, res.Data.(string))
output = append(output, res)
}
return output
}
@ -432,15 +432,17 @@ func (m *MMClient) UpdateLastViewed(channelId string) {
}
func (m *MMClient) UsernamesInChannel(channelId string) []string {
ceiRes, err := m.Client.GetChannelExtraInfo(channelId, 5000, "")
res, err := m.Client.GetMyChannelMembers()
if err != nil {
m.log.Errorf("UsernamesInChannel(%s) failed: %s", channelId, err)
return []string{}
}
extra := ceiRes.Data.(*model.ChannelExtra)
members := res.Data.(*model.ChannelMembers)
result := []string{}
for _, member := range extra.Members {
result = append(result, member.Username)
for _, channel := range *members {
if channel.ChannelId == channelId {
result = append(result, m.GetUser(channel.UserId).Username)
}
}
return result
}
@ -500,10 +502,10 @@ func (m *MMClient) GetChannels() []*model.Channel {
defer m.RUnlock()
var channels []*model.Channel
// our primary team channels first
channels = append(channels, m.Team.Channels.Channels...)
channels = append(channels, *m.Team.Channels...)
for _, t := range m.OtherTeams {
if t.Id != m.Team.Id {
channels = append(channels, t.Channels.Channels...)
channels = append(channels, *t.Channels...)
}
}
return channels
@ -515,7 +517,7 @@ func (m *MMClient) GetMoreChannels() []*model.Channel {
defer m.RUnlock()
var channels []*model.Channel
for _, t := range m.OtherTeams {
channels = append(channels, t.MoreChannels.Channels...)
channels = append(channels, *t.MoreChannels...)
}
return channels
}
@ -526,8 +528,8 @@ func (m *MMClient) GetTeamFromChannel(channelId string) string {
defer m.RUnlock()
var channels []*model.Channel
for _, t := range m.OtherTeams {
channels = append(channels, t.Channels.Channels...)
channels = append(channels, t.MoreChannels.Channels...)
channels = append(channels, *t.Channels...)
channels = append(channels, *t.MoreChannels...)
for _, c := range channels {
if c.Id == channelId {
return t.Id
@ -540,12 +542,12 @@ func (m *MMClient) GetTeamFromChannel(channelId string) string {
func (m *MMClient) GetLastViewedAt(channelId string) int64 {
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
if _, ok := t.Channels.Members[channelId]; ok {
return t.Channels.Members[channelId].LastViewedAt
}
res, err := m.Client.GetChannel(channelId, "")
if err != nil {
return model.GetMillis()
}
return 0
data := res.Data.(*model.ChannelData)
return data.Member.LastViewedAt
}
func (m *MMClient) GetUsers() map[string]*model.User {
@ -619,7 +621,7 @@ func (m *MMClient) initUser() error {
//m.log.Debug("initUser(): loading all team data")
for _, v := range initData.Teams {
m.Client.SetTeamId(v.Id)
mmusers, _ := m.Client.GetProfiles(v.Id, "")
mmusers, _ := m.Client.GetProfiles(0, 50000, "")
t := &Team{Team: v, Users: mmusers.Data.(map[string]*model.User), Id: v.Id}
mmchannels, _ := m.Client.GetChannels("")
t.Channels = mmchannels.Data.(*model.ChannelList)

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Syfaro
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,650 @@
// Package tgbotapi has functions and types used for interacting with
// the Telegram Bot API.
package tgbotapi
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/technoweenie/multipartstreamer"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
// BotAPI allows you to interact with the Telegram Bot API.
type BotAPI struct {
Token string `json:"token"`
Debug bool `json:"debug"`
Self User `json:"-"`
Client *http.Client `json:"-"`
}
// NewBotAPI creates a new BotAPI instance.
//
// It requires a token, provided by @BotFather on Telegram.
func NewBotAPI(token string) (*BotAPI, error) {
return NewBotAPIWithClient(token, &http.Client{})
}
// NewBotAPIWithClient creates a new BotAPI instance
// and allows you to pass a http.Client.
//
// It requires a token, provided by @BotFather on Telegram.
func NewBotAPIWithClient(token string, client *http.Client) (*BotAPI, error) {
bot := &BotAPI{
Token: token,
Client: client,
}
self, err := bot.GetMe()
if err != nil {
return &BotAPI{}, err
}
bot.Self = self
return bot, nil
}
// MakeRequest makes a request to a specific endpoint with our token.
func (bot *BotAPI) MakeRequest(endpoint string, params url.Values) (APIResponse, error) {
method := fmt.Sprintf(APIEndpoint, bot.Token, endpoint)
resp, err := bot.Client.PostForm(method, params)
if err != nil {
return APIResponse{}, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusForbidden {
return APIResponse{}, errors.New(ErrAPIForbidden)
}
bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return APIResponse{}, err
}
if bot.Debug {
log.Println(endpoint, string(bytes))
}
var apiResp APIResponse
json.Unmarshal(bytes, &apiResp)
if !apiResp.Ok {
return APIResponse{}, errors.New(apiResp.Description)
}
return apiResp, nil
}
// makeMessageRequest makes a request to a method that returns a Message.
func (bot *BotAPI) makeMessageRequest(endpoint string, params url.Values) (Message, error) {
resp, err := bot.MakeRequest(endpoint, params)
if err != nil {
return Message{}, err
}
var message Message
json.Unmarshal(resp.Result, &message)
bot.debugLog(endpoint, params, message)
return message, nil
}
// UploadFile makes a request to the API with a file.
//
// Requires the parameter to hold the file not be in the params.
// File should be a string to a file path, a FileBytes struct,
// or a FileReader struct.
//
// Note that if your FileReader has a size set to -1, it will read
// the file into memory to calculate a size.
func (bot *BotAPI) UploadFile(endpoint string, params map[string]string, fieldname string, file interface{}) (APIResponse, error) {
ms := multipartstreamer.New()
ms.WriteFields(params)
switch f := file.(type) {
case string:
fileHandle, err := os.Open(f)
if err != nil {
return APIResponse{}, err
}
defer fileHandle.Close()
fi, err := os.Stat(f)
if err != nil {
return APIResponse{}, err
}
ms.WriteReader(fieldname, fileHandle.Name(), fi.Size(), fileHandle)
case FileBytes:
buf := bytes.NewBuffer(f.Bytes)
ms.WriteReader(fieldname, f.Name, int64(len(f.Bytes)), buf)
case FileReader:
if f.Size != -1 {
ms.WriteReader(fieldname, f.Name, f.Size, f.Reader)
break
}
data, err := ioutil.ReadAll(f.Reader)
if err != nil {
return APIResponse{}, err
}
buf := bytes.NewBuffer(data)
ms.WriteReader(fieldname, f.Name, int64(len(data)), buf)
default:
return APIResponse{}, errors.New(ErrBadFileType)
}
method := fmt.Sprintf(APIEndpoint, bot.Token, endpoint)
req, err := http.NewRequest("POST", method, nil)
if err != nil {
return APIResponse{}, err
}
ms.SetupRequest(req)
res, err := bot.Client.Do(req)
if err != nil {
return APIResponse{}, err
}
defer res.Body.Close()
bytes, err := ioutil.ReadAll(res.Body)
if err != nil {
return APIResponse{}, err
}
if bot.Debug {
log.Println(string(bytes))
}
var apiResp APIResponse
json.Unmarshal(bytes, &apiResp)
if !apiResp.Ok {
return APIResponse{}, errors.New(apiResp.Description)
}
return apiResp, nil
}
// GetFileDirectURL returns direct URL to file
//
// It requires the FileID.
func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
file, err := bot.GetFile(FileConfig{fileID})
if err != nil {
return "", err
}
return file.Link(bot.Token), nil
}
// GetMe fetches the currently authenticated bot.
//
// This method is called upon creation to validate the token,
// and so you may get this data from BotAPI.Self without the need for
// another request.
func (bot *BotAPI) GetMe() (User, error) {
resp, err := bot.MakeRequest("getMe", nil)
if err != nil {
return User{}, err
}
var user User
json.Unmarshal(resp.Result, &user)
bot.debugLog("getMe", nil, user)
return user, nil
}
// IsMessageToMe returns true if message directed to this bot.
//
// It requires the Message.
func (bot *BotAPI) IsMessageToMe(message Message) bool {
return strings.Contains(message.Text, "@"+bot.Self.UserName)
}
// Send will send a Chattable item to Telegram.
//
// It requires the Chattable to send.
func (bot *BotAPI) Send(c Chattable) (Message, error) {
switch c.(type) {
case Fileable:
return bot.sendFile(c.(Fileable))
default:
return bot.sendChattable(c)
}
}
// debugLog checks if the bot is currently running in debug mode, and if
// so will display information about the request and response in the
// debug log.
func (bot *BotAPI) debugLog(context string, v url.Values, message interface{}) {
if bot.Debug {
log.Printf("%s req : %+v\n", context, v)
log.Printf("%s resp: %+v\n", context, message)
}
}
// sendExisting will send a Message with an existing file to Telegram.
func (bot *BotAPI) sendExisting(method string, config Fileable) (Message, error) {
v, err := config.values()
if err != nil {
return Message{}, err
}
message, err := bot.makeMessageRequest(method, v)
if err != nil {
return Message{}, err
}
return message, nil
}
// uploadAndSend will send a Message with a new file to Telegram.
func (bot *BotAPI) uploadAndSend(method string, config Fileable) (Message, error) {
params, err := config.params()
if err != nil {
return Message{}, err
}
file := config.getFile()
resp, err := bot.UploadFile(method, params, config.name(), file)
if err != nil {
return Message{}, err
}
var message Message
json.Unmarshal(resp.Result, &message)
bot.debugLog(method, nil, message)
return message, nil
}
// sendFile determines if the file is using an existing file or uploading
// a new file, then sends it as needed.
func (bot *BotAPI) sendFile(config Fileable) (Message, error) {
if config.useExistingFile() {
return bot.sendExisting(config.method(), config)
}
return bot.uploadAndSend(config.method(), config)
}
// sendChattable sends a Chattable.
func (bot *BotAPI) sendChattable(config Chattable) (Message, error) {
v, err := config.values()
if err != nil {
return Message{}, err
}
message, err := bot.makeMessageRequest(config.method(), v)
if err != nil {
return Message{}, err
}
return message, nil
}
// GetUserProfilePhotos gets a user's profile photos.
//
// It requires UserID.
// Offset and Limit are optional.
func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
v := url.Values{}
v.Add("user_id", strconv.Itoa(config.UserID))
if config.Offset != 0 {
v.Add("offset", strconv.Itoa(config.Offset))
}
if config.Limit != 0 {
v.Add("limit", strconv.Itoa(config.Limit))
}
resp, err := bot.MakeRequest("getUserProfilePhotos", v)
if err != nil {
return UserProfilePhotos{}, err
}
var profilePhotos UserProfilePhotos
json.Unmarshal(resp.Result, &profilePhotos)
bot.debugLog("GetUserProfilePhoto", v, profilePhotos)
return profilePhotos, nil
}
// GetFile returns a File which can download a file from Telegram.
//
// Requires FileID.
func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
v := url.Values{}
v.Add("file_id", config.FileID)
resp, err := bot.MakeRequest("getFile", v)
if err != nil {
return File{}, err
}
var file File
json.Unmarshal(resp.Result, &file)
bot.debugLog("GetFile", v, file)
return file, nil
}
// GetUpdates fetches updates.
// If a WebHook is set, this will not return any data!
//
// Offset, Limit, and Timeout are optional.
// To avoid stale items, set Offset to one higher than the previous item.
// Set Timeout to a large number to reduce requests so you can get updates
// instantly instead of having to wait between requests.
func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
v := url.Values{}
if config.Offset != 0 {
v.Add("offset", strconv.Itoa(config.Offset))
}
if config.Limit > 0 {
v.Add("limit", strconv.Itoa(config.Limit))
}
if config.Timeout > 0 {
v.Add("timeout", strconv.Itoa(config.Timeout))
}
resp, err := bot.MakeRequest("getUpdates", v)
if err != nil {
return []Update{}, err
}
var updates []Update
json.Unmarshal(resp.Result, &updates)
bot.debugLog("getUpdates", v, updates)
return updates, nil
}
// RemoveWebhook unsets the webhook.
func (bot *BotAPI) RemoveWebhook() (APIResponse, error) {
return bot.MakeRequest("setWebhook", url.Values{})
}
// SetWebhook sets a webhook.
//
// If this is set, GetUpdates will not get any data!
//
// If you do not have a legitimate TLS certificate, you need to include
// your self signed certificate with the config.
func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) {
if config.Certificate == nil {
v := url.Values{}
v.Add("url", config.URL.String())
return bot.MakeRequest("setWebhook", v)
}
params := make(map[string]string)
params["url"] = config.URL.String()
resp, err := bot.UploadFile("setWebhook", params, "certificate", config.Certificate)
if err != nil {
return APIResponse{}, err
}
var apiResp APIResponse
json.Unmarshal(resp.Result, &apiResp)
if bot.Debug {
log.Printf("setWebhook resp: %+v\n", apiResp)
}
return apiResp, nil
}
// GetUpdatesChan starts and returns a channel for getting updates.
func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (<-chan Update, error) {
updatesChan := make(chan Update, 100)
go func() {
for {
updates, err := bot.GetUpdates(config)
if err != nil {
log.Println(err)
log.Println("Failed to get updates, retrying in 3 seconds...")
time.Sleep(time.Second * 3)
continue
}
for _, update := range updates {
if update.UpdateID >= config.Offset {
config.Offset = update.UpdateID + 1
updatesChan <- update
}
}
}
}()
return updatesChan, nil
}
// ListenForWebhook registers a http handler for a webhook.
func (bot *BotAPI) ListenForWebhook(pattern string) <-chan Update {
updatesChan := make(chan Update, 100)
http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
bytes, _ := ioutil.ReadAll(r.Body)
var update Update
json.Unmarshal(bytes, &update)
updatesChan <- update
})
return updatesChan
}
// AnswerInlineQuery sends a response to an inline query.
//
// Note that you must respond to an inline query within 30 seconds.
func (bot *BotAPI) AnswerInlineQuery(config InlineConfig) (APIResponse, error) {
v := url.Values{}
v.Add("inline_query_id", config.InlineQueryID)
v.Add("cache_time", strconv.Itoa(config.CacheTime))
v.Add("is_personal", strconv.FormatBool(config.IsPersonal))
v.Add("next_offset", config.NextOffset)
data, err := json.Marshal(config.Results)
if err != nil {
return APIResponse{}, err
}
v.Add("results", string(data))
v.Add("switch_pm_text", config.SwitchPMText)
v.Add("switch_pm_parameter", config.SwitchPMParameter)
bot.debugLog("answerInlineQuery", v, nil)
return bot.MakeRequest("answerInlineQuery", v)
}
// AnswerCallbackQuery sends a response to an inline query callback.
func (bot *BotAPI) AnswerCallbackQuery(config CallbackConfig) (APIResponse, error) {
v := url.Values{}
v.Add("callback_query_id", config.CallbackQueryID)
v.Add("text", config.Text)
v.Add("show_alert", strconv.FormatBool(config.ShowAlert))
bot.debugLog("answerCallbackQuery", v, nil)
return bot.MakeRequest("answerCallbackQuery", v)
}
// 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
// will be unable to rejoin until they are unbanned.
func (bot *BotAPI) KickChatMember(config ChatMemberConfig) (APIResponse, error) {
v := url.Values{}
if config.SuperGroupUsername == "" {
v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
} else {
v.Add("chat_id", config.SuperGroupUsername)
}
v.Add("user_id", strconv.Itoa(config.UserID))
bot.debugLog("kickChatMember", v, nil)
return bot.MakeRequest("kickChatMember", v)
}
// LeaveChat makes the bot leave the chat.
func (bot *BotAPI) LeaveChat(config ChatConfig) (APIResponse, error) {
v := url.Values{}
if config.SuperGroupUsername == "" {
v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
} else {
v.Add("chat_id", config.SuperGroupUsername)
}
bot.debugLog("leaveChat", v, nil)
return bot.MakeRequest("leaveChat", v)
}
// GetChat gets information about a chat.
func (bot *BotAPI) GetChat(config ChatConfig) (Chat, 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("getChat", v)
if err != nil {
return Chat{}, err
}
var chat Chat
err = json.Unmarshal(resp.Result, &chat)
bot.debugLog("getChat", v, chat)
return chat, err
}
// GetChatAdministrators gets a list of administrators in the chat.
//
// If none have been appointed, only the creator will be returned.
// Bots are not shown, even if they are an administrator.
func (bot *BotAPI) GetChatAdministrators(config ChatConfig) ([]ChatMember, 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("getChatAdministrators", v)
if err != nil {
return []ChatMember{}, err
}
var members []ChatMember
err = json.Unmarshal(resp.Result, &members)
bot.debugLog("getChatAdministrators", v, members)
return members, err
}
// GetChatMembersCount gets the number of users in a chat.
func (bot *BotAPI) GetChatMembersCount(config ChatConfig) (int, 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("getChatMembersCount", v)
if err != nil {
return -1, err
}
var count int
err = json.Unmarshal(resp.Result, &count)
bot.debugLog("getChatMembersCount", v, count)
return count, err
}
// GetChatMember gets a specific chat member.
func (bot *BotAPI) GetChatMember(config ChatConfigWithUser) (ChatMember, error) {
v := url.Values{}
if config.SuperGroupUsername == "" {
v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
} else {
v.Add("chat_id", config.SuperGroupUsername)
}
v.Add("user_id", strconv.Itoa(config.UserID))
resp, err := bot.MakeRequest("getChatMember", v)
if err != nil {
return ChatMember{}, err
}
var member ChatMember
err = json.Unmarshal(resp.Result, &member)
bot.debugLog("getChatMember", v, member)
return member, err
}
// UnbanChatMember unbans a user from a chat. Note that this only will work
// in supergroups, and requires the bot to be an admin.
func (bot *BotAPI) UnbanChatMember(config ChatMemberConfig) (APIResponse, error) {
v := url.Values{}
if config.SuperGroupUsername == "" {
v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
} else {
v.Add("chat_id", config.SuperGroupUsername)
}
v.Add("user_id", strconv.Itoa(config.UserID))
bot.debugLog("unbanChatMember", v, nil)
return bot.MakeRequest("unbanChatMember", v)
}

View File

@ -0,0 +1,694 @@
package tgbotapi
import (
"encoding/json"
"io"
"net/url"
"strconv"
)
// Telegram constants
const (
// APIEndpoint is the endpoint for all API methods,
// with formatting for Sprintf.
APIEndpoint = "https://api.telegram.org/bot%s/%s"
// FileEndpoint is the endpoint for downloading a file from Telegram.
FileEndpoint = "https://api.telegram.org/file/bot%s/%s"
)
// Constant values for ChatActions
const (
ChatTyping = "typing"
ChatUploadPhoto = "upload_photo"
ChatRecordVideo = "record_video"
ChatUploadVideo = "upload_video"
ChatRecordAudio = "record_audio"
ChatUploadAudio = "upload_audio"
ChatUploadDocument = "upload_document"
ChatFindLocation = "find_location"
)
// API errors
const (
// ErrAPIForbidden happens when a token is bad
ErrAPIForbidden = "forbidden"
)
// Constant values for ParseMode in MessageConfig
const (
ModeMarkdown = "Markdown"
ModeHTML = "HTML"
)
// Library errors
const (
// ErrBadFileType happens when you pass an unknown type
ErrBadFileType = "bad file type"
ErrBadURL = "bad or empty url"
)
// Chattable is any config type that can be sent.
type Chattable interface {
values() (url.Values, error)
method() string
}
// Fileable is any config type that can be sent that includes a file.
type Fileable interface {
Chattable
params() (map[string]string, error)
name() string
getFile() interface{}
useExistingFile() bool
}
// BaseChat is base type for all chat config types.
type BaseChat struct {
ChatID int64 // required
ChannelUsername string
ReplyToMessageID int
ReplyMarkup interface{}
DisableNotification bool
}
// values returns url.Values representation of BaseChat
func (chat *BaseChat) values() (url.Values, error) {
v := url.Values{}
if chat.ChannelUsername != "" {
v.Add("chat_id", chat.ChannelUsername)
} else {
v.Add("chat_id", strconv.FormatInt(chat.ChatID, 10))
}
if chat.ReplyToMessageID != 0 {
v.Add("reply_to_message_id", strconv.Itoa(chat.ReplyToMessageID))
}
if chat.ReplyMarkup != nil {
data, err := json.Marshal(chat.ReplyMarkup)
if err != nil {
return v, err
}
v.Add("reply_markup", string(data))
}
v.Add("disable_notification", strconv.FormatBool(chat.DisableNotification))
return v, nil
}
// BaseFile is a base type for all file config types.
type BaseFile struct {
BaseChat
File interface{}
FileID string
UseExisting bool
MimeType string
FileSize int
}
// params returns a map[string]string representation of BaseFile.
func (file BaseFile) params() (map[string]string, error) {
params := make(map[string]string)
if file.ChannelUsername != "" {
params["chat_id"] = file.ChannelUsername
} else {
params["chat_id"] = strconv.FormatInt(file.ChatID, 10)
}
if file.ReplyToMessageID != 0 {
params["reply_to_message_id"] = strconv.Itoa(file.ReplyToMessageID)
}
if file.ReplyMarkup != nil {
data, err := json.Marshal(file.ReplyMarkup)
if err != nil {
return params, err
}
params["reply_markup"] = string(data)
}
if file.MimeType != "" {
params["mime_type"] = file.MimeType
}
if file.FileSize > 0 {
params["file_size"] = strconv.Itoa(file.FileSize)
}
params["disable_notification"] = strconv.FormatBool(file.DisableNotification)
return params, nil
}
// getFile returns the file.
func (file BaseFile) getFile() interface{} {
return file.File
}
// useExistingFile returns if the BaseFile has already been uploaded.
func (file BaseFile) useExistingFile() bool {
return file.UseExisting
}
// BaseEdit is base type of all chat edits.
type BaseEdit struct {
ChatID int64
ChannelUsername string
MessageID int
InlineMessageID string
ReplyMarkup *InlineKeyboardMarkup
}
func (edit BaseEdit) values() (url.Values, error) {
v := url.Values{}
if edit.InlineMessageID == "" {
if edit.ChannelUsername != "" {
v.Add("chat_id", edit.ChannelUsername)
} else {
v.Add("chat_id", strconv.FormatInt(edit.ChatID, 10))
}
v.Add("message_id", strconv.Itoa(edit.MessageID))
} else {
v.Add("inline_message_id", edit.InlineMessageID)
}
if edit.ReplyMarkup != nil {
data, err := json.Marshal(edit.ReplyMarkup)
if err != nil {
return v, err
}
v.Add("reply_markup", string(data))
}
return v, nil
}
// MessageConfig contains information about a SendMessage request.
type MessageConfig struct {
BaseChat
Text string
ParseMode string
DisableWebPagePreview bool
}
// values returns a url.Values representation of MessageConfig.
func (config MessageConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v.Add("text", config.Text)
v.Add("disable_web_page_preview", strconv.FormatBool(config.DisableWebPagePreview))
if config.ParseMode != "" {
v.Add("parse_mode", config.ParseMode)
}
return v, nil
}
// method returns Telegram API method name for sending Message.
func (config MessageConfig) method() string {
return "sendMessage"
}
// ForwardConfig contains information about a ForwardMessage request.
type ForwardConfig struct {
BaseChat
FromChatID int64 // required
FromChannelUsername string
MessageID int // required
}
// values returns a url.Values representation of ForwardConfig.
func (config ForwardConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v.Add("from_chat_id", strconv.FormatInt(config.FromChatID, 10))
v.Add("message_id", strconv.Itoa(config.MessageID))
return v, nil
}
// method returns Telegram API method name for sending Forward.
func (config ForwardConfig) method() string {
return "forwardMessage"
}
// PhotoConfig contains information about a SendPhoto request.
type PhotoConfig struct {
BaseFile
Caption string
}
// Params returns a map[string]string representation of PhotoConfig.
func (config PhotoConfig) params() (map[string]string, error) {
params, _ := config.BaseFile.params()
if config.Caption != "" {
params["caption"] = config.Caption
}
return params, nil
}
// Values returns a url.Values representation of PhotoConfig.
func (config PhotoConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v.Add(config.name(), config.FileID)
if config.Caption != "" {
v.Add("caption", config.Caption)
}
return v, nil
}
// name returns the field name for the Photo.
func (config PhotoConfig) name() string {
return "photo"
}
// method returns Telegram API method name for sending Photo.
func (config PhotoConfig) method() string {
return "sendPhoto"
}
// AudioConfig contains information about a SendAudio request.
type AudioConfig struct {
BaseFile
Duration int
Performer string
Title string
}
// values returns a url.Values representation of AudioConfig.
func (config AudioConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v.Add(config.name(), config.FileID)
if config.Duration != 0 {
v.Add("duration", strconv.Itoa(config.Duration))
}
if config.Performer != "" {
v.Add("performer", config.Performer)
}
if config.Title != "" {
v.Add("title", config.Title)
}
return v, nil
}
// params returns a map[string]string representation of AudioConfig.
func (config AudioConfig) params() (map[string]string, error) {
params, _ := config.BaseFile.params()
if config.Duration != 0 {
params["duration"] = strconv.Itoa(config.Duration)
}
if config.Performer != "" {
params["performer"] = config.Performer
}
if config.Title != "" {
params["title"] = config.Title
}
return params, nil
}
// name returns the field name for the Audio.
func (config AudioConfig) name() string {
return "audio"
}
// method returns Telegram API method name for sending Audio.
func (config AudioConfig) method() string {
return "sendAudio"
}
// DocumentConfig contains information about a SendDocument request.
type DocumentConfig struct {
BaseFile
}
// values returns a url.Values representation of DocumentConfig.
func (config DocumentConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v.Add(config.name(), config.FileID)
return v, nil
}
// params returns a map[string]string representation of DocumentConfig.
func (config DocumentConfig) params() (map[string]string, error) {
params, _ := config.BaseFile.params()
return params, nil
}
// name returns the field name for the Document.
func (config DocumentConfig) name() string {
return "document"
}
// method returns Telegram API method name for sending Document.
func (config DocumentConfig) method() string {
return "sendDocument"
}
// StickerConfig contains information about a SendSticker request.
type StickerConfig struct {
BaseFile
}
// values returns a url.Values representation of StickerConfig.
func (config StickerConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v.Add(config.name(), config.FileID)
return v, nil
}
// params returns a map[string]string representation of StickerConfig.
func (config StickerConfig) params() (map[string]string, error) {
params, _ := config.BaseFile.params()
return params, nil
}
// name returns the field name for the Sticker.
func (config StickerConfig) name() string {
return "sticker"
}
// method returns Telegram API method name for sending Sticker.
func (config StickerConfig) method() string {
return "sendSticker"
}
// VideoConfig contains information about a SendVideo request.
type VideoConfig struct {
BaseFile
Duration int
Caption string
}
// values returns a url.Values representation of VideoConfig.
func (config VideoConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v.Add(config.name(), config.FileID)
if config.Duration != 0 {
v.Add("duration", strconv.Itoa(config.Duration))
}
if config.Caption != "" {
v.Add("caption", config.Caption)
}
return v, nil
}
// params returns a map[string]string representation of VideoConfig.
func (config VideoConfig) params() (map[string]string, error) {
params, _ := config.BaseFile.params()
return params, nil
}
// name returns the field name for the Video.
func (config VideoConfig) name() string {
return "video"
}
// method returns Telegram API method name for sending Video.
func (config VideoConfig) method() string {
return "sendVideo"
}
// VoiceConfig contains information about a SendVoice request.
type VoiceConfig struct {
BaseFile
Duration int
}
// values returns a url.Values representation of VoiceConfig.
func (config VoiceConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v.Add(config.name(), config.FileID)
if config.Duration != 0 {
v.Add("duration", strconv.Itoa(config.Duration))
}
return v, nil
}
// params returns a map[string]string representation of VoiceConfig.
func (config VoiceConfig) params() (map[string]string, error) {
params, _ := config.BaseFile.params()
if config.Duration != 0 {
params["duration"] = strconv.Itoa(config.Duration)
}
return params, nil
}
// name returns the field name for the Voice.
func (config VoiceConfig) name() string {
return "voice"
}
// method returns Telegram API method name for sending Voice.
func (config VoiceConfig) method() string {
return "sendVoice"
}
// LocationConfig contains information about a SendLocation request.
type LocationConfig struct {
BaseChat
Latitude float64 // required
Longitude float64 // required
}
// values returns a url.Values representation of LocationConfig.
func (config LocationConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v.Add("latitude", strconv.FormatFloat(config.Latitude, 'f', 6, 64))
v.Add("longitude", strconv.FormatFloat(config.Longitude, 'f', 6, 64))
return v, nil
}
// method returns Telegram API method name for sending Location.
func (config LocationConfig) method() string {
return "sendLocation"
}
// VenueConfig contains information about a SendVenue request.
type VenueConfig struct {
BaseChat
Latitude float64 // required
Longitude float64 // required
Title string // required
Address string // required
FoursquareID string
}
func (config VenueConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v.Add("latitude", strconv.FormatFloat(config.Latitude, 'f', 6, 64))
v.Add("longitude", strconv.FormatFloat(config.Longitude, 'f', 6, 64))
v.Add("title", config.Title)
v.Add("address", config.Address)
if config.FoursquareID != "" {
v.Add("foursquare_id", config.FoursquareID)
}
return v, nil
}
func (config VenueConfig) method() string {
return "sendVenue"
}
// ContactConfig allows you to send a contact.
type ContactConfig struct {
BaseChat
PhoneNumber string
FirstName string
LastName string
}
func (config ContactConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v.Add("phone_number", config.PhoneNumber)
v.Add("first_name", config.FirstName)
v.Add("last_name", config.LastName)
return v, nil
}
func (config ContactConfig) method() string {
return "sendContact"
}
// ChatActionConfig contains information about a SendChatAction request.
type ChatActionConfig struct {
BaseChat
Action string // required
}
// values returns a url.Values representation of ChatActionConfig.
func (config ChatActionConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v.Add("action", config.Action)
return v, nil
}
// method returns Telegram API method name for sending ChatAction.
func (config ChatActionConfig) method() string {
return "sendChatAction"
}
// EditMessageTextConfig allows you to modify the text in a message.
type EditMessageTextConfig struct {
BaseEdit
Text string
ParseMode string
DisableWebPagePreview bool
}
func (config EditMessageTextConfig) values() (url.Values, error) {
v, _ := config.BaseEdit.values()
v.Add("text", config.Text)
v.Add("parse_mode", config.ParseMode)
v.Add("disable_web_page_preview", strconv.FormatBool(config.DisableWebPagePreview))
return v, nil
}
func (config EditMessageTextConfig) method() string {
return "editMessageText"
}
// EditMessageCaptionConfig allows you to modify the caption of a message.
type EditMessageCaptionConfig struct {
BaseEdit
Caption string
}
func (config EditMessageCaptionConfig) values() (url.Values, error) {
v, _ := config.BaseEdit.values()
v.Add("caption", config.Caption)
return v, nil
}
func (config EditMessageCaptionConfig) method() string {
return "editMessageCaption"
}
// EditMessageReplyMarkupConfig allows you to modify the reply markup
// of a message.
type EditMessageReplyMarkupConfig struct {
BaseEdit
}
func (config EditMessageReplyMarkupConfig) values() (url.Values, error) {
return config.BaseEdit.values()
}
func (config EditMessageReplyMarkupConfig) method() string {
return "editMessageReplyMarkup"
}
// UserProfilePhotosConfig contains information about a
// GetUserProfilePhotos request.
type UserProfilePhotosConfig struct {
UserID int
Offset int
Limit int
}
// FileConfig has information about a file hosted on Telegram.
type FileConfig struct {
FileID string
}
// UpdateConfig contains information about a GetUpdates request.
type UpdateConfig struct {
Offset int
Limit int
Timeout int
}
// WebhookConfig contains information about a SetWebhook request.
type WebhookConfig struct {
URL *url.URL
Certificate interface{}
}
// FileBytes contains information about a set of bytes to upload
// as a File.
type FileBytes struct {
Name string
Bytes []byte
}
// FileReader contains information about a reader to upload as a File.
// If Size is -1, it will read the entire Reader into memory to
// calculate a Size.
type FileReader struct {
Name string
Reader io.Reader
Size int64
}
// InlineConfig contains information on making an InlineQuery response.
type InlineConfig struct {
InlineQueryID string `json:"inline_query_id"`
Results []interface{} `json:"results"`
CacheTime int `json:"cache_time"`
IsPersonal bool `json:"is_personal"`
NextOffset string `json:"next_offset"`
SwitchPMText string `json:"switch_pm_text"`
SwitchPMParameter string `json:"switch_pm_parameter"`
}
// CallbackConfig contains information on making a CallbackQuery response.
type CallbackConfig struct {
CallbackQueryID string `json:"callback_query_id"`
Text string `json:"text"`
ShowAlert bool `json:"show_alert"`
}
// ChatMemberConfig contains information about a user in a chat for use
// with administrative functions such as kicking or unbanning a user.
type ChatMemberConfig struct {
ChatID int64
SuperGroupUsername string
UserID int
}
// ChatConfig contains information about getting information on a chat.
type ChatConfig struct {
ChatID int64
SuperGroupUsername string
}
// ChatConfigWithUser contains information about getting information on
// a specific user within a chat.
type ChatConfigWithUser struct {
ChatID int64
SuperGroupUsername string
UserID int
}

View File

@ -0,0 +1,598 @@
package tgbotapi
import (
"net/url"
)
// NewMessage creates a new Message.
//
// chatID is where to send it, text is the message text.
func NewMessage(chatID int64, text string) MessageConfig {
return MessageConfig{
BaseChat: BaseChat{
ChatID: chatID,
ReplyToMessageID: 0,
},
Text: text,
DisableWebPagePreview: false,
}
}
// NewMessageToChannel creates a new Message that is sent to a channel
// by username.
// username is the username of the channel, text is the message text.
func NewMessageToChannel(username string, text string) MessageConfig {
return MessageConfig{
BaseChat: BaseChat{
ChannelUsername: username,
},
Text: text,
}
}
// NewForward creates a new forward.
//
// chatID is where to send it, fromChatID is the source chat,
// and messageID is the ID of the original message.
func NewForward(chatID int64, fromChatID int64, messageID int) ForwardConfig {
return ForwardConfig{
BaseChat: BaseChat{ChatID: chatID},
FromChatID: fromChatID,
MessageID: messageID,
}
}
// NewPhotoUpload creates a new photo uploader.
//
// chatID is where to send it, file is a string path to the file,
// FileReader, or FileBytes.
//
// Note that you must send animated GIFs as a document.
func NewPhotoUpload(chatID int64, file interface{}) PhotoConfig {
return PhotoConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
File: file,
UseExisting: false,
},
}
}
// NewPhotoShare shares an existing photo.
// You may use this to reshare an existing photo without reuploading it.
//
// chatID is where to send it, fileID is the ID of the file
// already uploaded.
func NewPhotoShare(chatID int64, fileID string) PhotoConfig {
return PhotoConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
FileID: fileID,
UseExisting: true,
},
}
}
// NewAudioUpload creates a new audio uploader.
//
// chatID is where to send it, file is a string path to the file,
// FileReader, or FileBytes.
func NewAudioUpload(chatID int64, file interface{}) AudioConfig {
return AudioConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
File: file,
UseExisting: false,
},
}
}
// NewAudioShare shares an existing audio file.
// You may use this to reshare an existing audio file without
// reuploading it.
//
// chatID is where to send it, fileID is the ID of the audio
// already uploaded.
func NewAudioShare(chatID int64, fileID string) AudioConfig {
return AudioConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
FileID: fileID,
UseExisting: true,
},
}
}
// NewDocumentUpload creates a new document uploader.
//
// chatID is where to send it, file is a string path to the file,
// FileReader, or FileBytes.
func NewDocumentUpload(chatID int64, file interface{}) DocumentConfig {
return DocumentConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
File: file,
UseExisting: false,
},
}
}
// NewDocumentShare shares an existing document.
// You may use this to reshare an existing document without
// reuploading it.
//
// chatID is where to send it, fileID is the ID of the document
// already uploaded.
func NewDocumentShare(chatID int64, fileID string) DocumentConfig {
return DocumentConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
FileID: fileID,
UseExisting: true,
},
}
}
// NewStickerUpload creates a new sticker uploader.
//
// chatID is where to send it, file is a string path to the file,
// FileReader, or FileBytes.
func NewStickerUpload(chatID int64, file interface{}) StickerConfig {
return StickerConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
File: file,
UseExisting: false,
},
}
}
// NewStickerShare shares an existing sticker.
// You may use this to reshare an existing sticker without
// reuploading it.
//
// chatID is where to send it, fileID is the ID of the sticker
// already uploaded.
func NewStickerShare(chatID int64, fileID string) StickerConfig {
return StickerConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
FileID: fileID,
UseExisting: true,
},
}
}
// NewVideoUpload creates a new video uploader.
//
// chatID is where to send it, file is a string path to the file,
// FileReader, or FileBytes.
func NewVideoUpload(chatID int64, file interface{}) VideoConfig {
return VideoConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
File: file,
UseExisting: false,
},
}
}
// NewVideoShare 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 NewVideoShare(chatID int64, fileID string) VideoConfig {
return VideoConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
FileID: fileID,
UseExisting: true,
},
}
}
// NewVoiceUpload creates a new voice uploader.
//
// chatID is where to send it, file is a string path to the file,
// FileReader, or FileBytes.
func NewVoiceUpload(chatID int64, file interface{}) VoiceConfig {
return VoiceConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
File: file,
UseExisting: false,
},
}
}
// NewVoiceShare shares an existing voice.
// You may use this to reshare an existing voice without reuploading it.
//
// chatID is where to send it, fileID is the ID of the video
// already uploaded.
func NewVoiceShare(chatID int64, fileID string) VoiceConfig {
return VoiceConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
FileID: fileID,
UseExisting: true,
},
}
}
// NewContact allows you to send a shared contact.
func NewContact(chatID int64, phoneNumber, firstName string) ContactConfig {
return ContactConfig{
BaseChat: BaseChat{
ChatID: chatID,
},
PhoneNumber: phoneNumber,
FirstName: firstName,
}
}
// NewLocation shares your location.
//
// chatID is where to send it, latitude and longitude are coordinates.
func NewLocation(chatID int64, latitude float64, longitude float64) LocationConfig {
return LocationConfig{
BaseChat: BaseChat{
ChatID: chatID,
},
Latitude: latitude,
Longitude: longitude,
}
}
// NewVenue allows you to send a venue and its location.
func NewVenue(chatID int64, title, address string, latitude, longitude float64) VenueConfig {
return VenueConfig{
BaseChat: BaseChat{
ChatID: chatID,
},
Title: title,
Address: address,
Latitude: latitude,
Longitude: longitude,
}
}
// NewChatAction sets a chat action.
// Actions last for 5 seconds, or until your next action.
//
// chatID is where to send it, action should be set via Chat constants.
func NewChatAction(chatID int64, action string) ChatActionConfig {
return ChatActionConfig{
BaseChat: BaseChat{ChatID: chatID},
Action: action,
}
}
// NewUserProfilePhotos gets user profile photos.
//
// userID is the ID of the user you wish to get profile photos from.
func NewUserProfilePhotos(userID int) UserProfilePhotosConfig {
return UserProfilePhotosConfig{
UserID: userID,
Offset: 0,
Limit: 0,
}
}
// NewUpdate gets updates since the last Offset.
//
// offset is the last Update ID to include.
// You likely want to set this to the last Update ID plus 1.
func NewUpdate(offset int) UpdateConfig {
return UpdateConfig{
Offset: offset,
Limit: 0,
Timeout: 0,
}
}
// NewWebhook creates a new webhook.
//
// link is the url parsable link you wish to get the updates.
func NewWebhook(link string) WebhookConfig {
u, _ := url.Parse(link)
return WebhookConfig{
URL: u,
}
}
// NewWebhookWithCert creates a new webhook with a certificate.
//
// link is the url you wish to get webhooks,
// file contains a string to a file, FileReader, or FileBytes.
func NewWebhookWithCert(link string, file interface{}) WebhookConfig {
u, _ := url.Parse(link)
return WebhookConfig{
URL: u,
Certificate: file,
}
}
// NewInlineQueryResultArticle creates a new inline query article.
func NewInlineQueryResultArticle(id, title, messageText string) InlineQueryResultArticle {
return InlineQueryResultArticle{
Type: "article",
ID: id,
Title: title,
InputMessageContent: InputTextMessageContent{
Text: messageText,
},
}
}
// NewInlineQueryResultArticleMarkdown creates a new inline query article with Markdown parsing.
func NewInlineQueryResultArticleMarkdown(id, title, messageText string) InlineQueryResultArticle {
return InlineQueryResultArticle{
Type: "article",
ID: id,
Title: title,
InputMessageContent: InputTextMessageContent{
Text: messageText,
ParseMode: "Markdown",
},
}
}
// NewInlineQueryResultArticleHTML creates a new inline query article with HTML parsing.
func NewInlineQueryResultArticleHTML(id, title, messageText string) InlineQueryResultArticle {
return InlineQueryResultArticle{
Type: "article",
ID: id,
Title: title,
InputMessageContent: InputTextMessageContent{
Text: messageText,
ParseMode: "HTML",
},
}
}
// NewInlineQueryResultGIF creates a new inline query GIF.
func NewInlineQueryResultGIF(id, url string) InlineQueryResultGIF {
return InlineQueryResultGIF{
Type: "gif",
ID: id,
URL: url,
}
}
// NewInlineQueryResultMPEG4GIF creates a new inline query MPEG4 GIF.
func NewInlineQueryResultMPEG4GIF(id, url string) InlineQueryResultMPEG4GIF {
return InlineQueryResultMPEG4GIF{
Type: "mpeg4_gif",
ID: id,
URL: url,
}
}
// NewInlineQueryResultPhoto creates a new inline query photo.
func NewInlineQueryResultPhoto(id, url string) InlineQueryResultPhoto {
return InlineQueryResultPhoto{
Type: "photo",
ID: id,
URL: url,
}
}
// NewInlineQueryResultPhotoWithThumb creates a new inline query photo.
func NewInlineQueryResultPhotoWithThumb(id, url, thumb string) InlineQueryResultPhoto {
return InlineQueryResultPhoto{
Type: "photo",
ID: id,
URL: url,
ThumbURL: thumb,
}
}
// NewInlineQueryResultVideo creates a new inline query video.
func NewInlineQueryResultVideo(id, url string) InlineQueryResultVideo {
return InlineQueryResultVideo{
Type: "video",
ID: id,
URL: url,
}
}
// NewInlineQueryResultAudio creates a new inline query audio.
func NewInlineQueryResultAudio(id, url, title string) InlineQueryResultAudio {
return InlineQueryResultAudio{
Type: "audio",
ID: id,
URL: url,
Title: title,
}
}
// NewInlineQueryResultVoice creates a new inline query voice.
func NewInlineQueryResultVoice(id, url, title string) InlineQueryResultVoice {
return InlineQueryResultVoice{
Type: "voice",
ID: id,
URL: url,
Title: title,
}
}
// NewInlineQueryResultDocument creates a new inline query document.
func NewInlineQueryResultDocument(id, url, title, mimeType string) InlineQueryResultDocument {
return InlineQueryResultDocument{
Type: "document",
ID: id,
URL: url,
Title: title,
MimeType: mimeType,
}
}
// NewInlineQueryResultLocation creates a new inline query location.
func NewInlineQueryResultLocation(id, title string, latitude, longitude float64) InlineQueryResultLocation {
return InlineQueryResultLocation{
Type: "location",
ID: id,
Title: title,
Latitude: latitude,
Longitude: longitude,
}
}
// NewEditMessageText allows you to edit the text of a message.
func NewEditMessageText(chatID int64, messageID int, text string) EditMessageTextConfig {
return EditMessageTextConfig{
BaseEdit: BaseEdit{
ChatID: chatID,
MessageID: messageID,
},
Text: text,
}
}
// NewEditMessageCaption allows you to edit the caption of a message.
func NewEditMessageCaption(chatID int64, messageID int, caption string) EditMessageCaptionConfig {
return EditMessageCaptionConfig{
BaseEdit: BaseEdit{
ChatID: chatID,
MessageID: messageID,
},
Caption: caption,
}
}
// NewEditMessageReplyMarkup allows you to edit the inline
// keyboard markup.
func NewEditMessageReplyMarkup(chatID int64, messageID int, replyMarkup InlineKeyboardMarkup) EditMessageReplyMarkupConfig {
return EditMessageReplyMarkupConfig{
BaseEdit: BaseEdit{
ChatID: chatID,
MessageID: messageID,
ReplyMarkup: &replyMarkup,
},
}
}
// NewHideKeyboard hides the keyboard, with the option for being selective
// or hiding for everyone.
func NewHideKeyboard(selective bool) ReplyKeyboardHide {
return ReplyKeyboardHide{
HideKeyboard: true,
Selective: selective,
}
}
// NewKeyboardButton creates a regular keyboard button.
func NewKeyboardButton(text string) KeyboardButton {
return KeyboardButton{
Text: text,
}
}
// NewKeyboardButtonContact creates a keyboard button that requests
// user contact information upon click.
func NewKeyboardButtonContact(text string) KeyboardButton {
return KeyboardButton{
Text: text,
RequestContact: true,
}
}
// NewKeyboardButtonLocation creates a keyboard button that requests
// user location information upon click.
func NewKeyboardButtonLocation(text string) KeyboardButton {
return KeyboardButton{
Text: text,
RequestLocation: true,
}
}
// NewKeyboardButtonRow creates a row of keyboard buttons.
func NewKeyboardButtonRow(buttons ...KeyboardButton) []KeyboardButton {
var row []KeyboardButton
row = append(row, buttons...)
return row
}
// NewReplyKeyboard creates a new regular keyboard with sane defaults.
func NewReplyKeyboard(rows ...[]KeyboardButton) ReplyKeyboardMarkup {
var keyboard [][]KeyboardButton
keyboard = append(keyboard, rows...)
return ReplyKeyboardMarkup{
ResizeKeyboard: true,
Keyboard: keyboard,
}
}
// NewInlineKeyboardButtonData creates an inline keyboard button with text
// and data for a callback.
func NewInlineKeyboardButtonData(text, data string) InlineKeyboardButton {
return InlineKeyboardButton{
Text: text,
CallbackData: &data,
}
}
// NewInlineKeyboardButtonURL creates an inline keyboard button with text
// which goes to a URL.
func NewInlineKeyboardButtonURL(text, url string) InlineKeyboardButton {
return InlineKeyboardButton{
Text: text,
URL: &url,
}
}
// NewInlineKeyboardButtonSwitch creates an inline keyboard button with
// text which allows the user to switch to a chat or return to a chat.
func NewInlineKeyboardButtonSwitch(text, sw string) InlineKeyboardButton {
return InlineKeyboardButton{
Text: text,
SwitchInlineQuery: &sw,
}
}
// NewInlineKeyboardRow creates an inline keyboard row with buttons.
func NewInlineKeyboardRow(buttons ...InlineKeyboardButton) []InlineKeyboardButton {
var row []InlineKeyboardButton
row = append(row, buttons...)
return row
}
// NewInlineKeyboardMarkup creates a new inline keyboard.
func NewInlineKeyboardMarkup(rows ...[]InlineKeyboardButton) InlineKeyboardMarkup {
var keyboard [][]InlineKeyboardButton
keyboard = append(keyboard, rows...)
return InlineKeyboardMarkup{
InlineKeyboard: keyboard,
}
}
// NewCallback creates a new callback message.
func NewCallback(id, text string) CallbackConfig {
return CallbackConfig{
CallbackQueryID: id,
Text: text,
ShowAlert: false,
}
}
// NewCallbackWithAlert creates a new callback message that alerts
// the user.
func NewCallbackWithAlert(id, text string) CallbackConfig {
return CallbackConfig{
CallbackQueryID: id,
Text: text,
ShowAlert: true,
}
}

View File

@ -0,0 +1,550 @@
package tgbotapi
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"time"
)
// APIResponse is a response from the Telegram API with the result
// stored raw.
type APIResponse struct {
Ok bool `json:"ok"`
Result json.RawMessage `json:"result"`
ErrorCode int `json:"error_code"`
Description string `json:"description"`
}
// Update is an update response, from GetUpdates.
type Update struct {
UpdateID int `json:"update_id"`
Message *Message `json:"message"`
EditedMessage *Message `json:"edited_message"`
InlineQuery *InlineQuery `json:"inline_query"`
ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result"`
CallbackQuery *CallbackQuery `json:"callback_query"`
}
// User is a user on Telegram.
type User struct {
ID int `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"` // optional
UserName string `json:"username"` // optional
}
// String displays a simple text version of a user.
//
// It is normally a user's username, but falls back to a first/last
// name as available.
func (u *User) String() string {
if u.UserName != "" {
return u.UserName
}
name := u.FirstName
if u.LastName != "" {
name += " " + u.LastName
}
return name
}
// GroupChat is a group chat.
type GroupChat struct {
ID int `json:"id"`
Title string `json:"title"`
}
// Chat contains information about the place a message was sent.
type Chat struct {
ID int64 `json:"id"`
Type string `json:"type"`
Title string `json:"title"` // optional
UserName string `json:"username"` // optional
FirstName string `json:"first_name"` // optional
LastName string `json:"last_name"` // optional
}
// IsPrivate returns if the Chat is a private conversation.
func (c Chat) IsPrivate() bool {
return c.Type == "private"
}
// IsGroup returns if the Chat is a group.
func (c Chat) IsGroup() bool {
return c.Type == "group"
}
// IsSuperGroup returns if the Chat is a supergroup.
func (c Chat) IsSuperGroup() bool {
return c.Type == "supergroup"
}
// IsChannel returns if the Chat is a channel.
func (c Chat) IsChannel() bool {
return c.Type == "channel"
}
// ChatConfig returns a ChatConfig struct for chat related methods.
func (c Chat) ChatConfig() ChatConfig {
return ChatConfig{ChatID: c.ID}
}
// Message is returned by almost every request, and contains data about
// almost anything.
type Message struct {
MessageID int `json:"message_id"`
From *User `json:"from"` // optional
Date int `json:"date"`
Chat *Chat `json:"chat"`
ForwardFrom *User `json:"forward_from"` // optional
ForwardFromChat *Chat `json:"forward_from_chat"` // optional
ForwardDate int `json:"forward_date"` // optional
ReplyToMessage *Message `json:"reply_to_message"` // optional
EditDate int `json:"edit_date"` // optional
Text string `json:"text"` // optional
Entities *[]MessageEntity `json:"entities"` // optional
Audio *Audio `json:"audio"` // optional
Document *Document `json:"document"` // optional
Photo *[]PhotoSize `json:"photo"` // optional
Sticker *Sticker `json:"sticker"` // optional
Video *Video `json:"video"` // optional
Voice *Voice `json:"voice"` // optional
Caption string `json:"caption"` // optional
Contact *Contact `json:"contact"` // optional
Location *Location `json:"location"` // optional
Venue *Venue `json:"venue"` // optional
NewChatMember *User `json:"new_chat_member"` // optional
LeftChatMember *User `json:"left_chat_member"` // optional
NewChatTitle string `json:"new_chat_title"` // optional
NewChatPhoto *[]PhotoSize `json:"new_chat_photo"` // optional
DeleteChatPhoto bool `json:"delete_chat_photo"` // optional
GroupChatCreated bool `json:"group_chat_created"` // optional
SuperGroupChatCreated bool `json:"supergroup_chat_created"` // optional
ChannelChatCreated bool `json:"channel_chat_created"` // optional
MigrateToChatID int64 `json:"migrate_to_chat_id"` // optional
MigrateFromChatID int64 `json:"migrate_from_chat_id"` // optional
PinnedMessage *Message `json:"pinned_message"` // optional
}
// Time converts the message timestamp into a Time.
func (m *Message) Time() time.Time {
return time.Unix(int64(m.Date), 0)
}
// IsCommand returns true if message starts with '/'.
func (m *Message) IsCommand() bool {
return m.Text != "" && m.Text[0] == '/'
}
// Command checks if the message was a command and if it was, returns the
// command. If the Message was not a command, it returns an empty string.
//
// If the command contains the at bot syntax, it removes the bot name.
func (m *Message) Command() string {
if !m.IsCommand() {
return ""
}
command := strings.SplitN(m.Text, " ", 2)[0][1:]
if i := strings.Index(command, "@"); i != -1 {
command = command[:i]
}
return command
}
// CommandArguments checks if the message was a command and if it was,
// returns all text after the command name. If the Message was not a
// command, it returns an empty string.
func (m *Message) CommandArguments() string {
if !m.IsCommand() {
return ""
}
split := strings.SplitN(m.Text, " ", 2)
if len(split) != 2 {
return ""
}
return strings.SplitN(m.Text, " ", 2)[1]
}
// MessageEntity contains information about data in a Message.
type MessageEntity struct {
Type string `json:"type"`
Offset int `json:"offset"`
Length int `json:"length"`
URL string `json:"url"` // optional
User *User `json:"user"` // optional
}
// ParseURL attempts to parse a URL contained within a MessageEntity.
func (entity MessageEntity) ParseURL() (*url.URL, error) {
if entity.URL == "" {
return nil, errors.New(ErrBadURL)
}
return url.Parse(entity.URL)
}
// PhotoSize contains information about photos.
type PhotoSize struct {
FileID string `json:"file_id"`
Width int `json:"width"`
Height int `json:"height"`
FileSize int `json:"file_size"` // optional
}
// Audio contains information about audio.
type Audio struct {
FileID string `json:"file_id"`
Duration int `json:"duration"`
Performer string `json:"performer"` // optional
Title string `json:"title"` // optional
MimeType string `json:"mime_type"` // optional
FileSize int `json:"file_size"` // optional
}
// Document contains information about a document.
type Document struct {
FileID string `json:"file_id"`
Thumbnail *PhotoSize `json:"thumb"` // optional
FileName string `json:"file_name"` // optional
MimeType string `json:"mime_type"` // optional
FileSize int `json:"file_size"` // optional
}
// Sticker contains information about a sticker.
type Sticker struct {
FileID string `json:"file_id"`
Width int `json:"width"`
Height int `json:"height"`
Thumbnail *PhotoSize `json:"thumb"` // optional
Emoji string `json:"emoji"` // optional
FileSize int `json:"file_size"` // optional
}
// Video contains information about a video.
type Video struct {
FileID string `json:"file_id"`
Width int `json:"width"`
Height int `json:"height"`
Duration int `json:"duration"`
Thumbnail *PhotoSize `json:"thumb"` // optional
MimeType string `json:"mime_type"` // optional
FileSize int `json:"file_size"` // optional
}
// Voice contains information about a voice.
type Voice struct {
FileID string `json:"file_id"`
Duration int `json:"duration"`
MimeType string `json:"mime_type"` // optional
FileSize int `json:"file_size"` // optional
}
// Contact contains information about a contact.
//
// Note that LastName and UserID may be empty.
type Contact struct {
PhoneNumber string `json:"phone_number"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"` // optional
UserID int `json:"user_id"` // optional
}
// Location contains information about a place.
type Location struct {
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
}
// Venue contains information about a venue, including its Location.
type Venue struct {
Location Location `json:"location"`
Title string `json:"title"`
Address string `json:"address"`
FoursquareID string `json:"foursquare_id"` // optional
}
// UserProfilePhotos contains a set of user profile photos.
type UserProfilePhotos struct {
TotalCount int `json:"total_count"`
Photos [][]PhotoSize `json:"photos"`
}
// File contains information about a file to download from Telegram.
type File struct {
FileID string `json:"file_id"`
FileSize int `json:"file_size"` // optional
FilePath string `json:"file_path"` // optional
}
// Link returns a full path to the download URL for a File.
//
// It requires the Bot Token to create the link.
func (f *File) Link(token string) string {
return fmt.Sprintf(FileEndpoint, token, f.FilePath)
}
// ReplyKeyboardMarkup allows the Bot to set a custom keyboard.
type ReplyKeyboardMarkup struct {
Keyboard [][]KeyboardButton `json:"keyboard"`
ResizeKeyboard bool `json:"resize_keyboard"` // optional
OneTimeKeyboard bool `json:"one_time_keyboard"` // optional
Selective bool `json:"selective"` // optional
}
// KeyboardButton is a button within a custom keyboard.
type KeyboardButton struct {
Text string `json:"text"`
RequestContact bool `json:"request_contact"`
RequestLocation bool `json:"request_location"`
}
// ReplyKeyboardHide allows the Bot to hide a custom keyboard.
type ReplyKeyboardHide struct {
HideKeyboard bool `json:"hide_keyboard"`
Selective bool `json:"selective"` // optional
}
// InlineKeyboardMarkup is a custom keyboard presented for an inline bot.
type InlineKeyboardMarkup struct {
InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard"`
}
// InlineKeyboardButton is a button within a custom keyboard for
// inline query responses.
//
// Note that some values are references as even an empty string
// will change behavior.
type InlineKeyboardButton struct {
Text string `json:"text"`
URL *string `json:"url,omitempty"` // optional
CallbackData *string `json:"callback_data,omitempty"` // optional
SwitchInlineQuery *string `json:"switch_inline_query,omitempty"` // optional
}
// CallbackQuery is data sent when a keyboard button with callback data
// is clicked.
type CallbackQuery struct {
ID string `json:"id"`
From *User `json:"from"`
Message *Message `json:"message"` // optional
InlineMessageID string `json:"inline_message_id"` // optional
Data string `json:"data"` // optional
}
// ForceReply allows the Bot to have users directly reply to it without
// additional interaction.
type ForceReply struct {
ForceReply bool `json:"force_reply"`
Selective bool `json:"selective"` // optional
}
// ChatMember is information about a member in a chat.
type ChatMember struct {
User *User `json:"user"`
Status string `json:"status"`
}
// IsCreator returns if the ChatMember was the creator of the chat.
func (chat ChatMember) IsCreator() bool { return chat.Status == "creator" }
// IsAdministrator returns if the ChatMember is a chat administrator.
func (chat ChatMember) IsAdministrator() bool { return chat.Status == "administrator" }
// IsMember returns if the ChatMember is a current member of the chat.
func (chat ChatMember) IsMember() bool { return chat.Status == "member" }
// HasLeft returns if the ChatMember left the chat.
func (chat ChatMember) HasLeft() bool { return chat.Status == "left" }
// WasKicked returns if the ChatMember was kicked from the chat.
func (chat ChatMember) WasKicked() bool { return chat.Status == "kicked" }
// InlineQuery is a Query from Telegram for an inline request.
type InlineQuery struct {
ID string `json:"id"`
From *User `json:"from"`
Location *Location `json:"location"` // optional
Query string `json:"query"`
Offset string `json:"offset"`
}
// InlineQueryResultArticle is an inline query response article.
type InlineQueryResultArticle struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
Title string `json:"title"` // required
InputMessageContent interface{} `json:"input_message_content,omitempty"` // required
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
URL string `json:"url"`
HideURL bool `json:"hide_url"`
Description string `json:"description"`
ThumbURL string `json:"thumb_url"`
ThumbWidth int `json:"thumb_width"`
ThumbHeight int `json:"thumb_height"`
}
// InlineQueryResultPhoto is an inline query response photo.
type InlineQueryResultPhoto struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
URL string `json:"photo_url"` // required
MimeType string `json:"mime_type"`
Width int `json:"photo_width"`
Height int `json:"photo_height"`
ThumbURL string `json:"thumb_url"`
Title string `json:"title"`
Description string `json:"description"`
Caption string `json:"caption"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
}
// InlineQueryResultGIF is an inline query response GIF.
type InlineQueryResultGIF struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
URL string `json:"gif_url"` // required
Width int `json:"gif_width"`
Height int `json:"gif_height"`
ThumbURL string `json:"thumb_url"`
Title string `json:"title"`
Caption string `json:"caption"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
}
// InlineQueryResultMPEG4GIF is an inline query response MPEG4 GIF.
type InlineQueryResultMPEG4GIF struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
URL string `json:"mpeg4_url"` // required
Width int `json:"mpeg4_width"`
Height int `json:"mpeg4_height"`
ThumbURL string `json:"thumb_url"`
Title string `json:"title"`
Caption string `json:"caption"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
}
// InlineQueryResultVideo is an inline query response video.
type InlineQueryResultVideo struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
URL string `json:"video_url"` // required
MimeType string `json:"mime_type"` // required
ThumbURL string `json:"thumb_url"`
Title string `json:"title"`
Caption string `json:"caption"`
Width int `json:"video_width"`
Height int `json:"video_height"`
Duration int `json:"video_duration"`
Description string `json:"description"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
}
// InlineQueryResultAudio is an inline query response audio.
type InlineQueryResultAudio struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
URL string `json:"audio_url"` // required
Title string `json:"title"` // required
Performer string `json:"performer"`
Duration int `json:"audio_duration"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
}
// InlineQueryResultVoice is an inline query response voice.
type InlineQueryResultVoice struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
URL string `json:"voice_url"` // required
Title string `json:"title"` // required
Duration int `json:"voice_duration"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
}
// InlineQueryResultDocument is an inline query response document.
type InlineQueryResultDocument struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
Title string `json:"title"` // required
Caption string `json:"caption"`
URL string `json:"document_url"` // required
MimeType string `json:"mime_type"` // required
Description string `json:"description"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
ThumbURL string `json:"thumb_url"`
ThumbWidth int `json:"thumb_width"`
ThumbHeight int `json:"thumb_height"`
}
// InlineQueryResultLocation is an inline query response location.
type InlineQueryResultLocation struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
Latitude float64 `json:"latitude"` // required
Longitude float64 `json:"longitude"` // required
Title string `json:"title"` // required
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
ThumbURL string `json:"thumb_url"`
ThumbWidth int `json:"thumb_width"`
ThumbHeight int `json:"thumb_height"`
}
// ChosenInlineResult is an inline query result chosen by a User
type ChosenInlineResult struct {
ResultID string `json:"result_id"`
From *User `json:"from"`
Location *Location `json:"location"`
InlineMessageID string `json:"inline_message_id"`
Query string `json:"query"`
}
// InputTextMessageContent contains text for displaying
// as an inline query result.
type InputTextMessageContent struct {
Text string `json:"message_text"`
ParseMode string `json:"parse_mode"`
DisableWebPagePreview bool `json:"disable_web_page_preview"`
}
// InputLocationMessageContent contains a location for displaying
// as an inline query result.
type InputLocationMessageContent struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}
// InputVenueMessageContent contains a venue for displaying
// as an inline query result.
type InputVenueMessageContent struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Title string `json:"title"`
Address string `json:"address"`
FoursquareID string `json:"foursquare_id"`
}
// InputContactMessageContent contains a contact for displaying
// as an inline query result.
type InputContactMessageContent struct {
PhoneNumber string `json:"phone_number"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}

View File

@ -13,7 +13,6 @@ type ClusterInterface interface {
GetClusterInfos() []*model.ClusterInfo
RemoveAllSessionsForUserId(userId string)
InvalidateCacheForUser(userId string)
InvalidateCacheForChannel(channelId string)
Publish(event *model.WebSocketEvent)
UpdateStatus(status *model.Status)
GetLogs() ([]string, *model.AppError)

View File

@ -8,7 +8,7 @@ import (
)
type MfaInterface interface {
GenerateQrCode(user *model.User) ([]byte, *model.AppError)
GenerateSecret(user *model.User) (string, []byte, *model.AppError)
Activate(user *model.User, token string) *model.AppError
Deactivate(userId string) *model.AppError
ValidateToken(secret, token string) (bool, *model.AppError)

View File

@ -0,0 +1,372 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
type Permission struct {
Id string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}
type Role struct {
Id string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Permissions []string `json:"permissions"`
}
var PERMISSION_INVITE_USER *Permission
var PERMISSION_ADD_USER_TO_TEAM *Permission
var PERMISSION_USE_SLASH_COMMANDS *Permission
var PERMISSION_MANAGE_SLASH_COMMANDS *Permission
var PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS *Permission
var PERMISSION_CREATE_PUBLIC_CHANNEL *Permission
var PERMISSION_CREATE_PRIVATE_CHANNEL *Permission
var PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS *Permission
var PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS *Permission
var PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE *Permission
var PERMISSION_MANAGE_ROLES *Permission
var PERMISSION_CREATE_DIRECT_CHANNEL *Permission
var PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES *Permission
var PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES *Permission
var PERMISSION_LIST_TEAM_CHANNELS *Permission
var PERMISSION_JOIN_PUBLIC_CHANNELS *Permission
var PERMISSION_DELETE_PUBLIC_CHANNEL *Permission
var PERMISSION_DELETE_PRIVATE_CHANNEL *Permission
var PERMISSION_EDIT_OTHER_USERS *Permission
var PERMISSION_READ_CHANNEL *Permission
var PERMISSION_PERMANENT_DELETE_USER *Permission
var PERMISSION_UPLOAD_FILE *Permission
var PERMISSION_GET_PUBLIC_LINK *Permission
var PERMISSION_MANAGE_WEBHOOKS *Permission
var PERMISSION_MANAGE_OTHERS_WEBHOOKS *Permission
var PERMISSION_MANAGE_OAUTH *Permission
var PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH *Permission
var PERMISSION_CREATE_POST *Permission
var PERMISSION_EDIT_POST *Permission
var PERMISSION_EDIT_OTHERS_POSTS *Permission
var PERMISSION_REMOVE_USER_FROM_TEAM *Permission
var PERMISSION_MANAGE_TEAM *Permission
var PERMISSION_IMPORT_TEAM *Permission
// General permission that encompases all system admin functions
// in the future this could be broken up to allow access to some
// admin functions but not others
var PERMISSION_MANAGE_SYSTEM *Permission
var ROLE_SYSTEM_USER *Role
var ROLE_SYSTEM_ADMIN *Role
var ROLE_TEAM_USER *Role
var ROLE_TEAM_ADMIN *Role
var ROLE_CHANNEL_USER *Role
var ROLE_CHANNEL_ADMIN *Role
var ROLE_CHANNEL_GUEST *Role
var BuiltInRoles map[string]*Role
func InitalizePermissions() {
PERMISSION_INVITE_USER = &Permission{
"invite_user",
"authentication.permissions.team_invite_user.name",
"authentication.permissions.team_invite_user.description",
}
PERMISSION_ADD_USER_TO_TEAM = &Permission{
"add_user_to_team",
"authentication.permissions.add_user_to_team.name",
"authentication.permissions.add_user_to_team.description",
}
PERMISSION_USE_SLASH_COMMANDS = &Permission{
"use_slash_commands",
"authentication.permissions.team_use_slash_commands.name",
"authentication.permissions.team_use_slash_commands.description",
}
PERMISSION_MANAGE_SLASH_COMMANDS = &Permission{
"manage_slash_commands",
"authentication.permissions.manage_slash_commands.name",
"authentication.permissions.manage_slash_commands.description",
}
PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS = &Permission{
"manage_others_slash_commands",
"authentication.permissions.manage_others_slash_commands.name",
"authentication.permissions.manage_others_slash_commands.description",
}
PERMISSION_CREATE_PUBLIC_CHANNEL = &Permission{
"create_public_channel",
"authentication.permissions.create_public_channel.name",
"authentication.permissions.create_public_channel.description",
}
PERMISSION_CREATE_PRIVATE_CHANNEL = &Permission{
"create_private_channel",
"authentication.permissions.create_private_channel.name",
"authentication.permissions.create_private_channel.description",
}
PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS = &Permission{
"manage_public_channel_members",
"authentication.permissions.manage_public_channel_members.name",
"authentication.permissions.manage_public_channel_members.description",
}
PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS = &Permission{
"manage_private_channel_members",
"authentication.permissions.manage_private_channel_members.name",
"authentication.permissions.manage_private_channel_members.description",
}
PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE = &Permission{
"assign_system_admin_role",
"authentication.permissions.assign_system_admin_role.name",
"authentication.permissions.assign_system_admin_role.description",
}
PERMISSION_MANAGE_ROLES = &Permission{
"manage_roles",
"authentication.permissions.manage_roles.name",
"authentication.permissions.manage_roles.description",
}
PERMISSION_MANAGE_SYSTEM = &Permission{
"manage_system",
"authentication.permissions.manage_system.name",
"authentication.permissions.manage_system.description",
}
PERMISSION_CREATE_DIRECT_CHANNEL = &Permission{
"create_direct_channel",
"authentication.permissions.create_direct_channel.name",
"authentication.permissions.create_direct_channel.description",
}
PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES = &Permission{
"manage__publicchannel_properties",
"authentication.permissions.manage_public_channel_properties.name",
"authentication.permissions.manage_public_channel_properties.description",
}
PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES = &Permission{
"manage_private_channel_properties",
"authentication.permissions.manage_private_channel_properties.name",
"authentication.permissions.manage_private_channel_properties.description",
}
PERMISSION_LIST_TEAM_CHANNELS = &Permission{
"list_team_channels",
"authentication.permissions.list_team_channels.name",
"authentication.permissions.list_team_channels.description",
}
PERMISSION_JOIN_PUBLIC_CHANNELS = &Permission{
"join_public_channels",
"authentication.permissions.join_public_channels.name",
"authentication.permissions.join_public_channels.description",
}
PERMISSION_DELETE_PUBLIC_CHANNEL = &Permission{
"delete_public_channel",
"authentication.permissions.delete_public_channel.name",
"authentication.permissions.delete_public_channel.description",
}
PERMISSION_DELETE_PRIVATE_CHANNEL = &Permission{
"delete_private_channel",
"authentication.permissions.delete_private_channel.name",
"authentication.permissions.delete_private_channel.description",
}
PERMISSION_EDIT_OTHER_USERS = &Permission{
"edit_other_users",
"authentication.permissions.edit_other_users.name",
"authentication.permissions.edit_other_users.description",
}
PERMISSION_READ_CHANNEL = &Permission{
"read_channel",
"authentication.permissions.read_channel.name",
"authentication.permissions.read_channel.description",
}
PERMISSION_PERMANENT_DELETE_USER = &Permission{
"permanent_delete_user",
"authentication.permissions.permanent_delete_user.name",
"authentication.permissions.permanent_delete_user.description",
}
PERMISSION_UPLOAD_FILE = &Permission{
"upload_file",
"authentication.permissions.upload_file.name",
"authentication.permissions.upload_file.description",
}
PERMISSION_GET_PUBLIC_LINK = &Permission{
"get_public_link",
"authentication.permissions.get_public_link.name",
"authentication.permissions.get_public_link.description",
}
PERMISSION_MANAGE_WEBHOOKS = &Permission{
"manage_webhooks",
"authentication.permissions.manage_webhooks.name",
"authentication.permissions.manage_webhooks.description",
}
PERMISSION_MANAGE_OTHERS_WEBHOOKS = &Permission{
"manage_others_webhooks",
"authentication.permissions.manage_others_webhooks.name",
"authentication.permissions.manage_others_webhooks.description",
}
PERMISSION_MANAGE_OAUTH = &Permission{
"manage_oauth",
"authentication.permissions.manage_oauth.name",
"authentication.permissions.manage_oauth.description",
}
PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH = &Permission{
"manage_sytem_wide_oauth",
"authentication.permissions.manage_sytem_wide_oauth.name",
"authentication.permissions.manage_sytem_wide_oauth.description",
}
PERMISSION_CREATE_POST = &Permission{
"create_post",
"authentication.permissions.create_post.name",
"authentication.permissions.create_post.description",
}
PERMISSION_EDIT_POST = &Permission{
"edit_post",
"authentication.permissions.edit_post.name",
"authentication.permissions.edit_post.description",
}
PERMISSION_EDIT_OTHERS_POSTS = &Permission{
"edit_others_posts",
"authentication.permissions.edit_others_posts.name",
"authentication.permissions.edit_others_posts.description",
}
PERMISSION_REMOVE_USER_FROM_TEAM = &Permission{
"remove_user_from_team",
"authentication.permissions.remove_user_from_team.name",
"authentication.permissions.remove_user_from_team.description",
}
PERMISSION_MANAGE_TEAM = &Permission{
"manage_team",
"authentication.permissions.manage_team.name",
"authentication.permissions.manage_team.description",
}
PERMISSION_IMPORT_TEAM = &Permission{
"import_team",
"authentication.permissions.import_team.name",
"authentication.permissions.import_team.description",
}
}
func InitalizeRoles() {
InitalizePermissions()
BuiltInRoles = make(map[string]*Role)
ROLE_CHANNEL_USER = &Role{
"channel_user",
"authentication.roles.channel_user.name",
"authentication.roles.channel_user.description",
[]string{
PERMISSION_READ_CHANNEL.Id,
PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id,
PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id,
PERMISSION_UPLOAD_FILE.Id,
PERMISSION_GET_PUBLIC_LINK.Id,
PERMISSION_CREATE_POST.Id,
PERMISSION_EDIT_POST.Id,
PERMISSION_USE_SLASH_COMMANDS.Id,
},
}
BuiltInRoles[ROLE_CHANNEL_USER.Id] = ROLE_CHANNEL_USER
ROLE_CHANNEL_ADMIN = &Role{
"channel_admin",
"authentication.roles.channel_admin.name",
"authentication.roles.channel_admin.description",
[]string{},
}
BuiltInRoles[ROLE_CHANNEL_ADMIN.Id] = ROLE_CHANNEL_ADMIN
ROLE_CHANNEL_GUEST = &Role{
"guest",
"authentication.roles.global_guest.name",
"authentication.roles.global_guest.description",
[]string{},
}
BuiltInRoles[ROLE_CHANNEL_GUEST.Id] = ROLE_CHANNEL_GUEST
ROLE_TEAM_USER = &Role{
"team_user",
"authentication.roles.team_user.name",
"authentication.roles.team_user.description",
[]string{
PERMISSION_LIST_TEAM_CHANNELS.Id,
PERMISSION_JOIN_PUBLIC_CHANNELS.Id,
},
}
BuiltInRoles[ROLE_TEAM_USER.Id] = ROLE_TEAM_USER
ROLE_TEAM_ADMIN = &Role{
"team_admin",
"authentication.roles.team_admin.name",
"authentication.roles.team_admin.description",
[]string{
PERMISSION_EDIT_OTHERS_POSTS.Id,
PERMISSION_ADD_USER_TO_TEAM.Id,
PERMISSION_REMOVE_USER_FROM_TEAM.Id,
PERMISSION_MANAGE_TEAM.Id,
PERMISSION_IMPORT_TEAM.Id,
PERMISSION_MANAGE_ROLES.Id,
PERMISSION_MANAGE_OTHERS_WEBHOOKS.Id,
PERMISSION_MANAGE_SLASH_COMMANDS.Id,
PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS.Id,
PERMISSION_MANAGE_WEBHOOKS.Id,
},
}
BuiltInRoles[ROLE_TEAM_ADMIN.Id] = ROLE_TEAM_ADMIN
ROLE_SYSTEM_USER = &Role{
"system_user",
"authentication.roles.global_user.name",
"authentication.roles.global_user.description",
[]string{
PERMISSION_CREATE_DIRECT_CHANNEL.Id,
PERMISSION_PERMANENT_DELETE_USER.Id,
PERMISSION_MANAGE_OAUTH.Id,
},
}
BuiltInRoles[ROLE_SYSTEM_USER.Id] = ROLE_SYSTEM_USER
ROLE_SYSTEM_ADMIN = &Role{
"system_admin",
"authentication.roles.global_admin.name",
"authentication.roles.global_admin.description",
// System admins can do anything channel and team admins can do
// plus everything members of teams and channels can do to all teams
// and channels on the system
append(
append(
append(
append(
[]string{
PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE.Id,
PERMISSION_MANAGE_SYSTEM.Id,
PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES.Id,
PERMISSION_DELETE_PUBLIC_CHANNEL.Id,
PERMISSION_CREATE_PUBLIC_CHANNEL.Id,
PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES.Id,
PERMISSION_DELETE_PRIVATE_CHANNEL.Id,
PERMISSION_CREATE_PRIVATE_CHANNEL.Id,
PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH.Id,
PERMISSION_MANAGE_OTHERS_WEBHOOKS.Id,
PERMISSION_EDIT_OTHER_USERS.Id,
PERMISSION_MANAGE_OAUTH.Id,
PERMISSION_INVITE_USER.Id,
},
ROLE_TEAM_USER.Permissions...,
),
ROLE_CHANNEL_USER.Permissions...,
),
ROLE_TEAM_ADMIN.Permissions...,
),
ROLE_CHANNEL_ADMIN.Permissions...,
),
}
BuiltInRoles[ROLE_SYSTEM_ADMIN.Id] = ROLE_SYSTEM_ADMIN
}
func RoleIdsToString(roles []string) string {
output := ""
for _, role := range roles {
output += role + ", "
}
if output == "" {
return "[<NO ROLES>]"
}
return output[:len(output)-1]
}
func init() {
InitalizeRoles()
}

View File

@ -0,0 +1,58 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type UserAutocompleteInChannel struct {
InChannel []*User `json:"in_channel"`
OutOfChannel []*User `json:"out_of_channel"`
}
type UserAutocompleteInTeam struct {
InTeam []*User `json:"in_team"`
}
func (o *UserAutocompleteInChannel) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func UserAutocompleteInChannelFromJson(data io.Reader) *UserAutocompleteInChannel {
decoder := json.NewDecoder(data)
var o UserAutocompleteInChannel
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}
func (o *UserAutocompleteInTeam) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func UserAutocompleteInTeamFromJson(data io.Reader) *UserAutocompleteInTeam {
decoder := json.NewDecoder(data)
var o UserAutocompleteInTeam
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

View File

@ -10,10 +10,14 @@ import (
)
const (
CHANNEL_OPEN = "O"
CHANNEL_PRIVATE = "P"
CHANNEL_DIRECT = "D"
DEFAULT_CHANNEL = "town-square"
CHANNEL_OPEN = "O"
CHANNEL_PRIVATE = "P"
CHANNEL_DIRECT = "D"
DEFAULT_CHANNEL = "town-square"
CHANNEL_DISPLAY_NAME_MAX_RUNES = 64
CHANNEL_NAME_MAX_LENGTH = 64
CHANNEL_HEADER_MAX_RUNES = 1024
CHANNEL_PURPOSE_MAX_RUNES = 250
)
type Channel struct {
@ -57,8 +61,8 @@ func (o *Channel) Etag() string {
return Etag(o.Id, o.UpdateAt)
}
func (o *Channel) ExtraEtag(memberLimit int) string {
return Etag(o.Id, o.ExtraUpdateAt, memberLimit)
func (o *Channel) StatsEtag() string {
return Etag(o.Id, o.ExtraUpdateAt)
}
func (o *Channel) IsValid() *AppError {
@ -75,11 +79,11 @@ func (o *Channel) IsValid() *AppError {
return NewLocAppError("Channel.IsValid", "model.channel.is_valid.update_at.app_error", nil, "id="+o.Id)
}
if utf8.RuneCountInString(o.DisplayName) > 64 {
if utf8.RuneCountInString(o.DisplayName) > CHANNEL_DISPLAY_NAME_MAX_RUNES {
return NewLocAppError("Channel.IsValid", "model.channel.is_valid.display_name.app_error", nil, "id="+o.Id)
}
if len(o.Name) > 64 {
if len(o.Name) > CHANNEL_NAME_MAX_LENGTH {
return NewLocAppError("Channel.IsValid", "model.channel.is_valid.name.app_error", nil, "id="+o.Id)
}
@ -91,11 +95,11 @@ func (o *Channel) IsValid() *AppError {
return NewLocAppError("Channel.IsValid", "model.channel.is_valid.type.app_error", nil, "id="+o.Id)
}
if utf8.RuneCountInString(o.Header) > 1024 {
if utf8.RuneCountInString(o.Header) > CHANNEL_HEADER_MAX_RUNES {
return NewLocAppError("Channel.IsValid", "model.channel.is_valid.header.app_error", nil, "id="+o.Id)
}
if utf8.RuneCountInString(o.Purpose) > 128 {
if utf8.RuneCountInString(o.Purpose) > CHANNEL_PURPOSE_MAX_RUNES {
return NewLocAppError("Channel.IsValid", "model.channel.is_valid.purpose.app_error", nil, "id="+o.Id)
}

View File

@ -1,49 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type ExtraMember struct {
Id string `json:"id"`
Nickname string `json:"nickname"`
Email string `json:"email"`
Roles string `json:"roles"`
Username string `json:"username"`
}
func (o *ExtraMember) Sanitize(options map[string]bool) {
if len(options) == 0 || !options["email"] {
o.Email = ""
}
}
type ChannelExtra struct {
Id string `json:"id"`
Members []ExtraMember `json:"members"`
MemberCount int64 `json:"member_count"`
}
func (o *ChannelExtra) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func ChannelExtraFromJson(data io.Reader) *ChannelExtra {
decoder := json.NewDecoder(data)
var o ChannelExtra
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

View File

@ -8,15 +8,11 @@ import (
"io"
)
type ChannelList struct {
Channels []*Channel `json:"channels"`
Members map[string]*ChannelMember `json:"members"`
}
type ChannelList []*Channel
func (o *ChannelList) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
if b, err := json.Marshal(o); err != nil {
return "[]"
} else {
return string(b)
}
@ -28,7 +24,7 @@ func (o *ChannelList) Etag() string {
var t int64 = 0
var delta int64 = 0
for _, v := range o.Channels {
for _, v := range *o {
if v.LastPostAt > t {
t = v.LastPostAt
id = v.Id
@ -39,30 +35,9 @@ func (o *ChannelList) Etag() string {
id = v.Id
}
member := o.Members[v.Id]
if member != nil {
max := v.LastPostAt
if v.UpdateAt > max {
max = v.UpdateAt
}
delta += max - member.LastViewedAt
if member.LastViewedAt > t {
t = member.LastViewedAt
id = v.Id
}
if member.LastUpdateAt > t {
t = member.LastUpdateAt
id = v.Id
}
}
}
return Etag(id, t, delta, len(o.Channels))
return Etag(id, t, delta, len(*o))
}
func ChannelListFromJson(data io.Reader) *ChannelList {

View File

@ -10,7 +10,6 @@ import (
)
const (
CHANNEL_ROLE_ADMIN = "admin"
CHANNEL_NOTIFY_DEFAULT = "default"
CHANNEL_NOTIFY_ALL = "all"
CHANNEL_NOTIFY_MENTION = "mention"
@ -30,6 +29,27 @@ type ChannelMember struct {
LastUpdateAt int64 `json:"last_update_at"`
}
type ChannelMembers []ChannelMember
func (o *ChannelMembers) ToJson() string {
if b, err := json.Marshal(o); err != nil {
return "[]"
} else {
return string(b)
}
}
func ChannelMembersFromJson(data io.Reader) *ChannelMembers {
decoder := json.NewDecoder(data)
var o ChannelMembers
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}
func (o *ChannelMember) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
@ -60,12 +80,6 @@ func (o *ChannelMember) IsValid() *AppError {
return NewLocAppError("ChannelMember.IsValid", "model.channel_member.is_valid.user_id.app_error", nil, "")
}
for _, role := range strings.Split(o.Roles, " ") {
if !(role == "" || role == CHANNEL_ROLE_ADMIN) {
return NewLocAppError("ChannelMember.IsValid", "model.channel_member.is_valid.role.app_error", nil, "role="+role)
}
}
notifyLevel := o.NotifyProps["desktop"]
if len(notifyLevel) > 20 || !IsChannelNotifyLevelValid(notifyLevel) {
return NewLocAppError("ChannelMember.IsValid", "model.channel_member.is_valid.notify_level.app_error",
@ -89,6 +103,10 @@ func (o *ChannelMember) PreUpdate() {
o.LastUpdateAt = GetMillis()
}
func (o *ChannelMember) GetRoles() []string {
return strings.Fields(o.Roles)
}
func IsChannelNotifyLevelValid(notifyLevel string) bool {
return notifyLevel == CHANNEL_NOTIFY_DEFAULT ||
notifyLevel == CHANNEL_NOTIFY_ALL ||

View File

@ -0,0 +1,34 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type ChannelStats struct {
ChannelId string `json:"channel_id"`
MemberCount int64 `json:"member_count"`
}
func (o *ChannelStats) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func ChannelStatsFromJson(data io.Reader) *ChannelStats {
decoder := json.NewDecoder(data)
var o ChannelStats
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

View File

@ -108,6 +108,10 @@ func (c *Client) GetChannelRoute(channelId string) string {
return fmt.Sprintf("/teams/%v/channels/%v", c.GetTeamId(), channelId)
}
func (c *Client) GetUserRequiredRoute(userId string) string {
return fmt.Sprintf("/users/%v", userId)
}
func (c *Client) GetChannelNameRoute(channelName string) string {
return fmt.Sprintf("/teams/%v/channels/name/%v", c.GetTeamId(), channelName)
}
@ -120,9 +124,14 @@ func (c *Client) GetGeneralRoute() string {
return "/general"
}
func (c *Client) GetFileRoute(fileId string) string {
return fmt.Sprintf("/files/%v", fileId)
}
func (c *Client) DoPost(url, data, contentType string) (*http.Response, *AppError) {
rq, _ := http.NewRequest("POST", c.Url+url, strings.NewReader(data))
rq.Header.Set("Content-Type", contentType)
rq.Close = true
if rp, err := c.HttpClient.Do(rq); err != nil {
return nil, NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error())
@ -136,6 +145,7 @@ func (c *Client) DoPost(url, data, contentType string) (*http.Response, *AppErro
func (c *Client) DoApiPost(url string, data string) (*http.Response, *AppError) {
rq, _ := http.NewRequest("POST", c.ApiUrl+url, strings.NewReader(data))
rq.Close = true
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken)
@ -153,6 +163,7 @@ func (c *Client) DoApiPost(url string, data string) (*http.Response, *AppError)
func (c *Client) DoApiGet(url string, data string, etag string) (*http.Response, *AppError) {
rq, _ := http.NewRequest("GET", c.ApiUrl+url, strings.NewReader(data))
rq.Close = true
if len(etag) > 0 {
rq.Header.Set(HEADER_ETAG_CLIENT, etag)
@ -500,10 +511,9 @@ func (c *Client) GetMe(etag string) (*Result, *AppError) {
}
}
// GetProfilesForDirectMessageList returns a map of users for a team that can be direct
// messaged, using user id as the key. Must be authenticated.
func (c *Client) GetProfilesForDirectMessageList(teamId string) (*Result, *AppError) {
if r, err := c.DoApiGet("/users/profiles_for_dm_list/"+teamId, "", ""); err != nil {
// GetProfiles returns a map of users using user id as the key. Must be authenticated.
func (c *Client) GetProfiles(offset int, limit int, etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(fmt.Sprintf("/users/%v/%v", offset, limit), "", etag); err != nil {
return nil, err
} else {
defer closeBody(r)
@ -512,10 +522,10 @@ func (c *Client) GetProfilesForDirectMessageList(teamId string) (*Result, *AppEr
}
}
// GetProfiles returns a map of users for a team using user id as the key. Must
// GetProfilesInTeam returns a map of users for a team using user id as the key. Must
// be authenticated.
func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) {
if r, err := c.DoApiGet("/users/profiles/"+teamId, "", etag); err != nil {
func (c *Client) GetProfilesInTeam(teamId string, offset int, limit int, etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(fmt.Sprintf("/teams/%v/users/%v/%v", teamId, offset, limit), "", etag); err != nil {
return nil, err
} else {
defer closeBody(r)
@ -524,10 +534,10 @@ func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) {
}
}
// GetDirectProfiles gets a map of users that are currently shown in the sidebar,
// using user id as the key. Must be authenticated.
func (c *Client) GetDirectProfiles(etag string) (*Result, *AppError) {
if r, err := c.DoApiGet("/users/direct_profiles", "", etag); err != nil {
// GetProfilesInChannel returns a map of users for a channel using user id as the key. Must
// be authenticated.
func (c *Client) GetProfilesInChannel(channelId string, offset int, limit int, etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(fmt.Sprintf(c.GetChannelRoute(channelId)+"/users/%v/%v", offset, limit), "", etag); err != nil {
return nil, err
} else {
defer closeBody(r)
@ -536,6 +546,70 @@ func (c *Client) GetDirectProfiles(etag string) (*Result, *AppError) {
}
}
// GetProfilesNotInChannel returns a map of users not in a channel but on the team using user id as the key. Must
// be authenticated.
func (c *Client) GetProfilesNotInChannel(channelId string, offset int, limit int, etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(fmt.Sprintf(c.GetChannelRoute(channelId)+"/users/not_in_channel/%v/%v", offset, limit), "", etag); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), UserMapFromJson(r.Body)}, nil
}
}
// GetProfilesByIds returns a map of users based on the user ids provided. Must
// be authenticated.
func (c *Client) GetProfilesByIds(userIds []string) (*Result, *AppError) {
if r, err := c.DoApiPost("/users/ids", ArrayToJson(userIds)); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), UserMapFromJson(r.Body)}, nil
}
}
// SearchUsers returns a list of users that have a username matching or similar to the search term. Must
// be authenticated.
func (c *Client) SearchUsers(params UserSearch) (*Result, *AppError) {
if r, err := c.DoApiPost("/users/search", params.ToJson()); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), UserListFromJson(r.Body)}, nil
}
}
// AutocompleteUsersInChannel returns two lists for autocompletion of users in a channel. The first list "in_channel",
// specifies users in the channel. The second list "out_of_channel" specifies users outside of the
// channel. Term, the string to search against, is required, channel id is also required. Must be authenticated.
func (c *Client) AutocompleteUsersInChannel(term string, channelId string) (*Result, *AppError) {
url := fmt.Sprintf("%s/users/autocomplete?term=%s", c.GetChannelRoute(channelId), url.QueryEscape(term))
if r, err := c.DoApiGet(url, "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), UserAutocompleteInChannelFromJson(r.Body)}, nil
}
}
// AutocompleteUsersInTeam returns a list for autocompletion of users in a team. The list "in_team" specifies
// the users in the team that match the provided term, matching against username, full name and
// nickname. Must be authenticated.
func (c *Client) AutocompleteUsersInTeam(term string) (*Result, *AppError) {
url := fmt.Sprintf("%s/users/autocomplete?term=%s", c.GetTeamRoute(), url.QueryEscape(term))
if r, err := c.DoApiGet(url, "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), UserAutocompleteInTeamFromJson(r.Body)}, nil
}
}
// LoginById authenticates a user by user id and password.
func (c *Client) LoginById(id string, password string) (*Result, *AppError) {
m := make(map[string]string)
@ -622,15 +696,16 @@ func (c *Client) CheckMfa(loginId string) (*Result, *AppError) {
}
}
// GenerateMfaQrCode returns a QR code imagem containing the secret, to be scanned
// by a multi-factor authentication mobile application. Must be authenticated.
func (c *Client) GenerateMfaQrCode() (*Result, *AppError) {
if r, err := c.DoApiGet("/users/generate_mfa_qr", "", ""); err != nil {
// GenerateMfaSecret returns a QR code image containing the secret, to be scanned
// by a multi-factor authentication mobile application. It also returns the secret
// for manual entry. Must be authenticated.
func (c *Client) GenerateMfaSecret() (*Result, *AppError) {
if r, err := c.DoApiGet("/users/generate_mfa_secret", "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), r.Body}, nil
r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
}
}
@ -934,6 +1009,7 @@ func (c *Client) SaveComplianceReport(job *Compliance) (*Result, *AppError) {
func (c *Client) DownloadComplianceReport(id string) (*Result, *AppError) {
var rq *http.Request
rq, _ = http.NewRequest("GET", c.ApiUrl+"/admin/download_compliance_report/"+id, nil)
rq.Close = true
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
@ -1047,13 +1123,13 @@ func (c *Client) UpdateNotifyProps(data map[string]string) (*Result, *AppError)
}
}
func (c *Client) GetChannels(etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/", "", etag); err != nil {
func (c *Client) GetMyChannelMembers() (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/members", "", ""); 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
r.Header.Get(HEADER_ETAG_SERVER), ChannelMembersFromJson(r.Body)}, nil
}
}
@ -1087,6 +1163,16 @@ func (c *Client) GetChannelCounts(etag string) (*Result, *AppError) {
}
}
func (c *Client) GetChannels(etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/", "", 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
}
}
func (c *Client) JoinChannel(id string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetChannelRoute(id)+"/join", ""); err != nil {
return nil, err
@ -1166,13 +1252,23 @@ func (c *Client) UpdateLastViewedAt(channelId string, active bool) (*Result, *Ap
}
}
func (c *Client) GetChannelExtraInfo(id string, memberLimit int, etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetChannelRoute(id)+"/extra_info/"+strconv.FormatInt(int64(memberLimit), 10), "", etag); err != nil {
func (c *Client) GetChannelStats(id string, etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetChannelRoute(id)+"/stats", "", etag); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ChannelExtraFromJson(r.Body)}, nil
r.Header.Get(HEADER_ETAG_SERVER), ChannelStatsFromJson(r.Body)}, nil
}
}
func (c *Client) GetChannelMember(channelId string, userId string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/members/"+userId, "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ChannelMemberFromJson(r.Body)}, nil
}
}
@ -1285,13 +1381,39 @@ func (c *Client) UploadProfileFile(data []byte, contentType string) (*Result, *A
return c.uploadFile(c.ApiUrl+"/users/newimage", data, contentType)
}
func (c *Client) UploadPostAttachment(data []byte, contentType string) (*Result, *AppError) {
return c.uploadFile(c.ApiUrl+c.GetTeamRoute()+"/files/upload", data, contentType)
func (c *Client) UploadPostAttachment(data []byte, channelId string, filename string) (*FileUploadResponse, *AppError) {
c.clearExtraProperties()
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
if part, err := writer.CreateFormFile("files", filename); err != nil {
return nil, NewLocAppError("UploadPostAttachment", "model.client.upload_post_attachment.file.app_error", nil, err.Error())
} else if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil {
return nil, NewLocAppError("UploadPostAttachment", "model.client.upload_post_attachment.file.app_error", nil, err.Error())
}
if part, err := writer.CreateFormField("channel_id"); err != nil {
return nil, NewLocAppError("UploadPostAttachment", "model.client.upload_post_attachment.channel_id.app_error", nil, err.Error())
} else if _, err = io.Copy(part, strings.NewReader(channelId)); err != nil {
return nil, NewLocAppError("UploadPostAttachment", "model.client.upload_post_attachment.channel_id.app_error", nil, err.Error())
}
if err := writer.Close(); err != nil {
return nil, NewLocAppError("UploadPostAttachment", "model.client.upload_post_attachment.writer.app_error", nil, err.Error())
}
if result, err := c.uploadFile(c.ApiUrl+c.GetTeamRoute()+"/files/upload", body.Bytes(), writer.FormDataContentType()); err != nil {
return nil, err
} else {
return result.Data.(*FileUploadResponse), nil
}
}
func (c *Client) uploadFile(url string, data []byte, contentType string) (*Result, *AppError) {
rq, _ := http.NewRequest("POST", url, bytes.NewReader(data))
rq.Header.Set("Content-Type", contentType)
rq.Close = true
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
@ -1308,55 +1430,51 @@ func (c *Client) uploadFile(url string, data []byte, contentType string) (*Resul
}
}
func (c *Client) GetFile(url string, isFullUrl bool) (*Result, *AppError) {
var rq *http.Request
if isFullUrl {
rq, _ = http.NewRequest("GET", url, nil)
func (c *Client) GetFile(fileId string) (io.ReadCloser, *AppError) {
if r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/get", "", ""); err != nil {
return nil, err
} else {
rq, _ = http.NewRequest("GET", c.ApiUrl+c.GetTeamRoute()+"/files/get"+url, nil)
}
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
}
if rp, err := c.HttpClient.Do(rq); err != nil {
return nil, NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error())
} else if rp.StatusCode >= 300 {
return nil, AppErrorFromJson(rp.Body)
} else {
defer closeBody(rp)
return &Result{rp.Header.Get(HEADER_REQUEST_ID),
rp.Header.Get(HEADER_ETAG_SERVER), rp.Body}, nil
c.fillInExtraProperties(r)
return r.Body, nil
}
}
func (c *Client) GetFileInfo(url string) (*Result, *AppError) {
var rq *http.Request
rq, _ = http.NewRequest("GET", c.ApiUrl+c.GetTeamRoute()+"/files/get_info"+url, nil)
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
}
if rp, err := c.HttpClient.Do(rq); err != nil {
return nil, NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error())
} else if rp.StatusCode >= 300 {
return nil, AppErrorFromJson(rp.Body)
func (c *Client) GetFileThumbnail(fileId string) (io.ReadCloser, *AppError) {
if r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/get_thumbnail", "", ""); err != nil {
return nil, err
} else {
defer closeBody(rp)
return &Result{rp.Header.Get(HEADER_REQUEST_ID),
rp.Header.Get(HEADER_ETAG_SERVER), FileInfoFromJson(rp.Body)}, nil
c.fillInExtraProperties(r)
return r.Body, nil
}
}
func (c *Client) GetPublicLink(filename string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetTeamRoute()+"/files/get_public_link", MapToJson(map[string]string{"filename": filename})); err != nil {
func (c *Client) GetFilePreview(fileId string) (io.ReadCloser, *AppError) {
if r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/get_preview", "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), StringFromJson(r.Body)}, nil
c.fillInExtraProperties(r)
return r.Body, nil
}
}
func (c *Client) GetFileInfo(fileId string) (*FileInfo, *AppError) {
if r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/get_info", "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
c.fillInExtraProperties(r)
return FileInfoFromJson(r.Body), nil
}
}
func (c *Client) GetPublicLink(fileId string) (string, *AppError) {
if r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/get_public_link", "", ""); err != nil {
return "", err
} else {
defer closeBody(r)
c.fillInExtraProperties(r)
return StringFromJson(r.Body), nil
}
}
@ -1370,8 +1488,25 @@ func (c *Client) UpdateUser(user *User) (*Result, *AppError) {
}
}
func (c *Client) UpdateUserRoles(data map[string]string) (*Result, *AppError) {
if r, err := c.DoApiPost("/users/update_roles", MapToJson(data)); err != nil {
func (c *Client) UpdateUserRoles(userId string, roles string) (*Result, *AppError) {
data := make(map[string]string)
data["new_roles"] = roles
if r, err := c.DoApiPost(c.GetUserRequiredRoute(userId)+"/update_roles", 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) UpdateTeamRoles(userId string, roles string) (*Result, *AppError) {
data := make(map[string]string)
data["new_roles"] = roles
data["user_id"] = userId
if r, err := c.DoApiPost(c.GetTeamRoute()+"/update_member_roles", MapToJson(data)); err != nil {
return nil, err
} else {
defer closeBody(r)
@ -1479,6 +1614,18 @@ func (c *Client) GetStatuses() (*Result, *AppError) {
}
}
// GetStatusesByIds returns a map of string statuses using user id as the key,
// based on the provided user ids
func (c *Client) GetStatusesByIds(userIds []string) (*Result, *AppError) {
if r, err := c.DoApiPost("/users/status/ids", ArrayToJson(userIds)); 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
}
}
// 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.
@ -1504,8 +1651,46 @@ func (c *Client) GetMyTeam(etag string) (*Result, *AppError) {
}
}
func (c *Client) GetTeamMembers(teamId string) (*Result, *AppError) {
if r, err := c.DoApiGet("/teams/members/"+teamId, "", ""); err != nil {
// GetTeamMembers will return a page of team member objects as an array paged based on the
// team id, offset and limit provided. Must be authenticated.
func (c *Client) GetTeamMembers(teamId string, offset int, limit int) (*Result, *AppError) {
if r, err := c.DoApiGet(fmt.Sprintf("/teams/%v/members/%v/%v", teamId, offset, limit), "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), TeamMembersFromJson(r.Body)}, nil
}
}
// GetTeamMember will return a team member object based on the team id and user id provided.
// Must be authenticated.
func (c *Client) GetTeamMember(teamId string, userId string) (*Result, *AppError) {
if r, err := c.DoApiGet(fmt.Sprintf("/teams/%v/members/%v", teamId, userId), "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), TeamMemberFromJson(r.Body)}, nil
}
}
// GetTeamStats will return a team stats object containing the number of users on the team
// based on the team id provided. Must be authenticated.
func (c *Client) GetTeamStats(teamId string) (*Result, *AppError) {
if r, err := c.DoApiGet(fmt.Sprintf("/teams/%v/stats", teamId), "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), TeamStatsFromJson(r.Body)}, nil
}
}
// GetTeamMembersByIds will return team member objects as an array based on the
// team id and a list of user ids provided. Must be authenticated.
func (c *Client) GetTeamMembersByIds(teamId string, userIds []string) (*Result, *AppError) {
if r, err := c.DoApiPost(fmt.Sprintf("/teams/%v/members/ids", teamId), ArrayToJson(userIds)); err != nil {
return nil, err
} else {
defer closeBody(r)
@ -1820,6 +2005,7 @@ func (c *Client) CreateEmoji(emoji *Emoji, image []byte, filename string) (*Emoj
rq, _ := http.NewRequest("POST", c.ApiUrl+c.GetEmojiRoute()+"/create", body)
rq.Header.Set("Content-Type", writer.FormDataContentType())
rq.Close = true
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
@ -1862,6 +2048,7 @@ func (c *Client) UploadCertificateFile(data []byte, contentType string) *AppErro
url := c.ApiUrl + "/admin/add_certificate"
rq, _ := http.NewRequest("POST", url, bytes.NewReader(data))
rq.Header.Set("Content-Type", contentType)
rq.Close = true
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
@ -1898,3 +2085,28 @@ func (c *Client) SamlCertificateStatus(filename string) (map[string]interface{},
return StringInterfaceFromJson(r.Body), nil
}
}
// GetWebrtcToken if Successful returns a map with a valid token, stun server and turn server with credentials to use with
// the Mattermost WebRTC service, otherwise returns an AppError. Must be authenticated user.
func (c *Client) GetWebrtcToken() (map[string]string, *AppError) {
if r, err := c.DoApiPost("/webrtc/token", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return MapFromJson(r.Body), nil
}
}
// GetFileInfosForPost returns a list of FileInfo objects for a given post id, if successful.
// Otherwise, it returns an error.
func (c *Client) GetFileInfosForPost(channelId string, postId string, etag string) ([]*FileInfo, *AppError) {
c.clearExtraProperties()
if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/get_file_infos", postId), "", etag); err != nil {
return nil, err
} else {
defer closeBody(r)
c.fillInExtraProperties(r)
return FileInfosFromJson(r.Body), nil
}
}

View File

@ -34,7 +34,7 @@ type CompliancePost struct {
PostType string
PostProps string
PostHashtags string
PostFilenames string
PostFileIds string
}
func CompliancePostHeader() []string {
@ -60,7 +60,7 @@ func CompliancePostHeader() []string {
"PostType",
"PostProps",
"PostHashtags",
"PostFilenames",
"PostFileIds",
}
}
@ -99,6 +99,6 @@ func (me *CompliancePost) Row() []string {
me.PostType,
me.PostProps,
me.PostHashtags,
me.PostFilenames,
me.PostFileIds,
}
}

View File

@ -57,6 +57,14 @@ const (
type ServiceSettings struct {
SiteURL *string
ListenAddress string
ConnectionSecurity *string
TLSCertFile *string
TLSKeyFile *string
UseLetsEncrypt *bool
LetsEncryptCertificateCacheFile *string
Forward80To443 *bool
ReadTimeout *int
WriteTimeout *int
MaximumLoginAttempts int
SegmentDeveloperKey string
GoogleDeveloperKey string
@ -130,26 +138,24 @@ type PasswordSettings struct {
}
type FileSettings struct {
MaxFileSize *int64
DriverName string
Directory string
EnablePublicLink bool
PublicLinkSalt *string
ThumbnailWidth int
ThumbnailHeight int
PreviewWidth int
PreviewHeight int
ProfileWidth int
ProfileHeight int
InitialFont string
AmazonS3AccessKeyId string
AmazonS3SecretAccessKey string
AmazonS3Bucket string
AmazonS3Region string
AmazonS3Endpoint string
AmazonS3BucketEndpoint string
AmazonS3LocationConstraint *bool
AmazonS3LowercaseBucket *bool
MaxFileSize *int64
DriverName string
Directory string
EnablePublicLink bool
PublicLinkSalt *string
ThumbnailWidth int
ThumbnailHeight int
PreviewWidth int
PreviewHeight int
ProfileWidth int
ProfileHeight int
InitialFont string
AmazonS3AccessKeyId string
AmazonS3SecretAccessKey string
AmazonS3Bucket string
AmazonS3Region string
AmazonS3Endpoint string
AmazonS3SSL *bool
}
type EmailSettings struct {
@ -177,11 +183,12 @@ type EmailSettings struct {
}
type RateLimitSettings struct {
EnableRateLimiter bool
PerSec int
MemoryStoreSize int
VaryByRemoteAddr bool
VaryByHeader string
Enable *bool
PerSec int
MaxBurst *int
MemoryStoreSize int
VaryByRemoteAddr bool
VaryByHeader string
}
type PrivacySettings struct {
@ -205,7 +212,6 @@ type TeamSettings struct {
EnableUserCreation bool
EnableOpenServer *bool
RestrictCreationToDomains string
RestrictTeamNames *bool
EnableCustomBrand *bool
CustomBrandText *string
CustomDescriptionText *string
@ -214,6 +220,7 @@ type TeamSettings struct {
RestrictPublicChannelManagement *string
RestrictPrivateChannelManagement *string
UserStatusAwayTimeout *int64
MaxChannelsPerTeam *int64
}
type LdapSettings struct {
@ -292,6 +299,17 @@ type NativeAppSettings struct {
IosAppDownloadLink *string
}
type WebrtcSettings struct {
Enable *bool
GatewayWebsocketUrl *string
GatewayAdminUrl *string
GatewayAdminSecret *string
StunURI *string
TurnURI *string
TurnUsername *string
TurnSharedKey *string
}
type Config struct {
ServiceSettings ServiceSettings
TeamSettings TeamSettings
@ -312,6 +330,7 @@ type Config struct {
SamlSettings SamlSettings
NativeAppSettings NativeAppSettings
ClusterSettings ClusterSettings
WebrtcSettings WebrtcSettings
}
func (o *Config) ToJson() string {
@ -353,26 +372,27 @@ func (o *Config) SetDefaults() {
o.SqlSettings.AtRestEncryptKey = NewRandomString(32)
}
if o.FileSettings.AmazonS3Endpoint == "" {
// Defaults to "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 {
o.FileSettings.AmazonS3SSL = new(bool)
*o.FileSettings.AmazonS3SSL = true // Secure by default.
}
if o.FileSettings.MaxFileSize == nil {
o.FileSettings.MaxFileSize = new(int64)
*o.FileSettings.MaxFileSize = 52428800 // 50 MB
}
if len(*o.FileSettings.PublicLinkSalt) == 0 {
o.FileSettings.PublicLinkSalt = new(string)
*o.FileSettings.PublicLinkSalt = NewRandomString(32)
}
if o.FileSettings.AmazonS3LocationConstraint == nil {
o.FileSettings.AmazonS3LocationConstraint = new(bool)
*o.FileSettings.AmazonS3LocationConstraint = false
}
if o.FileSettings.AmazonS3LowercaseBucket == nil {
o.FileSettings.AmazonS3LowercaseBucket = new(bool)
*o.FileSettings.AmazonS3LowercaseBucket = false
}
if len(o.EmailSettings.InviteSalt) == 0 {
o.EmailSettings.InviteSalt = NewRandomString(32)
}
@ -431,11 +451,6 @@ func (o *Config) SetDefaults() {
*o.PasswordSettings.Symbol = false
}
if o.TeamSettings.RestrictTeamNames == nil {
o.TeamSettings.RestrictTeamNames = new(bool)
*o.TeamSettings.RestrictTeamNames = true
}
if o.TeamSettings.EnableCustomBrand == nil {
o.TeamSettings.EnableCustomBrand = new(bool)
*o.TeamSettings.EnableCustomBrand = false
@ -481,6 +496,11 @@ func (o *Config) SetDefaults() {
*o.TeamSettings.UserStatusAwayTimeout = 300
}
if o.TeamSettings.MaxChannelsPerTeam == nil {
o.TeamSettings.MaxChannelsPerTeam = new(int64)
*o.TeamSettings.MaxChannelsPerTeam = 2000
}
if o.EmailSettings.EnableSignInWithEmail == nil {
o.EmailSettings.EnableSignInWithEmail = new(bool)
@ -881,6 +901,58 @@ func (o *Config) SetDefaults() {
o.NativeAppSettings.IosAppDownloadLink = new(string)
*o.NativeAppSettings.IosAppDownloadLink = "https://about.mattermost.com/mattermost-ios-app/"
}
if o.RateLimitSettings.Enable == nil {
o.RateLimitSettings.Enable = new(bool)
*o.RateLimitSettings.Enable = false
}
if o.RateLimitSettings.MaxBurst == nil {
o.RateLimitSettings.MaxBurst = new(int)
*o.RateLimitSettings.MaxBurst = 100
}
if o.ServiceSettings.ConnectionSecurity == nil {
o.ServiceSettings.ConnectionSecurity = new(string)
*o.ServiceSettings.ConnectionSecurity = ""
}
if o.ServiceSettings.TLSKeyFile == nil {
o.ServiceSettings.TLSKeyFile = new(string)
*o.ServiceSettings.TLSKeyFile = ""
}
if o.ServiceSettings.TLSCertFile == nil {
o.ServiceSettings.TLSCertFile = new(string)
*o.ServiceSettings.TLSCertFile = ""
}
if o.ServiceSettings.UseLetsEncrypt == nil {
o.ServiceSettings.UseLetsEncrypt = new(bool)
*o.ServiceSettings.UseLetsEncrypt = false
}
if o.ServiceSettings.LetsEncryptCertificateCacheFile == nil {
o.ServiceSettings.LetsEncryptCertificateCacheFile = new(string)
*o.ServiceSettings.LetsEncryptCertificateCacheFile = "./config/letsencrypt.cache"
}
if o.ServiceSettings.ReadTimeout == nil {
o.ServiceSettings.ReadTimeout = new(int)
*o.ServiceSettings.ReadTimeout = 300
}
if o.ServiceSettings.WriteTimeout == nil {
o.ServiceSettings.WriteTimeout = new(int)
*o.ServiceSettings.WriteTimeout = 300
}
if o.ServiceSettings.Forward80To443 == nil {
o.ServiceSettings.Forward80To443 = new(bool)
*o.ServiceSettings.Forward80To443 = false
}
o.defaultWebrtcSettings()
}
func (o *Config) IsValid() *AppError {
@ -911,6 +983,10 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.max_users.app_error", nil, "")
}
if *o.TeamSettings.MaxChannelsPerTeam <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.max_channels.app_error", nil, "")
}
if !(*o.TeamSettings.RestrictDirectMessage == DIRECT_MESSAGE_ANY || *o.TeamSettings.RestrictDirectMessage == DIRECT_MESSAGE_TEAM) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.restrict_direct_message.app_error", nil, "")
}
@ -1083,6 +1159,26 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.sitename_length.app_error", map[string]interface{}{"MaxLength": SITENAME_MAX_LENGTH}, "")
}
if *o.RateLimitSettings.MaxBurst <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.max_burst.app_error", nil, "")
}
if err := o.isValidWebrtcSettings(); err != nil {
return err
}
if !(*o.ServiceSettings.ConnectionSecurity == CONN_SECURITY_NONE || *o.ServiceSettings.ConnectionSecurity == CONN_SECURITY_TLS) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.webserver_security.app_error", nil, "")
}
if *o.ServiceSettings.ReadTimeout <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.read_timeout.app_error", nil, "")
}
if *o.ServiceSettings.WriteTimeout <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.write_timeout.app_error", nil, "")
}
return nil
}
@ -1121,3 +1217,71 @@ func (o *Config) Sanitize() {
o.SqlSettings.DataSourceReplicas[i] = FAKE_SETTING
}
}
func (o *Config) defaultWebrtcSettings() {
if o.WebrtcSettings.Enable == nil {
o.WebrtcSettings.Enable = new(bool)
*o.WebrtcSettings.Enable = false
}
if o.WebrtcSettings.GatewayWebsocketUrl == nil {
o.WebrtcSettings.GatewayWebsocketUrl = new(string)
*o.WebrtcSettings.GatewayWebsocketUrl = ""
}
if o.WebrtcSettings.GatewayAdminUrl == nil {
o.WebrtcSettings.GatewayAdminUrl = new(string)
*o.WebrtcSettings.GatewayAdminUrl = ""
}
if o.WebrtcSettings.GatewayAdminSecret == nil {
o.WebrtcSettings.GatewayAdminSecret = new(string)
*o.WebrtcSettings.GatewayAdminSecret = ""
}
if o.WebrtcSettings.StunURI == nil {
o.WebrtcSettings.StunURI = new(string)
*o.WebrtcSettings.StunURI = ""
}
if o.WebrtcSettings.TurnURI == nil {
o.WebrtcSettings.TurnURI = new(string)
*o.WebrtcSettings.TurnURI = ""
}
if o.WebrtcSettings.TurnUsername == nil {
o.WebrtcSettings.TurnUsername = new(string)
*o.WebrtcSettings.TurnUsername = ""
}
if o.WebrtcSettings.TurnSharedKey == nil {
o.WebrtcSettings.TurnSharedKey = new(string)
*o.WebrtcSettings.TurnSharedKey = ""
}
}
func (o *Config) isValidWebrtcSettings() *AppError {
if *o.WebrtcSettings.Enable {
if len(*o.WebrtcSettings.GatewayWebsocketUrl) == 0 || !IsValidWebsocketUrl(*o.WebrtcSettings.GatewayWebsocketUrl) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.webrtc_gateway_ws_url.app_error", nil, "")
} else if len(*o.WebrtcSettings.GatewayAdminUrl) == 0 || !IsValidHttpUrl(*o.WebrtcSettings.GatewayAdminUrl) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.webrtc_gateway_admin_url.app_error", nil, "")
} else if len(*o.WebrtcSettings.GatewayAdminSecret) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.webrtc_gateway_admin_secret.app_error", nil, "")
} else if len(*o.WebrtcSettings.StunURI) != 0 && !IsValidTurnOrStunServer(*o.WebrtcSettings.StunURI) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.webrtc_stun_uri.app_error", nil, "")
} else if len(*o.WebrtcSettings.TurnURI) != 0 {
if !IsValidTurnOrStunServer(*o.WebrtcSettings.TurnURI) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.webrtc_turn_uri.app_error", nil, "")
}
if len(*o.WebrtcSettings.TurnUsername) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.webrtc_turn_username.app_error", nil, "")
} else if len(*o.WebrtcSettings.TurnSharedKey) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.webrtc_turn_shared_key.app_error", nil, "")
}
}
}
return nil
}

View File

@ -14,8 +14,8 @@ var (
)
type FileUploadResponse struct {
Filenames []string `json:"filenames"`
ClientIds []string `json:"client_ids"`
FileInfos []*FileInfo `json:"file_infos"`
ClientIds []string `json:"client_ids"`
}
func FileUploadResponseFromJson(data io.Reader) *FileUploadResponse {

View File

@ -6,54 +6,31 @@ package model
import (
"bytes"
"encoding/json"
"image"
"image/gif"
"io"
"mime"
"path/filepath"
"strings"
)
type FileInfo struct {
Filename string `json:"filename"`
Size int `json:"size"`
Id string `json:"id"`
CreatorId string `json:"user_id"`
PostId string `json:"post_id,omitempty"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
Path string `json:"-"` // not sent back to the client
ThumbnailPath string `json:"-"` // not sent back to the client
PreviewPath string `json:"-"` // not sent back to the client
Name string `json:"name"`
Extension string `json:"extension"`
Size int64 `json:"size"`
MimeType string `json:"mime_type"`
HasPreviewImage bool `json:"has_preview_image"`
}
func GetInfoForBytes(filename string, data []byte) (*FileInfo, *AppError) {
size := len(data)
var mimeType string
extension := filepath.Ext(filename)
isImage := IsFileExtImage(extension)
if isImage {
mimeType = GetImageMimeType(extension)
} else {
mimeType = mime.TypeByExtension(extension)
}
if extension != "" && extension[0] == '.' {
// the client expects a file extension without the leading period
extension = extension[1:]
}
hasPreviewImage := isImage
if mimeType == "image/gif" {
// just show the gif itself instead of a preview image for animated gifs
if gifImage, err := gif.DecodeAll(bytes.NewReader(data)); err != nil {
return nil, NewLocAppError("GetInfoForBytes", "model.file_info.get.gif.app_error", nil, "filename="+filename)
} else {
hasPreviewImage = len(gifImage.Image) == 1
}
}
return &FileInfo{
Filename: filename,
Size: size,
Extension: extension,
MimeType: mimeType,
HasPreviewImage: hasPreviewImage,
}, nil
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
HasPreviewImage bool `json:"has_preview_image,omitempty"`
}
func (info *FileInfo) ToJson() string {
@ -75,3 +52,123 @@ func FileInfoFromJson(data io.Reader) *FileInfo {
return &info
}
}
func FileInfosToJson(infos []*FileInfo) string {
b, err := json.Marshal(infos)
if err != nil {
return ""
} else {
return string(b)
}
}
func FileInfosFromJson(data io.Reader) []*FileInfo {
decoder := json.NewDecoder(data)
var infos []*FileInfo
if err := decoder.Decode(&infos); err != nil {
return nil
} else {
return infos
}
}
func (o *FileInfo) PreSave() {
if o.Id == "" {
o.Id = NewId()
}
if o.CreateAt == 0 {
o.CreateAt = GetMillis()
o.UpdateAt = o.CreateAt
}
}
func (o *FileInfo) IsValid() *AppError {
if len(o.Id) != 26 {
return NewLocAppError("FileInfo.IsValid", "model.file_info.is_valid.id.app_error", nil, "")
}
if len(o.CreatorId) != 26 {
return NewLocAppError("FileInfo.IsValid", "model.file_info.is_valid.user_id.app_error", nil, "id="+o.Id)
}
if len(o.PostId) != 0 && len(o.PostId) != 26 {
return NewLocAppError("FileInfo.IsValid", "model.file_info.is_valid.post_id.app_error", nil, "id="+o.Id)
}
if o.CreateAt == 0 {
return NewLocAppError("FileInfo.IsValid", "model.file_info.is_valid.create_at.app_error", nil, "id="+o.Id)
}
if o.UpdateAt == 0 {
return NewLocAppError("FileInfo.IsValid", "model.file_info.is_valid.update_at.app_error", nil, "id="+o.Id)
}
if o.Path == "" {
return NewLocAppError("FileInfo.IsValid", "model.file_info.is_valid.path.app_error", nil, "id="+o.Id)
}
return nil
}
func (o *FileInfo) IsImage() bool {
return strings.HasPrefix(o.MimeType, "image")
}
func GetInfoForBytes(name string, data []byte) (*FileInfo, *AppError) {
info := &FileInfo{
Name: name,
Size: int64(len(data)),
}
var err *AppError
extension := strings.ToLower(filepath.Ext(name))
info.MimeType = mime.TypeByExtension(extension)
if extension != "" && extension[0] == '.' {
// The client expects a file extension without the leading period
info.Extension = extension[1:]
} else {
info.Extension = extension
}
if info.IsImage() {
// Only set the width and height if it's actually an image that we can understand
if config, _, err := image.DecodeConfig(bytes.NewReader(data)); err == nil {
info.Width = config.Width
info.Height = config.Height
if info.MimeType == "image/gif" {
// Just show the gif itself instead of a preview image for animated gifs
if gifConfig, err := gif.DecodeAll(bytes.NewReader(data)); err != nil {
// Still return the rest of the info even though it doesn't appear to be an actual gif
info.HasPreviewImage = true
err = NewLocAppError("GetInfoForBytes", "model.file_info.get.gif.app_error", nil, "name="+name)
} else {
info.HasPreviewImage = len(gifConfig.Image) == 1
}
} else {
info.HasPreviewImage = true
}
}
}
return info, err
}
func GetEtagForFileInfos(infos []*FileInfo) string {
if len(infos) == 0 {
return Etag()
}
var maxUpdateAt int64
for _, info := range infos {
if info.UpdateAt > maxUpdateAt {
maxUpdateAt = info.UpdateAt
}
}
return Etag(infos[0].PostId, maxUpdateAt)
}

View File

@ -9,14 +9,13 @@ import (
)
type InitialLoad struct {
User *User `json:"user"`
TeamMembers []*TeamMember `json:"team_members"`
Teams []*Team `json:"teams"`
DirectProfiles map[string]*User `json:"direct_profiles"`
Preferences Preferences `json:"preferences"`
ClientCfg map[string]string `json:"client_cfg"`
LicenseCfg map[string]string `json:"license_cfg"`
NoAccounts bool `json:"no_accounts"`
User *User `json:"user"`
TeamMembers []*TeamMember `json:"team_members"`
Teams []*Team `json:"teams"`
Preferences Preferences `json:"preferences"`
ClientCfg map[string]string `json:"client_cfg"`
LicenseCfg map[string]string `json:"license_cfg"`
NoAccounts bool `json:"no_accounts"`
}
func (me *InitialLoad) ToJson() string {

View File

@ -43,7 +43,8 @@ type Features struct {
MHPNS *bool `json:"mhpns"`
SAML *bool `json:"saml"`
PasswordRequirements *bool `json:"password_requirements"`
FutureFeatures *bool `json:"future_features"`
// after we enabled more features for webrtc we'll need to control them with this
FutureFeatures *bool `json:"future_features"`
}
func (f *Features) ToMap() map[string]interface{} {

View File

@ -35,7 +35,8 @@ type Post struct {
Type string `json:"type"`
Props StringInterface `json:"props"`
Hashtags string `json:"hashtags"`
Filenames StringArray `json:"filenames"`
Filenames StringArray `json:"filenames,omitempty"` // Deprecated, do not use this field any more
FileIds StringArray `json:"file_ids,omitempty"`
PendingPostId string `json:"pending_post_id" db:"-"`
}
@ -118,6 +119,10 @@ func (o *Post) IsValid() *AppError {
return NewLocAppError("Post.IsValid", "model.post.is_valid.filenames.app_error", nil, "id="+o.Id)
}
if utf8.RuneCountInString(ArrayToJson(o.FileIds)) > 150 {
return NewLocAppError("Post.IsValid", "model.post.is_valid.file_ids.app_error", nil, "id="+o.Id)
}
if utf8.RuneCountInString(StringInterfaceToJson(o.Props)) > 8000 {
return NewLocAppError("Post.IsValid", "model.post.is_valid.props.app_error", nil, "id="+o.Id)
}
@ -145,15 +150,16 @@ func (o *Post) PreSave() {
if o.Filenames == nil {
o.Filenames = []string{}
}
if o.FileIds == nil {
o.FileIds = []string{}
}
}
func (o *Post) MakeNonNil() {
if o.Props == nil {
o.Props = make(map[string]interface{})
}
if o.Filenames == nil {
o.Filenames = []string{}
}
}
func (o *Post) AddProp(key string, value interface{}) {

View File

@ -11,7 +11,7 @@ import (
const (
SESSION_COOKIE_TOKEN = "MMAUTHTOKEN"
SESSION_CACHE_SIZE = 10000
SESSION_CACHE_SIZE = 25000
SESSION_PROP_PLATFORM = "platform"
SESSION_PROP_OS = "os"
SESSION_PROP_BROWSER = "browser"
@ -115,6 +115,10 @@ func (me *Session) IsMobileApp() bool {
(strings.HasPrefix(me.DeviceId, PUSH_NOTIFY_APPLE+":") || strings.HasPrefix(me.DeviceId, PUSH_NOTIFY_ANDROID+":"))
}
func (me *Session) GetUserRoles() []string {
return strings.Fields(me.Roles)
}
func SessionsToJson(o []*Session) string {
if b, err := json.Marshal(o); err != nil {
return "[]"

View File

@ -12,8 +12,9 @@ const (
STATUS_OFFLINE = "offline"
STATUS_AWAY = "away"
STATUS_ONLINE = "online"
STATUS_CACHE_SIZE = 10000
STATUS_CHANNEL_TIMEOUT = 20000 // 20 seconds
STATUS_CACHE_SIZE = 25000
STATUS_CHANNEL_TIMEOUT = 20000 // 20 seconds
STATUS_MIN_UPDATE_TIME = 120000 // 2 minutes
)
type Status struct {

View File

@ -100,7 +100,7 @@ func (o *Team) Etag() string {
return Etag(o.Id, o.UpdateAt)
}
func (o *Team) IsValid(restrictTeamNames bool) *AppError {
func (o *Team) IsValid() *AppError {
if len(o.Id) != 26 {
return NewLocAppError("Team.IsValid", "model.team.is_valid.id.app_error", nil, "")
@ -130,7 +130,7 @@ func (o *Team) IsValid(restrictTeamNames bool) *AppError {
return NewLocAppError("Team.IsValid", "model.team.is_valid.url.app_error", nil, "id="+o.Id)
}
if restrictTeamNames && IsReservedTeamName(o.Name) {
if IsReservedTeamName(o.Name) {
return NewLocAppError("Team.IsValid", "model.team.is_valid.reserved.app_error", nil, "id="+o.Id)
}
@ -188,7 +188,7 @@ func IsValidTeamName(s string) bool {
return false
}
if len(s) <= 3 {
if len(s) <= 1 {
return false
}

View File

@ -9,10 +9,6 @@ import (
"strings"
)
const (
ROLE_TEAM_ADMIN = "admin"
)
type TeamMember struct {
TeamId string `json:"team_id"`
UserId string `json:"user_id"`
@ -59,48 +55,6 @@ func TeamMembersFromJson(data io.Reader) []*TeamMember {
}
}
func IsValidTeamRoles(teamRoles string) bool {
roles := strings.Split(teamRoles, " ")
for _, r := range roles {
if !isValidTeamRole(r) {
return false
}
}
return true
}
func isValidTeamRole(role string) bool {
if role == "" {
return true
}
if role == ROLE_TEAM_ADMIN {
return true
}
return false
}
func IsInTeamRole(teamRoles string, inRole string) bool {
roles := strings.Split(teamRoles, " ")
for _, r := range roles {
if r == inRole {
return true
}
}
return false
}
func (o *TeamMember) IsTeamAdmin() bool {
return IsInTeamRole(o.Roles, ROLE_TEAM_ADMIN)
}
func (o *TeamMember) IsValid() *AppError {
if len(o.TeamId) != 26 {
@ -111,11 +65,12 @@ func (o *TeamMember) IsValid() *AppError {
return NewLocAppError("TeamMember.IsValid", "model.team_member.is_valid.user_id.app_error", nil, "")
}
for _, role := range strings.Split(o.Roles, " ") {
if !(role == "" || role == ROLE_TEAM_ADMIN) {
return NewLocAppError("TeamMember.IsValid", "model.team_member.is_valid.role.app_error", nil, "role="+role)
}
}
return nil
}
func (o *TeamMember) PreUpdate() {
}
func (o *TeamMember) GetRoles() []string {
return strings.Fields(o.Roles)
}

View File

@ -0,0 +1,35 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type TeamStats struct {
TeamId string `json:"team_id"`
TotalMemberCount int64 `json:"total_member_count"`
ActiveMemberCount int64 `json:"active_member_count"`
}
func (o *TeamStats) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func TeamStatsFromJson(data io.Reader) *TeamStats {
decoder := json.NewDecoder(data)
var o TeamStats
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

View File

@ -15,7 +15,6 @@ import (
)
const (
ROLE_SYSTEM_ADMIN = "system_admin"
USER_NOTIFY_ALL = "all"
USER_NOTIFY_MENTION = "mention"
USER_NOTIFY_NONE = "none"
@ -233,14 +232,15 @@ func (u *User) Sanitize(options map[string]bool) {
if len(options) != 0 && !options["passwordupdate"] {
u.LastPasswordUpdate = 0
}
if len(options) != 0 && !options["authservice"] {
u.AuthService = ""
}
}
func (u *User) ClearNonProfileFields() {
u.Password = ""
u.AuthData = new(string)
*u.AuthData = ""
u.AuthService = ""
u.MfaActive = false
u.MfaSecret = ""
u.EmailVerified = false
u.AllowMarketing = false
@ -319,9 +319,17 @@ func (u *User) GetDisplayNameForPreference(nameFormat string) string {
return displayName
}
func (u *User) GetRoles() []string {
return strings.Fields(u.Roles)
}
func (u *User) GetRawRoles() string {
return u.Roles
}
func IsValidUserRoles(userRoles string) bool {
roles := strings.Split(userRoles, " ")
roles := strings.Fields(userRoles)
for _, r := range roles {
if !isValidRole(r) {
@ -329,19 +337,17 @@ func IsValidUserRoles(userRoles string) bool {
}
}
// Exclude just the system_admin role explicitly to prevent mistakes
if len(roles) == 1 && roles[0] == "system_admin" {
return false
}
return true
}
func isValidRole(role string) bool {
if role == "" {
return true
}
if role == ROLE_SYSTEM_ADMIN {
return true
}
return false
func isValidRole(roleId string) bool {
_, ok := BuiltInRoles[roleId]
return ok
}
// Make sure you acually want to use this function. In context.go there are functions to check permissions
@ -411,6 +417,26 @@ func UserMapFromJson(data io.Reader) map[string]*User {
}
}
func UserListToJson(u []*User) string {
b, err := json.Marshal(u)
if err != nil {
return ""
} else {
return string(b)
}
}
func UserListFromJson(data io.Reader) []*User {
decoder := json.NewDecoder(data)
var users []*User
err := decoder.Decode(&users)
if err == nil {
return users
} else {
return nil
}
}
// HashPassword generates a hash using the bcrypt.GenerateFromPassword
func HashPassword(password string) string {
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)

View File

@ -0,0 +1,39 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type UserSearch struct {
Term string `json:"term"`
TeamId string `json:"team_id"`
InChannelId string `json:"in_channel_id"`
NotInChannelId string `json:"not_in_channel_id"`
AllowInactive bool `json:"allow_inactive"`
}
// ToJson convert a User to a json string
func (u *UserSearch) ToJson() string {
b, err := json.Marshal(u)
if err != nil {
return ""
} else {
return string(b)
}
}
// UserSearchFromJson will decode the input and return a User
func UserSearchFromJson(data io.Reader) *UserSearch {
decoder := json.NewDecoder(data)
var us UserSearch
err := decoder.Decode(&us)
if err == nil {
return &us
} else {
return nil
}
}

View File

@ -10,6 +10,7 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/mail"
"net/url"
"regexp"
@ -74,13 +75,21 @@ func (er *AppError) ToJson() string {
// AppErrorFromJson will decode the input and return an AppError
func AppErrorFromJson(data io.Reader) *AppError {
decoder := json.NewDecoder(data)
str := ""
bytes, rerr := ioutil.ReadAll(data)
if rerr != nil {
str = rerr.Error()
} else {
str = string(bytes)
}
decoder := json.NewDecoder(strings.NewReader(str))
var er AppError
err := decoder.Decode(&er)
if err == nil {
return &er
} else {
return NewLocAppError("AppErrorFromJson", "model.utils.decode_json.app_error", nil, err.Error())
return NewLocAppError("AppErrorFromJson", "model.utils.decode_json.app_error", nil, "body: "+str)
}
}
@ -166,6 +175,23 @@ func ArrayFromJson(data io.Reader) []string {
}
}
func ArrayFromInterface(data interface{}) []string {
stringArray := []string{}
dataArray, ok := data.([]interface{})
if !ok {
return stringArray
}
for _, v := range dataArray {
if str, ok := v.(string); ok {
stringArray = append(stringArray, str)
}
}
return stringArray
}
func StringInterfaceToJson(objmap map[string]interface{}) string {
if b, err := json.Marshal(objmap); err != nil {
return ""
@ -227,58 +253,15 @@ func IsValidEmail(email string) bool {
}
var reservedName = []string{
"www",
"web",
"signup",
"login",
"admin",
"support",
"notify",
"test",
"demo",
"mail",
"team",
"channel",
"internal",
"localhost",
"dockerhost",
"stag",
"post",
"cluster",
"api",
"oauth",
}
var wwwStart = regexp.MustCompile(`^www`)
var betaStart = regexp.MustCompile(`^beta`)
var ciStart = regexp.MustCompile(`^ci`)
func GetSubDomain(s string) (string, string) {
s = strings.Replace(s, "http://", "", 1)
s = strings.Replace(s, "https://", "", 1)
match := wwwStart.MatchString(s)
if match {
return "", ""
}
match = betaStart.MatchString(s)
if match {
return "", ""
}
match = ciStart.MatchString(s)
if match {
return "", ""
}
parts := strings.Split(s, ".")
if len(parts) != 3 {
return "", ""
}
return parts[0], parts[1]
}
func IsValidChannelIdentifier(s string) bool {
if !IsValidAlphaNum(s, true) {
@ -413,6 +396,18 @@ func IsValidHttpsUrl(rawUrl string) bool {
return true
}
func IsValidTurnOrStunServer(rawUri string) bool {
if strings.Index(rawUri, "turn:") != 0 && strings.Index(rawUri, "stun:") != 0 {
return false
}
if _, err := url.ParseRequestURI(rawUri); err != nil {
return false
}
return true
}
func IsSafeLink(link *string) bool {
if link != nil {
if IsValidHttpUrl(*link) {
@ -426,3 +421,15 @@ func IsSafeLink(link *string) bool {
return true
}
func IsValidWebsocketUrl(rawUrl string) bool {
if strings.Index(rawUrl, "ws://") != 0 && strings.Index(rawUrl, "wss://") != 0 {
return false
}
if _, err := url.ParseRequestURI(rawUrl); err != nil {
return false
}
return true
}

View File

@ -13,6 +13,7 @@ import (
// It should be maitained in chronological order with most current
// release at the front of the list.
var versions = []string{
"3.5.0",
"3.4.0",
"3.3.0",
"3.2.0",

21
vendor/github.com/mattermost/platform/model/webrtc.go generated vendored Normal file
View File

@ -0,0 +1,21 @@
package model
import (
"encoding/json"
"io"
)
type GatewayResponse struct {
Status string `json:"janus"`
}
func GatewayResponseFromJson(data io.Reader) *GatewayResponse {
decoder := json.NewDecoder(data)
var o GatewayResponse
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

View File

@ -6,7 +6,6 @@ package model
import (
"encoding/json"
"github.com/gorilla/websocket"
"net/http"
)
type WebSocketClient struct {
@ -17,19 +16,18 @@ type WebSocketClient struct {
Sequence int64 // The ever-incrementing sequence attached to each WebSocket action
EventChannel chan *WebSocketEvent
ResponseChannel chan *WebSocketResponse
ListenError *AppError
}
// NewWebSocketClient constructs a new WebSocket client with convienence
// methods for talking to the server.
func NewWebSocketClient(url, authToken string) (*WebSocketClient, *AppError) {
header := http.Header{}
header.Set(HEADER_AUTH, "BEARER "+authToken)
conn, _, err := websocket.DefaultDialer.Dial(url+API_URL_SUFFIX+"/users/websocket", header)
conn, _, err := websocket.DefaultDialer.Dial(url+API_URL_SUFFIX+"/users/websocket", nil)
if err != nil {
return nil, NewLocAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error())
}
return &WebSocketClient{
client := &WebSocketClient{
url,
url + API_URL_SUFFIX,
conn,
@ -37,19 +35,26 @@ func NewWebSocketClient(url, authToken string) (*WebSocketClient, *AppError) {
1,
make(chan *WebSocketEvent, 100),
make(chan *WebSocketResponse, 100),
}, nil
nil,
}
client.SendMessage(WEBSOCKET_AUTHENTICATION_CHALLENGE, map[string]interface{}{"token": authToken})
return client, nil
}
func (wsc *WebSocketClient) Connect() *AppError {
header := http.Header{}
header.Set(HEADER_AUTH, "BEARER "+wsc.AuthToken)
var err error
wsc.Conn, _, err = websocket.DefaultDialer.Dial(wsc.ApiUrl+"/users/websocket", header)
wsc.Conn, _, err = websocket.DefaultDialer.Dial(wsc.ApiUrl+"/users/websocket", nil)
if err != nil {
return NewLocAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error())
}
wsc.EventChannel = make(chan *WebSocketEvent, 100)
wsc.ResponseChannel = make(chan *WebSocketResponse, 100)
wsc.SendMessage(WEBSOCKET_AUTHENTICATION_CHALLENGE, map[string]interface{}{"token": wsc.AuthToken})
return nil
}
@ -59,10 +64,20 @@ func (wsc *WebSocketClient) Close() {
func (wsc *WebSocketClient) Listen() {
go func() {
defer func() {
wsc.Conn.Close()
close(wsc.EventChannel)
close(wsc.ResponseChannel)
}()
for {
var rawMsg json.RawMessage
var err error
if _, rawMsg, err = wsc.Conn.ReadMessage(); err != nil {
if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) {
wsc.ListenError = NewLocAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error())
}
return
}
@ -77,6 +92,7 @@ func (wsc *WebSocketClient) Listen() {
wsc.ResponseChannel <- &response
continue
}
}
}()
}
@ -107,3 +123,12 @@ func (wsc *WebSocketClient) UserTyping(channelId, parentId string) {
func (wsc *WebSocketClient) GetStatuses() {
wsc.SendMessage("get_statuses", nil)
}
// GetStatusesByIds will fetch certain user statuses based on ids and return
// a map of string statuses using user id as the key
func (wsc *WebSocketClient) GetStatusesByIds(userIds []string) {
data := map[string]interface{}{
"user_ids": userIds,
}
wsc.SendMessage("get_statuses_by_ids", data)
}

View File

@ -25,33 +25,57 @@ const (
WEBSOCKET_EVENT_EPHEMERAL_MESSAGE = "ephemeral_message"
WEBSOCKET_EVENT_STATUS_CHANGE = "status_change"
WEBSOCKET_EVENT_HELLO = "hello"
WEBSOCKET_EVENT_WEBRTC = "webrtc"
WEBSOCKET_AUTHENTICATION_CHALLENGE = "authentication_challenge"
)
type WebSocketMessage interface {
ToJson() string
IsValid() bool
DoPreComputeJson()
GetPreComputeJson() []byte
}
type WebsocketBroadcast struct {
OmitUsers map[string]bool `json:"omit_users"` // broadcast is omitted for users listed here
UserId string `json:"user_id"` // broadcast only occurs for this user
ChannelId string `json:"channel_id"` // broadcast only occurs for users in this channel
TeamId string `json:"team_id"` // broadcast only occurs for users in this team
}
type WebSocketEvent struct {
TeamId string `json:"team_id"`
ChannelId string `json:"channel_id"`
UserId string `json:"user_id"`
Event string `json:"event"`
Data map[string]interface{} `json:"data"`
Event string `json:"event"`
Data map[string]interface{} `json:"data"`
Broadcast *WebsocketBroadcast `json:"broadcast"`
PreComputeJson []byte `json:"-"`
}
func (m *WebSocketEvent) Add(key string, value interface{}) {
m.Data[key] = value
}
func NewWebSocketEvent(teamId string, channelId string, userId string, event string) *WebSocketEvent {
return &WebSocketEvent{TeamId: teamId, ChannelId: channelId, UserId: userId, Event: event, Data: make(map[string]interface{})}
func NewWebSocketEvent(event, teamId, channelId, userId string, omitUsers map[string]bool) *WebSocketEvent {
return &WebSocketEvent{Event: event, Data: make(map[string]interface{}),
Broadcast: &WebsocketBroadcast{TeamId: teamId, ChannelId: channelId, UserId: userId, OmitUsers: omitUsers}}
}
func (o *WebSocketEvent) IsValid() bool {
return o.Event != ""
}
func (o *WebSocketEvent) DoPreComputeJson() {
b, err := json.Marshal(o)
if err != nil {
o.PreComputeJson = []byte("")
} else {
o.PreComputeJson = b
}
}
func (o *WebSocketEvent) GetPreComputeJson() []byte {
return o.PreComputeJson
}
func (o *WebSocketEvent) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
@ -73,10 +97,11 @@ func WebSocketEventFromJson(data io.Reader) *WebSocketEvent {
}
type WebSocketResponse struct {
Status string `json:"status"`
SeqReply int64 `json:"seq_reply,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`
Error *AppError `json:"error,omitempty"`
Status string `json:"status"`
SeqReply int64 `json:"seq_reply,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`
Error *AppError `json:"error,omitempty"`
PreComputeJson []byte `json:"-"`
}
func (m *WebSocketResponse) Add(key string, value interface{}) {
@ -104,6 +129,19 @@ func (o *WebSocketResponse) ToJson() string {
}
}
func (o *WebSocketResponse) DoPreComputeJson() {
b, err := json.Marshal(o)
if err != nil {
o.PreComputeJson = []byte("")
} else {
o.PreComputeJson = b
}
}
func (o *WebSocketResponse) GetPreComputeJson() []byte {
return o.PreComputeJson
}
func WebSocketResponseFromJson(data io.Reader) *WebSocketResponse {
decoder := json.NewDecoder(data)
var o WebSocketResponse

View File

@ -536,12 +536,13 @@ func (c *Client) IsEncrypted() bool {
// Chat is an incoming or outgoing XMPP chat message.
type Chat struct {
Remote string
Type string
Text string
Roster Roster
Other []string
Stamp time.Time
Remote string
Type string
Text string
Roster Roster
Other []string
OtherElem []XMLElement
Stamp time.Time
}
type Roster []Contact
@ -584,11 +585,12 @@ func (c *Client) Recv() (stanza interface{}, err error) {
v.Delay.Stamp,
)
chat := Chat{
Remote: v.From,
Type: v.Type,
Text: v.Body,
Other: v.Other,
Stamp: stamp,
Remote: v.From,
Type: v.Type,
Text: v.Body,
Other: v.OtherStrings(),
OtherElem: v.Other,
Stamp: stamp,
}
return chat, nil
case *clientQuery:
@ -600,6 +602,12 @@ func (c *Client) Recv() (stanza interface{}, err error) {
case *clientPresence:
return Presence{v.From, v.To, v.Type, v.Show, v.Status}, nil
case *clientIQ:
if bytes.Equal(v.Query, []byte(`<ping xmlns='urn:xmpp:ping'/>`)) {
err := c.SendResultPing(v.ID, v.From)
if err != nil {
return Chat{}, err
}
}
return IQ{ID: v.ID, From: v.From, To: v.To, Type: v.Type, Query: v.Query}, nil
}
}
@ -714,11 +722,46 @@ type clientMessage struct {
Thread string `xml:"thread"`
// Any hasn't matched element
Other []string `xml:",any"`
Other []XMLElement `xml:",any"`
Delay Delay `xml:"delay"`
}
func (m *clientMessage) OtherStrings() []string {
a := make([]string, len(m.Other))
for i, e := range m.Other {
a[i] = e.String()
}
return a
}
type XMLElement struct {
XMLName xml.Name
InnerXML string `xml:",innerxml"`
}
func (e *XMLElement) String() string {
r := bytes.NewReader([]byte(e.InnerXML))
d := xml.NewDecoder(r)
var buf bytes.Buffer
for {
tok, err := d.Token()
if err != nil {
break
}
switch v := tok.(type) {
case xml.StartElement:
err = d.Skip()
case xml.CharData:
_, err = buf.Write(v)
}
if err != nil {
break
}
}
return buf.String()
}
type Delay struct {
Stamp string `xml:"stamp,attr"`
}

View File

@ -25,3 +25,9 @@ func (c *Client) PingS2S(fromServer, toServer string) error {
xmlEscape(fromServer), xmlEscape(toServer))
return err
}
func (c *Client) SendResultPing(id, toServer string) error {
_, err := fmt.Fprintf(c.conn, "<iq type='result' to='%s' id='%s'/>",
xmlEscape(toServer), xmlEscape(id))
return err
}

View File

@ -0,0 +1,7 @@
Copyright (c) 2013-* rick olson
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,31 @@
package main
import (
"bytes"
"flag"
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
)
func main() {
defaultPath, _ := os.Getwd()
defaultFile := filepath.Join(defaultPath, "streamer.go")
fullpath := flag.String("path", defaultFile, "Path to the include in the multipart data.")
flag.Parse()
buffer := bytes.NewBufferString("")
writer := multipart.NewWriter(buffer)
fmt.Println("Adding the file to the multipart writer")
fileWriter, _ := writer.CreateFormFile("file", *fullpath)
fileData, _ := os.Open(*fullpath)
io.Copy(fileWriter, fileData)
writer.Close()
fmt.Println("Writing the multipart data to a file")
output, _ := os.Create("multiparttest")
io.Copy(output, buffer)
}

View File

@ -0,0 +1,27 @@
package main
import (
"flag"
"fmt"
"github.com/technoweenie/multipartstreamer"
"io"
"os"
"path/filepath"
)
func main() {
defaultPath, _ := os.Getwd()
defaultFile := filepath.Join(defaultPath, "streamer.go")
fullpath := flag.String("path", defaultFile, "Path to the include in the multipart data.")
flag.Parse()
ms := multipartstreamer.New()
fmt.Println("Adding the file to the multipart writer")
ms.WriteFile("file", *fullpath)
reader := ms.GetReader()
fmt.Println("Writing the multipart data to a file")
file, _ := os.Create("streamtest")
io.Copy(file, reader)
}

View File

@ -0,0 +1,101 @@
/*
Package multipartstreamer helps you encode large files in MIME multipart format
without reading the entire content into memory. It uses io.MultiReader to
combine an inner multipart.Reader with a file handle.
*/
package multipartstreamer
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"os"
"path/filepath"
)
type MultipartStreamer struct {
ContentType string
bodyBuffer *bytes.Buffer
bodyWriter *multipart.Writer
closeBuffer *bytes.Buffer
reader io.Reader
contentLength int64
}
// New initializes a new MultipartStreamer.
func New() (m *MultipartStreamer) {
m = &MultipartStreamer{bodyBuffer: new(bytes.Buffer)}
m.bodyWriter = multipart.NewWriter(m.bodyBuffer)
boundary := m.bodyWriter.Boundary()
m.ContentType = "multipart/form-data; boundary=" + boundary
closeBoundary := fmt.Sprintf("\r\n--%s--\r\n", boundary)
m.closeBuffer = bytes.NewBufferString(closeBoundary)
return
}
// WriteFields writes multiple form fields to the multipart.Writer.
func (m *MultipartStreamer) WriteFields(fields map[string]string) error {
var err error
for key, value := range fields {
err = m.bodyWriter.WriteField(key, value)
if err != nil {
return err
}
}
return nil
}
// WriteReader adds an io.Reader to get the content of a file. The reader is
// not accessed until the multipart.Reader is copied to some output writer.
func (m *MultipartStreamer) WriteReader(key, filename string, size int64, reader io.Reader) (err error) {
m.reader = reader
m.contentLength = size
_, err = m.bodyWriter.CreateFormFile(key, filename)
return
}
// WriteFile is a shortcut for adding a local file as an io.Reader.
func (m *MultipartStreamer) WriteFile(key, filename string) error {
fh, err := os.Open(filename)
if err != nil {
return err
}
stat, err := fh.Stat()
if err != nil {
return err
}
return m.WriteReader(key, filepath.Base(filename), stat.Size(), fh)
}
// SetupRequest sets up the http.Request body, and some crucial HTTP headers.
func (m *MultipartStreamer) SetupRequest(req *http.Request) {
req.Body = m.GetReader()
req.Header.Add("Content-Type", m.ContentType)
req.ContentLength = m.Len()
}
func (m *MultipartStreamer) Boundary() string {
return m.bodyWriter.Boundary()
}
// Len calculates the byte size of the multipart content.
func (m *MultipartStreamer) Len() int64 {
return m.contentLength + int64(m.bodyBuffer.Len()) + int64(m.closeBuffer.Len())
}
// GetReader gets an io.ReadCloser for passing to an http.Request.
func (m *MultipartStreamer) GetReader() io.ReadCloser {
reader := io.MultiReader(m.bodyBuffer, m.reader, m.closeBuffer)
return ioutil.NopCloser(reader)
}

26
vendor/manifest vendored
View File

@ -59,6 +59,14 @@
"branch": "master",
"notests": true
},
{
"importpath": "github.com/go-telegram-bot-api/telegram-bot-api",
"repository": "https://github.com/go-telegram-bot-api/telegram-bot-api",
"vcs": "git",
"revision": "a7f48eb2dd301356942677e65bebe0c9aef07013",
"branch": "master",
"notests": true
},
{
"importpath": "github.com/gorilla/schema",
"repository": "https://github.com/gorilla/schema",
@ -87,8 +95,8 @@
"importpath": "github.com/mattermost/platform/einterfaces",
"repository": "https://github.com/mattermost/platform",
"vcs": "git",
"revision": "57f25fa59c71821cc38fd220b133aa6a40815e12",
"branch": "release-3.4",
"revision": "b55ec6148caa93d54b660afe55408c643d217108",
"branch": "release-3.5",
"path": "/einterfaces",
"notests": true
},
@ -96,8 +104,8 @@
"importpath": "github.com/mattermost/platform/model",
"repository": "https://github.com/mattermost/platform",
"vcs": "git",
"revision": "57f25fa59c71821cc38fd220b133aa6a40815e12",
"branch": "release-3.4",
"revision": "b55ec6148caa93d54b660afe55408c643d217108",
"branch": "release-3.5",
"path": "/model",
"notests": true
},
@ -105,7 +113,7 @@
"importpath": "github.com/mattn/go-xmpp",
"repository": "https://github.com/mattn/go-xmpp",
"vcs": "git",
"revision": "e44d1877bb457f5c3991903e9934a31e55c3a2ad",
"revision": "f4550b5399387339df5ce4c3f88c1ef85333bdd5",
"branch": "master",
"notests": true
},
@ -166,6 +174,14 @@
"branch": "master",
"notests": true
},
{
"importpath": "github.com/technoweenie/multipartstreamer",
"repository": "https://github.com/technoweenie/multipartstreamer",
"vcs": "git",
"revision": "a90a01d73ae432e2611d178c18367fbaa13e0154",
"branch": "master",
"notests": true
},
{
"importpath": "github.com/thoj/go-ircevent",
"repository": "https://github.com/thoj/go-ircevent",