4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-06-26 17:49:23 +00:00

Compare commits

...

30 Commits

Author SHA1 Message Date
Wim
a7a4554a85 Release v1.7.0 2018-01-28 19:36:02 +01:00
Wim
6bd808ce91 Lowercase irc channels in config. Closes #348 2018-01-28 19:15:13 +01:00
Wim
a5c143bc46 Allow xmpp to receive the extra messages when text is empty. #295 2018-01-27 16:32:38 +01:00
87c9cac756 Use cmosh/alpine-arm to build arm docker images (#347) 2018-01-27 13:49:13 +01:00
Wim
6a047f8722 Print only debug messages when specified (xmpp). Closes #345 2018-01-26 21:54:09 +01:00
Wim
6523494e83 Obey the Gateway value from the json (api). Closes #344 2018-01-21 12:21:55 +01:00
Wim
7c6ce8bb90 Fix xmpp badge, add twitch badge 2018-01-20 23:59:54 +01:00
Wim
dafbfe4021 Add twitch support (irc) to README 2018-01-20 23:38:58 +01:00
Wim
a4d5c94d9b Make edits/delete work for bridges that gets reused. Closes #342 2018-01-20 21:58:59 +01:00
Wim
7119e378a7 Add an extension to images without one (matrix). #331 2018-01-20 18:19:17 +01:00
Wim
e1dc3032c1 Ignore <subject> messages (xmpp). #272 2018-01-14 23:43:34 +01:00
Wim
5de03b8921 Update xmpp 2018-01-14 22:31:45 +01:00
Wim
7631d43c48 Change RemoteNickFormat replacement order. Closes #336 2018-01-14 16:55:32 +01:00
Wim
d0b2ee5c85 Add support for docker arm builds. #328 2018-01-10 00:04:24 +01:00
Wim
8830a5a1df Fix possible panics (matrix). Closes #333 2018-01-09 23:25:58 +01:00
Wim
ee87626a93 Update for 1.6.3 2018-01-09 00:13:46 +01:00
Wim
9f15d38c1c Use upstream again (slack) 2018-01-08 22:41:58 +01:00
Wim
4a96a977c0 Update vendor (slack) 2018-01-08 22:41:38 +01:00
9a95293bdf Convert received IRC channel names to lowercase. Fixes #329 (#330) 2018-01-06 22:55:03 +01:00
Wim
0b3a06d263 Log ConnectionErrorEvent (slack) 2018-01-03 14:06:28 +01:00
Wim
9a6249c4f5 Increase debug logging (slack) 2018-01-02 14:39:27 +01:00
Wim
50bd51e461 Use a better check to join channel (slack) 2018-01-02 14:31:44 +01:00
Wim
04f8013314 Bump version 2018-01-01 15:13:05 +01:00
Wim
a0aaf0057a Update for 1.6.2 2018-01-01 15:12:32 +01:00
Wim
8e78b3e6be Fix regression in mattermost bridge (mattermost). Closes #327 2018-01-01 14:20:16 +01:00
Wim
57a503818d Release v1.6.1 2017-12-26 19:22:50 +01:00
Wim
25d2ff3e9b Fix regression. Closes #323 2017-12-26 19:13:27 +01:00
Wim
31902d3e57 Add support for deleting messages from/to matrix (matrix). Closes #320 2017-12-25 00:55:39 +01:00
Wim
16f3fa6bae Vendor github.com/matterbridge/gomatrix 2017-12-25 00:54:39 +01:00
Wim
1f706673cf Bump version 2017-12-23 00:53:12 +01:00
80 changed files with 2661 additions and 239 deletions

View File

@ -1,7 +1,7 @@
# matterbridge
Click on one of the badges below to join the chat
[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?colorB=42f4242)](https://gitter.im/42wim/matterbridge) [![Join the IRC chat at https://webchat.freenode.net/?channels=matterbridgechat](https://img.shields.io/badge/IRC-matterbridgechat-green.svg?colorB=42f4242)](https://webchat.freenode.net/?channels=matterbridgechat) [![Discord](https://img.shields.io/badge/discord-matterbridge-green.svg?colorB=42f4242)](https://discord.gg/AkKPtrQ) [![Matrix](https://img.shields.io/badge/matrix-matterbridge-green.svg?colorB=42f4242)](https://riot.im/app/#/room/#matterbridge:matrix.org) [![Slack](https://img.shields.io/badge/slack-matterbridgechat-green.svg?colorB=42f4242)](https://join.slack.com/matterbridgechat/shared_invite/MjEwODMxNjU1NDMwLTE0OTk2MTU3NTMtMzZkZmRiNDZhOA) [![Mattermost](https://img.shields.io/badge/mattermost-matterbridge-green.svg?colorB=42f4242)](https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e) ![Xmpp](https://img.shields.io/badge/xmpp-matterbridge@muc.im.koderoot.net-green.svg?colorB=42f4242)
[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?colorB=42f4242)](https://gitter.im/42wim/matterbridge) [![Join the IRC chat at https://webchat.freenode.net/?channels=matterbridgechat](https://img.shields.io/badge/IRC-matterbridgechat-green.svg?colorB=42f4242)](https://webchat.freenode.net/?channels=matterbridgechat) [![Discord](https://img.shields.io/badge/discord-matterbridge-green.svg?colorB=42f4242)](https://discord.gg/AkKPtrQ) [![Matrix](https://img.shields.io/badge/matrix-matterbridge-green.svg?colorB=42f4242)](https://riot.im/app/#/room/#matterbridge:matrix.org) [![Slack](https://img.shields.io/badge/slack-matterbridgechat-green.svg?colorB=42f4242)](https://join.slack.com/matterbridgechat/shared_invite/MjEwODMxNjU1NDMwLTE0OTk2MTU3NTMtMzZkZmRiNDZhOA) [![Mattermost](https://img.shields.io/badge/mattermost-matterbridge-green.svg?colorB=42f4242)](https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e) [![Xmpp](https://img.shields.io/badge/xmpp-matterbridge@conference.jabber.de-green.svg?colorB=42f4242)](https://inverse.chat) [![Twitch](https://img.shields.io/badge/twitch-matterbridge-green.svg?colorB=42f4242)](https://www.twitch.tv/matterbridge)
[![Download stable](https://img.shields.io/github/release/42wim/matterbridge.svg?label=download%20stable)](https://github.com/42wim/matterbridge/releases/latest) [![Download dev](https://img.shields.io/bintray/v/42wim/nightly/Matterbridge.svg?label=download%20dev&colorB=007ec6)](https://bintray.com/42wim/nightly/Matterbridge/_latestVersion)
@ -48,13 +48,14 @@ Accounts to one of the supported bridges
* [Rocket.chat](https://rocket.chat)
* [Matrix](https://matrix.org)
* [Steam](https://store.steampowered.com/)
* [Twitch](https://twitch.tv)
# Screenshots
See https://github.com/42wim/matterbridge/wiki
# Installing
## Binaries
* Latest stable release [v1.6.0](https://github.com/42wim/matterbridge/releases/latest)
* Latest stable release [v1.7.0](https://github.com/42wim/matterbridge/releases/latest)
* Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/)
## Building

View File

@ -88,6 +88,7 @@ func New(cfg *config.Config, bridge *config.Bridge, c chan config.Message) *Brid
bridgeConfig.Config = cfg.Api[name]
b.Bridger = api.New(bridgeConfig)
}
b.Config = bridgeConfig.Config
return b
}

View File

@ -53,6 +53,7 @@ type Protocol struct {
BindAddress string // mattermost, slack // DEPRECATED
Buffer int // api
Charset string // irc
Debug bool // general
EditSuffix string // mattermost, slack, discord, telegram, gitter
EditDisable bool // mattermost, slack, discord, telegram, gitter
IconURL string // mattermost, slack

View File

@ -323,7 +323,7 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
if event.Source.Name == b.Nick {
return
}
rmsg := config.Message{Username: event.Source.Name, Channel: event.Params[0], Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host}
rmsg := config.Message{Username: event.Source.Name, Channel: strings.ToLower(event.Params[0]), Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host}
flog.Debugf("handlePrivMsg() %s %s %#v", event.Source.Name, event.Trailing, event)
msg := ""
if event.IsAction() {

View File

@ -10,7 +10,7 @@ import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
log "github.com/Sirupsen/logrus"
matrix "github.com/matrix-org/gomatrix"
matrix "github.com/matterbridge/gomatrix"
)
type Bmatrix struct {
@ -75,16 +75,26 @@ func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error {
func (b *Bmatrix) Send(msg config.Message) (string, error) {
flog.Debugf("Receiving %#v", msg)
channel := b.getRoomID(msg.Channel)
// ignore delete messages
if msg.Event == config.EVENT_MSG_DELETE {
return "", nil
if msg.ID == "" {
return "", nil
}
resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{})
if err != nil {
return "", err
}
return resp.EventID, err
}
channel := b.getRoomID(msg.Channel)
flog.Debugf("Sending to channel %s", channel)
if msg.Event == config.EVENT_USER_ACTION {
b.mc.SendMessageEvent(channel, "m.room.message",
resp, err := b.mc.SendMessageEvent(channel, "m.room.message",
matrix.TextMessage{"m.emote", msg.Username + msg.Text})
return "", nil
if err != nil {
return "", err
}
return resp.EventID, err
}
if msg.Extra != nil {
@ -124,8 +134,11 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
}
}
b.mc.SendText(channel, msg.Username+msg.Text)
return "", nil
resp, err := b.mc.SendText(channel, msg.Username+msg.Text)
if err != nil {
return "", err
}
return resp.EventID, err
}
func (b *Bmatrix) getRoomID(channel string) string {
@ -138,58 +151,11 @@ func (b *Bmatrix) getRoomID(channel string) string {
}
return ""
}
func (b *Bmatrix) handlematrix() error {
syncer := b.mc.Syncer.(*matrix.DefaultSyncer)
syncer.OnEventType("m.room.message", func(ev *matrix.Event) {
flog.Debugf("Received: %#v", ev)
if (ev.Content["msgtype"].(string) == "m.text" ||
ev.Content["msgtype"].(string) == "m.notice" ||
ev.Content["msgtype"].(string) == "m.emote" ||
ev.Content["msgtype"].(string) == "m.file" ||
ev.Content["msgtype"].(string) == "m.image" ||
ev.Content["msgtype"].(string) == "m.video") && ev.Sender != b.UserID {
b.RLock()
channel, ok := b.RoomMap[ev.RoomID]
b.RUnlock()
if !ok {
flog.Debugf("Unknown room %s", ev.RoomID)
return
}
username := ev.Sender[1:]
if b.Config.NoHomeServerSuffix {
re := regexp.MustCompile("(.*?):.*")
username = re.ReplaceAllString(username, `$1`)
}
rmsg := config.Message{Username: username, Text: ev.Content["body"].(string), Channel: channel, Account: b.Account, UserID: ev.Sender}
if ev.Content["msgtype"].(string) == "m.emote" {
rmsg.Event = config.EVENT_USER_ACTION
}
if ev.Content["msgtype"].(string) == "m.image" ||
ev.Content["msgtype"].(string) == "m.video" ||
ev.Content["msgtype"].(string) == "m.file" {
flog.Debugf("ev: %#v", ev)
rmsg.Extra = make(map[string][]interface{})
url := ev.Content["url"].(string)
url = strings.Replace(url, "mxc://", b.Config.Server+"/_matrix/media/v1/download/", -1)
info := ev.Content["info"].(map[string]interface{})
size := info["size"].(float64)
name := ev.Content["body"].(string)
flog.Debugf("trying to download %#v with size %#v", name, size)
if size <= float64(b.General.MediaDownloadSize) {
data, err := helper.DownloadFile(url)
if err != nil {
flog.Errorf("download %s failed %#v", url, err)
} else {
flog.Debugf("download OK %#v %#v %#v", name, len(*data), len(url))
rmsg.Extra["file"] = append(rmsg.Extra["file"], config.FileInfo{Name: name, Data: data})
}
}
rmsg.Text = ""
}
flog.Debugf("Sending message from %s on %s to gateway", ev.Sender, b.Account)
b.Remote <- rmsg
}
})
syncer.OnEventType("m.room.redaction", b.handleEvent)
syncer.OnEventType("m.room.message", b.handleEvent)
go func() {
for {
if err := b.mc.Sync(); err != nil {
@ -199,3 +165,73 @@ func (b *Bmatrix) handlematrix() error {
}()
return nil
}
func (b *Bmatrix) handleEvent(ev *matrix.Event) {
flog.Debugf("Received: %#v", ev)
if ev.Sender != b.UserID {
b.RLock()
channel, ok := b.RoomMap[ev.RoomID]
b.RUnlock()
if !ok {
flog.Debugf("Unknown room %s", ev.RoomID)
return
}
username := ev.Sender[1:]
if b.Config.NoHomeServerSuffix {
re := regexp.MustCompile("(.*?):.*")
username = re.ReplaceAllString(username, `$1`)
}
var text string
text, _ = ev.Content["body"].(string)
rmsg := config.Message{Username: username, Text: text, Channel: channel, Account: b.Account, UserID: ev.Sender}
rmsg.ID = ev.ID
if ev.Type == "m.room.redaction" {
rmsg.Event = config.EVENT_MSG_DELETE
rmsg.ID = ev.Redacts
rmsg.Text = config.EVENT_MSG_DELETE
b.Remote <- rmsg
return
}
if ev.Content["msgtype"].(string) == "m.emote" {
rmsg.Event = config.EVENT_USER_ACTION
}
if ev.Content["msgtype"] != nil && ev.Content["msgtype"].(string) == "m.image" ||
ev.Content["msgtype"].(string) == "m.video" ||
ev.Content["msgtype"].(string) == "m.file" {
flog.Debugf("ev: %#v", ev)
rmsg.Extra = make(map[string][]interface{})
url := ev.Content["url"].(string)
url = strings.Replace(url, "mxc://", b.Config.Server+"/_matrix/media/v1/download/", -1)
info := ev.Content["info"].(map[string]interface{})
size := info["size"].(float64)
name := ev.Content["body"].(string)
// check if we have an image uploaded without extension
if !strings.Contains(name, ".") {
if ev.Content["msgtype"].(string) == "m.image" {
if mtype, ok := ev.Content["mimetype"].(string); ok {
mext, _ := mime.ExtensionsByType(mtype)
if len(mext) > 0 {
name = name + mext[0]
}
} else {
// just a default .png extension if we don't have mime info
name = name + ".png"
}
}
}
flog.Debugf("trying to download %#v with size %#v", name, size)
if size <= float64(b.General.MediaDownloadSize) {
data, err := helper.DownloadFile(url)
if err != nil {
flog.Errorf("download %s failed %#v", url, err)
} else {
flog.Debugf("download OK %#v %#v %#v", name, len(*data), len(url))
rmsg.Extra["file"] = append(rmsg.Extra["file"], config.FileInfo{Name: name, Data: data})
}
}
rmsg.Text = ""
}
flog.Debugf("Sending message from %s on %s to gateway", ev.Sender, b.Account)
b.Remote <- rmsg
}
}

View File

@ -32,10 +32,7 @@ type MMMessage struct {
type Bmattermost struct {
MMhook
MMapi
Config *config.Protocol
Remote chan config.Message
TeamId string
Account string
TeamId string
*config.BridgeConfig
}

View File

@ -7,7 +7,7 @@ import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus"
"github.com/matterbridge/slack"
"github.com/nlopes/slack"
"html"
"io"
"net/http"
@ -107,7 +107,7 @@ func (b *Bslack) Disconnect() error {
func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
// we can only join channels using the API
if b.Config.WebhookURL == "" && b.Config.WebhookBindAddress == "" {
if b.sc != nil {
if strings.HasPrefix(b.Config.Token, "xoxb") {
// TODO check if bot has already joined channel
return nil
@ -309,9 +309,11 @@ func (b *Bslack) handleSlack() {
func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
for msg := range b.rtm.IncomingEvents {
if msg.Type != "user_typing" && msg.Type != "latency_report" {
flog.Debugf("Receiving from slackclient %#v", msg.Data)
}
switch ev := msg.Data.(type) {
case *slack.MessageEvent:
flog.Debugf("Receiving from slackclient %#v", ev)
if len(ev.Attachments) > 0 {
// skip messages we made ourselves
if ev.Attachments[0].CallbackID == "matterbridge" {
@ -394,6 +396,8 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
}
case *slack.InvalidAuthEvent:
flog.Fatalf("Invalid Token %#v", ev)
case *slack.ConnectionErrorEvent:
flog.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj)
default:
}
}

View File

@ -110,7 +110,7 @@ func (b *Bxmpp) createXMPP() (*xmpp.Client, error) {
TLSConfig: tc,
//StartTLS: false,
Debug: true,
Debug: b.General.Debug,
Session: true,
Status: "",
StatusMessage: "",
@ -166,7 +166,7 @@ func (b *Bxmpp) handleXmpp() error {
if len(s) == 2 {
nick = s[1]
}
if nick != b.Config.Nick && v.Stamp == nodelay && v.Text != "" {
if nick != b.Config.Nick && v.Stamp == nodelay && v.Text != "" && !strings.Contains(v.Text, "</subject>") {
rmsg := config.Message{Username: nick, Text: v.Text, Channel: channel, Account: b.Account, UserID: v.Remote}
rmsg.Text, ok = b.replaceAction(rmsg.Text)
if ok {

View File

@ -1,3 +1,33 @@
# v1.7.0
## New features
* matrix: Add support for deleting messages from/to matrix (matrix). Closes #320
* xmpp: Ignore <subject> messages (xmpp). #272
* irc: Add twitch support (irc) to README / wiki
## Bugfix
* general: Change RemoteNickFormat replacement order. Closes #336
* general: Make edits/delete work for bridges that gets reused. Closes #342
* general: Lowercase irc channels in config. Closes #348
* matrix: Fix possible panics (matrix). Closes #333
* matrix: Add an extension to images without one (matrix). #331
* api: Obey the Gateway value from the json (api). Closes #344
* xmpp: Print only debug messages when specified (xmpp). Closes #345
* xmpp: Allow xmpp to receive the extra messages (file uploads) when text is empty. #295
# v1.6.3
## Bugfix
* slack: Fix connection issues
* slack: Add more debug messages
* irc: Convert received IRC channel names to lowercase. Fixes #329 (#330)
# v1.6.2
## Bugfix
* mattermost: Crashes while connecting to Mattermost (regression). Closes #327
# v1.6.1
## Bugfix
* general: Display of nicks not longer working (regression). Closes #323
# v1.6.0
## New features
* sshchat: New protocol support added (https://github.com/shazow/ssh-chat)

11
docker/arm/Dockerfile Normal file
View File

@ -0,0 +1,11 @@
FROM cmosh/alpine-arm:edge
ENTRYPOINT ["/bin/matterbridge"]
COPY . /go/src/github.com/42wim/matterbridge
RUN apk update && apk add go git gcc musl-dev ca-certificates \
&& cd /go/src/github.com/42wim/matterbridge \
&& export GOPATH=/go \
&& go get \
&& go build -x -ldflags "-X main.githash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge \
&& rm -rf /go \
&& apk del --purge git go gcc musl-dev

View File

@ -29,8 +29,9 @@ type Gateway struct {
}
type BrMsgID struct {
br *bridge.Bridge
ID string
br *bridge.Bridge
ID string
ChannelID string
}
func New(cfg config.Gateway, r *Router) *Gateway {
@ -93,6 +94,10 @@ func (gw *Gateway) mapChannelConfig(cfg []config.Bridge, direction string) {
if isApi(br.Account) {
br.Channel = "api"
}
// make sure to lowercase irc channels in config #348
if strings.HasPrefix(br.Account, "irc.") {
br.Channel = strings.ToLower(br.Channel)
}
ID := br.Channel + br.Account
if _, ok := gw.Channels[ID]; !ok {
channel := &config.ChannelInfo{Name: br.Channel, Direction: direction, ID: ID, Options: br.Options, Account: br.Account,
@ -118,6 +123,12 @@ func (gw *Gateway) mapChannels() error {
func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []config.ChannelInfo {
var channels []config.ChannelInfo
// for messages received from the api check that the gateway is the specified one
if msg.Protocol == "api" && gw.Name != msg.Gateway {
return channels
}
// if source channel is in only, do nothing
for _, channel := range gw.Channels {
// lookup the channel from the message
@ -159,7 +170,8 @@ func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrM
dest.Protocol != "slack" &&
dest.Protocol != "mattermost" &&
dest.Protocol != "telegram" &&
dest.Protocol != "matrix" {
dest.Protocol != "matrix" &&
dest.Protocol != "xmpp" {
if msg.Text == "" {
return brMsgIDs
}
@ -190,7 +202,9 @@ func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrM
if res, ok := gw.Messages.Get(origmsg.ID); ok {
IDs := res.([]*BrMsgID)
for _, id := range IDs {
if dest.Protocol == id.br.Protocol {
// check protocol, bridge name and channelname
// for people that reuse the same bridge multiple times. see #342
if dest.Protocol == id.br.Protocol && dest.Name == id.br.Name && channel.ID == id.ChannelID {
msg.ID = id.ID
}
}
@ -205,7 +219,7 @@ func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrM
}
// append the message ID (mID) from this bridge (dest) to our brMsgIDs slice
if mID != "" {
brMsgIDs = append(brMsgIDs, &BrMsgID{dest, mID})
brMsgIDs = append(brMsgIDs, &BrMsgID{dest, mID, channel.ID})
}
}
return brMsgIDs
@ -284,9 +298,9 @@ func (gw *Gateway) modifyUsername(msg config.Message, dest *bridge.Bridge) strin
}
nick = strings.Replace(nick, "{NOPINGNICK}", msg.Username[:i]+""+msg.Username[i:], -1)
}
nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1)
nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1)
nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
return nick
}
@ -318,7 +332,11 @@ func (gw *Gateway) modifyMessage(msg *config.Message) {
}
msg.Text = re.ReplaceAllString(msg.Text, replace)
}
msg.Gateway = gw.Name
// messages from api have Gateway specified, don't overwrite
if msg.Protocol != "api" {
msg.Gateway = gw.Name
}
}
func (gw *Gateway) handleFiles(msg *config.Message) {

View File

@ -12,7 +12,7 @@ import (
)
var (
version = "1.6.0"
version = "1.7.0"
githash string
)
@ -43,6 +43,7 @@ func main() {
log.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.")
}
cfg := config.NewConfig(*flagConfig)
cfg.General.Debug = *flagDebug
r, err := gateway.NewRouter(cfg)
if err != nil {
log.Fatalf("Starting gateway failed: %s", err)

201
vendor/github.com/matterbridge/gomatrix/LICENSE generated vendored Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

703
vendor/github.com/matterbridge/gomatrix/client.go generated vendored Normal file
View File

@ -0,0 +1,703 @@
// Package gomatrix implements the Matrix Client-Server API.
//
// Specification can be found at http://matrix.org/docs/spec/client_server/r0.2.0.html
package gomatrix
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"path"
"strconv"
"sync"
"time"
)
// Client represents a Matrix client.
type Client struct {
HomeserverURL *url.URL // The base homeserver URL
Prefix string // The API prefix eg '/_matrix/client/r0'
UserID string // The user ID of the client. Used for forming HTTP paths which use the client's user ID.
AccessToken string // The access_token for the client.
Client *http.Client // The underlying HTTP client which will be used to make HTTP requests.
Syncer Syncer // The thing which can process /sync responses
Store Storer // The thing which can store rooms/tokens/ids
// The ?user_id= query parameter for application services. This must be set *prior* to calling a method. If this is empty,
// no user_id parameter will be sent.
// See http://matrix.org/docs/spec/application_service/unstable.html#identity-assertion
AppServiceUserID string
syncingMutex sync.Mutex // protects syncingID
syncingID uint32 // Identifies the current Sync. Only one Sync can be active at any given time.
}
// HTTPError An HTTP Error response, which may wrap an underlying native Go Error.
type HTTPError struct {
WrappedError error
Message string
Code int
}
func (e HTTPError) Error() string {
var wrappedErrMsg string
if e.WrappedError != nil {
wrappedErrMsg = e.WrappedError.Error()
}
return fmt.Sprintf("msg=%s code=%d wrapped=%s", e.Message, e.Code, wrappedErrMsg)
}
// BuildURL builds a URL with the Client's homserver/prefix/access_token set already.
func (cli *Client) BuildURL(urlPath ...string) string {
ps := []string{cli.Prefix}
for _, p := range urlPath {
ps = append(ps, p)
}
return cli.BuildBaseURL(ps...)
}
// BuildBaseURL builds a URL with the Client's homeserver/access_token set already. You must
// supply the prefix in the path.
func (cli *Client) BuildBaseURL(urlPath ...string) string {
// copy the URL. Purposefully ignore error as the input is from a valid URL already
hsURL, _ := url.Parse(cli.HomeserverURL.String())
parts := []string{hsURL.Path}
parts = append(parts, urlPath...)
hsURL.Path = path.Join(parts...)
query := hsURL.Query()
if cli.AccessToken != "" {
query.Set("access_token", cli.AccessToken)
}
if cli.AppServiceUserID != "" {
query.Set("user_id", cli.AppServiceUserID)
}
hsURL.RawQuery = query.Encode()
return hsURL.String()
}
// BuildURLWithQuery builds a URL with query parameters in addition to the Client's homeserver/prefix/access_token set already.
func (cli *Client) BuildURLWithQuery(urlPath []string, urlQuery map[string]string) string {
u, _ := url.Parse(cli.BuildURL(urlPath...))
q := u.Query()
for k, v := range urlQuery {
q.Set(k, v)
}
u.RawQuery = q.Encode()
return u.String()
}
// SetCredentials sets the user ID and access token on this client instance.
func (cli *Client) SetCredentials(userID, accessToken string) {
cli.AccessToken = accessToken
cli.UserID = userID
}
// ClearCredentials removes the user ID and access token on this client instance.
func (cli *Client) ClearCredentials() {
cli.AccessToken = ""
cli.UserID = ""
}
// Sync starts syncing with the provided Homeserver. If Sync() is called twice then the first sync will be stopped and the
// error will be nil.
//
// This function will block until a fatal /sync error occurs, so it should almost always be started as a new goroutine.
// Fatal sync errors can be caused by:
// - The failure to create a filter.
// - Client.Syncer.OnFailedSync returning an error in response to a failed sync.
// - Client.Syncer.ProcessResponse returning an error.
// If you wish to continue retrying in spite of these fatal errors, call Sync() again.
func (cli *Client) Sync() error {
// Mark the client as syncing.
// We will keep syncing until the syncing state changes. Either because
// Sync is called or StopSync is called.
syncingID := cli.incrementSyncingID()
nextBatch := cli.Store.LoadNextBatch(cli.UserID)
filterID := cli.Store.LoadFilterID(cli.UserID)
if filterID == "" {
filterJSON := cli.Syncer.GetFilterJSON(cli.UserID)
resFilter, err := cli.CreateFilter(filterJSON)
if err != nil {
return err
}
filterID = resFilter.FilterID
cli.Store.SaveFilterID(cli.UserID, filterID)
}
for {
resSync, err := cli.SyncRequest(30000, nextBatch, filterID, false, "")
if err != nil {
duration, err2 := cli.Syncer.OnFailedSync(resSync, err)
if err2 != nil {
return err2
}
time.Sleep(duration)
continue
}
// Check that the syncing state hasn't changed
// Either because we've stopped syncing or another sync has been started.
// We discard the response from our sync.
if cli.getSyncingID() != syncingID {
return nil
}
// Save the token now *before* processing it. This means it's possible
// to not process some events, but it means that we won't get constantly stuck processing
// a malformed/buggy event which keeps making us panic.
cli.Store.SaveNextBatch(cli.UserID, resSync.NextBatch)
if err = cli.Syncer.ProcessResponse(resSync, nextBatch); err != nil {
return err
}
nextBatch = resSync.NextBatch
}
}
func (cli *Client) incrementSyncingID() uint32 {
cli.syncingMutex.Lock()
defer cli.syncingMutex.Unlock()
cli.syncingID++
return cli.syncingID
}
func (cli *Client) getSyncingID() uint32 {
cli.syncingMutex.Lock()
defer cli.syncingMutex.Unlock()
return cli.syncingID
}
// StopSync stops the ongoing sync started by Sync.
func (cli *Client) StopSync() {
// Advance the syncing state so that any running Syncs will terminate.
cli.incrementSyncingID()
}
// MakeRequest makes a JSON HTTP request to the given URL.
// If "resBody" is not nil, the response body will be json.Unmarshalled into it.
//
// Returns the HTTP body as bytes on 2xx with a nil error. Returns an error if the response is not 2xx along
// with the HTTP body bytes if it got that far. This error is an HTTPError which includes the returned
// HTTP status code and possibly a RespError as the WrappedError, if the HTTP body could be decoded as a RespError.
func (cli *Client) MakeRequest(method string, httpURL string, reqBody interface{}, resBody interface{}) ([]byte, error) {
var req *http.Request
var err error
if reqBody != nil {
var jsonStr []byte
jsonStr, err = json.Marshal(reqBody)
if err != nil {
return nil, err
}
req, err = http.NewRequest(method, httpURL, bytes.NewBuffer(jsonStr))
} else {
req, err = http.NewRequest(method, httpURL, nil)
}
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
res, err := cli.Client.Do(req)
if res != nil {
defer res.Body.Close()
}
if err != nil {
return nil, err
}
contents, err := ioutil.ReadAll(res.Body)
if res.StatusCode/100 != 2 { // not 2xx
var wrap error
var respErr RespError
if _ = json.Unmarshal(contents, &respErr); respErr.ErrCode != "" {
wrap = respErr
}
// If we failed to decode as RespError, don't just drop the HTTP body, include it in the
// HTTP error instead (e.g proxy errors which return HTML).
msg := "Failed to " + method + " JSON to " + req.URL.Path
if wrap == nil {
msg = msg + ": " + string(contents)
}
return contents, HTTPError{
Code: res.StatusCode,
Message: msg,
WrappedError: wrap,
}
}
if err != nil {
return nil, err
}
if resBody != nil {
if err = json.Unmarshal(contents, &resBody); err != nil {
return nil, err
}
}
return contents, nil
}
// CreateFilter makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-user-userid-filter
func (cli *Client) CreateFilter(filter json.RawMessage) (resp *RespCreateFilter, err error) {
urlPath := cli.BuildURL("user", cli.UserID, "filter")
_, err = cli.MakeRequest("POST", urlPath, &filter, &resp)
return
}
// SyncRequest makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-sync
func (cli *Client) SyncRequest(timeout int, since, filterID string, fullState bool, setPresence string) (resp *RespSync, err error) {
query := map[string]string{
"timeout": strconv.Itoa(timeout),
}
if since != "" {
query["since"] = since
}
if filterID != "" {
query["filter"] = filterID
}
if setPresence != "" {
query["set_presence"] = setPresence
}
if fullState {
query["full_state"] = "true"
}
urlPath := cli.BuildURLWithQuery([]string{"sync"}, query)
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
func (cli *Client) register(u string, req *ReqRegister) (resp *RespRegister, uiaResp *RespUserInteractive, err error) {
var bodyBytes []byte
bodyBytes, err = cli.MakeRequest("POST", u, req, nil)
if err != nil {
httpErr, ok := err.(HTTPError)
if !ok { // network error
return
}
if httpErr.Code == 401 {
// body should be RespUserInteractive, if it isn't, fail with the error
err = json.Unmarshal(bodyBytes, &uiaResp)
return
}
return
}
// body should be RespRegister
err = json.Unmarshal(bodyBytes, &resp)
return
}
// Register makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
//
// Registers with kind=user. For kind=guest, see RegisterGuest.
func (cli *Client) Register(req *ReqRegister) (*RespRegister, *RespUserInteractive, error) {
u := cli.BuildURL("register")
return cli.register(u, req)
}
// RegisterGuest makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
// with kind=guest.
//
// For kind=user, see Register.
func (cli *Client) RegisterGuest(req *ReqRegister) (*RespRegister, *RespUserInteractive, error) {
query := map[string]string{
"kind": "guest",
}
u := cli.BuildURLWithQuery([]string{"register"}, query)
return cli.register(u, req)
}
// RegisterDummy performs m.login.dummy registration according to https://matrix.org/docs/spec/client_server/r0.2.0.html#dummy-auth
//
// Only a username and password need to be provided on the ReqRegister struct. Most local/developer homeservers will allow registration
// this way. If the homeserver does not, an error is returned.
//
// This does not set credentials on the client instance. See SetCredentials() instead.
//
// res, err := cli.RegisterDummy(&gomatrix.ReqRegister{
// Username: "alice",
// Password: "wonderland",
// })
// if err != nil {
// panic(err)
// }
// token := res.AccessToken
func (cli *Client) RegisterDummy(req *ReqRegister) (*RespRegister, error) {
res, uia, err := cli.Register(req)
if err != nil && uia == nil {
return nil, err
}
if uia != nil && uia.HasSingleStageFlow("m.login.dummy") {
req.Auth = struct {
Type string `json:"type"`
Session string `json:"session,omitempty"`
}{"m.login.dummy", uia.Session}
res, _, err = cli.Register(req)
if err != nil {
return nil, err
}
}
if res == nil {
return nil, fmt.Errorf("registration failed: does this server support m.login.dummy?")
}
return res, nil
}
// Login a user to the homeserver according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-login
// This does not set credentials on this client instance. See SetCredentials() instead.
func (cli *Client) Login(req *ReqLogin) (resp *RespLogin, err error) {
urlPath := cli.BuildURL("login")
_, err = cli.MakeRequest("POST", urlPath, req, &resp)
return
}
// Logout the current user. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-logout
// This does not clear the credentials from the client instance. See ClearCredentials() instead.
func (cli *Client) Logout() (resp *RespLogout, err error) {
urlPath := cli.BuildURL("logout")
_, err = cli.MakeRequest("POST", urlPath, nil, &resp)
return
}
// Versions returns the list of supported Matrix versions on this homeserver. See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-versions
func (cli *Client) Versions() (resp *RespVersions, err error) {
urlPath := cli.BuildBaseURL("_matrix", "client", "versions")
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
// JoinRoom joins the client to a room ID or alias. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-join-roomidoralias
//
// If serverName is specified, this will be added as a query param to instruct the homeserver to join via that server. If content is specified, it will
// be JSON encoded and used as the request body.
func (cli *Client) JoinRoom(roomIDorAlias, serverName string, content interface{}) (resp *RespJoinRoom, err error) {
var urlPath string
if serverName != "" {
urlPath = cli.BuildURLWithQuery([]string{"join", roomIDorAlias}, map[string]string{
"server_name": serverName,
})
} else {
urlPath = cli.BuildURL("join", roomIDorAlias)
}
_, err = cli.MakeRequest("POST", urlPath, content, &resp)
return
}
// GetDisplayName returns the display name of the user from the specified MXID. See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname
func (cli *Client) GetDisplayName(mxid string) (resp *RespUserDisplayName, err error) {
urlPath := cli.BuildURL("profile", mxid, "displayname")
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
// GetOwnDisplayName returns the user's display name. See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname
func (cli *Client) GetOwnDisplayName() (resp *RespUserDisplayName, err error) {
urlPath := cli.BuildURL("profile", cli.UserID, "displayname")
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
// SetDisplayName sets the user's profile display name. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-profile-userid-displayname
func (cli *Client) SetDisplayName(displayName string) (err error) {
urlPath := cli.BuildURL("profile", cli.UserID, "displayname")
s := struct {
DisplayName string `json:"displayname"`
}{displayName}
_, err = cli.MakeRequest("PUT", urlPath, &s, nil)
return
}
// GetAvatarURL gets the user's avatar URL. See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-avatar-url
func (cli *Client) GetAvatarURL() (url string, err error) {
urlPath := cli.BuildURL("profile", cli.UserID, "avatar_url")
s := struct {
AvatarURL string `json:"avatar_url"`
}{}
_, err = cli.MakeRequest("GET", urlPath, nil, &s)
if err != nil {
return "", err
}
return s.AvatarURL, nil
}
// SetAvatarURL sets the user's avatar URL. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-profile-userid-avatar-url
func (cli *Client) SetAvatarURL(url string) (err error) {
urlPath := cli.BuildURL("profile", cli.UserID, "avatar_url")
s := struct {
AvatarURL string `json:"avatar_url"`
}{url}
_, err = cli.MakeRequest("PUT", urlPath, &s, nil)
if err != nil {
return err
}
return nil
}
// SendMessageEvent sends a message event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid
// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
func (cli *Client) SendMessageEvent(roomID string, eventType string, contentJSON interface{}) (resp *RespSendEvent, err error) {
txnID := txnID()
urlPath := cli.BuildURL("rooms", roomID, "send", eventType, txnID)
_, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp)
return
}
// SendStateEvent sends a state event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-state-eventtype-statekey
// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
func (cli *Client) SendStateEvent(roomID, eventType, stateKey string, contentJSON interface{}) (resp *RespSendEvent, err error) {
urlPath := cli.BuildURL("rooms", roomID, "state", eventType, stateKey)
_, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp)
return
}
// SendText sends an m.room.message event into the given room with a msgtype of m.text
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-text
func (cli *Client) SendText(roomID, text string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, "m.room.message",
TextMessage{"m.text", text})
}
// SendImage sends an m.room.message event into the given room with a msgtype of m.image
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-image
func (cli *Client) SendImage(roomID, body, url string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, "m.room.message",
ImageMessage{
MsgType: "m.image",
Body: body,
URL: url,
})
}
// SendVideo sends an m.room.message event into the given room with a msgtype of m.video
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
func (cli *Client) SendVideo(roomID, body, url string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, "m.room.message",
VideoMessage{
MsgType: "m.video",
Body: body,
URL: url,
})
}
// SendNotice sends an m.room.message event into the given room with a msgtype of m.notice
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-notice
func (cli *Client) SendNotice(roomID, text string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, "m.room.message",
TextMessage{"m.notice", text})
}
// RedactEvent redacts the given event. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid
func (cli *Client) RedactEvent(roomID, eventID string, req *ReqRedact) (resp *RespSendEvent, err error) {
txnID := txnID()
urlPath := cli.BuildURL("rooms", roomID, "redact", eventID, txnID)
_, err = cli.MakeRequest("PUT", urlPath, req, &resp)
return
}
// CreateRoom creates a new Matrix room. See https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
// resp, err := cli.CreateRoom(&gomatrix.ReqCreateRoom{
// Preset: "public_chat",
// })
// fmt.Println("Room:", resp.RoomID)
func (cli *Client) CreateRoom(req *ReqCreateRoom) (resp *RespCreateRoom, err error) {
urlPath := cli.BuildURL("createRoom")
_, err = cli.MakeRequest("POST", urlPath, req, &resp)
return
}
// LeaveRoom leaves the given room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-leave
func (cli *Client) LeaveRoom(roomID string) (resp *RespLeaveRoom, err error) {
u := cli.BuildURL("rooms", roomID, "leave")
_, err = cli.MakeRequest("POST", u, struct{}{}, &resp)
return
}
// ForgetRoom forgets a room entirely. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-forget
func (cli *Client) ForgetRoom(roomID string) (resp *RespForgetRoom, err error) {
u := cli.BuildURL("rooms", roomID, "forget")
_, err = cli.MakeRequest("POST", u, struct{}{}, &resp)
return
}
// InviteUser invites a user to a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-invite
func (cli *Client) InviteUser(roomID string, req *ReqInviteUser) (resp *RespInviteUser, err error) {
u := cli.BuildURL("rooms", roomID, "invite")
_, err = cli.MakeRequest("POST", u, struct{}{}, &resp)
return
}
// InviteUserByThirdParty invites a third-party identifier to a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#invite-by-third-party-id-endpoint
func (cli *Client) InviteUserByThirdParty(roomID string, req *ReqInvite3PID) (resp *RespInviteUser, err error) {
u := cli.BuildURL("rooms", roomID, "invite")
_, err = cli.MakeRequest("POST", u, req, &resp)
return
}
// KickUser kicks a user from a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-kick
func (cli *Client) KickUser(roomID string, req *ReqKickUser) (resp *RespKickUser, err error) {
u := cli.BuildURL("rooms", roomID, "kick")
_, err = cli.MakeRequest("POST", u, req, &resp)
return
}
// BanUser bans a user from a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-ban
func (cli *Client) BanUser(roomID string, req *ReqBanUser) (resp *RespBanUser, err error) {
u := cli.BuildURL("rooms", roomID, "ban")
_, err = cli.MakeRequest("POST", u, req, &resp)
return
}
// UnbanUser unbans a user from a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban
func (cli *Client) UnbanUser(roomID string, req *ReqUnbanUser) (resp *RespUnbanUser, err error) {
u := cli.BuildURL("rooms", roomID, "unban")
_, err = cli.MakeRequest("POST", u, req, &resp)
return
}
// UserTyping sets the typing status of the user. See https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid
func (cli *Client) UserTyping(roomID string, typing bool, timeout int64) (resp *RespTyping, err error) {
req := ReqTyping{Typing: typing, Timeout: timeout}
u := cli.BuildURL("rooms", roomID, "typing", cli.UserID)
_, err = cli.MakeRequest("PUT", u, req, &resp)
return
}
// StateEvent gets a single state event in a room. It will attempt to JSON unmarshal into the given "outContent" struct with
// the HTTP response body, or return an error.
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-state-eventtype-statekey
func (cli *Client) StateEvent(roomID, eventType, stateKey string, outContent interface{}) (err error) {
u := cli.BuildURL("rooms", roomID, "state", eventType, stateKey)
_, err = cli.MakeRequest("GET", u, nil, outContent)
return
}
// UploadLink uploads an HTTP URL and then returns an MXC URI.
func (cli *Client) UploadLink(link string) (*RespMediaUpload, error) {
res, err := cli.Client.Get(link)
if res != nil {
defer res.Body.Close()
}
if err != nil {
return nil, err
}
return cli.UploadToContentRepo(res.Body, res.Header.Get("Content-Type"), res.ContentLength)
}
// UploadToContentRepo uploads the given bytes to the content repository and returns an MXC URI.
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-media-r0-upload
func (cli *Client) UploadToContentRepo(content io.Reader, contentType string, contentLength int64) (*RespMediaUpload, error) {
req, err := http.NewRequest("POST", cli.BuildBaseURL("_matrix/media/r0/upload"), content)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", contentType)
req.ContentLength = contentLength
res, err := cli.Client.Do(req)
if res != nil {
defer res.Body.Close()
}
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
contents, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, HTTPError{
Message: "Upload request failed - Failed to read response body: " + err.Error(),
Code: res.StatusCode,
}
}
return nil, HTTPError{
Message: "Upload request failed: " + string(contents),
Code: res.StatusCode,
}
}
var m RespMediaUpload
if err := json.NewDecoder(res.Body).Decode(&m); err != nil {
return nil, err
}
return &m, nil
}
// JoinedMembers returns a map of joined room members. See TODO-SPEC. https://github.com/matrix-org/synapse/pull/1680
//
// In general, usage of this API is discouraged in favour of /sync, as calling this API can race with incoming membership changes.
// This API is primarily designed for application services which may want to efficiently look up joined members in a room.
func (cli *Client) JoinedMembers(roomID string) (resp *RespJoinedMembers, err error) {
u := cli.BuildURL("rooms", roomID, "joined_members")
_, err = cli.MakeRequest("GET", u, nil, &resp)
return
}
// JoinedRooms returns a list of rooms which the client is joined to. See TODO-SPEC. https://github.com/matrix-org/synapse/pull/1680
//
// In general, usage of this API is discouraged in favour of /sync, as calling this API can race with incoming membership changes.
// This API is primarily designed for application services which may want to efficiently look up joined rooms.
func (cli *Client) JoinedRooms() (resp *RespJoinedRooms, err error) {
u := cli.BuildURL("joined_rooms")
_, err = cli.MakeRequest("GET", u, nil, &resp)
return
}
// Messages returns a list of message and state events for a room. It uses
// pagination query parameters to paginate history in the room.
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-messages
func (cli *Client) Messages(roomID, from, to string, dir rune, limit int) (resp *RespMessages, err error) {
query := map[string]string{
"from": from,
"dir": string(dir),
}
if to != "" {
query["to"] = to
}
if limit != 0 {
query["limit"] = strconv.Itoa(limit)
}
urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "messages"}, query)
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
// TurnServer returns turn server details and credentials for the client to use when initiating calls.
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-voip-turnserver
func (cli *Client) TurnServer() (resp *RespTurnServer, err error) {
urlPath := cli.BuildURL("voip", "turnServer")
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
func txnID() string {
return "go" + strconv.FormatInt(time.Now().UnixNano(), 10)
}
// NewClient creates a new Matrix Client ready for syncing
func NewClient(homeserverURL, userID, accessToken string) (*Client, error) {
hsURL, err := url.Parse(homeserverURL)
if err != nil {
return nil, err
}
// By default, use an in-memory store which will never save filter ids / next batch tokens to disk.
// The client will work with this storer: it just won't remember across restarts.
// In practice, a database backend should be used.
store := NewInMemoryStore()
cli := Client{
AccessToken: accessToken,
HomeserverURL: hsURL,
UserID: userID,
Prefix: "/_matrix/client/r0",
Syncer: NewDefaultSyncer(userID, store),
Store: store,
}
// By default, use the default HTTP client.
cli.Client = http.DefaultClient
return &cli, nil
}

102
vendor/github.com/matterbridge/gomatrix/events.go generated vendored Normal file
View File

@ -0,0 +1,102 @@
package gomatrix
import (
"html"
"regexp"
)
// Event represents a single Matrix event.
type Event struct {
StateKey *string `json:"state_key,omitempty"` // The state key for the event. Only present on State Events.
Sender string `json:"sender"` // The user ID of the sender of the event
Type string `json:"type"` // The event type
Timestamp int64 `json:"origin_server_ts"` // The unix timestamp when this message was sent by the origin server
ID string `json:"event_id"` // The unique ID of this event
RoomID string `json:"room_id"` // The room the event was sent to. May be nil (e.g. for presence)
Content map[string]interface{} `json:"content"` // The JSON content of the event.
Redacts string `json:"redacts,omitempty"` // The event ID that was redacted if a m.room.redaction event
}
// Body returns the value of the "body" key in the event content if it is
// present and is a string.
func (event *Event) Body() (body string, ok bool) {
value, exists := event.Content["body"]
if !exists {
return
}
body, ok = value.(string)
return
}
// MessageType returns the value of the "msgtype" key in the event content if
// it is present and is a string.
func (event *Event) MessageType() (msgtype string, ok bool) {
value, exists := event.Content["msgtype"]
if !exists {
return
}
msgtype, ok = value.(string)
return
}
// TextMessage is the contents of a Matrix formated message event.
type TextMessage struct {
MsgType string `json:"msgtype"`
Body string `json:"body"`
}
// ImageInfo contains info about an image - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-image
type ImageInfo struct {
Height uint `json:"h,omitempty"`
Width uint `json:"w,omitempty"`
Mimetype string `json:"mimetype,omitempty"`
Size uint `json:"size,omitempty"`
}
// VideoInfo contains info about a video - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
type VideoInfo struct {
Mimetype string `json:"mimetype,omitempty"`
ThumbnailInfo ImageInfo `json:"thumbnail_info"`
ThumbnailURL string `json:"thumbnail_url,omitempty"`
Height uint `json:"h,omitempty"`
Width uint `json:"w,omitempty"`
Duration uint `json:"duration,omitempty"`
Size uint `json:"size,omitempty"`
}
// VideoMessage is an m.video - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
type VideoMessage struct {
MsgType string `json:"msgtype"`
Body string `json:"body"`
URL string `json:"url"`
Info VideoInfo `json:"info"`
}
// ImageMessage is an m.image event
type ImageMessage struct {
MsgType string `json:"msgtype"`
Body string `json:"body"`
URL string `json:"url"`
Info ImageInfo `json:"info"`
}
// An HTMLMessage is the contents of a Matrix HTML formated message event.
type HTMLMessage struct {
Body string `json:"body"`
MsgType string `json:"msgtype"`
Format string `json:"format"`
FormattedBody string `json:"formatted_body"`
}
var htmlRegex = regexp.MustCompile("<[^<]+?>")
// GetHTMLMessage returns an HTMLMessage with the body set to a stripped version of the provided HTML, in addition
// to the provided HTML.
func GetHTMLMessage(msgtype, htmlText string) HTMLMessage {
return HTMLMessage{
Body: html.UnescapeString(htmlRegex.ReplaceAllLiteralString(htmlText, "")),
MsgType: msgtype,
Format: "org.matrix.custom.html",
FormattedBody: htmlText,
}
}

43
vendor/github.com/matterbridge/gomatrix/filter.go generated vendored Normal file
View File

@ -0,0 +1,43 @@
// Copyright 2017 Jan Christian Grünhage
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gomatrix
//Filter is used by clients to specify how the server should filter responses to e.g. sync requests
//Specified by: https://matrix.org/docs/spec/client_server/r0.2.0.html#filtering
type Filter struct {
AccountData FilterPart `json:"account_data,omitempty"`
EventFields []string `json:"event_fields,omitempty"`
EventFormat string `json:"event_format,omitempty"`
Presence FilterPart `json:"presence,omitempty"`
Room struct {
AccountData FilterPart `json:"account_data,omitempty"`
Ephemeral FilterPart `json:"ephemeral,omitempty"`
IncludeLeave bool `json:"include_leave,omitempty"`
NotRooms []string `json:"not_rooms,omitempty"`
Rooms []string `json:"rooms,omitempty"`
State FilterPart `json:"state,omitempty"`
Timeline FilterPart `json:"timeline,omitempty"`
} `json:"room,omitempty"`
}
type FilterPart struct {
NotRooms []string `json:"not_rooms,omitempty"`
Rooms []string `json:"rooms,omitempty"`
Limit *int `json:"limit,omitempty"`
NotSenders []string `json:"not_senders,omitempty"`
NotTypes []string `json:"not_types,omitempty"`
Senders []string `json:"senders,omitempty"`
Types []string `json:"types,omitempty"`
}

78
vendor/github.com/matterbridge/gomatrix/requests.go generated vendored Normal file
View File

@ -0,0 +1,78 @@
package gomatrix
// ReqRegister is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
type ReqRegister struct {
Username string `json:"username,omitempty"`
BindEmail bool `json:"bind_email,omitempty"`
Password string `json:"password,omitempty"`
DeviceID string `json:"device_id,omitempty"`
InitialDeviceDisplayName string `json:"initial_device_display_name"`
Auth interface{} `json:"auth,omitempty"`
}
// ReqLogin is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-login
type ReqLogin struct {
Type string `json:"type"`
Password string `json:"password,omitempty"`
Medium string `json:"medium,omitempty"`
User string `json:"user,omitempty"`
Address string `json:"address,omitempty"`
Token string `json:"token,omitempty"`
DeviceID string `json:"device_id,omitempty"`
InitialDeviceDisplayName string `json:"initial_device_display_name,omitempty"`
}
// ReqCreateRoom is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
type ReqCreateRoom struct {
Visibility string `json:"visibility,omitempty"`
RoomAliasName string `json:"room_alias_name,omitempty"`
Name string `json:"name,omitempty"`
Topic string `json:"topic,omitempty"`
Invite []string `json:"invite,omitempty"`
Invite3PID []ReqInvite3PID `json:"invite_3pid,omitempty"`
CreationContent map[string]interface{} `json:"creation_content,omitempty"`
InitialState []Event `json:"initial_state,omitempty"`
Preset string `json:"preset,omitempty"`
IsDirect bool `json:"is_direct,omitempty"`
}
// ReqRedact is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid
type ReqRedact struct {
Reason string `json:"reason,omitempty"`
}
// ReqInvite3PID is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#id57
// It is also a JSON object used in https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
type ReqInvite3PID struct {
IDServer string `json:"id_server"`
Medium string `json:"medium"`
Address string `json:"address"`
}
// ReqInviteUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-invite
type ReqInviteUser struct {
UserID string `json:"user_id"`
}
// ReqKickUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-kick
type ReqKickUser struct {
Reason string `json:"reason,omitempty"`
UserID string `json:"user_id"`
}
// ReqBanUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-ban
type ReqBanUser struct {
Reason string `json:"reason,omitempty"`
UserID string `json:"user_id"`
}
// ReqUnbanUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban
type ReqUnbanUser struct {
UserID string `json:"user_id"`
}
// ReqTyping is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid
type ReqTyping struct {
Typing bool `json:"typing"`
Timeout int64 `json:"timeout"`
}

176
vendor/github.com/matterbridge/gomatrix/responses.go generated vendored Normal file
View File

@ -0,0 +1,176 @@
package gomatrix
// RespError is the standard JSON error response from Homeservers. It also implements the Golang "error" interface.
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#api-standards
type RespError struct {
ErrCode string `json:"errcode"`
Err string `json:"error"`
}
// Error returns the errcode and error message.
func (e RespError) Error() string {
return e.ErrCode + ": " + e.Err
}
// RespCreateFilter is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-user-userid-filter
type RespCreateFilter struct {
FilterID string `json:"filter_id"`
}
// RespVersions is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-versions
type RespVersions struct {
Versions []string `json:"versions"`
}
// RespJoinRoom is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-join
type RespJoinRoom struct {
RoomID string `json:"room_id"`
}
// RespLeaveRoom is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-leave
type RespLeaveRoom struct{}
// RespForgetRoom is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-forget
type RespForgetRoom struct{}
// RespInviteUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-invite
type RespInviteUser struct{}
// RespKickUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-kick
type RespKickUser struct{}
// RespBanUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-ban
type RespBanUser struct{}
// RespUnbanUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban
type RespUnbanUser struct{}
// RespTyping is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid
type RespTyping struct{}
// RespJoinedRooms is the JSON response for TODO-SPEC https://github.com/matrix-org/synapse/pull/1680
type RespJoinedRooms struct {
JoinedRooms []string `json:"joined_rooms"`
}
// RespJoinedMembers is the JSON response for TODO-SPEC https://github.com/matrix-org/synapse/pull/1680
type RespJoinedMembers struct {
Joined map[string]struct {
DisplayName *string `json:"display_name"`
AvatarURL *string `json:"avatar_url"`
} `json:"joined"`
}
// RespMessages is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-messages
type RespMessages struct {
Start string `json:"start"`
Chunk []Event `json:"chunk"`
End string `json:"end"`
}
// RespSendEvent is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid
type RespSendEvent struct {
EventID string `json:"event_id"`
}
// RespMediaUpload is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-media-r0-upload
type RespMediaUpload struct {
ContentURI string `json:"content_uri"`
}
// RespUserInteractive is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#user-interactive-authentication-api
type RespUserInteractive struct {
Flows []struct {
Stages []string `json:"stages"`
} `json:"flows"`
Params map[string]interface{} `json:"params"`
Session string `json:"string"`
Completed []string `json:"completed"`
ErrCode string `json:"errcode"`
Error string `json:"error"`
}
// HasSingleStageFlow returns true if there exists at least 1 Flow with a single stage of stageName.
func (r RespUserInteractive) HasSingleStageFlow(stageName string) bool {
for _, f := range r.Flows {
if len(f.Stages) == 1 && f.Stages[0] == stageName {
return true
}
}
return false
}
// RespUserDisplayName is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname
type RespUserDisplayName struct {
DisplayName string `json:"displayname"`
}
// RespRegister is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
type RespRegister struct {
AccessToken string `json:"access_token"`
DeviceID string `json:"device_id"`
HomeServer string `json:"home_server"`
RefreshToken string `json:"refresh_token"`
UserID string `json:"user_id"`
}
// RespLogin is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-login
type RespLogin struct {
AccessToken string `json:"access_token"`
DeviceID string `json:"device_id"`
HomeServer string `json:"home_server"`
UserID string `json:"user_id"`
}
// RespLogout is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-logout
type RespLogout struct{}
// RespCreateRoom is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
type RespCreateRoom struct {
RoomID string `json:"room_id"`
}
// RespSync is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-sync
type RespSync struct {
NextBatch string `json:"next_batch"`
AccountData struct {
Events []Event `json:"events"`
} `json:"account_data"`
Presence struct {
Events []Event `json:"events"`
} `json:"presence"`
Rooms struct {
Leave map[string]struct {
State struct {
Events []Event `json:"events"`
} `json:"state"`
Timeline struct {
Events []Event `json:"events"`
Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"`
} `json:"timeline"`
} `json:"leave"`
Join map[string]struct {
State struct {
Events []Event `json:"events"`
} `json:"state"`
Timeline struct {
Events []Event `json:"events"`
Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"`
} `json:"timeline"`
} `json:"join"`
Invite map[string]struct {
State struct {
Events []Event
} `json:"invite_state"`
} `json:"invite"`
} `json:"rooms"`
}
type RespTurnServer struct {
Username string `json:"username"`
Password string `json:"password"`
TTL int `json:"ttl"`
URIs []string `json:"uris"`
}

50
vendor/github.com/matterbridge/gomatrix/room.go generated vendored Normal file
View File

@ -0,0 +1,50 @@
package gomatrix
// Room represents a single Matrix room.
type Room struct {
ID string
State map[string]map[string]*Event
}
// UpdateState updates the room's current state with the given Event. This will clobber events based
// on the type/state_key combination.
func (room Room) UpdateState(event *Event) {
_, exists := room.State[event.Type]
if !exists {
room.State[event.Type] = make(map[string]*Event)
}
room.State[event.Type][*event.StateKey] = event
}
// GetStateEvent returns the state event for the given type/state_key combo, or nil.
func (room Room) GetStateEvent(eventType string, stateKey string) *Event {
stateEventMap, _ := room.State[eventType]
event, _ := stateEventMap[stateKey]
return event
}
// GetMembershipState returns the membership state of the given user ID in this room. If there is
// no entry for this member, 'leave' is returned for consistency with left users.
func (room Room) GetMembershipState(userID string) string {
state := "leave"
event := room.GetStateEvent("m.room.member", userID)
if event != nil {
membershipState, found := event.Content["membership"]
if found {
mState, isString := membershipState.(string)
if isString {
state = mState
}
}
}
return state
}
// NewRoom creates a new Room with the given ID
func NewRoom(roomID string) *Room {
// Init the State map and return a pointer to the Room
return &Room{
ID: roomID,
State: make(map[string]map[string]*Event),
}
}

65
vendor/github.com/matterbridge/gomatrix/store.go generated vendored Normal file
View File

@ -0,0 +1,65 @@
package gomatrix
// Storer is an interface which must be satisfied to store client data.
//
// You can either write a struct which persists this data to disk, or you can use the
// provided "InMemoryStore" which just keeps data around in-memory which is lost on
// restarts.
type Storer interface {
SaveFilterID(userID, filterID string)
LoadFilterID(userID string) string
SaveNextBatch(userID, nextBatchToken string)
LoadNextBatch(userID string) string
SaveRoom(room *Room)
LoadRoom(roomID string) *Room
}
// InMemoryStore implements the Storer interface.
//
// Everything is persisted in-memory as maps. It is not safe to load/save filter IDs
// or next batch tokens on any goroutine other than the syncing goroutine: the one
// which called Client.Sync().
type InMemoryStore struct {
Filters map[string]string
NextBatch map[string]string
Rooms map[string]*Room
}
// SaveFilterID to memory.
func (s *InMemoryStore) SaveFilterID(userID, filterID string) {
s.Filters[userID] = filterID
}
// LoadFilterID from memory.
func (s *InMemoryStore) LoadFilterID(userID string) string {
return s.Filters[userID]
}
// SaveNextBatch to memory.
func (s *InMemoryStore) SaveNextBatch(userID, nextBatchToken string) {
s.NextBatch[userID] = nextBatchToken
}
// LoadNextBatch from memory.
func (s *InMemoryStore) LoadNextBatch(userID string) string {
return s.NextBatch[userID]
}
// SaveRoom to memory.
func (s *InMemoryStore) SaveRoom(room *Room) {
s.Rooms[room.ID] = room
}
// LoadRoom from memory.
func (s *InMemoryStore) LoadRoom(roomID string) *Room {
return s.Rooms[roomID]
}
// NewInMemoryStore constructs a new InMemoryStore.
func NewInMemoryStore() *InMemoryStore {
return &InMemoryStore{
Filters: make(map[string]string),
NextBatch: make(map[string]string),
Rooms: make(map[string]*Room),
}
}

164
vendor/github.com/matterbridge/gomatrix/sync.go generated vendored Normal file
View File

@ -0,0 +1,164 @@
package gomatrix
import (
"encoding/json"
"fmt"
"runtime/debug"
"time"
)
// Syncer represents an interface that must be satisfied in order to do /sync requests on a client.
type Syncer interface {
// Process the /sync response. The since parameter is the since= value that was used to produce the response.
// This is useful for detecting the very first sync (since=""). If an error is return, Syncing will be stopped
// permanently.
ProcessResponse(resp *RespSync, since string) error
// OnFailedSync returns either the time to wait before retrying or an error to stop syncing permanently.
OnFailedSync(res *RespSync, err error) (time.Duration, error)
// GetFilterJSON for the given user ID. NOT the filter ID.
GetFilterJSON(userID string) json.RawMessage
}
// DefaultSyncer is the default syncing implementation. You can either write your own syncer, or selectively
// replace parts of this default syncer (e.g. the ProcessResponse method). The default syncer uses the observer
// pattern to notify callers about incoming events. See DefaultSyncer.OnEventType for more information.
type DefaultSyncer struct {
UserID string
Store Storer
listeners map[string][]OnEventListener // event type to listeners array
}
// OnEventListener can be used with DefaultSyncer.OnEventType to be informed of incoming events.
type OnEventListener func(*Event)
// NewDefaultSyncer returns an instantiated DefaultSyncer
func NewDefaultSyncer(userID string, store Storer) *DefaultSyncer {
return &DefaultSyncer{
UserID: userID,
Store: store,
listeners: make(map[string][]OnEventListener),
}
}
// ProcessResponse processes the /sync response in a way suitable for bots. "Suitable for bots" means a stream of
// unrepeating events. Returns a fatal error if a listener panics.
func (s *DefaultSyncer) ProcessResponse(res *RespSync, since string) (err error) {
if !s.shouldProcessResponse(res, since) {
return
}
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("ProcessResponse panicked! userID=%s since=%s panic=%s\n%s", s.UserID, since, r, debug.Stack())
}
}()
for roomID, roomData := range res.Rooms.Join {
room := s.getOrCreateRoom(roomID)
for _, event := range roomData.State.Events {
event.RoomID = roomID
room.UpdateState(&event)
s.notifyListeners(&event)
}
for _, event := range roomData.Timeline.Events {
event.RoomID = roomID
s.notifyListeners(&event)
}
}
for roomID, roomData := range res.Rooms.Invite {
room := s.getOrCreateRoom(roomID)
for _, event := range roomData.State.Events {
event.RoomID = roomID
room.UpdateState(&event)
s.notifyListeners(&event)
}
}
for roomID, roomData := range res.Rooms.Leave {
room := s.getOrCreateRoom(roomID)
for _, event := range roomData.Timeline.Events {
if event.StateKey != nil {
event.RoomID = roomID
room.UpdateState(&event)
s.notifyListeners(&event)
}
}
}
return
}
// OnEventType allows callers to be notified when there are new events for the given event type.
// There are no duplicate checks.
func (s *DefaultSyncer) OnEventType(eventType string, callback OnEventListener) {
_, exists := s.listeners[eventType]
if !exists {
s.listeners[eventType] = []OnEventListener{}
}
s.listeners[eventType] = append(s.listeners[eventType], callback)
}
// shouldProcessResponse returns true if the response should be processed. May modify the response to remove
// stuff that shouldn't be processed.
func (s *DefaultSyncer) shouldProcessResponse(resp *RespSync, since string) bool {
if since == "" {
return false
}
// This is a horrible hack because /sync will return the most recent messages for a room
// as soon as you /join it. We do NOT want to process those events in that particular room
// because they may have already been processed (if you toggle the bot in/out of the room).
//
// Work around this by inspecting each room's timeline and seeing if an m.room.member event for us
// exists and is "join" and then discard processing that room entirely if so.
// TODO: We probably want to process messages from after the last join event in the timeline.
for roomID, roomData := range resp.Rooms.Join {
for i := len(roomData.Timeline.Events) - 1; i >= 0; i-- {
e := roomData.Timeline.Events[i]
if e.Type == "m.room.member" && e.StateKey != nil && *e.StateKey == s.UserID {
m := e.Content["membership"]
mship, ok := m.(string)
if !ok {
continue
}
if mship == "join" {
_, ok := resp.Rooms.Join[roomID]
if !ok {
continue
}
delete(resp.Rooms.Join, roomID) // don't re-process messages
delete(resp.Rooms.Invite, roomID) // don't re-process invites
break
}
}
}
}
return true
}
// getOrCreateRoom must only be called by the Sync() goroutine which calls ProcessResponse()
func (s *DefaultSyncer) getOrCreateRoom(roomID string) *Room {
room := s.Store.LoadRoom(roomID)
if room == nil { // create a new Room
room = NewRoom(roomID)
s.Store.SaveRoom(room)
}
return room
}
func (s *DefaultSyncer) notifyListeners(event *Event) {
listeners, exists := s.listeners[event.Type]
if !exists {
return
}
for _, fn := range listeners {
fn(event)
}
}
// OnFailedSync always returns a 10 second wait period between failed /syncs, never a fatal error.
func (s *DefaultSyncer) OnFailedSync(res *RespSync, err error) (time.Duration, error) {
return 10 * time.Second, nil
}
// GetFilterJSON returns a filter with a timeline limit of 50.
func (s *DefaultSyncer) GetFilterJSON(userID string) json.RawMessage {
return json.RawMessage(`{"room":{"timeline":{"limit":50}}}`)
}

130
vendor/github.com/matterbridge/gomatrix/userids.go generated vendored Normal file
View File

@ -0,0 +1,130 @@
package gomatrix
import (
"bytes"
"encoding/hex"
"fmt"
"strings"
)
const lowerhex = "0123456789abcdef"
// encode the given byte using quoted-printable encoding (e.g "=2f")
// and writes it to the buffer
// See https://golang.org/src/mime/quotedprintable/writer.go
func encode(buf *bytes.Buffer, b byte) {
buf.WriteByte('=')
buf.WriteByte(lowerhex[b>>4])
buf.WriteByte(lowerhex[b&0x0f])
}
// escape the given alpha character and writes it to the buffer
func escape(buf *bytes.Buffer, b byte) {
buf.WriteByte('_')
if b == '_' {
buf.WriteByte('_') // another _
} else {
buf.WriteByte(b + 0x20) // ASCII shift A-Z to a-z
}
}
func shouldEncode(b byte) bool {
return b != '-' && b != '.' && b != '_' && !(b >= '0' && b <= '9') && !(b >= 'a' && b <= 'z') && !(b >= 'A' && b <= 'Z')
}
func shouldEscape(b byte) bool {
return (b >= 'A' && b <= 'Z') || b == '_'
}
func isValidByte(b byte) bool {
return isValidEscapedChar(b) || (b >= '0' && b <= '9') || b == '.' || b == '=' || b == '-'
}
func isValidEscapedChar(b byte) bool {
return b == '_' || (b >= 'a' && b <= 'z')
}
// EncodeUserLocalpart encodes the given string into Matrix-compliant user ID localpart form.
// See http://matrix.org/docs/spec/intro.html#mapping-from-other-character-sets
//
// This returns a string with only the characters "a-z0-9._=-". The uppercase range A-Z
// are encoded using leading underscores ("_"). Characters outside the aforementioned ranges
// (including literal underscores ("_") and equals ("=")) are encoded as UTF8 code points (NOT NCRs)
// and converted to lower-case hex with a leading "=". For example:
// Alph@Bet_50up => _alph=40_bet=5f50up
func EncodeUserLocalpart(str string) string {
strBytes := []byte(str)
var outputBuffer bytes.Buffer
for _, b := range strBytes {
if shouldEncode(b) {
encode(&outputBuffer, b)
} else if shouldEscape(b) {
escape(&outputBuffer, b)
} else {
outputBuffer.WriteByte(b)
}
}
return outputBuffer.String()
}
// DecodeUserLocalpart decodes the given string back into the original input string.
// Returns an error if the given string is not a valid user ID localpart encoding.
// See http://matrix.org/docs/spec/intro.html#mapping-from-other-character-sets
//
// This decodes quoted-printable bytes back into UTF8, and unescapes casing. For
// example:
// _alph=40_bet=5f50up => Alph@Bet_50up
// Returns an error if the input string contains characters outside the
// range "a-z0-9._=-", has an invalid quote-printable byte (e.g. not hex), or has
// an invalid _ escaped byte (e.g. "_5").
func DecodeUserLocalpart(str string) (string, error) {
strBytes := []byte(str)
var outputBuffer bytes.Buffer
for i := 0; i < len(strBytes); i++ {
b := strBytes[i]
if !isValidByte(b) {
return "", fmt.Errorf("Byte pos %d: Invalid byte", i)
}
if b == '_' { // next byte is a-z and should be upper-case or is another _ and should be a literal _
if i+1 >= len(strBytes) {
return "", fmt.Errorf("Byte pos %d: expected _[a-z_] encoding but ran out of string", i)
}
if !isValidEscapedChar(strBytes[i+1]) { // invalid escaping
return "", fmt.Errorf("Byte pos %d: expected _[a-z_] encoding", i)
}
if strBytes[i+1] == '_' {
outputBuffer.WriteByte('_')
} else {
outputBuffer.WriteByte(strBytes[i+1] - 0x20) // ASCII shift a-z to A-Z
}
i++ // skip next byte since we just handled it
} else if b == '=' { // next 2 bytes are hex and should be buffered ready to be read as utf8
if i+2 >= len(strBytes) {
return "", fmt.Errorf("Byte pos: %d: expected quote-printable encoding but ran out of string", i)
}
dst := make([]byte, 1)
_, err := hex.Decode(dst, strBytes[i+1:i+3])
if err != nil {
return "", err
}
outputBuffer.WriteByte(dst[0])
i += 2 // skip next 2 bytes since we just handled it
} else { // pass through
outputBuffer.WriteByte(b)
}
}
return outputBuffer.String(), nil
}
// ExtractUserLocalpart extracts the localpart portion of a user ID.
// See http://matrix.org/docs/spec/intro.html#user-identifiers
func ExtractUserLocalpart(userID string) (string, error) {
if len(userID) == 0 || userID[0] != '@' {
return "", fmt.Errorf("%s is not a valid user id", userID)
}
return strings.TrimPrefix(
strings.SplitN(userID, ":", 2)[0], // @foo:bar:8448 => [ "@foo", "bar:8448" ]
"@", // remove "@" prefix
), nil
}

View File

@ -1,20 +0,0 @@
package slack
import (
"net"
"net/url"
)
var portMapping = map[string]string{"ws": "80", "wss": "443"}
func websocketizeURLPort(orig string) (string, error) {
urlObj, err := url.ParseRequestURI(orig)
if err != nil {
return "", err
}
_, _, err = net.SplitHostPort(urlObj.Host)
if err != nil {
return urlObj.Scheme + "://" + urlObj.Host + ":" + portMapping[urlObj.Scheme] + urlObj.Path, nil
}
return orig, nil
}

View File

@ -25,6 +25,7 @@ type AttachmentAction struct {
SelectedOptions []AttachmentActionOption `json:"selected_options,omitempty"` // Optional. The first element of this array will be set as the pre-selected option for this menu.
OptionGroups []AttachmentActionOptionGroup `json:"option_groups,omitempty"` // Optional.
Confirm *ConfirmationField `json:"confirm,omitempty"` // Optional.
URL string `json:"url,omitempty"` // Optional.
}
// AttachmentActionOption the individual option to appear in action menu.
@ -48,6 +49,9 @@ type AttachmentActionCallback struct {
Channel Channel `json:"channel"`
User User `json:"user"`
Name string `json:"name"`
Value string `json:"value"`
OriginalMessage Message `json:"original_message"`
ActionTs string `json:"action_ts"`

View File

@ -38,51 +38,51 @@ func channelRequest(ctx context.Context, path string, values url.Values, debug b
}
// ArchiveChannel archives the given channel
func (api *Client) ArchiveChannel(channel string) error {
return api.ArchiveChannelContext(context.Background(), channel)
// see https://api.slack.com/methods/channels.archive
func (api *Client) ArchiveChannel(channelID string) error {
return api.ArchiveChannelContext(context.Background(), channelID)
}
// ArchiveChannelContext archives the given channel with a custom context
func (api *Client) ArchiveChannelContext(ctx context.Context, channel string) error {
// see https://api.slack.com/methods/channels.archive
func (api *Client) ArchiveChannelContext(ctx context.Context, channelID string) error {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
}
_, err := channelRequest(ctx, "channels.archive", values, api.debug)
if err != nil {
return err
}
return nil
return err
}
// UnarchiveChannel unarchives the given channel
func (api *Client) UnarchiveChannel(channel string) error {
return api.UnarchiveChannelContext(context.Background(), channel)
// see https://api.slack.com/methods/channels.unarchive
func (api *Client) UnarchiveChannel(channelID string) error {
return api.UnarchiveChannelContext(context.Background(), channelID)
}
// UnarchiveChannelContext unarchives the given channel with a custom context
func (api *Client) UnarchiveChannelContext(ctx context.Context, channel string) error {
// see https://api.slack.com/methods/channels.unarchive
func (api *Client) UnarchiveChannelContext(ctx context.Context, channelID string) error {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
}
_, err := channelRequest(ctx, "channels.unarchive", values, api.debug)
if err != nil {
return err
}
return nil
return err
}
// CreateChannel creates a channel with the given name and returns a *Channel
func (api *Client) CreateChannel(channel string) (*Channel, error) {
return api.CreateChannelContext(context.Background(), channel)
// see https://api.slack.com/methods/channels.create
func (api *Client) CreateChannel(channelName string) (*Channel, error) {
return api.CreateChannelContext(context.Background(), channelName)
}
// CreateChannelContext creates a channel with the given name and returns a *Channel with a custom context
func (api *Client) CreateChannelContext(ctx context.Context, channel string) (*Channel, error) {
// see https://api.slack.com/methods/channels.create
func (api *Client) CreateChannelContext(ctx context.Context, channelName string) (*Channel, error) {
values := url.Values{
"token": {api.config.token},
"name": {channel},
"name": {channelName},
}
response, err := channelRequest(ctx, "channels.create", values, api.debug)
if err != nil {
@ -92,15 +92,17 @@ func (api *Client) CreateChannelContext(ctx context.Context, channel string) (*C
}
// GetChannelHistory retrieves the channel history
func (api *Client) GetChannelHistory(channel string, params HistoryParameters) (*History, error) {
return api.GetChannelHistoryContext(context.Background(), channel, params)
// see https://api.slack.com/methods/channels.history
func (api *Client) GetChannelHistory(channelID string, params HistoryParameters) (*History, error) {
return api.GetChannelHistoryContext(context.Background(), channelID, params)
}
// GetChannelHistoryContext retrieves the channel history with a custom context
func (api *Client) GetChannelHistoryContext(ctx context.Context, channel string, params HistoryParameters) (*History, error) {
// see https://api.slack.com/methods/channels.history
func (api *Client) GetChannelHistoryContext(ctx context.Context, channelID string, params HistoryParameters) (*History, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
}
if params.Latest != DEFAULT_HISTORY_LATEST {
values.Add("latest", params.Latest)
@ -133,15 +135,17 @@ func (api *Client) GetChannelHistoryContext(ctx context.Context, channel string,
}
// GetChannelInfo retrieves the given channel
func (api *Client) GetChannelInfo(channel string) (*Channel, error) {
return api.GetChannelInfoContext(context.Background(), channel)
// see https://api.slack.com/methods/channels.info
func (api *Client) GetChannelInfo(channelID string) (*Channel, error) {
return api.GetChannelInfoContext(context.Background(), channelID)
}
// GetChannelInfoContext retrieves the given channel with a custom context
func (api *Client) GetChannelInfoContext(ctx context.Context, channel string) (*Channel, error) {
// see https://api.slack.com/methods/channels.info
func (api *Client) GetChannelInfoContext(ctx context.Context, channelID string) (*Channel, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
}
response, err := channelRequest(ctx, "channels.info", values, api.debug)
if err != nil {
@ -151,15 +155,17 @@ func (api *Client) GetChannelInfoContext(ctx context.Context, channel string) (*
}
// InviteUserToChannel invites a user to a given channel and returns a *Channel
func (api *Client) InviteUserToChannel(channel, user string) (*Channel, error) {
return api.InviteUserToChannelContext(context.Background(), channel, user)
// see https://api.slack.com/methods/channels.invite
func (api *Client) InviteUserToChannel(channelID, user string) (*Channel, error) {
return api.InviteUserToChannelContext(context.Background(), channelID, user)
}
// InviteUserToChannelCustom invites a user to a given channel and returns a *Channel with a custom context
func (api *Client) InviteUserToChannelContext(ctx context.Context, channel, user string) (*Channel, error) {
// see https://api.slack.com/methods/channels.invite
func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, user string) (*Channel, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
"user": {user},
}
response, err := channelRequest(ctx, "channels.invite", values, api.debug)
@ -170,15 +176,17 @@ func (api *Client) InviteUserToChannelContext(ctx context.Context, channel, user
}
// JoinChannel joins the currently authenticated user to a channel
func (api *Client) JoinChannel(channel string) (*Channel, error) {
return api.JoinChannelContext(context.Background(), channel)
// see https://api.slack.com/methods/channels.join
func (api *Client) JoinChannel(channelName string) (*Channel, error) {
return api.JoinChannelContext(context.Background(), channelName)
}
// JoinChannelContext joins the currently authenticated user to a channel with a custom context
func (api *Client) JoinChannelContext(ctx context.Context, channel string) (*Channel, error) {
// see https://api.slack.com/methods/channels.join
func (api *Client) JoinChannelContext(ctx context.Context, channelName string) (*Channel, error) {
values := url.Values{
"token": {api.config.token},
"name": {channel},
"name": {channelName},
}
response, err := channelRequest(ctx, "channels.join", values, api.debug)
if err != nil {
@ -188,15 +196,17 @@ func (api *Client) JoinChannelContext(ctx context.Context, channel string) (*Cha
}
// LeaveChannel makes the authenticated user leave the given channel
func (api *Client) LeaveChannel(channel string) (bool, error) {
return api.LeaveChannelContext(context.Background(), channel)
// see https://api.slack.com/methods/channels.leave
func (api *Client) LeaveChannel(channelID string) (bool, error) {
return api.LeaveChannelContext(context.Background(), channelID)
}
// LeaveChannelContext makes the authenticated user leave the given channel with a custom context
func (api *Client) LeaveChannelContext(ctx context.Context, channel string) (bool, error) {
// see https://api.slack.com/methods/channels.leave
func (api *Client) LeaveChannelContext(ctx context.Context, channelID string) (bool, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
}
response, err := channelRequest(ctx, "channels.leave", values, api.debug)
if err != nil {
@ -209,30 +219,31 @@ func (api *Client) LeaveChannelContext(ctx context.Context, channel string) (boo
}
// KickUserFromChannel kicks a user from a given channel
func (api *Client) KickUserFromChannel(channel, user string) error {
return api.KickUserFromChannelContext(context.Background(), channel, user)
// see https://api.slack.com/methods/channels.kick
func (api *Client) KickUserFromChannel(channelID, user string) error {
return api.KickUserFromChannelContext(context.Background(), channelID, user)
}
// KickUserFromChannelContext kicks a user from a given channel with a custom context
func (api *Client) KickUserFromChannelContext(ctx context.Context, channel, user string) error {
// see https://api.slack.com/methods/channels.kick
func (api *Client) KickUserFromChannelContext(ctx context.Context, channelID, user string) error {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
"user": {user},
}
_, err := channelRequest(ctx, "channels.kick", values, api.debug)
if err != nil {
return err
}
return nil
return err
}
// GetChannels retrieves all the channels
// see https://api.slack.com/methods/channels.list
func (api *Client) GetChannels(excludeArchived bool) ([]Channel, error) {
return api.GetChannelsContext(context.Background(), excludeArchived)
}
// GetChannelsContext retrieves all the channels with a custom context
// see https://api.slack.com/methods/channels.list
func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool) ([]Channel, error) {
values := url.Values{
"token": {api.config.token},
@ -252,35 +263,36 @@ func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool)
// timer before making the call. In this way, any further updates needed during the timeout will not generate extra calls
// (just one per channel). This is useful for when reading scroll-back history, or following a busy live channel. A
// timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout.
func (api *Client) SetChannelReadMark(channel, ts string) error {
return api.SetChannelReadMarkContext(context.Background(), channel, ts)
// see https://api.slack.com/methods/channels.mark
func (api *Client) SetChannelReadMark(channelID, ts string) error {
return api.SetChannelReadMarkContext(context.Background(), channelID, ts)
}
// SetChannelReadMarkContext sets the read mark of a given channel to a specific point with a custom context
// For more details see SetChannelReadMark documentation
func (api *Client) SetChannelReadMarkContext(ctx context.Context, channel, ts string) error {
// see https://api.slack.com/methods/channels.mark
func (api *Client) SetChannelReadMarkContext(ctx context.Context, channelID, ts string) error {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
"ts": {ts},
}
_, err := channelRequest(ctx, "channels.mark", values, api.debug)
if err != nil {
return err
}
return nil
return err
}
// RenameChannel renames a given channel
func (api *Client) RenameChannel(channel, name string) (*Channel, error) {
return api.RenameChannelContext(context.Background(), channel, name)
// see https://api.slack.com/methods/channels.rename
func (api *Client) RenameChannel(channelID, name string) (*Channel, error) {
return api.RenameChannelContext(context.Background(), channelID, name)
}
// RenameChannelContext renames a given channel with a custom context
func (api *Client) RenameChannelContext(ctx context.Context, channel, name string) (*Channel, error) {
// see https://api.slack.com/methods/channels.rename
func (api *Client) RenameChannelContext(ctx context.Context, channelID, name string) (*Channel, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
"name": {name},
}
// XXX: the created entry in this call returns a string instead of a number
@ -293,15 +305,17 @@ func (api *Client) RenameChannelContext(ctx context.Context, channel, name strin
}
// SetChannelPurpose sets the channel purpose and returns the purpose that was successfully set
func (api *Client) SetChannelPurpose(channel, purpose string) (string, error) {
return api.SetChannelPurposeContext(context.Background(), channel, purpose)
// see https://api.slack.com/methods/channels.setPurpose
func (api *Client) SetChannelPurpose(channelID, purpose string) (string, error) {
return api.SetChannelPurposeContext(context.Background(), channelID, purpose)
}
// SetChannelPurposeContext sets the channel purpose and returns the purpose that was successfully set with a custom context
func (api *Client) SetChannelPurposeContext(ctx context.Context, channel, purpose string) (string, error) {
// see https://api.slack.com/methods/channels.setPurpose
func (api *Client) SetChannelPurposeContext(ctx context.Context, channelID, purpose string) (string, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
"purpose": {purpose},
}
response, err := channelRequest(ctx, "channels.setPurpose", values, api.debug)
@ -312,15 +326,17 @@ func (api *Client) SetChannelPurposeContext(ctx context.Context, channel, purpos
}
// SetChannelTopic sets the channel topic and returns the topic that was successfully set
func (api *Client) SetChannelTopic(channel, topic string) (string, error) {
return api.SetChannelTopicContext(context.Background(), channel, topic)
// see https://api.slack.com/methods/channels.setTopic
func (api *Client) SetChannelTopic(channelID, topic string) (string, error) {
return api.SetChannelTopicContext(context.Background(), channelID, topic)
}
// SetChannelTopicContext sets the channel topic and returns the topic that was successfully set with a custom context
func (api *Client) SetChannelTopicContext(ctx context.Context, channel, topic string) (string, error) {
// see https://api.slack.com/methods/channels.setTopic
func (api *Client) SetChannelTopicContext(ctx context.Context, channelID, topic string) (string, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
"topic": {topic},
}
response, err := channelRequest(ctx, "channels.setTopic", values, api.debug)
@ -331,15 +347,17 @@ func (api *Client) SetChannelTopicContext(ctx context.Context, channel, topic st
}
// GetChannelReplies gets an entire thread (a message plus all the messages in reply to it).
func (api *Client) GetChannelReplies(channel, thread_ts string) ([]Message, error) {
return api.GetChannelRepliesContext(context.Background(), channel, thread_ts)
// see https://api.slack.com/methods/channels.replies
func (api *Client) GetChannelReplies(channelID, thread_ts string) ([]Message, error) {
return api.GetChannelRepliesContext(context.Background(), channelID, thread_ts)
}
// GetChannelRepliesContext gets an entire thread (a message plus all the messages in reply to it) with a custom context
func (api *Client) GetChannelRepliesContext(ctx context.Context, channel, thread_ts string) ([]Message, error) {
// see https://api.slack.com/methods/channels.replies
func (api *Client) GetChannelRepliesContext(ctx context.Context, channelID, thread_ts string) ([]Message, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
"thread_ts": {thread_ts},
}
response, err := channelRequest(ctx, "channels.replies", values, api.debug)

View File

@ -11,6 +11,7 @@ import (
const (
DEFAULT_MESSAGE_USERNAME = ""
DEFAULT_MESSAGE_THREAD_TIMESTAMP = ""
DEFAULT_MESSAGE_REPLY_BROADCAST = false
DEFAULT_MESSAGE_ASUSER = false
DEFAULT_MESSAGE_PARSE = ""
DEFAULT_MESSAGE_LINK_NAMES = 0
@ -36,6 +37,7 @@ type PostMessageParameters struct {
AsUser bool `json:"as_user"`
Parse string `json:"parse"`
ThreadTimestamp string `json:"thread_ts"`
ReplyBroadcast bool `json:"reply_broadcast"`
LinkNames int `json:"link_names"`
Attachments []Attachment `json:"attachments"`
UnfurlLinks bool `json:"unfurl_links"`
@ -44,12 +46,17 @@ type PostMessageParameters struct {
IconEmoji string `json:"icon_emoji"`
Markdown bool `json:"mrkdwn,omitempty"`
EscapeText bool `json:"escape_text"`
// chat.postEphemeral support
Channel string `json:"channel"`
User string `json:"user"`
}
// NewPostMessageParameters provides an instance of PostMessageParameters with all the sane default values set
func NewPostMessageParameters() PostMessageParameters {
return PostMessageParameters{
Username: DEFAULT_MESSAGE_USERNAME,
User: DEFAULT_MESSAGE_USERNAME,
AsUser: DEFAULT_MESSAGE_ASUSER,
Parse: DEFAULT_MESSAGE_PARSE,
LinkNames: DEFAULT_MESSAGE_LINK_NAMES,
@ -102,6 +109,37 @@ func (api *Client) PostMessageContext(ctx context.Context, channel, text string,
return respChannel, respTimestamp, err
}
// PostEphemeral sends an ephemeral message to a user in a channel.
// Message is escaped by default according to https://api.slack.com/docs/formatting
// Use http://davestevens.github.io/slack-message-builder/ to help crafting your message.
func (api *Client) PostEphemeral(channel, userID string, options ...MsgOption) (string, error) {
options = append(options, MsgOptionPostEphemeral())
return api.PostEphemeralContext(
context.Background(),
channel,
userID,
options...,
)
}
// PostEphemeralContext sends an ephemeal message to a user in a channel with a custom context
// For more details, see PostEphemeral documentation
func (api *Client) PostEphemeralContext(ctx context.Context, channel, userID string, options ...MsgOption) (string, error) {
path, values, err := ApplyMsgOptions(api.config.token, channel, options...)
if err != nil {
return "", err
}
values.Add("user", userID)
response, err := chatRequest(ctx, path, values, api.debug)
if err != nil {
return "", err
}
return response.Timestamp, nil
}
// UpdateMessage updates a message in a channel
func (api *Client) UpdateMessage(channel, timestamp, text string) (string, string, string, error) {
return api.UpdateMessageContext(context.Background(), channel, timestamp, text)
@ -171,9 +209,10 @@ func chatRequest(ctx context.Context, path string, values url.Values, debug bool
type sendMode string
const (
chatUpdate sendMode = "chat.update"
chatPostMessage sendMode = "chat.postMessage"
chatDelete sendMode = "chat.delete"
chatUpdate sendMode = "chat.update"
chatPostMessage sendMode = "chat.postMessage"
chatDelete sendMode = "chat.delete"
chatPostEphemeral sendMode = "chat.postEphemeral"
)
type sendConfig struct {
@ -193,6 +232,15 @@ func MsgOptionPost() MsgOption {
}
}
// MsgOptionPostEphemeral posts an ephemeral message
func MsgOptionPostEphemeral() MsgOption {
return func(config *sendConfig) error {
config.mode = chatPostEphemeral
config.values.Del("ts")
return nil
}
}
// MsgOptionUpdate updates a message based on the timestamp.
func MsgOptionUpdate(timestamp string) MsgOption {
return func(config *sendConfig) error {
@ -279,6 +327,11 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption {
config.values.Set("username", string(params.Username))
}
// chat.postEphemeral support
if params.User != DEFAULT_MESSAGE_USERNAME {
config.values.Set("user", params.User)
}
// never generates an error.
MsgOptionAsUser(params.AsUser)(config)
@ -314,6 +367,9 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption {
if params.ThreadTimestamp != DEFAULT_MESSAGE_THREAD_TIMESTAMP {
config.values.Set("thread_ts", params.ThreadTimestamp)
}
if params.ReplyBroadcast != DEFAULT_MESSAGE_REPLY_BROADCAST {
config.values.Set("reply_broadcast", "true")
}
return nil
}

View File

@ -0,0 +1,21 @@
package main
import (
"fmt"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR_TOKEN_HERE")
channels, err := api.GetChannels(false)
if err != nil {
fmt.Printf("%s\n", err)
return
}
for _, channel := range channels {
fmt.Println(channel.Name)
// channel is of type conversation & groupConversation
// see all available methods in `conversation.go`
}
}

30
vendor/github.com/nlopes/slack/examples/files/files.go generated vendored Normal file
View File

@ -0,0 +1,30 @@
package main
import (
"fmt"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR_TOKEN_HERE")
params := slack.FileUploadParameters{
Title: "Batman Example",
//Filetype: "txt",
File: "example.txt",
//Content: "Nan Nan Nan Nan Nan Nan Nan Nan Batman",
}
file, err := api.UploadFile(params)
if err != nil {
fmt.Printf("%s\n", err)
return
}
fmt.Printf("Name: %s, URL: %s\n", file.Name, file.URL)
err = api.DeleteFile(file.ID)
if err != nil {
fmt.Printf("%s\n", err)
return
}
fmt.Printf("File %s deleted successfully.\n", file.Name)
}

View File

@ -0,0 +1,22 @@
package main
import (
"fmt"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR_TOKEN_HERE")
// If you set debugging, it will log all requests to the console
// Useful when encountering issues
// api.SetDebug(true)
groups, err := api.GetGroups(false)
if err != nil {
fmt.Printf("%s\n", err)
return
}
for _, group := range groups {
fmt.Printf("ID: %s, Name: %s\n", group.ID, group.Name)
}
}

21
vendor/github.com/nlopes/slack/examples/ims/ims.go generated vendored Normal file
View File

@ -0,0 +1,21 @@
package main
import (
"fmt"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR_TOKEN_HERE")
userID := "USER_ID"
_, _, channelID, err := api.OpenIMChannel(userID)
if err != nil {
fmt.Printf("%s\n", err)
}
api.PostMessage(channelID, "Hello World!", slack.PostMessageParameters{})
}

View File

@ -0,0 +1,32 @@
package main
import (
"fmt"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR_TOKEN_HERE")
params := slack.PostMessageParameters{}
attachment := slack.Attachment{
Pretext: "some pretext",
Text: "some text",
// Uncomment the following part to send a field too
/*
Fields: []slack.AttachmentField{
slack.AttachmentField{
Title: "a",
Value: "no",
},
},
*/
}
params.Attachments = []slack.Attachment{attachment}
channelID, timestamp, err := api.PostMessage("CHANNEL_ID", "Some text", params)
if err != nil {
fmt.Printf("%s\n", err)
return
}
fmt.Printf("Message successfully sent to channel %s at %s", channelID, timestamp)
}

123
vendor/github.com/nlopes/slack/examples/pins/pins.go generated vendored Normal file
View File

@ -0,0 +1,123 @@
package main
import (
"flag"
"fmt"
"github.com/nlopes/slack"
)
/*
WARNING: This example is destructive in the sense that it create a channel called testpinning
*/
func main() {
var (
apiToken string
debug bool
)
flag.StringVar(&apiToken, "token", "YOUR_TOKEN_HERE", "Your Slack API Token")
flag.BoolVar(&debug, "debug", false, "Show JSON output")
flag.Parse()
api := slack.New(apiToken)
if debug {
api.SetDebug(true)
}
var (
postAsUserName string
postAsUserID string
postToChannelID string
)
// Find the user to post as.
authTest, err := api.AuthTest()
if err != nil {
fmt.Printf("Error getting channels: %s\n", err)
return
}
channelName := "testpinning"
// Post as the authenticated user.
postAsUserName = authTest.User
postAsUserID = authTest.UserID
// Create a temporary channel
channel, err := api.CreateChannel(channelName)
if err != nil {
// If the channel exists, that means we just need to unarchive it
if err.Error() == "name_taken" {
err = nil
channels, err := api.GetChannels(false)
if err != nil {
fmt.Println("Could not retrieve channels")
return
}
for _, archivedChannel := range channels {
if archivedChannel.Name == channelName {
if archivedChannel.IsArchived {
err = api.UnarchiveChannel(archivedChannel.ID)
if err != nil {
fmt.Printf("Could not unarchive %s: %s\n", archivedChannel.ID, err)
return
}
}
channel = &archivedChannel
break
}
}
}
if err != nil {
fmt.Printf("Error setting test channel for pinning: %s\n", err)
return
}
}
postToChannelID = channel.ID
fmt.Printf("Posting as %s (%s) in channel %s\n", postAsUserName, postAsUserID, postToChannelID)
// Post a message.
postParams := slack.PostMessageParameters{}
channelID, timestamp, err := api.PostMessage(postToChannelID, "Is this any good?", postParams)
if err != nil {
fmt.Printf("Error posting message: %s\n", err)
return
}
// Grab a reference to the message.
msgRef := slack.NewRefToMessage(channelID, timestamp)
// Add message pin to channel
if err := api.AddPin(channelID, msgRef); err != nil {
fmt.Printf("Error adding pin: %s\n", err)
return
}
// List all of the users pins.
listPins, _, err := api.ListPins(channelID)
if err != nil {
fmt.Printf("Error listing pins: %s\n", err)
return
}
fmt.Printf("\n")
fmt.Printf("All pins by %s...\n", authTest.User)
for _, item := range listPins {
fmt.Printf(" > Item type: %s\n", item.Type)
}
// Remove the pin.
err = api.RemovePin(channelID, msgRef)
if err != nil {
fmt.Printf("Error remove pin: %s\n", err)
return
}
if err = api.ArchiveChannel(channelID); err != nil {
fmt.Printf("Error archiving channel: %s\n", err)
return
}
}

View File

@ -0,0 +1,126 @@
package main
import (
"flag"
"fmt"
"github.com/nlopes/slack"
)
func main() {
var (
apiToken string
debug bool
)
flag.StringVar(&apiToken, "token", "YOUR_TOKEN_HERE", "Your Slack API Token")
flag.BoolVar(&debug, "debug", false, "Show JSON output")
flag.Parse()
api := slack.New(apiToken)
if debug {
api.SetDebug(true)
}
var (
postAsUserName string
postAsUserID string
postToUserName string
postToUserID string
postToChannelID string
)
// Find the user to post as.
authTest, err := api.AuthTest()
if err != nil {
fmt.Printf("Error getting channels: %s\n", err)
return
}
// Post as the authenticated user.
postAsUserName = authTest.User
postAsUserID = authTest.UserID
// Posting to DM with self causes a conversation with slackbot.
postToUserName = authTest.User
postToUserID = authTest.UserID
// Find the channel.
_, _, chanID, err := api.OpenIMChannel(postToUserID)
if err != nil {
fmt.Printf("Error opening IM: %s\n", err)
return
}
postToChannelID = chanID
fmt.Printf("Posting as %s (%s) in DM with %s (%s), channel %s\n", postAsUserName, postAsUserID, postToUserName, postToUserID, postToChannelID)
// Post a message.
postParams := slack.PostMessageParameters{}
channelID, timestamp, err := api.PostMessage(postToChannelID, "Is this any good?", postParams)
if err != nil {
fmt.Printf("Error posting message: %s\n", err)
return
}
// Grab a reference to the message.
msgRef := slack.NewRefToMessage(channelID, timestamp)
// React with :+1:
if err := api.AddReaction("+1", msgRef); err != nil {
fmt.Printf("Error adding reaction: %s\n", err)
return
}
// React with :-1:
if err := api.AddReaction("cry", msgRef); err != nil {
fmt.Printf("Error adding reaction: %s\n", err)
return
}
// Get all reactions on the message.
msgReactions, err := api.GetReactions(msgRef, slack.NewGetReactionsParameters())
if err != nil {
fmt.Printf("Error getting reactions: %s\n", err)
return
}
fmt.Printf("\n")
fmt.Printf("%d reactions to message...\n", len(msgReactions))
for _, r := range msgReactions {
fmt.Printf(" %d users say %s\n", r.Count, r.Name)
}
// List all of the users reactions.
listReactions, _, err := api.ListReactions(slack.NewListReactionsParameters())
if err != nil {
fmt.Printf("Error listing reactions: %s\n", err)
return
}
fmt.Printf("\n")
fmt.Printf("All reactions by %s...\n", authTest.User)
for _, item := range listReactions {
fmt.Printf("%d on a %s...\n", len(item.Reactions), item.Type)
for _, r := range item.Reactions {
fmt.Printf(" %s (along with %d others)\n", r.Name, r.Count-1)
}
}
// Remove the :cry: reaction.
err = api.RemoveReaction("cry", msgRef)
if err != nil {
fmt.Printf("Error remove reaction: %s\n", err)
return
}
// Get all reactions on the message.
msgReactions, err = api.GetReactions(msgRef, slack.NewGetReactionsParameters())
if err != nil {
fmt.Printf("Error getting reactions: %s\n", err)
return
}
fmt.Printf("\n")
fmt.Printf("%d reactions to message after removing cry...\n", len(msgReactions))
for _, r := range msgReactions {
fmt.Printf(" %d users say %s\n", r.Count, r.Name)
}
}

46
vendor/github.com/nlopes/slack/examples/stars/stars.go generated vendored Normal file
View File

@ -0,0 +1,46 @@
package main
import (
"flag"
"fmt"
"github.com/nlopes/slack"
)
func main() {
var (
apiToken string
debug bool
)
flag.StringVar(&apiToken, "token", "YOUR_TOKEN_HERE", "Your Slack API Token")
flag.BoolVar(&debug, "debug", false, "Show JSON output")
flag.Parse()
api := slack.New(apiToken)
if debug {
api.SetDebug(true)
}
// Get all stars for the usr.
params := slack.NewStarsParameters()
starredItems, _, err := api.GetStarred(params)
if err != nil {
fmt.Printf("Error getting stars: %s\n", err)
return
}
for _, s := range starredItems {
var desc string
switch s.Type {
case slack.TYPE_MESSAGE:
desc = s.Message.Text
case slack.TYPE_FILE:
desc = s.File.Name
case slack.TYPE_FILE_COMMENT:
desc = s.File.Name + " - " + s.Comment.Comment
case slack.TYPE_CHANNEL, slack.TYPE_IM, slack.TYPE_GROUP:
desc = s.Channel
}
fmt.Printf("Starred %s: %s\n", s.Type, desc)
}
}

25
vendor/github.com/nlopes/slack/examples/team/team.go generated vendored Normal file
View File

@ -0,0 +1,25 @@
package main
import (
"fmt"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR_TOKEN_HERE")
//Example for single user
billingActive, err := api.GetBillableInfo("U023BECGF")
if err != nil {
fmt.Printf("%s\n", err)
return
}
fmt.Printf("ID: U023BECGF, BillingActive: %v\n\n\n", billingActive["U023BECGF"])
//Example for team
billingActiveForTeam, _ := api.GetBillableInfoForTeam()
for id, value := range billingActiveForTeam {
fmt.Printf("ID: %v, BillingActive: %v\n", id, value)
}
}

17
vendor/github.com/nlopes/slack/examples/users/users.go generated vendored Normal file
View File

@ -0,0 +1,17 @@
package main
import (
"fmt"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR_TOKEN_HERE")
user, err := api.GetUserInfo("U023BECGF")
if err != nil {
fmt.Printf("%s\n", err)
return
}
fmt.Printf("ID: %s, Fullname: %s, Email: %s\n", user.ID, user.Profile.RealName, user.Profile.Email)
}

View File

@ -0,0 +1,54 @@
package main
import (
"fmt"
"log"
"os"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR TOKEN HERE")
logger := log.New(os.Stdout, "slack-bot: ", log.Lshortfile|log.LstdFlags)
slack.SetLogger(logger)
api.SetDebug(true)
rtm := api.NewRTM()
go rtm.ManageConnection()
for msg := range rtm.IncomingEvents {
fmt.Print("Event Received: ")
switch ev := msg.Data.(type) {
case *slack.HelloEvent:
// Ignore hello
case *slack.ConnectedEvent:
fmt.Println("Infos:", ev.Info)
fmt.Println("Connection counter:", ev.ConnectionCount)
// Replace C2147483705 with your Channel ID
rtm.SendMessage(rtm.NewOutgoingMessage("Hello world", "C2147483705"))
case *slack.MessageEvent:
fmt.Printf("Message: %v\n", ev)
case *slack.PresenceChangeEvent:
fmt.Printf("Presence Change: %v\n", ev)
case *slack.LatencyReport:
fmt.Printf("Current latency: %v\n", ev.Value)
case *slack.RTMError:
fmt.Printf("Error: %s\n", ev.Error())
case *slack.InvalidAuthEvent:
fmt.Printf("Invalid credentials")
return
default:
// Ignore other events..
// fmt.Printf("Unexpected: %v\n", msg.Data)
}
}
}

View File

@ -267,10 +267,7 @@ func (api *Client) DeleteFileContext(ctx context.Context, fileID string) error {
"file": {fileID},
}
_, err := fileRequest(ctx, "files.delete", values, api.debug)
if err != nil {
return err
}
return nil
return err
}

View File

@ -208,10 +208,7 @@ func (api *Client) LeaveGroupContext(ctx context.Context, group string) error {
"channel": {group},
}
_, err := groupRequest(ctx, "groups.leave", values, api.debug)
if err != nil {
return err
}
return nil
return err
}
// KickUserFromGroup kicks a user from a group
@ -227,10 +224,7 @@ func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user str
"user": {user},
}
_, err := groupRequest(ctx, "groups.kick", values, api.debug)
if err != nil {
return err
}
return nil
return err
}
// GetGroups retrieves all groups
@ -289,10 +283,7 @@ func (api *Client) SetGroupReadMarkContext(ctx context.Context, group, ts string
"ts": {ts},
}
_, err := groupRequest(ctx, "groups.mark", values, api.debug)
if err != nil {
return err
}
return nil
return err
}
// OpenGroup opens a private group

View File

@ -3,6 +3,7 @@ package slack
// OutgoingMessage is used for the realtime API, and seems incomplete.
type OutgoingMessage struct {
ID int `json:"id"`
// channel ID
Channel string `json:"channel,omitempty"`
Text string `json:"text,omitempty"`
Type string `json:"type,omitempty"`
@ -121,12 +122,12 @@ type Pong struct {
// NewOutgoingMessage prepares an OutgoingMessage that the user can
// use to send a message. Use this function to properly set the
// messageID.
func (rtm *RTM) NewOutgoingMessage(text string, channel string) *OutgoingMessage {
func (rtm *RTM) NewOutgoingMessage(text string, channelID string) *OutgoingMessage {
id := rtm.idGen.Next()
return &OutgoingMessage{
ID: id,
Type: "message",
Channel: channel,
Channel: channelID,
Text: text,
}
}
@ -134,11 +135,11 @@ func (rtm *RTM) NewOutgoingMessage(text string, channel string) *OutgoingMessage
// NewTypingMessage prepares an OutgoingMessage that the user can
// use to send as a typing indicator. Use this function to properly set the
// messageID.
func (rtm *RTM) NewTypingMessage(channel string) *OutgoingMessage {
func (rtm *RTM) NewTypingMessage(channelID string) *OutgoingMessage {
id := rtm.idGen.Next()
return &OutgoingMessage{
ID: id,
Type: "typing",
Channel: channel,
Channel: channelID,
}
}

View File

@ -13,6 +13,7 @@ import (
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
@ -42,6 +43,14 @@ func (s WebError) Error() string {
return string(s)
}
type RateLimitedError struct {
RetryAfter time.Duration
}
func (e *RateLimitedError) Error() string {
return fmt.Sprintf("Slack rate limit exceeded, retry after %s", e.RetryAfter)
}
func fileUploadReq(ctx context.Context, path, fieldname, filename string, values url.Values, r io.Reader) (*http.Request, error) {
body := &bytes.Buffer{}
wr := multipart.NewWriter(body)
@ -79,12 +88,7 @@ func parseResponseBody(body io.ReadCloser, intf *interface{}, debug bool) error
logger.Printf("parseResponseBody: %s\n", string(response))
}
err = json.Unmarshal(response, &intf)
if err != nil {
return err
}
return nil
return json.Unmarshal(response, &intf)
}
func postLocalWithMultipartResponse(ctx context.Context, path, fpath, fieldname string, values url.Values, intf interface{}, debug bool) error {
@ -112,8 +116,16 @@ func postWithMultipartResponse(ctx context.Context, path, name, fieldname string
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64)
if err != nil {
return err
}
return &RateLimitedError{time.Duration(retry) * time.Second}
}
// Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
if resp.StatusCode != 200 {
if resp.StatusCode != http.StatusOK {
logResponse(resp, debug)
return fmt.Errorf("Slack server error: %s.", resp.Status)
}
@ -136,8 +148,16 @@ func postForm(ctx context.Context, endpoint string, values url.Values, intf inte
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64)
if err != nil {
return err
}
return &RateLimitedError{time.Duration(retry) * time.Second}
}
// Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
if resp.StatusCode != 200 {
if resp.StatusCode != http.StatusOK {
logResponse(resp, debug)
return fmt.Errorf("Slack server error: %s.", resp.Status)
}

View File

@ -27,17 +27,8 @@ func (api *Client) StartRTMContext(ctx context.Context) (info *Info, websocketUR
if !response.Ok {
return nil, "", response.Error
}
// websocket.Dial does not accept url without the port (yet)
// Fixed by: https://github.com/golang/net/commit/5058c78c3627b31e484a81463acd51c7cecc06f3
// but slack returns the address with no port, so we have to fix it
api.Debugln("Using URL:", response.Info.URL)
websocketURL, err = websocketizeURLPort(response.Info.URL)
if err != nil {
return nil, "", fmt.Errorf("parsing response URL: %s", err)
}
return &response.Info, websocketURL, nil
return &response.Info, response.Info.URL, nil
}
// ConnectRTM calls the "rtm.connect" endpoint and returns the provided URL and the compact Info block.
@ -59,17 +50,8 @@ func (api *Client) ConnectRTMContext(ctx context.Context) (info *Info, websocket
if !response.Ok {
return nil, "", response.Error
}
// websocket.Dial does not accept url without the port (yet)
// Fixed by: https://github.com/golang/net/commit/5058c78c3627b31e484a81463acd51c7cecc06f3
// but slack returns the address with no port, so we have to fix it
api.Debugln("Using URL:", response.Info.URL)
websocketURL, err = websocketizeURLPort(response.Info.URL)
if err != nil {
return nil, "", fmt.Errorf("parsing response URL: %s", err)
}
return &response.Info, websocketURL, nil
return &response.Info, response.Info.URL, nil
}
// NewRTM returns a RTM, which provides a fully managed connection to
@ -90,6 +72,7 @@ func (api *Client) NewRTMWithOptions(options *RTMOptions) *RTM {
isConnected: false,
wasIntentional: true,
killChannel: make(chan bool),
disconnected: make(chan struct{}),
forcePing: make(chan bool),
rawEvents: make(chan json.RawMessage),
idGen: NewSafeID(1),

View File

@ -3,12 +3,13 @@ package slack
import (
"context"
"errors"
"fmt"
"log"
"net/url"
"os"
)
var logger *log.Logger // A logger that can be set by consumers
var logger stdLogger // A logger that can be set by consumers
/*
Added as a var so that we can change this for testing purposes
*/
@ -41,12 +42,31 @@ type Client struct {
debug bool
}
// stdLogger is a logger interface compatible with both stdlib and some
// 3rd party loggers such as logrus.
type stdLogger interface {
Print(...interface{})
Printf(string, ...interface{})
Println(...interface{})
Fatal(...interface{})
Fatalf(string, ...interface{})
Fatalln(...interface{})
Panic(...interface{})
Panicf(string, ...interface{})
Panicln(...interface{})
Output(int, string) error
}
// SetLogger let's library users supply a logger, so that api debugging
// can be logged along with the application's debugging info.
func SetLogger(l *log.Logger) {
func SetLogger(l stdLogger) {
logger = l
}
// New creates new Client.
func New(token string) *Client {
s := &Client{}
s.config.token = token
@ -83,12 +103,12 @@ func (api *Client) SetDebug(debug bool) {
func (api *Client) Debugf(format string, v ...interface{}) {
if api.debug {
logger.Printf(format, v...)
logger.Output(2, fmt.Sprintf(format, v...))
}
}
func (api *Client) Debugln(v ...interface{}) {
if api.debug {
logger.Println(v...)
logger.Output(2, fmt.Sprintln(v...))
}
}

View File

@ -200,10 +200,7 @@ func (api *Client) SetUserAsActiveContext(ctx context.Context) error {
"token": {api.config.token},
}
_, err := userRequest(ctx, "users.setActive", values, api.debug)
if err != nil {
return err
}
return nil
return err
}
// SetUserPresence changes the currently authenticated user presence
@ -247,8 +244,8 @@ func (api *Client) GetUserIdentityContext(ctx context.Context) (*UserIdentityRes
}
// SetUserPhoto changes the currently authenticated user's profile image
func (api *Client) SetUserPhoto(ctx context.Context, image string, params UserSetPhotoParams) error {
return api.SetUserPhoto(context.Background(), image, params)
func (api *Client) SetUserPhoto(image string, params UserSetPhotoParams) error {
return api.SetUserPhotoContext(context.Background(), image, params)
}
// SetUserPhotoContext changes the currently authenticated user's profile image using a custom context

View File

@ -27,6 +27,7 @@ type RTM struct {
IncomingEvents chan RTMEvent
outgoingMessages chan OutgoingMessage
killChannel chan bool
disconnected chan struct{} // disconnected is closed when Disconnect is invoked, regardless of connection state. Allows for ManagedConnection to not leak.
forcePing chan bool
rawEvents chan json.RawMessage
wasIntentional bool
@ -59,9 +60,14 @@ type RTMOptions struct {
// Disconnect and wait, blocking until a successful disconnection.
func (rtm *RTM) Disconnect() error {
// this channel is always closed on disconnect. lets the ManagedConnection() function
// properly clean up.
close(rtm.disconnected)
if !rtm.isConnected {
return errors.New("Invalid call to Disconnect - Slack API is already disconnected")
}
rtm.killChannel <- true
return nil
}

View File

@ -99,6 +99,15 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke
Attempt: boff.attempts,
ErrorObj: err,
}}
// check if Disconnect() has been invoked.
select {
case _ = <-rtm.disconnected:
rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{Intentional: true}}
return nil, nil, fmt.Errorf("disconnect received while trying to connect")
default:
}
// get time we should wait before attempting to connect again
dur := boff.Duration()
rtm.Debugf("reconnection %d failed: %s", boff.attempts+1, err)
@ -124,7 +133,8 @@ func (rtm *RTM) startRTMAndDial(useRTMStart bool) (*Info, *websocket.Conn, error
return nil, nil, err
}
conn, err := websocketProxyDial(url, "http://api.slack.com")
// Only use HTTPS for connections to prevent MITM attacks on the connection.
conn, err := websocketProxyDial(url, "https://api.slack.com")
if err != nil {
return nil, nil, err
}
@ -317,10 +327,13 @@ func (rtm *RTM) handleAck(event json.RawMessage) {
rtm.Debugln(" -> Erroneous 'ack' event:", string(event))
return
}
if ack.Ok {
rtm.IncomingEvents <- RTMEvent{"ack", ack}
} else {
} else if ack.RTMResponse.Error != nil {
rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{ack.Error}}
} else {
rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{fmt.Errorf("ack decode failure")}}
}
}

View File

@ -32,8 +32,7 @@ func websocketHTTPConnect(proxy, urlString string) (net.Conn, error) {
}
cc := httputil.NewProxyClientConn(p, nil)
cc.Do(&req)
if err != nil && err != httputil.ErrPersistEOF {
if _, err := cc.Do(&req); err != nil {
return nil, err
}

18
vendor/manifest vendored
View File

@ -295,11 +295,11 @@
"notests": true
},
{
"importpath": "github.com/matterbridge/slack",
"repository": "https://github.com/matterbridge/slack",
"importpath": "github.com/matterbridge/gomatrix",
"repository": "https://github.com/matterbridge/gomatrix",
"vcs": "git",
"revision": "1c6e6305bf9c07fc603c9cf28f09ab0517a03120",
"branch": "matterbridge",
"revision": "78ac6a1a0f5fb9eb1684d85a3ed581a742fe4c79",
"branch": "work",
"notests": true
},
{
@ -441,6 +441,14 @@
"path": "/i18n",
"notests": true
},
{
"importpath": "github.com/nlopes/slack",
"repository": "https://github.com/nlopes/slack",
"vcs": "git",
"revision": "107290b5bbaf3e634833346bb4ff389b1c782bc7",
"branch": "master",
"notests": true
},
{
"importpath": "github.com/paulrosania/go-charset",
"repository": "https://github.com/paulrosania/go-charset",
@ -712,7 +720,7 @@
"importpath": "golang.org/x/net/websocket",
"repository": "https://go.googlesource.com/net",
"vcs": "git",
"revision": "6c23252515492caf9b228a9d5cabcdbde29f7f82",
"revision": "434ec0c7fe3742c984919a691b2018a6e9694425",
"branch": "master",
"path": "/websocket",
"notests": true