mirror of
https://github.com/cwinfo/matterbridge.git
synced 2025-06-27 02:59:24 +00:00
Compare commits
156 Commits
Author | SHA1 | Date | |
---|---|---|---|
222cccf388 | |||
bab308508e | |||
dedb83c867 | |||
723a90cdd6 | |||
67d2398fa8 | |||
5f3b6ec007 | |||
55ab0c12f1 | |||
d1227b5fc9 | |||
6ea368c383 | |||
e92b6de09f | |||
e622587db4 | |||
f2efc06d1f | |||
a2b94452db | |||
4c506f7cc3 | |||
7886f05e88 | |||
f58be0d1c1 | |||
1152394bc1 | |||
a082b5a590 | |||
bae9484df2 | |||
6f78485878 | |||
fd0fe3390b | |||
2522158127 | |||
8be107cecc | |||
5aab158c0b | |||
1d33e60e36 | |||
83c28cb857 | |||
df5bce27b0 | |||
2b15739b48 | |||
3480c88e90 | |||
432cd0f99d | |||
e8b3e9b22d | |||
d4a47671ea | |||
0bcd1e62f3 | |||
80822b7fff | |||
78f1011f52 | |||
67f6257617 | |||
169c614489 | |||
da908c438a | |||
9c9c4bf1f9 | |||
7764493298 | |||
64a20ee61b | |||
62d1af8c37 | |||
0f5274fdf6 | |||
2e2187ebf4 | |||
762c3350f4 | |||
e1a4d7f77e | |||
a7a4554a85 | |||
6bd808ce91 | |||
a5c143bc46 | |||
87c9cac756 | |||
6a047f8722 | |||
6523494e83 | |||
7c6ce8bb90 | |||
dafbfe4021 | |||
a4d5c94d9b | |||
7119e378a7 | |||
e1dc3032c1 | |||
5de03b8921 | |||
7631d43c48 | |||
d0b2ee5c85 | |||
8830a5a1df | |||
ee87626a93 | |||
9f15d38c1c | |||
4a96a977c0 | |||
9a95293bdf | |||
0b3a06d263 | |||
9a6249c4f5 | |||
50bd51e461 | |||
04f8013314 | |||
a0aaf0057a | |||
8e78b3e6be | |||
57a503818d | |||
25d2ff3e9b | |||
31902d3e57 | |||
16f3fa6bae | |||
1f706673cf | |||
fac5f69ad2 | |||
97c944bb63 | |||
d0c4fe78ee | |||
265457b451 | |||
4a4a29c9f6 | |||
0a91b9e1c9 | |||
f56163295c | |||
d1c87c068b | |||
fa20761110 | |||
e4a0e0a0e9 | |||
d30ae19e2a | |||
5c919e6bff | |||
434393d1c3 | |||
af9aa5d7cb | |||
05eb75442a | |||
3496ed0c7e | |||
1b89604c7a | |||
67a9d133e9 | |||
ed9118b346 | |||
59e55cfbd5 | |||
788d3b32ac | |||
1d414cf2fd | |||
cc3c168162 | |||
1ee6837f0e | |||
27dcea7c5b | |||
dcda7f7b8c | |||
e0cbb69a4f | |||
7ec95f786d | |||
1efe40add5 | |||
cbd73ee313 | |||
34227a7a39 | |||
71cb9b2d1d | |||
cd4c9b194f | |||
98762a0235 | |||
2fd1fd9573 | |||
aff3964078 | |||
2778580397 | |||
962062fe44 | |||
0578b21270 | |||
36a800c3f5 | |||
6d21f84187 | |||
f1e9833310 | |||
46f5acc4f9 | |||
95d4dcaeb3 | |||
64c542e614 | |||
13d081ea80 | |||
c0f9d86287 | |||
bcdecdaa73 | |||
daac3ebca2 | |||
639f9cf966 | |||
4fc48b5aa4 | |||
307ff77b42 | |||
9b500bc5f7 | |||
e313154134 | |||
27e94c438d | |||
58392876df | |||
115c4b1aa7 | |||
ba5649d259 | |||
1b30575510 | |||
7dbebd3ea7 | |||
6f18790352 | |||
d1e04a2ece | |||
bea0bbd0c2 | |||
0530503ef2 | |||
d1e8ff814b | |||
4f8ae761a2 | |||
b530e92834 | |||
b2a6777995 | |||
b461fc5e40 | |||
b7a8c6b60f | |||
41aa8ad799 | |||
7973baedd0 | |||
299b71d982 | |||
76aafe1fa8 | |||
95a0229aaf | |||
915a8fbad7 | |||
d4d7fef313 | |||
4e1dc9f885 | |||
155ae80d22 | |||
c7e336efd9 |
28
.github/ISSUE_TEMPLATE.md
vendored
28
.github/ISSUE_TEMPLATE.md
vendored
@ -1,22 +1,36 @@
|
||||
If you have a configuration problem, please first try to create a basic configuration following the instructions on [the wiki](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) before filing an issue.
|
||||
<!-- This is a bug report template. By following the instructions below and
|
||||
filling out the sections with your information, you will help the us to get all
|
||||
the necessary data to fix your issue.
|
||||
|
||||
Please answer the following questions.
|
||||
You can also preview your report before submitting it.
|
||||
|
||||
### Which version of matterbridge are you using?
|
||||
run ```matterbridge -version```
|
||||
Text between <!-- and --> marks will be invisible in the report.
|
||||
-->
|
||||
|
||||
### If you're having problems with mattermost please specify mattermost version.
|
||||
<!-- If you have a configuration problem, please first try to create a basic configuration following the instructions on [the wiki](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) before filing an issue. -->
|
||||
|
||||
|
||||
### Environment
|
||||
<!-- run `matterbridge -version` -->
|
||||
<!-- If you're having problems with mattermost also specify the mattermost version. -->
|
||||
Version:
|
||||
|
||||
<!-- What operating system are you using ? (be as specific as possible) -->
|
||||
Operating system:
|
||||
|
||||
<!-- If you compiled matterbridge yourself:
|
||||
* Specify the output of `go version`
|
||||
* Specify the output of `git rev-parse HEAD` -->
|
||||
|
||||
### Please describe the expected behavior.
|
||||
|
||||
|
||||
### Please describe the actual behavior.
|
||||
#### Use logs from running ```matterbridge -debug``` if possible.
|
||||
<!-- Use logs from running `matterbridge -debug` if possible. -->
|
||||
|
||||
|
||||
### Any steps to reproduce the behavior?
|
||||
|
||||
|
||||
### Please add your configuration file
|
||||
#### (be sure to exclude or anonymize private data (tokens/passwords))
|
||||
<!-- (be sure to exclude or anonymize private data (tokens/passwords)) -->
|
||||
|
@ -34,7 +34,7 @@ before_script:
|
||||
# flunk the build and immediately stop. It's sorta like having
|
||||
# set -e enabled in bash.
|
||||
script:
|
||||
- test -z $(gofmt -s -l $GO_FILES) # Fail if a .go file hasn't been formatted with gofmt
|
||||
#- test -z $(gofmt -s -l $GO_FILES) # Fail if a .go file hasn't been formatted with gofmt
|
||||
- go test -v -race $PKGS # Run all the tests with the race detector enabled
|
||||
- go vet $PKGS # go vet is the official Go static analyzer
|
||||
- megacheck $PKGS # "go vet on steroids" + linter
|
||||
|
39
README.md
39
README.md
@ -1,17 +1,18 @@
|
||||
# matterbridge
|
||||
Click on one of the badges below to join the chat
|
||||
|
||||
[](https://gitter.im/42wim/matterbridge) [](https://webchat.freenode.net/?channels=matterbridgechat) [](https://discord.gg/AkKPtrQ) [](https://riot.im/app/#/room/#matterbridge:matrix.org) [](https://join.slack.com/matterbridgechat/shared_invite/MjEwODMxNjU1NDMwLTE0OTk2MTU3NTMtMzZkZmRiNDZhOA) [](https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e) 
|
||||
[](https://gitter.im/42wim/matterbridge) [](https://webchat.freenode.net/?channels=matterbridgechat) [](https://discord.gg/AkKPtrQ) [](https://riot.im/app/#/room/#matterbridge:matrix.org) [](https://join.slack.com/matterbridgechat/shared_invite/MjEwODMxNjU1NDMwLTE0OTk2MTU3NTMtMzZkZmRiNDZhOA) [](https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e) [](https://inverse.chat) [](https://www.twitch.tv/matterbridge)
|
||||
|
||||
[](https://github.com/42wim/matterbridge/releases/latest) [](https://bintray.com/42wim/nightly/Matterbridge/_latestVersion)
|
||||
|
||||

|
||||
|
||||
Simple bridge between Mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram, Rocket.Chat, Hipchat(via xmpp), Matrix and Steam.
|
||||
Simple bridge between Mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram, Rocket.Chat, Hipchat(via xmpp), Matrix, Steam and ssh-chat
|
||||
Has a REST API.
|
||||
Minecraft server chat support via [MatterLink](https://github.com/elytra/MatterLink)
|
||||
|
||||
# Table of Contents
|
||||
* [Features](#features)
|
||||
* [Features](https://github.com/42wim/matterbridge/wiki/Features)
|
||||
* [Requirements](#requirements)
|
||||
* [Screenshots](https://github.com/42wim/matterbridge/wiki/)
|
||||
* [Installing](#installing)
|
||||
@ -27,17 +28,25 @@ Has a REST API.
|
||||
* [Thanks](#thanks)
|
||||
|
||||
# Features
|
||||
* Relays public channel messages between multiple mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram, Rocket.Chat, Hipchat (via xmpp), Matrix and Steam.
|
||||
Pick and mix.
|
||||
* Support private groups on your mattermost/slack.
|
||||
* Allow for bridging the same bridges, which means you can eg bridge between multiple mattermosts.
|
||||
* The bridge is now a gateway which has support multiple in and out bridges. (and supports multiple gateways).
|
||||
* Edits and delete messages across bridges that support it (mattermost,slack,discord,gitter,telegram)
|
||||
* REST API to read/post messages to bridges (WIP).
|
||||
* [Support bridging between any protocols](https://github.com/42wim/matterbridge/wiki/Features#support-bridging-between-any-protocols)
|
||||
* [Support multiple gateways(bridges) for your protocols](https://github.com/42wim/matterbridge/wiki/Features#support-multiple-gatewaysbridges-for-your-protocols)
|
||||
* [Message edits and deletes](https://github.com/42wim/matterbridge/wiki/Features#message-edits-and-deletes)
|
||||
* [Attachment / files handling](https://github.com/42wim/matterbridge/wiki/Features#attachment--files-handling)
|
||||
* [Username and avatar spoofing](https://github.com/42wim/matterbridge/wiki/Features#username-and-avatar-spoofing)
|
||||
* [Private groups](https://github.com/42wim/matterbridge/wiki/Features#private-groups)
|
||||
* [API](https://github.com/42wim/matterbridge/wiki/Features#api)
|
||||
|
||||
## API
|
||||
The API is very basic at the moment and rather undocumented.
|
||||
|
||||
Used by at least 2 projects. Feel free to make a PR to add your project to this list.
|
||||
|
||||
* [MatterLink](https://github.com/elytra/MatterLink) (Matterbridge link for Minecraft Server chat)
|
||||
* [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
|
||||
|
||||
# Requirements
|
||||
Accounts to one of the supported bridges
|
||||
* [Mattermost](https://github.com/mattermost/platform/) 3.8.x - 3.10.x, 4.0.x - 4.2.x
|
||||
* [Mattermost](https://github.com/mattermost/platform/) 3.8.x - 3.10.x, 4.x
|
||||
* [IRC](http://www.mirc.com/servers.html)
|
||||
* [XMPP](https://jabber.org)
|
||||
* [Gitter](https://gitter.im)
|
||||
@ -48,17 +57,19 @@ 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)
|
||||
* [Ssh-chat](https://github.com/shazow/ssh-chat)
|
||||
|
||||
# Screenshots
|
||||
See https://github.com/42wim/matterbridge/wiki
|
||||
|
||||
# Installing
|
||||
## Binaries
|
||||
* Latest stable release [v1.3.0](https://github.com/42wim/matterbridge/releases/latest)
|
||||
* Latest stable release [v1.8.0](https://github.com/42wim/matterbridge/releases/latest)
|
||||
* Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/)
|
||||
|
||||
## Building
|
||||
Go 1.7+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed, including setting up your [GOPATH] (https://golang.org/doc/code.html#GOPATH)
|
||||
Go 1.8+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed, including setting up your [GOPATH] (https://golang.org/doc/code.html#GOPATH)
|
||||
|
||||
```
|
||||
cd $GOPATH
|
||||
@ -175,7 +186,7 @@ Matterbridge wouldn't exist without these libraries:
|
||||
* echo - https://github.com/labstack/echo
|
||||
* gitter - https://github.com/sromku/go-gitter
|
||||
* gops - https://github.com/google/gops
|
||||
* irc - https://github.com/thoj/go-ircevent
|
||||
* irc - https://github.com/lrstanley/girc
|
||||
* mattermost - https://github.com/mattermost/platform
|
||||
* matrix - https://github.com/matrix-org/gomatrix
|
||||
* slack - https://github.com/nlopes/slack
|
||||
|
@ -1,21 +1,21 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/labstack/echo"
|
||||
"github.com/labstack/echo/middleware"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/zfjagann/golang-ring"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Api struct {
|
||||
Config *config.Protocol
|
||||
Remote chan config.Message
|
||||
Account string
|
||||
Messages ring.Ring
|
||||
sync.RWMutex
|
||||
*config.BridgeConfig
|
||||
}
|
||||
|
||||
type ApiMessage struct {
|
||||
@ -30,26 +30,30 @@ var flog *log.Entry
|
||||
var protocol = "api"
|
||||
|
||||
func init() {
|
||||
flog = log.WithFields(log.Fields{"module": protocol})
|
||||
flog = log.WithFields(log.Fields{"prefix": protocol})
|
||||
}
|
||||
|
||||
func New(cfg config.Protocol, account string, c chan config.Message) *Api {
|
||||
b := &Api{}
|
||||
func New(cfg *config.BridgeConfig) *Api {
|
||||
b := &Api{BridgeConfig: cfg}
|
||||
e := echo.New()
|
||||
e.HideBanner = true
|
||||
e.HidePort = true
|
||||
b.Messages = ring.Ring{}
|
||||
b.Messages.SetCapacity(cfg.Buffer)
|
||||
b.Config = &cfg
|
||||
b.Account = account
|
||||
b.Remote = c
|
||||
b.Messages.SetCapacity(b.Config.Buffer)
|
||||
if b.Config.Token != "" {
|
||||
e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
|
||||
return key == b.Config.Token, nil
|
||||
}))
|
||||
}
|
||||
e.GET("/api/messages", b.handleMessages)
|
||||
e.GET("/api/stream", b.handleStream)
|
||||
e.POST("/api/message", b.handlePostMessage)
|
||||
go func() {
|
||||
flog.Fatal(e.Start(cfg.BindAddress))
|
||||
if b.Config.BindAddress == "" {
|
||||
flog.Fatalf("No BindAddress configured.")
|
||||
}
|
||||
flog.Infof("Listening on %s", b.Config.BindAddress)
|
||||
flog.Fatal(e.Start(b.Config.BindAddress))
|
||||
}()
|
||||
return b
|
||||
}
|
||||
@ -78,21 +82,18 @@ func (b *Api) Send(msg config.Message) (string, error) {
|
||||
}
|
||||
|
||||
func (b *Api) handlePostMessage(c echo.Context) error {
|
||||
message := &ApiMessage{}
|
||||
if err := c.Bind(message); err != nil {
|
||||
message := config.Message{}
|
||||
if err := c.Bind(&message); err != nil {
|
||||
return err
|
||||
}
|
||||
// these values are fixed
|
||||
message.Channel = "api"
|
||||
message.Protocol = "api"
|
||||
message.Account = b.Account
|
||||
message.ID = ""
|
||||
message.Timestamp = time.Now()
|
||||
flog.Debugf("Sending message from %s on %s to gateway", message.Username, "api")
|
||||
b.Remote <- config.Message{
|
||||
Text: message.Text,
|
||||
Username: message.Username,
|
||||
UserID: message.UserID,
|
||||
Channel: "api",
|
||||
Avatar: message.Avatar,
|
||||
Account: b.Account,
|
||||
Gateway: message.Gateway,
|
||||
Protocol: "api",
|
||||
}
|
||||
b.Remote <- message
|
||||
return c.JSON(http.StatusOK, message)
|
||||
}
|
||||
|
||||
@ -103,3 +104,24 @@ func (b *Api) handleMessages(c echo.Context) error {
|
||||
b.Messages = ring.Ring{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Api) handleStream(c echo.Context) error {
|
||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
||||
c.Response().WriteHeader(http.StatusOK)
|
||||
closeNotifier := c.Response().CloseNotify()
|
||||
for {
|
||||
select {
|
||||
case <-closeNotifier:
|
||||
return nil
|
||||
default:
|
||||
msg := b.Messages.Dequeue()
|
||||
if msg != nil {
|
||||
if err := json.NewEncoder(c.Response()).Encode(msg); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Response().Flush()
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,10 +10,11 @@ import (
|
||||
"github.com/42wim/matterbridge/bridge/mattermost"
|
||||
"github.com/42wim/matterbridge/bridge/rocketchat"
|
||||
"github.com/42wim/matterbridge/bridge/slack"
|
||||
"github.com/42wim/matterbridge/bridge/sshchat"
|
||||
"github.com/42wim/matterbridge/bridge/steam"
|
||||
"github.com/42wim/matterbridge/bridge/telegram"
|
||||
"github.com/42wim/matterbridge/bridge/xmpp"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"strings"
|
||||
)
|
||||
@ -35,6 +36,12 @@ type Bridge struct {
|
||||
Joined map[string]bool
|
||||
}
|
||||
|
||||
var flog *log.Entry
|
||||
|
||||
func init() {
|
||||
flog = log.WithFields(log.Fields{"prefix": "bridge"})
|
||||
}
|
||||
|
||||
func New(cfg *config.Config, bridge *config.Bridge, c chan config.Message) *Bridge {
|
||||
b := new(Bridge)
|
||||
b.Channels = make(map[string]config.ChannelInfo)
|
||||
@ -45,44 +52,49 @@ func New(cfg *config.Config, bridge *config.Bridge, c chan config.Message) *Brid
|
||||
b.Protocol = protocol
|
||||
b.Account = bridge.Account
|
||||
b.Joined = make(map[string]bool)
|
||||
bridgeConfig := &config.BridgeConfig{General: &cfg.General, Account: bridge.Account, Remote: c}
|
||||
|
||||
// override config from environment
|
||||
config.OverrideCfgFromEnv(cfg, protocol, name)
|
||||
switch protocol {
|
||||
case "mattermost":
|
||||
b.Config = cfg.Mattermost[name]
|
||||
b.Bridger = bmattermost.New(cfg.Mattermost[name], bridge.Account, c)
|
||||
bridgeConfig.Config = cfg.Mattermost[name]
|
||||
b.Bridger = bmattermost.New(bridgeConfig)
|
||||
case "irc":
|
||||
b.Config = cfg.IRC[name]
|
||||
b.Bridger = birc.New(cfg.IRC[name], bridge.Account, c)
|
||||
bridgeConfig.Config = cfg.IRC[name]
|
||||
b.Bridger = birc.New(bridgeConfig)
|
||||
case "gitter":
|
||||
b.Config = cfg.Gitter[name]
|
||||
b.Bridger = bgitter.New(cfg.Gitter[name], bridge.Account, c)
|
||||
bridgeConfig.Config = cfg.Gitter[name]
|
||||
b.Bridger = bgitter.New(bridgeConfig)
|
||||
case "slack":
|
||||
b.Config = cfg.Slack[name]
|
||||
b.Bridger = bslack.New(cfg.Slack[name], bridge.Account, c)
|
||||
bridgeConfig.Config = cfg.Slack[name]
|
||||
b.Bridger = bslack.New(bridgeConfig)
|
||||
case "xmpp":
|
||||
b.Config = cfg.Xmpp[name]
|
||||
b.Bridger = bxmpp.New(cfg.Xmpp[name], bridge.Account, c)
|
||||
bridgeConfig.Config = cfg.Xmpp[name]
|
||||
b.Bridger = bxmpp.New(bridgeConfig)
|
||||
case "discord":
|
||||
b.Config = cfg.Discord[name]
|
||||
b.Bridger = bdiscord.New(cfg.Discord[name], bridge.Account, c)
|
||||
bridgeConfig.Config = cfg.Discord[name]
|
||||
b.Bridger = bdiscord.New(bridgeConfig)
|
||||
case "telegram":
|
||||
b.Config = cfg.Telegram[name]
|
||||
b.Bridger = btelegram.New(cfg.Telegram[name], bridge.Account, c)
|
||||
bridgeConfig.Config = cfg.Telegram[name]
|
||||
b.Bridger = btelegram.New(bridgeConfig)
|
||||
case "rocketchat":
|
||||
b.Config = cfg.Rocketchat[name]
|
||||
b.Bridger = brocketchat.New(cfg.Rocketchat[name], bridge.Account, c)
|
||||
bridgeConfig.Config = cfg.Rocketchat[name]
|
||||
b.Bridger = brocketchat.New(bridgeConfig)
|
||||
case "matrix":
|
||||
b.Config = cfg.Matrix[name]
|
||||
b.Bridger = bmatrix.New(cfg.Matrix[name], bridge.Account, c)
|
||||
bridgeConfig.Config = cfg.Matrix[name]
|
||||
b.Bridger = bmatrix.New(bridgeConfig)
|
||||
case "steam":
|
||||
b.Config = cfg.Steam[name]
|
||||
b.Bridger = bsteam.New(cfg.Steam[name], bridge.Account, c)
|
||||
bridgeConfig.Config = cfg.Steam[name]
|
||||
b.Bridger = bsteam.New(bridgeConfig)
|
||||
case "sshchat":
|
||||
bridgeConfig.Config = cfg.Sshchat[name]
|
||||
b.Bridger = bsshchat.New(bridgeConfig)
|
||||
case "api":
|
||||
b.Config = cfg.Api[name]
|
||||
b.Bridger = api.New(cfg.Api[name], bridge.Account, c)
|
||||
bridgeConfig.Config = cfg.Api[name]
|
||||
b.Bridger = api.New(bridgeConfig)
|
||||
}
|
||||
b.Config = bridgeConfig.Config
|
||||
return b
|
||||
}
|
||||
|
||||
@ -94,7 +106,7 @@ func (b *Bridge) JoinChannels() error {
|
||||
func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map[string]bool) error {
|
||||
for ID, channel := range channels {
|
||||
if !exists[ID] {
|
||||
log.Infof("%s: joining %s (%s)", b.Account, channel.Name, ID)
|
||||
flog.Infof("%s: joining %s (ID: %s)", b.Account, channel.Name, ID)
|
||||
err := b.JoinChannel(channel)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -11,7 +11,10 @@ import (
|
||||
|
||||
const (
|
||||
EVENT_JOIN_LEAVE = "join_leave"
|
||||
EVENT_TOPIC_CHANGE = "topic_change"
|
||||
EVENT_FAILURE = "failure"
|
||||
EVENT_FILE_FAILURE_SIZE = "file_failure_size"
|
||||
EVENT_AVATAR_DOWNLOAD = "avatar_download"
|
||||
EVENT_REJOIN_CHANNELS = "rejoin_channels"
|
||||
EVENT_USER_ACTION = "user_action"
|
||||
EVENT_MSG_DELETE = "msg_delete"
|
||||
@ -35,6 +38,11 @@ type Message struct {
|
||||
type FileInfo struct {
|
||||
Name string
|
||||
Data *[]byte
|
||||
Comment string
|
||||
URL string
|
||||
Size int64
|
||||
Avatar bool
|
||||
SHA string
|
||||
}
|
||||
|
||||
type ChannelInfo struct {
|
||||
@ -51,13 +59,23 @@ 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
|
||||
IgnoreNicks string // all protocols
|
||||
IgnoreMessages string // all protocols
|
||||
Jid string // xmpp
|
||||
Label string // all protocols
|
||||
Login string // mattermost, matrix
|
||||
MediaDownloadSize int // all protocols
|
||||
MediaServerDownload string
|
||||
MediaServerUpload string
|
||||
MessageDelay int // IRC, time in millisecond to wait between messages
|
||||
MessageFormat string // telegram
|
||||
MessageLength int // IRC, max length of a message allowed
|
||||
MessageQueue int // IRC, size of message queue for flood control
|
||||
MessageSplit bool // IRC, split long messages with newlines on MessageLength instead of clipping
|
||||
Muc string // xmpp
|
||||
Name string // all protocols
|
||||
Nick string // all protocols
|
||||
@ -70,16 +88,17 @@ type Protocol struct {
|
||||
NoTLS bool // mattermost
|
||||
Password string // IRC,mattermost,XMPP,matrix
|
||||
PrefixMessagesWithNick bool // mattemost, slack
|
||||
Protocol string //all protocols
|
||||
MessageQueue int // IRC, size of message queue for flood control
|
||||
MessageDelay int // IRC, time in millisecond to wait between messages
|
||||
MessageLength int // IRC, max length of a message allowed
|
||||
MessageFormat string // telegram
|
||||
Protocol string // all protocols
|
||||
RejoinDelay int // IRC
|
||||
ReplaceMessages [][]string // all protocols
|
||||
ReplaceNicks [][]string // all protocols
|
||||
RemoteNickFormat string // all protocols
|
||||
Server string // IRC,mattermost,XMPP,discord
|
||||
ShowJoinPart bool // all protocols
|
||||
ShowTopicChange bool // slack
|
||||
ShowEmbeds bool // discord
|
||||
SkipTLSVerify bool // IRC, mattermost
|
||||
StripNick bool // all protocols
|
||||
Team string // mattermost
|
||||
Token string // gitter, slack, discord, api
|
||||
URL string // mattermost, slack // DEPRECATED
|
||||
@ -133,11 +152,19 @@ type Config struct {
|
||||
Discord map[string]Protocol
|
||||
Telegram map[string]Protocol
|
||||
Rocketchat map[string]Protocol
|
||||
Sshchat map[string]Protocol
|
||||
General Protocol
|
||||
Gateway []Gateway
|
||||
SameChannelGateway []SameChannelGateway
|
||||
}
|
||||
|
||||
type BridgeConfig struct {
|
||||
Config Protocol
|
||||
General *Protocol
|
||||
Account string
|
||||
Remote chan Message
|
||||
}
|
||||
|
||||
func NewConfig(cfgfile string) *Config {
|
||||
var cfg Config
|
||||
if _, err := toml.DecodeFile(cfgfile, &cfg); err != nil {
|
||||
@ -165,6 +192,9 @@ func NewConfig(cfgfile string) *Config {
|
||||
if fail {
|
||||
log.Fatalf("Fix your config. Please see changelog for more information")
|
||||
}
|
||||
if cfg.General.MediaDownloadSize == 0 {
|
||||
cfg.General.MediaDownloadSize = 1000000
|
||||
}
|
||||
return &cfg
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
package bdiscord
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/42wim/matterbridge/bridge/helper"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -11,9 +13,6 @@ import (
|
||||
|
||||
type bdiscord struct {
|
||||
c *discordgo.Session
|
||||
Config *config.Protocol
|
||||
Remote chan config.Message
|
||||
Account string
|
||||
Channels []*discordgo.Channel
|
||||
Nick string
|
||||
UseChannelID bool
|
||||
@ -23,20 +22,18 @@ type bdiscord struct {
|
||||
webhookToken string
|
||||
channelInfoMap map[string]*config.ChannelInfo
|
||||
sync.RWMutex
|
||||
*config.BridgeConfig
|
||||
}
|
||||
|
||||
var flog *log.Entry
|
||||
var protocol = "discord"
|
||||
|
||||
func init() {
|
||||
flog = log.WithFields(log.Fields{"module": protocol})
|
||||
flog = log.WithFields(log.Fields{"prefix": protocol})
|
||||
}
|
||||
|
||||
func New(cfg config.Protocol, account string, c chan config.Message) *bdiscord {
|
||||
b := &bdiscord{}
|
||||
b.Config = &cfg
|
||||
b.Remote = c
|
||||
b.Account = account
|
||||
func New(cfg *config.BridgeConfig) *bdiscord {
|
||||
b := &bdiscord{BridgeConfig: cfg}
|
||||
b.userMemberMap = make(map[string]*discordgo.Member)
|
||||
b.channelInfoMap = make(map[string]*config.ChannelInfo)
|
||||
if b.Config.WebhookURL != "" {
|
||||
@ -141,6 +138,27 @@ func (b *bdiscord) Send(msg config.Message) (string, error) {
|
||||
_, err := b.c.ChannelMessageEdit(channelID, msg.ID, msg.Username+msg.Text)
|
||||
return msg.ID, err
|
||||
}
|
||||
|
||||
if msg.Extra != nil {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text)
|
||||
}
|
||||
// check if we have files to upload (from slack, telegram or mattermost)
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
var err error
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
files := []*discordgo.File{}
|
||||
files = append(files, &discordgo.File{fi.Name, "", bytes.NewReader(*fi.Data)})
|
||||
_, err = b.c.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{Content: msg.Username + fi.Comment, Files: files})
|
||||
if err != nil {
|
||||
flog.Errorf("file upload failed: %#v", err)
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
res, err := b.c.ChannelMessageSend(channelID, msg.Username+msg.Text)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@ -184,6 +202,7 @@ func (b *bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdat
|
||||
}
|
||||
|
||||
func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||
var err error
|
||||
// not relay our own messages
|
||||
if m.Author.Username == b.Nick {
|
||||
return
|
||||
@ -202,13 +221,14 @@ func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
|
||||
var text string
|
||||
if m.Content != "" {
|
||||
flog.Debugf("Receiving message %#v", m.Message)
|
||||
if len(m.MentionRoles) > 0 {
|
||||
m.Message.Content = b.replaceRoleMentions(m.Message.Content)
|
||||
}
|
||||
m.Message.Content = b.stripCustomoji(m.Message.Content)
|
||||
m.Message.Content = b.replaceChannelMentions(m.Message.Content)
|
||||
text, err = m.ContentWithMoreMentionsReplaced(b.c)
|
||||
if err != nil {
|
||||
flog.Errorf("ContentWithMoreMentionsReplaced failed: %s", err)
|
||||
text = m.ContentWithMentionsReplaced()
|
||||
}
|
||||
}
|
||||
|
||||
rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg",
|
||||
UserID: m.Author.ID, ID: m.ID}
|
||||
@ -304,18 +324,6 @@ func (b *bdiscord) getChannelName(id string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b *bdiscord) replaceRoleMentions(text string) string {
|
||||
roles, err := b.c.GuildRoles(b.guildID)
|
||||
if err != nil {
|
||||
flog.Debugf("%#v", string(err.(*discordgo.RESTError).ResponseBody))
|
||||
return text
|
||||
}
|
||||
for _, role := range roles {
|
||||
text = strings.Replace(text, "<@&"+role.ID+">", "@"+role.Name, -1)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func (b *bdiscord) replaceChannelMentions(text string) string {
|
||||
var err error
|
||||
re := regexp.MustCompile("<#[0-9]+>")
|
||||
@ -351,6 +359,9 @@ func (b *bdiscord) stripCustomoji(text string) string {
|
||||
// splitURL splits a webhookURL and returns the id and token
|
||||
func (b *bdiscord) splitURL(url string) (string, string) {
|
||||
webhookURLSplit := strings.Split(url, "/")
|
||||
if len(webhookURLSplit) != 7 {
|
||||
log.Fatalf("%s is no correct discord WebhookURL", url)
|
||||
}
|
||||
return webhookURLSplit[len(webhookURLSplit)-2], webhookURLSplit[len(webhookURLSplit)-1]
|
||||
}
|
||||
|
||||
|
@ -4,33 +4,28 @@ import (
|
||||
"fmt"
|
||||
"github.com/42wim/go-gitter"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/42wim/matterbridge/bridge/helper"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Bgitter struct {
|
||||
c *gitter.Gitter
|
||||
Config *config.Protocol
|
||||
Remote chan config.Message
|
||||
Account string
|
||||
User *gitter.User
|
||||
Users []gitter.User
|
||||
Rooms []gitter.Room
|
||||
*config.BridgeConfig
|
||||
}
|
||||
|
||||
var flog *log.Entry
|
||||
var protocol = "gitter"
|
||||
|
||||
func init() {
|
||||
flog = log.WithFields(log.Fields{"module": protocol})
|
||||
flog = log.WithFields(log.Fields{"prefix": protocol})
|
||||
}
|
||||
|
||||
func New(cfg config.Protocol, account string, c chan config.Message) *Bgitter {
|
||||
b := &Bgitter{}
|
||||
b.Config = &cfg
|
||||
b.Remote = c
|
||||
b.Account = account
|
||||
return b
|
||||
func New(cfg *config.BridgeConfig) *Bgitter {
|
||||
return &Bgitter{BridgeConfig: cfg}
|
||||
}
|
||||
|
||||
func (b *Bgitter) Connect() error {
|
||||
@ -95,7 +90,7 @@ func (b *Bgitter) JoinChannel(channel config.ChannelInfo) error {
|
||||
flog.Errorf("connection with gitter closed for room %s", room)
|
||||
}
|
||||
}
|
||||
}(stream, room.Name)
|
||||
}(stream, room.URI)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -125,6 +120,29 @@ func (b *Bgitter) Send(msg config.Message) (string, error) {
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if msg.Extra != nil {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
b.c.SendMessage(roomID, rmsg.Username+rmsg.Text)
|
||||
}
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
if fi.Comment != "" {
|
||||
msg.Text += fi.Comment + ": "
|
||||
}
|
||||
if fi.URL != "" {
|
||||
msg.Text = fi.URL
|
||||
}
|
||||
_, err := b.c.SendMessage(roomID, msg.Username+msg.Text)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := b.c.SendMessage(roomID, msg.Username+msg.Text)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
63
bridge/helper/helper.go
Normal file
63
bridge/helper/helper.go
Normal file
@ -0,0 +1,63 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func DownloadFile(url string) (*[]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 5,
|
||||
}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
io.Copy(&buf, resp.Body)
|
||||
data := buf.Bytes()
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
func SplitStringLength(input string, length int) string {
|
||||
a := []rune(input)
|
||||
str := ""
|
||||
for i, r := range a {
|
||||
str = str + string(r)
|
||||
if i > 0 && (i+1)%length == 0 {
|
||||
str += "\n"
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// handle all the stuff we put into extra
|
||||
func HandleExtra(msg *config.Message, general *config.Protocol) []config.Message {
|
||||
extra := msg.Extra
|
||||
rmsg := []config.Message{}
|
||||
if len(extra[config.EVENT_FILE_FAILURE_SIZE]) > 0 {
|
||||
for _, f := range extra[config.EVENT_FILE_FAILURE_SIZE] {
|
||||
fi := f.(config.FileInfo)
|
||||
text := fmt.Sprintf("file %s too big to download (%#v > allowed size: %#v)", fi.Name, fi.Size, general.MediaDownloadSize)
|
||||
rmsg = append(rmsg, config.Message{Text: text, Username: "<system> ", Channel: msg.Channel})
|
||||
}
|
||||
return rmsg
|
||||
}
|
||||
return rmsg
|
||||
}
|
||||
|
||||
func GetAvatar(av map[string]string, userid string, general *config.Protocol) string {
|
||||
if sha, ok := av[userid]; ok {
|
||||
return general.MediaServerDownload + "/" + sha + "/" + userid + ".png"
|
||||
}
|
||||
return ""
|
||||
}
|
@ -4,48 +4,47 @@ import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/42wim/go-ircevent"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/42wim/matterbridge/bridge/helper"
|
||||
"github.com/lrstanley/girc"
|
||||
"github.com/paulrosania/go-charset/charset"
|
||||
_ "github.com/paulrosania/go-charset/data"
|
||||
"github.com/saintfish/chardet"
|
||||
ircm "github.com/sorcix/irc"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type Birc struct {
|
||||
i *irc.Connection
|
||||
i *girc.Client
|
||||
Nick string
|
||||
names map[string][]string
|
||||
Config *config.Protocol
|
||||
Remote chan config.Message
|
||||
connected chan struct{}
|
||||
Local chan config.Message // local queue for flood control
|
||||
Account string
|
||||
FirstConnection bool
|
||||
|
||||
*config.BridgeConfig
|
||||
}
|
||||
|
||||
var flog *log.Entry
|
||||
var protocol = "irc"
|
||||
|
||||
func init() {
|
||||
flog = log.WithFields(log.Fields{"module": protocol})
|
||||
flog = log.WithFields(log.Fields{"prefix": protocol})
|
||||
}
|
||||
|
||||
func New(cfg config.Protocol, account string, c chan config.Message) *Birc {
|
||||
func New(cfg *config.BridgeConfig) *Birc {
|
||||
b := &Birc{}
|
||||
b.Config = &cfg
|
||||
b.BridgeConfig = cfg
|
||||
b.Nick = b.Config.Nick
|
||||
b.Remote = c
|
||||
b.names = make(map[string][]string)
|
||||
b.Account = account
|
||||
b.connected = make(chan struct{})
|
||||
if b.Config.MessageDelay == 0 {
|
||||
b.Config.MessageDelay = 1300
|
||||
@ -63,9 +62,9 @@ func New(cfg config.Protocol, account string, c chan config.Message) *Birc {
|
||||
func (b *Birc) Command(msg *config.Message) string {
|
||||
switch msg.Text {
|
||||
case "!users":
|
||||
b.i.AddCallback(ircm.RPL_NAMREPLY, b.storeNames)
|
||||
b.i.AddCallback(ircm.RPL_ENDOFNAMES, b.endNames)
|
||||
b.i.SendRaw("NAMES " + msg.Channel)
|
||||
b.i.Handlers.Add(girc.RPL_NAMREPLY, b.storeNames)
|
||||
b.i.Handlers.Add(girc.RPL_ENDOFNAMES, b.endNames)
|
||||
b.i.Cmd.SendRaw("NAMES " + msg.Channel)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@ -73,26 +72,60 @@ func (b *Birc) Command(msg *config.Message) string {
|
||||
func (b *Birc) Connect() error {
|
||||
b.Local = make(chan config.Message, b.Config.MessageQueue+10)
|
||||
flog.Infof("Connecting %s", b.Config.Server)
|
||||
i := irc.IRC(b.Config.Nick, b.Config.Nick)
|
||||
if log.GetLevel() == log.DebugLevel {
|
||||
i.Debug = true
|
||||
}
|
||||
i.UseTLS = b.Config.UseTLS
|
||||
i.UseSASL = b.Config.UseSASL
|
||||
i.SASLLogin = b.Config.NickServNick
|
||||
i.SASLPassword = b.Config.NickServPassword
|
||||
i.TLSConfig = &tls.Config{InsecureSkipVerify: b.Config.SkipTLSVerify}
|
||||
i.KeepAlive = time.Minute
|
||||
i.PingFreq = time.Minute
|
||||
if b.Config.Password != "" {
|
||||
i.Password = b.Config.Password
|
||||
}
|
||||
i.AddCallback(ircm.RPL_WELCOME, b.handleNewConnection)
|
||||
i.AddCallback(ircm.RPL_ENDOFMOTD, b.handleOtherAuth)
|
||||
err := i.Connect(b.Config.Server)
|
||||
server, portstr, err := net.SplitHostPort(b.Config.Server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
port, err := strconv.Atoi(portstr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// fix strict user handling of girc
|
||||
user := b.Config.Nick
|
||||
for !girc.IsValidUser(user) {
|
||||
if len(user) == 1 {
|
||||
user = "matterbridge"
|
||||
break
|
||||
}
|
||||
user = user[1:]
|
||||
}
|
||||
|
||||
i := girc.New(girc.Config{
|
||||
Server: server,
|
||||
ServerPass: b.Config.Password,
|
||||
Port: port,
|
||||
Nick: b.Config.Nick,
|
||||
User: user,
|
||||
Name: b.Config.Nick,
|
||||
SSL: b.Config.UseTLS,
|
||||
TLSConfig: &tls.Config{InsecureSkipVerify: b.Config.SkipTLSVerify, ServerName: server},
|
||||
PingDelay: time.Minute,
|
||||
})
|
||||
|
||||
if b.Config.UseSASL {
|
||||
i.Config.SASL = &girc.SASLPlain{b.Config.NickServNick, b.Config.NickServPassword}
|
||||
}
|
||||
|
||||
i.Handlers.Add(girc.RPL_WELCOME, b.handleNewConnection)
|
||||
i.Handlers.Add(girc.RPL_ENDOFMOTD, b.handleOtherAuth)
|
||||
i.Handlers.Add("*", b.handleOther)
|
||||
go func() {
|
||||
for {
|
||||
if err := i.Connect(); err != nil {
|
||||
flog.Errorf("error: %s", err)
|
||||
flog.Info("reconnecting in 30 seconds...")
|
||||
time.Sleep(30 * time.Second)
|
||||
i.Handlers.Clear(girc.RPL_WELCOME)
|
||||
i.Handlers.Add(girc.RPL_WELCOME, func(client *girc.Client, event girc.Event) {
|
||||
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS}
|
||||
// set our correct nick on reconnect if necessary
|
||||
b.Nick = event.Source.Name
|
||||
})
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
b.i = i
|
||||
select {
|
||||
case <-b.connected:
|
||||
@ -100,15 +133,8 @@ func (b *Birc) Connect() error {
|
||||
case <-time.After(time.Second * 30):
|
||||
return fmt.Errorf("connection timed out")
|
||||
}
|
||||
i.Debug = false
|
||||
// clear on reconnects
|
||||
i.ClearCallback(ircm.RPL_WELCOME)
|
||||
i.AddCallback(ircm.RPL_WELCOME, func(event *irc.Event) {
|
||||
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS}
|
||||
// set our correct nick on reconnect if necessary
|
||||
b.Nick = event.Nick
|
||||
})
|
||||
go i.Loop()
|
||||
//i.Debug = false
|
||||
i.Handlers.Clear("*")
|
||||
go b.doSend()
|
||||
return nil
|
||||
}
|
||||
@ -122,9 +148,9 @@ func (b *Birc) Disconnect() error {
|
||||
func (b *Birc) JoinChannel(channel config.ChannelInfo) error {
|
||||
if channel.Options.Key != "" {
|
||||
flog.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name)
|
||||
b.i.Join(channel.Name + " " + channel.Options.Key)
|
||||
b.i.Cmd.JoinKey(channel.Name, channel.Options.Key)
|
||||
} else {
|
||||
b.i.Join(channel.Name)
|
||||
b.i.Cmd.Join(channel.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -151,9 +177,36 @@ func (b *Birc) Send(msg config.Message) (string, error) {
|
||||
msg.Text = buf.String()
|
||||
}
|
||||
|
||||
if msg.Extra != nil {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
b.Local <- rmsg
|
||||
}
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
if fi.Comment != "" {
|
||||
msg.Text += fi.Comment + ": "
|
||||
}
|
||||
if fi.URL != "" {
|
||||
msg.Text = fi.URL
|
||||
}
|
||||
b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
// split long messages on messageLength, to avoid clipped messages #281
|
||||
if b.Config.MessageSplit {
|
||||
msg.Text = helper.SplitStringLength(msg.Text, b.Config.MessageLength)
|
||||
}
|
||||
for _, text := range strings.Split(msg.Text, "\n") {
|
||||
if len(text) > b.Config.MessageLength {
|
||||
text = text[:b.Config.MessageLength] + " <message clipped>"
|
||||
text = text[:b.Config.MessageLength-len(" <message clipped>")]
|
||||
if r, size := utf8.DecodeLastRuneInString(text); r == utf8.RuneError {
|
||||
text = text[:len(text)-size]
|
||||
}
|
||||
text += " <message clipped>"
|
||||
}
|
||||
if len(b.Local) < b.Config.MessageQueue {
|
||||
if len(b.Local) == b.Config.MessageQueue-1 {
|
||||
@ -173,15 +226,15 @@ func (b *Birc) doSend() {
|
||||
for msg := range b.Local {
|
||||
<-throttle.C
|
||||
if msg.Event == config.EVENT_USER_ACTION {
|
||||
b.i.Action(msg.Channel, msg.Username+msg.Text)
|
||||
b.i.Cmd.Action(msg.Channel, msg.Username+msg.Text)
|
||||
} else {
|
||||
b.i.Privmsg(msg.Channel, msg.Username+msg.Text)
|
||||
b.i.Cmd.Message(msg.Channel, msg.Username+msg.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Birc) endNames(event *irc.Event) {
|
||||
channel := event.Arguments[1]
|
||||
func (b *Birc) endNames(client *girc.Client, event girc.Event) {
|
||||
channel := event.Params[1]
|
||||
sort.Strings(b.names[channel])
|
||||
maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow()
|
||||
continued := false
|
||||
@ -194,101 +247,101 @@ func (b *Birc) endNames(event *irc.Event) {
|
||||
b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel], continued),
|
||||
Channel: channel, Account: b.Account}
|
||||
b.names[channel] = nil
|
||||
b.i.ClearCallback(ircm.RPL_NAMREPLY)
|
||||
b.i.ClearCallback(ircm.RPL_ENDOFNAMES)
|
||||
b.i.Handlers.Clear(girc.RPL_NAMREPLY)
|
||||
b.i.Handlers.Clear(girc.RPL_ENDOFNAMES)
|
||||
}
|
||||
|
||||
func (b *Birc) handleNewConnection(event *irc.Event) {
|
||||
func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) {
|
||||
flog.Debug("Registering callbacks")
|
||||
i := b.i
|
||||
b.Nick = event.Arguments[0]
|
||||
i.AddCallback("PRIVMSG", b.handlePrivMsg)
|
||||
i.AddCallback("CTCP_ACTION", b.handlePrivMsg)
|
||||
i.AddCallback(ircm.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
|
||||
i.AddCallback(ircm.NOTICE, b.handleNotice)
|
||||
//i.AddCallback(ircm.RPL_MYINFO, func(e *irc.Event) { flog.Infof("%s: %s", e.Code, strings.Join(e.Arguments[1:], " ")) })
|
||||
i.AddCallback("PING", func(e *irc.Event) {
|
||||
i.SendRaw("PONG :" + e.Message())
|
||||
flog.Debugf("PING/PONG")
|
||||
})
|
||||
i.AddCallback("JOIN", b.handleJoinPart)
|
||||
i.AddCallback("PART", b.handleJoinPart)
|
||||
i.AddCallback("QUIT", b.handleJoinPart)
|
||||
i.AddCallback("KICK", b.handleJoinPart)
|
||||
i.AddCallback("*", b.handleOther)
|
||||
b.Nick = event.Params[0]
|
||||
|
||||
i.Handlers.Add(girc.RPL_ENDOFMOTD, b.handleOtherAuth)
|
||||
i.Handlers.Add("PRIVMSG", b.handlePrivMsg)
|
||||
i.Handlers.Add("CTCP_ACTION", b.handlePrivMsg)
|
||||
i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
|
||||
i.Handlers.Add(girc.NOTICE, b.handleNotice)
|
||||
i.Handlers.Add("JOIN", b.handleJoinPart)
|
||||
i.Handlers.Add("PART", b.handleJoinPart)
|
||||
i.Handlers.Add("QUIT", b.handleJoinPart)
|
||||
i.Handlers.Add("KICK", b.handleJoinPart)
|
||||
// we are now fully connected
|
||||
b.connected <- struct{}{}
|
||||
}
|
||||
|
||||
func (b *Birc) handleJoinPart(event *irc.Event) {
|
||||
channel := event.Arguments[0]
|
||||
if event.Code == "KICK" {
|
||||
flog.Infof("Got kicked from %s by %s", channel, event.Nick)
|
||||
func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
|
||||
if len(event.Params) == 0 {
|
||||
flog.Debugf("handleJoinPart: empty Params? %#v", event)
|
||||
return
|
||||
}
|
||||
channel := event.Params[0]
|
||||
if event.Command == "KICK" {
|
||||
flog.Infof("Got kicked from %s by %s", channel, event.Source.Name)
|
||||
time.Sleep(time.Duration(b.Config.RejoinDelay) * time.Second)
|
||||
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: channel, Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS}
|
||||
return
|
||||
}
|
||||
if event.Code == "QUIT" {
|
||||
if event.Nick == b.Nick && strings.Contains(event.Raw, "Ping timeout") {
|
||||
if event.Command == "QUIT" {
|
||||
if event.Source.Name == b.Nick && strings.Contains(event.Trailing, "Ping timeout") {
|
||||
flog.Infof("%s reconnecting ..", b.Account)
|
||||
b.Remote <- config.Message{Username: "system", Text: "reconnect", Channel: channel, Account: b.Account, Event: config.EVENT_FAILURE}
|
||||
return
|
||||
}
|
||||
}
|
||||
if event.Nick != b.Nick {
|
||||
if event.Source.Name != b.Nick {
|
||||
flog.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
|
||||
b.Remote <- config.Message{Username: "system", Text: event.Nick + " " + strings.ToLower(event.Code) + "s", Channel: channel, Account: b.Account, Event: config.EVENT_JOIN_LEAVE}
|
||||
b.Remote <- config.Message{Username: "system", Text: event.Source.Name + " " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EVENT_JOIN_LEAVE}
|
||||
return
|
||||
}
|
||||
flog.Debugf("handle %#v", event)
|
||||
}
|
||||
|
||||
func (b *Birc) handleNotice(event *irc.Event) {
|
||||
if strings.Contains(event.Message(), "This nickname is registered") && event.Nick == b.Config.NickServNick {
|
||||
b.i.Privmsg(b.Config.NickServNick, "IDENTIFY "+b.Config.NickServPassword)
|
||||
func (b *Birc) handleNotice(client *girc.Client, event girc.Event) {
|
||||
if strings.Contains(event.String(), "This nickname is registered") && event.Source.Name == b.Config.NickServNick {
|
||||
b.i.Cmd.Message(b.Config.NickServNick, "IDENTIFY "+b.Config.NickServPassword)
|
||||
} else {
|
||||
b.handlePrivMsg(event)
|
||||
b.handlePrivMsg(client, event)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Birc) handleOther(event *irc.Event) {
|
||||
switch event.Code {
|
||||
func (b *Birc) handleOther(client *girc.Client, event girc.Event) {
|
||||
switch event.Command {
|
||||
case "372", "375", "376", "250", "251", "252", "253", "254", "255", "265", "266", "002", "003", "004", "005":
|
||||
return
|
||||
}
|
||||
flog.Debugf("%#v", event.Raw)
|
||||
flog.Debugf("%#v", event.String())
|
||||
}
|
||||
|
||||
func (b *Birc) handleOtherAuth(event *irc.Event) {
|
||||
func (b *Birc) handleOtherAuth(client *girc.Client, event girc.Event) {
|
||||
if strings.EqualFold(b.Config.NickServNick, "Q@CServe.quakenet.org") {
|
||||
flog.Debugf("Authenticating %s against %s", b.Config.NickServUsername, b.Config.NickServNick)
|
||||
b.i.Privmsg(b.Config.NickServNick, "AUTH "+b.Config.NickServUsername+" "+b.Config.NickServPassword)
|
||||
b.i.Cmd.Message(b.Config.NickServNick, "AUTH "+b.Config.NickServUsername+" "+b.Config.NickServPassword)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Birc) handlePrivMsg(event *irc.Event) {
|
||||
func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
|
||||
b.Nick = b.i.GetNick()
|
||||
// freenode doesn't send 001 as first reply
|
||||
if event.Code == "NOTICE" {
|
||||
if event.Command == "NOTICE" {
|
||||
return
|
||||
}
|
||||
// don't forward queries to the bot
|
||||
if event.Arguments[0] == b.Nick {
|
||||
if event.Params[0] == b.Nick {
|
||||
return
|
||||
}
|
||||
// don't forward message from ourself
|
||||
if event.Nick == b.Nick {
|
||||
if event.Source.Name == b.Nick {
|
||||
return
|
||||
}
|
||||
rmsg := config.Message{Username: event.Nick, Channel: event.Arguments[0], Account: b.Account, UserID: event.User + "@" + event.Host}
|
||||
flog.Debugf("handlePrivMsg() %s %s %#v", event.Nick, event.Message(), event)
|
||||
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.Code == "CTCP_ACTION" {
|
||||
// msg = event.Nick + " "
|
||||
if event.IsAction() {
|
||||
rmsg.Event = config.EVENT_USER_ACTION
|
||||
}
|
||||
msg += event.Message()
|
||||
msg += event.StripAction()
|
||||
// strip IRC colors
|
||||
re := regexp.MustCompile(`[[:cntrl:]](\d+,|)\d+`)
|
||||
re := regexp.MustCompile(`[[:cntrl:]](?:\d{1,2}(?:,\d{1,2})?)?`)
|
||||
msg = re.ReplaceAllString(msg, "")
|
||||
|
||||
var r io.Reader
|
||||
@ -317,49 +370,35 @@ func (b *Birc) handlePrivMsg(event *irc.Event) {
|
||||
output, _ := ioutil.ReadAll(r)
|
||||
msg = string(output)
|
||||
|
||||
flog.Debugf("Sending message from %s on %s to gateway", event.Arguments[0], b.Account)
|
||||
flog.Debugf("Sending message from %s on %s to gateway", event.Params[0], b.Account)
|
||||
rmsg.Text = msg
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
|
||||
func (b *Birc) handleTopicWhoTime(event *irc.Event) {
|
||||
parts := strings.Split(event.Arguments[2], "!")
|
||||
t, err := strconv.ParseInt(event.Arguments[3], 10, 64)
|
||||
func (b *Birc) handleTopicWhoTime(client *girc.Client, event girc.Event) {
|
||||
parts := strings.Split(event.Params[2], "!")
|
||||
t, err := strconv.ParseInt(event.Params[3], 10, 64)
|
||||
if err != nil {
|
||||
flog.Errorf("Invalid time stamp: %s", event.Arguments[3])
|
||||
flog.Errorf("Invalid time stamp: %s", event.Params[3])
|
||||
}
|
||||
user := parts[0]
|
||||
if len(parts) > 1 {
|
||||
user += " [" + parts[1] + "]"
|
||||
}
|
||||
flog.Debugf("%s: Topic set by %s [%s]", event.Code, user, time.Unix(t, 0))
|
||||
flog.Debugf("%s: Topic set by %s [%s]", event.Command, user, time.Unix(t, 0))
|
||||
}
|
||||
|
||||
func (b *Birc) nicksPerRow() int {
|
||||
return 4
|
||||
/*
|
||||
if b.Config.Mattermost.NicksPerRow < 1 {
|
||||
return 4
|
||||
}
|
||||
return b.Config.Mattermost.NicksPerRow
|
||||
*/
|
||||
}
|
||||
|
||||
func (b *Birc) storeNames(event *irc.Event) {
|
||||
channel := event.Arguments[2]
|
||||
func (b *Birc) storeNames(client *girc.Client, event girc.Event) {
|
||||
channel := event.Params[2]
|
||||
b.names[channel] = append(
|
||||
b.names[channel],
|
||||
strings.Split(strings.TrimSpace(event.Message()), " ")...)
|
||||
strings.Split(strings.TrimSpace(event.Trailing), " ")...)
|
||||
}
|
||||
|
||||
func (b *Birc) formatnicks(nicks []string, continued bool) string {
|
||||
return plainformatter(nicks, b.nicksPerRow())
|
||||
/*
|
||||
switch b.Config.Mattermost.NickFormatter {
|
||||
case "table":
|
||||
return tableformatter(nicks, b.nicksPerRow(), continued)
|
||||
default:
|
||||
return plainformatter(nicks, b.nicksPerRow())
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
@ -1,37 +1,36 @@
|
||||
package bmatrix
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"mime"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
matrix "github.com/matrix-org/gomatrix"
|
||||
"github.com/42wim/matterbridge/bridge/helper"
|
||||
log "github.com/sirupsen/logrus"
|
||||
matrix "github.com/matterbridge/gomatrix"
|
||||
)
|
||||
|
||||
type Bmatrix struct {
|
||||
mc *matrix.Client
|
||||
Config *config.Protocol
|
||||
Remote chan config.Message
|
||||
Account string
|
||||
UserID string
|
||||
RoomMap map[string]string
|
||||
sync.RWMutex
|
||||
*config.BridgeConfig
|
||||
}
|
||||
|
||||
var flog *log.Entry
|
||||
var protocol = "matrix"
|
||||
|
||||
func init() {
|
||||
flog = log.WithFields(log.Fields{"module": protocol})
|
||||
flog = log.WithFields(log.Fields{"prefix": protocol})
|
||||
}
|
||||
|
||||
func New(cfg config.Protocol, account string, c chan config.Message) *Bmatrix {
|
||||
b := &Bmatrix{}
|
||||
func New(cfg *config.BridgeConfig) *Bmatrix {
|
||||
b := &Bmatrix{BridgeConfig: cfg}
|
||||
b.RoomMap = make(map[string]string)
|
||||
b.Config = &cfg
|
||||
b.Account = account
|
||||
b.Remote = c
|
||||
return b
|
||||
}
|
||||
|
||||
@ -76,19 +75,79 @@ 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 {
|
||||
if msg.ID == "" {
|
||||
return "", nil
|
||||
}
|
||||
channel := b.getRoomID(msg.Channel)
|
||||
resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.EventID, err
|
||||
}
|
||||
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})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.EventID, err
|
||||
}
|
||||
|
||||
if msg.Extra != nil {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
b.mc.SendText(channel, rmsg.Username+rmsg.Text)
|
||||
}
|
||||
// check if we have files to upload (from slack, telegram or mattermost)
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
content := bytes.NewReader(*fi.Data)
|
||||
sp := strings.Split(fi.Name, ".")
|
||||
mtype := mime.TypeByExtension("." + sp[len(sp)-1])
|
||||
if strings.Contains(mtype, "image") ||
|
||||
strings.Contains(mtype, "video") {
|
||||
if fi.Comment != "" {
|
||||
_, err := b.mc.SendText(channel, msg.Username+fi.Comment)
|
||||
if err != nil {
|
||||
flog.Errorf("file comment failed: %#v", err)
|
||||
}
|
||||
}
|
||||
flog.Debugf("uploading file: %s %s", fi.Name, mtype)
|
||||
res, err := b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data)))
|
||||
if err != nil {
|
||||
flog.Errorf("file upload failed: %#v", err)
|
||||
continue
|
||||
}
|
||||
if strings.Contains(mtype, "video") {
|
||||
flog.Debugf("sendVideo %s", res.ContentURI)
|
||||
_, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI)
|
||||
if err != nil {
|
||||
flog.Errorf("sendVideo failed: %#v", err)
|
||||
}
|
||||
}
|
||||
if strings.Contains(mtype, "image") {
|
||||
flog.Debugf("sendImage %s", res.ContentURI)
|
||||
_, err = b.mc.SendImage(channel, fi.Name, res.ContentURI)
|
||||
if err != nil {
|
||||
flog.Errorf("sendImage failed: %#v", err)
|
||||
}
|
||||
}
|
||||
flog.Debugf("result: %#v", res)
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
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 {
|
||||
@ -101,10 +160,24 @@ 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) {
|
||||
if (ev.Content["msgtype"].(string) == "m.text" || ev.Content["msgtype"].(string) == "m.notice" || ev.Content["msgtype"].(string) == "m.emote") && ev.Sender != b.UserID {
|
||||
syncer.OnEventType("m.room.redaction", b.handleEvent)
|
||||
syncer.OnEventType("m.room.message", b.handleEvent)
|
||||
go func() {
|
||||
for {
|
||||
if err := b.mc.Sync(); err != nil {
|
||||
flog.Println("Sync() returned ", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
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()
|
||||
@ -117,21 +190,61 @@ func (b *Bmatrix) handlematrix() error {
|
||||
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}
|
||||
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})
|
||||
}
|
||||
} else {
|
||||
flog.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", name, size, b.General.MediaDownloadSize)
|
||||
rmsg.Event = config.EVENT_FILE_FAILURE_SIZE
|
||||
rmsg.Extra[rmsg.Event] = append(rmsg.Extra[rmsg.Event], config.FileInfo{Name: name, Size: int64(size)})
|
||||
}
|
||||
rmsg.Text = ""
|
||||
}
|
||||
flog.Debugf("Sending message from %s on %s to gateway", ev.Sender, b.Account)
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
flog.Debugf("Received: %#v", ev)
|
||||
})
|
||||
go func() {
|
||||
for {
|
||||
if err := b.mc.Sync(); err != nil {
|
||||
flog.Println("Sync() returned ", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
@ -4,9 +4,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/42wim/matterbridge/bridge/helper"
|
||||
"github.com/42wim/matterbridge/matterclient"
|
||||
"github.com/42wim/matterbridge/matterhook"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -32,24 +33,20 @@ type MMMessage struct {
|
||||
type Bmattermost struct {
|
||||
MMhook
|
||||
MMapi
|
||||
Config *config.Protocol
|
||||
Remote chan config.Message
|
||||
TeamId string
|
||||
Account string
|
||||
*config.BridgeConfig
|
||||
avatarMap map[string]string
|
||||
}
|
||||
|
||||
var flog *log.Entry
|
||||
var protocol = "mattermost"
|
||||
|
||||
func init() {
|
||||
flog = log.WithFields(log.Fields{"module": protocol})
|
||||
flog = log.WithFields(log.Fields{"prefix": protocol})
|
||||
}
|
||||
|
||||
func New(cfg config.Protocol, account string, c chan config.Message) *Bmattermost {
|
||||
b := &Bmattermost{}
|
||||
b.Config = &cfg
|
||||
b.Remote = c
|
||||
b.Account = account
|
||||
func New(cfg *config.BridgeConfig) *Bmattermost {
|
||||
b := &Bmattermost{BridgeConfig: cfg, avatarMap: make(map[string]string)}
|
||||
b.mmMap = make(map[string]string)
|
||||
return b
|
||||
}
|
||||
@ -153,17 +150,46 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
|
||||
message := msg.Text
|
||||
channel := msg.Channel
|
||||
|
||||
// map the file SHA to our user (caches the avatar)
|
||||
if msg.Event == config.EVENT_AVATAR_DOWNLOAD {
|
||||
fi := msg.Extra["file"][0].(config.FileInfo)
|
||||
/* if we have a sha we have successfully uploaded the file to the media server,
|
||||
so we can now cache the sha */
|
||||
if fi.SHA != "" {
|
||||
flog.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID)
|
||||
b.avatarMap[msg.UserID] = fi.SHA
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if b.Config.PrefixMessagesWithNick {
|
||||
message = nick + message
|
||||
}
|
||||
if b.Config.WebhookURL != "" {
|
||||
|
||||
if msg.Extra != nil {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL, Channel: channel, UserName: rmsg.Username,
|
||||
Text: rmsg.Text, Props: make(map[string]interface{})}
|
||||
matterMessage.Props["matterbridge"] = true
|
||||
b.mh.Send(matterMessage)
|
||||
}
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
if fi.URL != "" {
|
||||
message += fi.URL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
|
||||
matterMessage.IconURL = msg.Avatar
|
||||
matterMessage.Channel = channel
|
||||
matterMessage.UserName = nick
|
||||
matterMessage.Type = ""
|
||||
matterMessage.Text = message
|
||||
matterMessage.Text = message
|
||||
matterMessage.Props = make(map[string]interface{})
|
||||
matterMessage.Props["matterbridge"] = true
|
||||
err := b.mh.Send(matterMessage)
|
||||
@ -180,6 +206,9 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
|
||||
return msg.ID, b.mc.DeleteMessage(msg.ID)
|
||||
}
|
||||
if msg.Extra != nil {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
b.mc.PostMessage(b.mc.GetChannelId(channel, ""), rmsg.Username+rmsg.Text)
|
||||
}
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
var err error
|
||||
var res, id string
|
||||
@ -190,9 +219,9 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
|
||||
flog.Debugf("ERROR %#v", err)
|
||||
return "", err
|
||||
}
|
||||
message = "uploaded a file: " + fi.Name
|
||||
message = fi.Comment
|
||||
if b.Config.PrefixMessagesWithNick {
|
||||
message = nick + "uploaded a file: " + fi.Name
|
||||
message = nick + fi.Comment
|
||||
}
|
||||
res, err = b.mc.PostMessageWithFiles(b.mc.GetChannelId(channel, ""), message, []string{id})
|
||||
}
|
||||
@ -219,7 +248,8 @@ func (b *Bmattermost) handleMatter() {
|
||||
go b.handleMatterClient(mchan)
|
||||
}
|
||||
for message := range mchan {
|
||||
rmsg := config.Message{Username: message.Username, Channel: message.Channel, Account: b.Account, UserID: message.UserID, ID: message.ID, Event: message.Event, Extra: message.Extra}
|
||||
avatar := helper.GetAvatar(b.avatarMap, message.UserID, b.General)
|
||||
rmsg := config.Message{Username: message.Username, Channel: message.Channel, Account: b.Account, UserID: message.UserID, ID: message.ID, Event: message.Event, Extra: message.Extra, Avatar: avatar}
|
||||
text, ok := b.replaceAction(message.Text)
|
||||
if ok {
|
||||
rmsg.Event = config.EVENT_USER_ACTION
|
||||
@ -245,6 +275,11 @@ func (b *Bmattermost) handleMatterClient(mchan chan *MMMessage) {
|
||||
continue
|
||||
}
|
||||
|
||||
// only download avatars if we have a place to upload them (configured mediaserver)
|
||||
if b.General.MediaServerUpload != "" {
|
||||
b.handleDownloadAvatar(message.UserID, message.Channel)
|
||||
}
|
||||
|
||||
m := &MMMessage{Extra: make(map[string][]interface{})}
|
||||
|
||||
props := message.Post.Props
|
||||
@ -281,8 +316,26 @@ func (b *Bmattermost) handleMatterClient(mchan chan *MMMessage) {
|
||||
m.Event = config.EVENT_MSG_DELETE
|
||||
}
|
||||
if len(message.Post.FileIds) > 0 {
|
||||
for _, link := range b.mc.GetFileLinks(message.Post.FileIds) {
|
||||
m.Text = m.Text + "\n" + link
|
||||
for _, id := range message.Post.FileIds {
|
||||
url, _ := b.mc.Client.GetFileLink(id)
|
||||
finfo, resp := b.mc.Client.GetFileInfo(id)
|
||||
if resp.Error != nil {
|
||||
continue
|
||||
}
|
||||
flog.Debugf("trying to download %#v fileid %#v with size %#v", finfo.Name, finfo.Id, finfo.Size)
|
||||
if int(finfo.Size) > b.General.MediaDownloadSize {
|
||||
flog.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", finfo.Name, finfo.Size, b.General.MediaDownloadSize)
|
||||
m.Event = config.EVENT_FILE_FAILURE_SIZE
|
||||
m.Extra[m.Event] = append(m.Extra[m.Event], config.FileInfo{Name: finfo.Name, Comment: message.Text, Size: int64(finfo.Size)})
|
||||
continue
|
||||
}
|
||||
data, resp := b.mc.Client.DownloadFile(id, true)
|
||||
if resp.Error != nil {
|
||||
flog.Errorf("download %s failed %#v", finfo.Name, resp.Error)
|
||||
continue
|
||||
}
|
||||
flog.Debugf("download OK %#v %#v", finfo.Name, len(data))
|
||||
m.Extra["file"] = append(m.Extra["file"], config.FileInfo{Name: finfo.Name, Data: &data, URL: url, Comment: message.Text})
|
||||
}
|
||||
}
|
||||
mchan <- m
|
||||
@ -311,6 +364,9 @@ func (b *Bmattermost) apiLogin() error {
|
||||
|
||||
b.mc = matterclient.New(b.Config.Login, password,
|
||||
b.Config.Team, b.Config.Server)
|
||||
if b.General.Debug {
|
||||
b.mc.SetLogLevel("debug")
|
||||
}
|
||||
b.mc.SkipTLSVerify = b.Config.SkipTLSVerify
|
||||
b.mc.NoTLS = b.Config.NoTLS
|
||||
flog.Infof("Connecting %s (team: %s) on %s", b.Config.Login, b.Config.Team, b.Config.Server)
|
||||
@ -331,3 +387,27 @@ func (b *Bmattermost) replaceAction(text string) (string, bool) {
|
||||
}
|
||||
return text, false
|
||||
}
|
||||
|
||||
// handleDownloadAvatar downloads the avatar of userid from channel
|
||||
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
|
||||
// logs an error message if it fails
|
||||
func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
|
||||
var name string
|
||||
msg := config.Message{Username: "system", Text: "avatar", Channel: channel, Account: b.Account, UserID: userid, Event: config.EVENT_AVATAR_DOWNLOAD, Extra: make(map[string][]interface{})}
|
||||
if _, ok := b.avatarMap[userid]; !ok {
|
||||
data, resp := b.mc.Client.GetProfileImage(userid, "")
|
||||
if resp.Error != nil {
|
||||
flog.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error)
|
||||
}
|
||||
if len(data) <= b.General.MediaDownloadSize {
|
||||
name = userid + ".png"
|
||||
flog.Debugf("download OK %#v %#v", name, len(data))
|
||||
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: name, Data: &data, Avatar: true})
|
||||
flog.Debugf("Sending avatar download message from %#v on %s to gateway", userid, b.Account)
|
||||
flog.Debugf("Message is %#v", msg)
|
||||
b.Remote <- msg
|
||||
} else {
|
||||
flog.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", name, len(data), b.General.MediaDownloadSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,10 @@ package brocketchat
|
||||
|
||||
import (
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/42wim/matterbridge/bridge/helper"
|
||||
"github.com/42wim/matterbridge/hook/rockethook"
|
||||
"github.com/42wim/matterbridge/matterhook"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type MMhook struct {
|
||||
@ -14,24 +15,18 @@ type MMhook struct {
|
||||
|
||||
type Brocketchat struct {
|
||||
MMhook
|
||||
Config *config.Protocol
|
||||
Remote chan config.Message
|
||||
Account string
|
||||
*config.BridgeConfig
|
||||
}
|
||||
|
||||
var flog *log.Entry
|
||||
var protocol = "rocketchat"
|
||||
|
||||
func init() {
|
||||
flog = log.WithFields(log.Fields{"module": protocol})
|
||||
flog = log.WithFields(log.Fields{"prefix": protocol})
|
||||
}
|
||||
|
||||
func New(cfg config.Protocol, account string, c chan config.Message) *Brocketchat {
|
||||
b := &Brocketchat{}
|
||||
b.Config = &cfg
|
||||
b.Remote = c
|
||||
b.Account = account
|
||||
return b
|
||||
func New(cfg *config.BridgeConfig) *Brocketchat {
|
||||
return &Brocketchat{BridgeConfig: cfg}
|
||||
}
|
||||
|
||||
func (b *Brocketchat) Command(cmd string) string {
|
||||
@ -63,6 +58,22 @@ func (b *Brocketchat) Send(msg config.Message) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
flog.Debugf("Receiving %#v", msg)
|
||||
if msg.Extra != nil {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL, Channel: rmsg.Channel, UserName: rmsg.Username,
|
||||
Text: rmsg.Text}
|
||||
b.mh.Send(matterMessage)
|
||||
}
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
if fi.URL != "" {
|
||||
msg.Text += fi.URL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
|
||||
matterMessage.Channel = msg.Channel
|
||||
matterMessage.UserName = msg.Username
|
||||
|
@ -5,8 +5,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/42wim/matterbridge/bridge/helper"
|
||||
"github.com/42wim/matterbridge/matterhook"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/nlopes/slack"
|
||||
"html"
|
||||
"io"
|
||||
@ -27,29 +28,23 @@ type MMMessage struct {
|
||||
type Bslack struct {
|
||||
mh *matterhook.Client
|
||||
sc *slack.Client
|
||||
Config *config.Protocol
|
||||
rtm *slack.RTM
|
||||
Plus bool
|
||||
Remote chan config.Message
|
||||
Users []slack.User
|
||||
Account string
|
||||
si *slack.Info
|
||||
channels []slack.Channel
|
||||
*config.BridgeConfig
|
||||
}
|
||||
|
||||
var flog *log.Entry
|
||||
var protocol = "slack"
|
||||
|
||||
func init() {
|
||||
flog = log.WithFields(log.Fields{"module": protocol})
|
||||
flog = log.WithFields(log.Fields{"prefix": protocol})
|
||||
}
|
||||
|
||||
func New(cfg config.Protocol, account string, c chan config.Message) *Bslack {
|
||||
b := &Bslack{}
|
||||
b.Config = &cfg
|
||||
b.Remote = c
|
||||
b.Account = account
|
||||
return b
|
||||
func New(cfg *config.BridgeConfig) *Bslack {
|
||||
return &Bslack{BridgeConfig: cfg}
|
||||
}
|
||||
|
||||
func (b *Bslack) Command(cmd string) string {
|
||||
@ -113,7 +108,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
|
||||
@ -140,6 +135,22 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
|
||||
message = nick + " " + message
|
||||
}
|
||||
if b.Config.WebhookURL != "" {
|
||||
if msg.Extra != nil {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL, Channel: channel, UserName: rmsg.Username,
|
||||
Text: rmsg.Text}
|
||||
b.mh.Send(matterMessage)
|
||||
}
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
if fi.URL != "" {
|
||||
message += fi.URL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
|
||||
matterMessage.Channel = channel
|
||||
matterMessage.UserName = nick
|
||||
@ -161,7 +172,7 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
|
||||
np.AsUser = true
|
||||
}
|
||||
np.Username = nick
|
||||
np.IconURL = config.GetIconURL(&msg, b.Config)
|
||||
np.IconURL = config.GetIconURL(&msg, &b.Config)
|
||||
if msg.Avatar != "" {
|
||||
np.IconURL = msg.Avatar
|
||||
}
|
||||
@ -187,6 +198,29 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
|
||||
b.sc.UpdateMessage(schannel.ID, ts[1], message)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if msg.Extra != nil {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
b.sc.PostMessage(schannel.ID, rmsg.Username+rmsg.Text, np)
|
||||
}
|
||||
// check if we have files to upload (from slack, telegram or mattermost)
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
var err error
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
_, err = b.sc.UploadFile(slack.FileUploadParameters{
|
||||
Reader: bytes.NewReader(*fi.Data),
|
||||
Filename: fi.Name,
|
||||
Channels: []string{schannel.ID},
|
||||
InitialComment: fi.Comment,
|
||||
})
|
||||
if err != nil {
|
||||
flog.Errorf("uploadfile %#v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, id, err := b.sc.PostMessage(schannel.ID, message, np)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@ -270,16 +304,29 @@ func (b *Bslack) handleSlack() {
|
||||
msg.Event = config.EVENT_MSG_DELETE
|
||||
msg.ID = "slack " + message.Raw.DeletedTimestamp
|
||||
}
|
||||
if message.Raw.SubType == "channel_topic" || message.Raw.SubType == "channel_purpose" {
|
||||
msg.Event = config.EVENT_TOPIC_CHANGE
|
||||
}
|
||||
|
||||
// if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra
|
||||
if message.Raw.File != nil {
|
||||
// limit to 1MB for now
|
||||
if message.Raw.File.Size <= 1000000 {
|
||||
comment := ""
|
||||
results := regexp.MustCompile(`.*?commented: (.*)`).FindAllStringSubmatch(msg.Text, -1)
|
||||
if len(results) > 0 {
|
||||
comment = results[0][1]
|
||||
}
|
||||
|
||||
if message.Raw.File.Size > b.General.MediaDownloadSize {
|
||||
flog.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", message.Raw.File.Name, message.Raw.File.Size, b.General.MediaDownloadSize)
|
||||
msg.Event = config.EVENT_FILE_FAILURE_SIZE
|
||||
msg.Extra[msg.Event] = append(msg.Extra[msg.Event], config.FileInfo{Name: message.Raw.File.Name, Comment: comment, Size: int64(message.Raw.File.Size)})
|
||||
} else {
|
||||
data, err := b.downloadFile(message.Raw.File.URLPrivateDownload)
|
||||
if err != nil {
|
||||
flog.Errorf("download %s failed %#v", message.Raw.File.URLPrivateDownload, err)
|
||||
} else {
|
||||
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: message.Raw.File.Name, Data: data})
|
||||
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: message.Raw.File.Name, Data: data, Comment: comment})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -290,9 +337,14 @@ 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 ev.SubType == "pinned_item" || ev.SubType == "unpinned_item" {
|
||||
continue
|
||||
}
|
||||
if len(ev.Attachments) > 0 {
|
||||
// skip messages we made ourselves
|
||||
if ev.Attachments[0].CallbackID == "matterbridge" {
|
||||
@ -316,13 +368,16 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
|
||||
continue
|
||||
}
|
||||
m := &MMMessage{}
|
||||
if ev.BotID == "" && ev.SubType != "message_deleted" {
|
||||
if ev.BotID == "" && ev.SubType != "message_deleted" && ev.SubType != "file_comment" {
|
||||
user, err := b.rtm.GetUserInfo(ev.User)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
m.UserID = user.ID
|
||||
m.Username = user.Name
|
||||
if user.Profile.DisplayName != "" {
|
||||
m.Username = user.Profile.DisplayName
|
||||
}
|
||||
}
|
||||
m.Channel = channel.Name
|
||||
m.Text = ev.Text
|
||||
@ -337,6 +392,8 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
|
||||
}
|
||||
m.Raw = ev
|
||||
m.Text = b.replaceMention(m.Text)
|
||||
m.Text = b.replaceVariable(m.Text)
|
||||
m.Text = b.replaceChannel(m.Text)
|
||||
// when using webhookURL we can't check if it's our webhook or not for now
|
||||
if ev.BotID != "" && b.Config.WebhookURL == "" {
|
||||
bot, err := b.rtm.GetBotInfo(ev.BotID)
|
||||
@ -345,9 +402,17 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
|
||||
}
|
||||
if bot.Name != "" {
|
||||
m.Username = bot.Name
|
||||
if ev.Username != "" {
|
||||
m.Username = ev.Username
|
||||
}
|
||||
m.UserID = bot.ID
|
||||
}
|
||||
}
|
||||
|
||||
if ev.SubType == "file_comment" {
|
||||
m.Username = "system"
|
||||
}
|
||||
|
||||
mchan <- m
|
||||
case *slack.OutgoingErrorEvent:
|
||||
flog.Debugf("%#v", ev.Error())
|
||||
@ -367,6 +432,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:
|
||||
}
|
||||
}
|
||||
@ -380,6 +447,8 @@ func (b *Bslack) handleMatterHook(mchan chan *MMMessage) {
|
||||
m.Username = message.UserName
|
||||
m.Text = message.Text
|
||||
m.Text = b.replaceMention(m.Text)
|
||||
m.Text = b.replaceVariable(m.Text)
|
||||
m.Text = b.replaceChannel(m.Text)
|
||||
m.Channel = message.ChannelName
|
||||
if m.Username == "slackbot" {
|
||||
continue
|
||||
@ -391,23 +460,45 @@ func (b *Bslack) handleMatterHook(mchan chan *MMMessage) {
|
||||
func (b *Bslack) userName(id string) string {
|
||||
for _, u := range b.Users {
|
||||
if u.ID == id {
|
||||
if u.Profile.DisplayName != "" {
|
||||
return u.Profile.DisplayName
|
||||
}
|
||||
return u.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
|
||||
func (b *Bslack) replaceMention(text string) string {
|
||||
results := regexp.MustCompile(`<@([a-zA-z0-9]+)>`).FindAllStringSubmatch(text, -1)
|
||||
for _, r := range results {
|
||||
text = strings.Replace(text, "<@"+r[1]+">", "@"+b.userName(r[1]), -1)
|
||||
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
|
||||
func (b *Bslack) replaceChannel(text string) string {
|
||||
results := regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`).FindAllStringSubmatch(text, -1)
|
||||
for _, r := range results {
|
||||
text = strings.Replace(text, r[0], "#"+r[1], -1)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// @see https://api.slack.com/docs/message-formatting#variables
|
||||
func (b *Bslack) replaceVariable(text string) string {
|
||||
results := regexp.MustCompile(`<!([a-zA-Z0-9]+)(\|.+?)?>`).FindAllStringSubmatch(text, -1)
|
||||
for _, r := range results {
|
||||
text = strings.Replace(text, r[0], "@"+r[1], -1)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// @see https://api.slack.com/docs/message-formatting#linking_to_urls
|
||||
func (b *Bslack) replaceURL(text string) string {
|
||||
results := regexp.MustCompile(`<(.*?)\|.*?>`).FindAllStringSubmatch(text, -1)
|
||||
results := regexp.MustCompile(`<(.*?)(\|.*?)?>`).FindAllStringSubmatch(text, -1)
|
||||
for _, r := range results {
|
||||
text = strings.Replace(text, r[0], r[1], -1)
|
||||
}
|
||||
|
139
bridge/sshchat/sshchat.go
Normal file
139
bridge/sshchat/sshchat.go
Normal file
@ -0,0 +1,139 @@
|
||||
package bsshchat
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/42wim/matterbridge/bridge/helper"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/shazow/ssh-chat/sshd"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Bsshchat struct {
|
||||
r *bufio.Scanner
|
||||
w io.WriteCloser
|
||||
*config.BridgeConfig
|
||||
}
|
||||
|
||||
var flog *log.Entry
|
||||
var protocol = "sshchat"
|
||||
|
||||
func init() {
|
||||
flog = log.WithFields(log.Fields{"prefix": protocol})
|
||||
}
|
||||
|
||||
func New(cfg *config.BridgeConfig) *Bsshchat {
|
||||
return &Bsshchat{BridgeConfig: cfg}
|
||||
}
|
||||
|
||||
func (b *Bsshchat) Connect() error {
|
||||
var err error
|
||||
flog.Infof("Connecting %s", b.Config.Server)
|
||||
go func() {
|
||||
err = sshd.ConnectShell(b.Config.Server, b.Config.Nick, func(r io.Reader, w io.WriteCloser) error {
|
||||
b.r = bufio.NewScanner(r)
|
||||
b.w = w
|
||||
b.r.Scan()
|
||||
w.Write([]byte("/theme mono\r\n"))
|
||||
b.handleSshChat()
|
||||
return nil
|
||||
})
|
||||
}()
|
||||
if err != nil {
|
||||
flog.Debugf("%#v", err)
|
||||
return err
|
||||
}
|
||||
flog.Info("Connection succeeded")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bsshchat) Disconnect() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bsshchat) JoinChannel(channel config.ChannelInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bsshchat) Send(msg config.Message) (string, error) {
|
||||
// ignore delete messages
|
||||
if msg.Event == config.EVENT_MSG_DELETE {
|
||||
return "", nil
|
||||
}
|
||||
flog.Debugf("Receiving %#v", msg)
|
||||
if msg.Extra != nil {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
b.w.Write([]byte(rmsg.Username + rmsg.Text + "\r\n"))
|
||||
}
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
if fi.Comment != "" {
|
||||
msg.Text += fi.Comment + ": "
|
||||
}
|
||||
if fi.URL != "" {
|
||||
msg.Text = fi.URL
|
||||
}
|
||||
b.w.Write([]byte(msg.Username + msg.Text))
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
b.w.Write([]byte(msg.Username + msg.Text + "\r\n"))
|
||||
return "", nil
|
||||
}
|
||||
|
||||
/*
|
||||
func (b *Bsshchat) sshchatKeepAlive() chan bool {
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
ticker := time.NewTicker(90 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
flog.Debugf("PING")
|
||||
err := b.xc.PingC2S("", "")
|
||||
if err != nil {
|
||||
flog.Debugf("PING failed %#v", err)
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return done
|
||||
}
|
||||
*/
|
||||
|
||||
func stripPrompt(s string) string {
|
||||
pos := strings.LastIndex(s, "\033[K")
|
||||
if pos < 0 {
|
||||
return s
|
||||
}
|
||||
return s[pos+3:]
|
||||
}
|
||||
|
||||
func (b *Bsshchat) handleSshChat() error {
|
||||
/*
|
||||
done := b.sshchatKeepAlive()
|
||||
defer close(done)
|
||||
*/
|
||||
wait := true
|
||||
for {
|
||||
if b.r.Scan() {
|
||||
res := strings.Split(stripPrompt(b.r.Text()), ":")
|
||||
if res[0] == "-> Set theme" {
|
||||
wait = false
|
||||
log.Debugf("mono found, allowing")
|
||||
continue
|
||||
}
|
||||
if !wait {
|
||||
flog.Debugf("message %#v", res)
|
||||
rmsg := config.Message{Username: res[0], Text: strings.Join(res[1:], ":"), Channel: "sshchat", Account: b.Account, UserID: "nick"}
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ import (
|
||||
"github.com/Philipp15b/go-steam"
|
||||
"github.com/Philipp15b/go-steam/protocol/steamlang"
|
||||
"github.com/Philipp15b/go-steam/steamid"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
//"io/ioutil"
|
||||
"strconv"
|
||||
"sync"
|
||||
@ -16,25 +16,20 @@ import (
|
||||
type Bsteam struct {
|
||||
c *steam.Client
|
||||
connected chan struct{}
|
||||
Config *config.Protocol
|
||||
Remote chan config.Message
|
||||
Account string
|
||||
userMap map[steamid.SteamId]string
|
||||
sync.RWMutex
|
||||
*config.BridgeConfig
|
||||
}
|
||||
|
||||
var flog *log.Entry
|
||||
var protocol = "steam"
|
||||
|
||||
func init() {
|
||||
flog = log.WithFields(log.Fields{"module": protocol})
|
||||
flog = log.WithFields(log.Fields{"prefix": protocol})
|
||||
}
|
||||
|
||||
func New(cfg config.Protocol, account string, c chan config.Message) *Bsteam {
|
||||
b := &Bsteam{}
|
||||
b.Config = &cfg
|
||||
b.Remote = c
|
||||
b.Account = account
|
||||
func New(cfg *config.BridgeConfig) *Bsteam {
|
||||
b := &Bsteam{BridgeConfig: cfg}
|
||||
b.userMap = make(map[steamid.SteamId]string)
|
||||
b.connected = make(chan struct{})
|
||||
return b
|
||||
@ -105,8 +100,13 @@ func (b *Bsteam) handleEvents() {
|
||||
case *steam.ChatMsgEvent:
|
||||
flog.Debugf("Receiving ChatMsgEvent: %#v", e)
|
||||
flog.Debugf("Sending message from %s on %s to gateway", b.getNick(e.ChatterId), b.Account)
|
||||
var channel int64
|
||||
if e.ChatRoomId == 0 {
|
||||
channel = int64(e.ChatterId)
|
||||
} else {
|
||||
// for some reason we have to remove 0x18000000000000
|
||||
channel := int64(e.ChatRoomId) - 0x18000000000000
|
||||
channel = int64(e.ChatRoomId) - 0x18000000000000
|
||||
}
|
||||
msg := config.Message{Username: b.getNick(e.ChatterId), Text: e.Message, Channel: strconv.FormatInt(channel, 10), Account: b.Account, UserID: strconv.FormatInt(int64(e.ChatterId), 10)}
|
||||
b.Remote <- msg
|
||||
case *steam.PersonaStateEvent:
|
||||
|
@ -1,33 +1,31 @@
|
||||
package btelegram
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/42wim/matterbridge/bridge/helper"
|
||||
"github.com/go-telegram-bot-api/telegram-bot-api"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Btelegram struct {
|
||||
c *tgbotapi.BotAPI
|
||||
Config *config.Protocol
|
||||
Remote chan config.Message
|
||||
Account string
|
||||
*config.BridgeConfig
|
||||
avatarMap map[string]string // keep cache of userid and avatar sha
|
||||
}
|
||||
|
||||
var flog *log.Entry
|
||||
var protocol = "telegram"
|
||||
|
||||
func init() {
|
||||
flog = log.WithFields(log.Fields{"module": protocol})
|
||||
flog = log.WithFields(log.Fields{"prefix": protocol})
|
||||
}
|
||||
|
||||
func New(cfg config.Protocol, account string, c chan config.Message) *Btelegram {
|
||||
b := &Btelegram{}
|
||||
b.Config = &cfg
|
||||
b.Remote = c
|
||||
b.Account = account
|
||||
return b
|
||||
func New(cfg *config.BridgeConfig) *Btelegram {
|
||||
return &Btelegram{BridgeConfig: cfg, avatarMap: make(map[string]string)}
|
||||
}
|
||||
|
||||
func (b *Btelegram) Connect() error {
|
||||
@ -38,7 +36,9 @@ func (b *Btelegram) Connect() error {
|
||||
flog.Debugf("%#v", err)
|
||||
return err
|
||||
}
|
||||
updates, err := b.c.GetUpdatesChan(tgbotapi.NewUpdate(0))
|
||||
u := tgbotapi.NewUpdate(0)
|
||||
u.Timeout = 60
|
||||
updates, err := b.c.GetUpdatesChan(u)
|
||||
if err != nil {
|
||||
flog.Debugf("%#v", err)
|
||||
return err
|
||||
@ -64,6 +64,18 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// map the file SHA to our user (caches the avatar)
|
||||
if msg.Event == config.EVENT_AVATAR_DOWNLOAD {
|
||||
fi := msg.Extra["file"][0].(config.FileInfo)
|
||||
/* if we have a sha we have successfully uploaded the file to the media server,
|
||||
so we can now cache the sha */
|
||||
if fi.SHA != "" {
|
||||
flog.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID)
|
||||
b.avatarMap[msg.UserID] = fi.SHA
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if b.Config.MessageFormat == "HTML" {
|
||||
msg.Text = makeHTML(msg.Text)
|
||||
}
|
||||
@ -87,6 +99,14 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text)
|
||||
if b.Config.MessageFormat == "HTML" {
|
||||
flog.Debug("Using mode HTML")
|
||||
m.ParseMode = tgbotapi.ModeHTML
|
||||
}
|
||||
if b.Config.MessageFormat == "Markdown" {
|
||||
flog.Debug("Using mode markdown")
|
||||
m.ParseMode = tgbotapi.ModeMarkdown
|
||||
}
|
||||
_, err = b.c.Send(m)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@ -94,25 +114,50 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
m := tgbotapi.NewMessage(chatid, msg.Username+msg.Text)
|
||||
if b.Config.MessageFormat == "HTML" {
|
||||
m.ParseMode = tgbotapi.ModeHTML
|
||||
if msg.Extra != nil {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
b.sendMessage(chatid, rmsg.Username+rmsg.Text)
|
||||
}
|
||||
res, err := b.c.Send(m)
|
||||
// check if we have files to upload (from slack, telegram or mattermost)
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
var c tgbotapi.Chattable
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
file := tgbotapi.FileBytes{fi.Name, *fi.Data}
|
||||
re := regexp.MustCompile(".(jpg|png)$")
|
||||
if re.MatchString(fi.Name) {
|
||||
c = tgbotapi.NewPhotoUpload(chatid, file)
|
||||
} else {
|
||||
c = tgbotapi.NewDocumentUpload(chatid, file)
|
||||
}
|
||||
_, err := b.c.Send(c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
log.Errorf("file upload failed: %#v", err)
|
||||
}
|
||||
return strconv.Itoa(res.MessageID), nil
|
||||
|
||||
if fi.Comment != "" {
|
||||
b.sendMessage(chatid, msg.Username+fi.Comment)
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
return b.sendMessage(chatid, msg.Username+msg.Text)
|
||||
}
|
||||
|
||||
func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
||||
for update := range updates {
|
||||
flog.Debugf("Receiving from telegram: %#v", update.Message)
|
||||
if update.Message == nil {
|
||||
flog.Error("Getting nil messages, this shouldn't happen.")
|
||||
continue
|
||||
}
|
||||
var message *tgbotapi.Message
|
||||
username := ""
|
||||
channel := ""
|
||||
text := ""
|
||||
|
||||
fmsg := config.Message{Extra: make(map[string][]interface{})}
|
||||
|
||||
// handle channels
|
||||
if update.ChannelPost != nil {
|
||||
message = update.ChannelPost
|
||||
@ -141,24 +186,54 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
||||
}
|
||||
text = message.Text
|
||||
channel = strconv.FormatInt(message.Chat.ID, 10)
|
||||
// only download avatars if we have a place to upload them (configured mediaserver)
|
||||
if b.General.MediaServerUpload != "" {
|
||||
b.handleDownloadAvatar(message.From.ID, channel)
|
||||
}
|
||||
}
|
||||
|
||||
if username == "" {
|
||||
username = "unknown"
|
||||
}
|
||||
if message.Sticker != nil && b.Config.UseInsecureURL {
|
||||
text = text + " " + b.getFileDirectURL(message.Sticker.FileID)
|
||||
if message.Sticker != nil {
|
||||
b.handleDownload(message.Sticker, message.Caption, &fmsg)
|
||||
}
|
||||
if message.Video != nil && b.Config.UseInsecureURL {
|
||||
text = text + " " + b.getFileDirectURL(message.Video.FileID)
|
||||
if message.Video != nil {
|
||||
b.handleDownload(message.Video, message.Caption, &fmsg)
|
||||
}
|
||||
if message.Photo != nil && b.Config.UseInsecureURL {
|
||||
photos := *message.Photo
|
||||
// last photo is the biggest
|
||||
text = text + " " + b.getFileDirectURL(photos[len(photos)-1].FileID)
|
||||
if message.Photo != nil {
|
||||
b.handleDownload(message.Photo, message.Caption, &fmsg)
|
||||
}
|
||||
if message.Document != nil && b.Config.UseInsecureURL {
|
||||
text = text + " " + message.Document.FileName + " : " + b.getFileDirectURL(message.Document.FileID)
|
||||
if message.Document != nil {
|
||||
b.handleDownload(message.Document, message.Caption, &fmsg)
|
||||
}
|
||||
if message.Voice != nil {
|
||||
b.handleDownload(message.Voice, message.Caption, &fmsg)
|
||||
}
|
||||
if message.Audio != nil {
|
||||
b.handleDownload(message.Audio, message.Caption, &fmsg)
|
||||
}
|
||||
|
||||
// If UseInsecureURL is used we'll have a text in fmsg.Text
|
||||
if fmsg.Text != "" {
|
||||
text = text + fmsg.Text
|
||||
}
|
||||
|
||||
if message.ForwardFrom != nil {
|
||||
usernameForward := ""
|
||||
if b.Config.UseFirstName {
|
||||
usernameForward = message.ForwardFrom.FirstName
|
||||
}
|
||||
if usernameForward == "" {
|
||||
usernameForward = message.ForwardFrom.UserName
|
||||
if usernameForward == "" {
|
||||
usernameForward = message.ForwardFrom.FirstName
|
||||
}
|
||||
}
|
||||
if usernameForward == "" {
|
||||
usernameForward = "unknown"
|
||||
}
|
||||
text = "Forwarded from " + usernameForward + ": " + text
|
||||
}
|
||||
|
||||
// quote the previous message
|
||||
@ -181,9 +256,10 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
||||
text = text + " (re @" + usernameReply + ":" + message.ReplyToMessage.Text + ")"
|
||||
}
|
||||
|
||||
if text != "" {
|
||||
if text != "" || len(fmsg.Extra) > 0 {
|
||||
avatar := helper.GetAvatar(b.avatarMap, strconv.Itoa(message.From.ID), b.General)
|
||||
flog.Debugf("Sending message from %s on %s to gateway", username, b.Account)
|
||||
msg := config.Message{Username: username, Text: text, Channel: channel, Account: b.Account, UserID: strconv.Itoa(message.From.ID), ID: strconv.Itoa(message.MessageID)}
|
||||
msg := config.Message{Username: username, Text: text, Channel: channel, Account: b.Account, UserID: strconv.Itoa(message.From.ID), ID: strconv.Itoa(message.MessageID), Extra: fmsg.Extra, Avatar: avatar}
|
||||
flog.Debugf("Message is %#v", msg)
|
||||
b.Remote <- msg
|
||||
}
|
||||
@ -197,3 +273,130 @@ func (b *Btelegram) getFileDirectURL(id string) string {
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// handleDownloadAvatar downloads the avatar of userid from channel
|
||||
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
|
||||
// logs an error message if it fails
|
||||
func (b *Btelegram) handleDownloadAvatar(userid int, channel string) {
|
||||
msg := config.Message{Username: "system", Text: "avatar", Channel: channel, Account: b.Account, UserID: strconv.Itoa(userid), Event: config.EVENT_AVATAR_DOWNLOAD, Extra: make(map[string][]interface{})}
|
||||
if _, ok := b.avatarMap[strconv.Itoa(userid)]; !ok {
|
||||
photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1})
|
||||
if err != nil {
|
||||
flog.Errorf("Userprofile download failed for %#v %s", userid, err)
|
||||
}
|
||||
if len(photos.Photos) > 0 {
|
||||
photo := photos.Photos[0][0]
|
||||
url := b.getFileDirectURL(photo.FileID)
|
||||
name := strconv.Itoa(userid) + ".png"
|
||||
flog.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)
|
||||
if photo.FileSize <= 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))
|
||||
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: name, Data: data, Avatar: true})
|
||||
flog.Debugf("Sending avatar download message from %#v on %s to gateway", userid, b.Account)
|
||||
flog.Debugf("Message is %#v", msg)
|
||||
b.Remote <- msg
|
||||
}
|
||||
} else {
|
||||
flog.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", name, photo.FileSize, b.General.MediaDownloadSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Btelegram) handleDownload(file interface{}, comment string, msg *config.Message) {
|
||||
size := 0
|
||||
url := ""
|
||||
name := ""
|
||||
text := ""
|
||||
fileid := ""
|
||||
switch v := file.(type) {
|
||||
case *tgbotapi.Audio:
|
||||
size = v.FileSize
|
||||
url = b.getFileDirectURL(v.FileID)
|
||||
urlPart := strings.Split(url, "/")
|
||||
name = urlPart[len(urlPart)-1]
|
||||
text = " " + url
|
||||
fileid = v.FileID
|
||||
case *tgbotapi.Voice:
|
||||
size = v.FileSize
|
||||
url = b.getFileDirectURL(v.FileID)
|
||||
urlPart := strings.Split(url, "/")
|
||||
name = urlPart[len(urlPart)-1]
|
||||
text = " " + url
|
||||
if !strings.HasSuffix(name, ".ogg") {
|
||||
name = name + ".ogg"
|
||||
}
|
||||
fileid = v.FileID
|
||||
case *tgbotapi.Sticker:
|
||||
size = v.FileSize
|
||||
url = b.getFileDirectURL(v.FileID)
|
||||
urlPart := strings.Split(url, "/")
|
||||
name = urlPart[len(urlPart)-1]
|
||||
if !strings.HasSuffix(name, ".webp") {
|
||||
name = name + ".webp"
|
||||
}
|
||||
text = " " + url
|
||||
fileid = v.FileID
|
||||
case *tgbotapi.Video:
|
||||
size = v.FileSize
|
||||
url = b.getFileDirectURL(v.FileID)
|
||||
urlPart := strings.Split(url, "/")
|
||||
name = urlPart[len(urlPart)-1]
|
||||
text = " " + url
|
||||
fileid = v.FileID
|
||||
case *[]tgbotapi.PhotoSize:
|
||||
photos := *v
|
||||
size = photos[len(photos)-1].FileSize
|
||||
url = b.getFileDirectURL(photos[len(photos)-1].FileID)
|
||||
urlPart := strings.Split(url, "/")
|
||||
name = urlPart[len(urlPart)-1]
|
||||
text = " " + url
|
||||
case *tgbotapi.Document:
|
||||
size = v.FileSize
|
||||
url = b.getFileDirectURL(v.FileID)
|
||||
name = v.FileName
|
||||
text = " " + v.FileName + " : " + url
|
||||
fileid = v.FileID
|
||||
}
|
||||
if b.Config.UseInsecureURL {
|
||||
flog.Debugf("Setting message text to :%s", text)
|
||||
msg.Text = text
|
||||
return
|
||||
}
|
||||
// if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra
|
||||
flog.Debugf("trying to download %#v fileid %#v with size %#v", name, fileid, size)
|
||||
if size <= 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))
|
||||
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: name, Data: data, Comment: comment})
|
||||
}
|
||||
} else {
|
||||
flog.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", name, size, b.General.MediaDownloadSize)
|
||||
msg.Event = config.EVENT_FILE_FAILURE_SIZE
|
||||
msg.Extra[msg.Event] = append(msg.Extra[msg.Event], config.FileInfo{Name: name, Comment: comment, Size: int64(size)})
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Btelegram) sendMessage(chatid int64, text string) (string, error) {
|
||||
m := tgbotapi.NewMessage(chatid, text)
|
||||
if b.Config.MessageFormat == "HTML" {
|
||||
flog.Debug("Using mode HTML")
|
||||
m.ParseMode = tgbotapi.ModeHTML
|
||||
}
|
||||
if b.Config.MessageFormat == "Markdown" {
|
||||
flog.Debug("Using mode markdown")
|
||||
m.ParseMode = tgbotapi.ModeMarkdown
|
||||
}
|
||||
res, err := b.c.Send(m)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strconv.Itoa(res.MessageID), nil
|
||||
}
|
||||
|
@ -3,7 +3,8 @@ package bxmpp
|
||||
import (
|
||||
"crypto/tls"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/42wim/matterbridge/bridge/helper"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/jpillora/backoff"
|
||||
"github.com/mattn/go-xmpp"
|
||||
|
||||
@ -14,24 +15,19 @@ import (
|
||||
type Bxmpp struct {
|
||||
xc *xmpp.Client
|
||||
xmppMap map[string]string
|
||||
Config *config.Protocol
|
||||
Remote chan config.Message
|
||||
Account string
|
||||
*config.BridgeConfig
|
||||
}
|
||||
|
||||
var flog *log.Entry
|
||||
var protocol = "xmpp"
|
||||
|
||||
func init() {
|
||||
flog = log.WithFields(log.Fields{"module": protocol})
|
||||
flog = log.WithFields(log.Fields{"prefix": protocol})
|
||||
}
|
||||
|
||||
func New(cfg config.Protocol, account string, c chan config.Message) *Bxmpp {
|
||||
b := &Bxmpp{}
|
||||
func New(cfg *config.BridgeConfig) *Bxmpp {
|
||||
b := &Bxmpp{BridgeConfig: cfg}
|
||||
b.xmppMap = make(map[string]string)
|
||||
b.Config = &cfg
|
||||
b.Account = account
|
||||
b.Remote = c
|
||||
return b
|
||||
}
|
||||
|
||||
@ -85,6 +81,25 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
flog.Debugf("Receiving %#v", msg)
|
||||
if msg.Extra != nil {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: rmsg.Channel + "@" + b.Config.Muc, Text: rmsg.Username + rmsg.Text})
|
||||
}
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
if fi.Comment != "" {
|
||||
msg.Text += fi.Comment + ": "
|
||||
}
|
||||
if fi.URL != "" {
|
||||
msg.Text += fi.URL
|
||||
}
|
||||
b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.Config.Muc, Text: msg.Username + msg.Text})
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.Config.Muc, Text: msg.Username + msg.Text})
|
||||
return "", nil
|
||||
}
|
||||
@ -102,7 +117,7 @@ func (b *Bxmpp) createXMPP() (*xmpp.Client, error) {
|
||||
TLSConfig: tc,
|
||||
|
||||
//StartTLS: false,
|
||||
Debug: true,
|
||||
Debug: b.General.Debug,
|
||||
Session: true,
|
||||
Status: "",
|
||||
StatusMessage: "",
|
||||
@ -158,7 +173,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 {
|
||||
|
122
changelog.md
122
changelog.md
@ -1,3 +1,125 @@
|
||||
# v1.8.0
|
||||
## New features
|
||||
* general: Send chat notification if media is too big to be re-uploaded to MediaServer. See #359
|
||||
* general: Download (and upload) avatar images from mattermost and telegram when mediaserver is configured. Closes #362
|
||||
* general: Add label support in RemoteNickFormat
|
||||
* general: Prettier info/debug log output
|
||||
* mattermost: Download files and reupload to supported bridges (mattermost). Closes #357
|
||||
* slack: Add ShowTopicChange option. Allow/disable topic change messages (currently only from slack). Closes #353
|
||||
* slack: Add support for file comments (slack). Closes #346
|
||||
* telegram: Add comment to file upload from telegram. Show comments on all bridges. Closes #358
|
||||
* telegram: Add markdown support (telegram). #355
|
||||
* api: Give api access to whole config.Message (and events). Closes #374
|
||||
|
||||
## Bugfix
|
||||
* discord: Check for a valid WebhookURL (discord). Closes #367
|
||||
* discord: Fix role mention replace issues
|
||||
* irc: Truncate messages sent to IRC based on byte count (#368)
|
||||
* mattermost: Add file download urls also to mattermost webhooks #356
|
||||
* telegram: Fix panic on nil messages (telegram). Closes #366
|
||||
* telegram: Fix the UseInsecureURL text (telegram). Closes #184
|
||||
|
||||
# v1.7.1
|
||||
## Bugfix
|
||||
* telegram: Enable Long Polling for Telegram. Reduces bandwidth consumption. (#350)
|
||||
|
||||
# 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)
|
||||
* general: Allow specifying maximum download size of media using MediaDownloadSize (slack,telegram,matrix)
|
||||
* api: Add (simple, one listener) long-polling support (api). Closes #307
|
||||
* telegram: Add support for forwarded messages. Closes #313
|
||||
* telegram: Add support for Audio/Voice files (telegram). Closes #314
|
||||
* irc: Add RejoinDelay option. Delay to rejoin after channel kick (irc). Closes #322
|
||||
|
||||
## Bugfix
|
||||
* telegram: Also use HTML in edited messages (telegram). Closes #315
|
||||
* matrix: Fix panic (matrix). Closes #316
|
||||
|
||||
# v1.5.1
|
||||
|
||||
## Bugfix
|
||||
* irc: Fix irc ACTION regression (irc). Closes #306
|
||||
* irc: Split on UTF-8 for MessageSplit (irc). Closes #308
|
||||
|
||||
# v1.5.0
|
||||
## New features
|
||||
* general: remote mediaserver support. See MediaServerDownload and MediaServerUpload in matterbridge.toml.sample
|
||||
more information on https://github.com/42wim/matterbridge/wiki/Mediaserver-setup-%5Badvanced%5D
|
||||
* general: Add support for ReplaceNicks using regexp to replace nicks. Closes #269 (see matterbridge.toml.sample)
|
||||
* general: Add support for ReplaceMessages using regexp to replace messages. #269 (see matterbridge.toml.sample)
|
||||
* irc: Add MessageSplit option to split messages on MessageLength (irc). Closes #281
|
||||
* matrix: Add support for uploading images/video (matrix). Closes #302
|
||||
* matrix: Add support for uploaded images/video (matrix)
|
||||
|
||||
## Bugfix
|
||||
* telegram: Add webp extension to stickers if necessary (telegram)
|
||||
* mattermost: Break when re-login fails (mattermost)
|
||||
|
||||
# v1.4.1
|
||||
## Bugfix
|
||||
* telegram: fix issue with uploading for images/documents/stickers
|
||||
* slack: remove double messages sent to other bridges when uploading files
|
||||
* irc: Fix strict user handling of girc (irc). Closes #298
|
||||
|
||||
# v1.4.0
|
||||
## Breaking changes
|
||||
* general: `[general]` settings don't override the specific bridge settings
|
||||
|
||||
## New features
|
||||
* irc: Replace sorcix/irc and go-ircevent with girc, this should be give better reconnects
|
||||
* steam: Add support for bridging to individual steam chats. (steam) (#294)
|
||||
* telegram: Download files from telegram and reupload to supported bridges (telegram). #278
|
||||
* slack: Add support to upload files to slack, from bridges with private urls like slack/mattermost/telegram. (slack)
|
||||
* discord: Add support to upload files to discord, from bridges with private urls like slack/mattermost/telegram. (discord)
|
||||
* general: Add systemd service file (#291)
|
||||
* general: Add support for DEBUG=1 envvar to enable debug. Closes #283
|
||||
* general: Add StripNick option, only allow alphanumerical nicks. Closes #285
|
||||
|
||||
## Bugfix
|
||||
* gitter: Use room.URI instead of room.Name. (gitter) (#293)
|
||||
* slack: Allow slack messages with variables (eg. @here) to be formatted correctly. (slack) (#288)
|
||||
* slack: Resolve slack channel to human-readable name. (slack) (#282)
|
||||
* slack: Use DisplayName instead of deprecated username (slack). Closes #276
|
||||
* slack: Allowed Slack bridge to extract simpler link format. (#287)
|
||||
* irc: Strip irc colors correct, strip also ctrl chars (irc)
|
||||
|
||||
# v1.3.1
|
||||
## New features
|
||||
* Support mattermost 4.3.0 and every other 4.x as api4 should be stable (mattermost)
|
||||
## Bugfix
|
||||
* Use bot username if specified (slack). Closes #273
|
||||
|
||||
# v1.3.0
|
||||
## New features
|
||||
* Relay slack_attachments from mattermost to slack (slack). Closes #260
|
||||
|
11
contrib/matterbridge.service
Normal file
11
contrib/matterbridge.service
Normal file
@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=matterbridge
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/matterbridge -conf /etc/matterbridge/bridge.toml
|
||||
User=matterbridge
|
||||
Group=matterbridge
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
11
docker/arm/Dockerfile
Normal file
11
docker/arm/Dockerfile
Normal 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
|
@ -1,13 +1,16 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/42wim/matterbridge/bridge"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
// "github.com/davecgh/go-spew/spew"
|
||||
"crypto/sha1"
|
||||
"github.com/hashicorp/golang-lru"
|
||||
"github.com/peterhellberg/emojilib"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
@ -28,6 +31,13 @@ type Gateway struct {
|
||||
type BrMsgID struct {
|
||||
br *bridge.Bridge
|
||||
ID string
|
||||
ChannelID string
|
||||
}
|
||||
|
||||
var flog *log.Entry
|
||||
|
||||
func init() {
|
||||
flog = log.WithFields(log.Fields{"prefix": "gateway"})
|
||||
}
|
||||
|
||||
func New(cfg config.Gateway, r *Router) *Gateway {
|
||||
@ -74,10 +84,10 @@ func (gw *Gateway) reconnectBridge(br *bridge.Bridge) {
|
||||
br.Disconnect()
|
||||
time.Sleep(time.Second * 5)
|
||||
RECONNECT:
|
||||
log.Infof("Reconnecting %s", br.Account)
|
||||
flog.Infof("Reconnecting %s", br.Account)
|
||||
err := br.Connect()
|
||||
if err != nil {
|
||||
log.Errorf("Reconnection failed: %s. Trying again in 60 seconds", err)
|
||||
flog.Errorf("Reconnection failed: %s. Trying again in 60 seconds", err)
|
||||
time.Sleep(time.Second * 60)
|
||||
goto RECONNECT
|
||||
}
|
||||
@ -90,6 +100,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,
|
||||
@ -115,6 +129,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
|
||||
@ -131,7 +151,7 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []con
|
||||
continue
|
||||
}
|
||||
|
||||
// do samechannelgateway logic
|
||||
// do samechannelgateway flogic
|
||||
if channel.SameChannel[msg.Gateway] {
|
||||
if msg.Channel == channel.Name && msg.Account != dest.Account {
|
||||
channels = append(channels, *channel)
|
||||
@ -152,30 +172,55 @@ func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrM
|
||||
// only slack now, check will have to be done in the different bridges.
|
||||
// we need to check if we can't use fallback or text in other bridges
|
||||
if msg.Extra != nil {
|
||||
if dest.Protocol != "slack" {
|
||||
if dest.Protocol != "discord" &&
|
||||
dest.Protocol != "slack" &&
|
||||
dest.Protocol != "mattermost" &&
|
||||
dest.Protocol != "telegram" &&
|
||||
dest.Protocol != "matrix" &&
|
||||
dest.Protocol != "xmpp" &&
|
||||
len(msg.Extra[config.EVENT_FILE_FAILURE_SIZE]) == 0 {
|
||||
if msg.Text == "" {
|
||||
return brMsgIDs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Avatar downloads are only relevant for telegram and mattermost for now
|
||||
if msg.Event == config.EVENT_AVATAR_DOWNLOAD {
|
||||
if dest.Protocol != "mattermost" &&
|
||||
dest.Protocol != "telegram" {
|
||||
return brMsgIDs
|
||||
}
|
||||
}
|
||||
// only relay join/part when configged
|
||||
if msg.Event == config.EVENT_JOIN_LEAVE && !gw.Bridges[dest.Account].Config.ShowJoinPart {
|
||||
return brMsgIDs
|
||||
}
|
||||
if msg.Event == config.EVENT_TOPIC_CHANGE && !gw.Bridges[dest.Account].Config.ShowTopicChange {
|
||||
return brMsgIDs
|
||||
}
|
||||
|
||||
// broadcast to every out channel (irc QUIT)
|
||||
if msg.Channel == "" && msg.Event != config.EVENT_JOIN_LEAVE {
|
||||
log.Debug("empty channel")
|
||||
flog.Debug("empty channel")
|
||||
return brMsgIDs
|
||||
}
|
||||
originchannel := msg.Channel
|
||||
origmsg := msg
|
||||
channels := gw.getDestChannel(&msg, *dest)
|
||||
for _, channel := range channels {
|
||||
// do not send to ourself
|
||||
// Only send the avatar download event to ourselves.
|
||||
if msg.Event == config.EVENT_AVATAR_DOWNLOAD {
|
||||
if channel.ID != getChannelID(origmsg) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// do not send to ourself for any other event
|
||||
if channel.ID == getChannelID(origmsg) {
|
||||
continue
|
||||
}
|
||||
log.Debugf("Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, originchannel, dest.Account, channel.Name)
|
||||
}
|
||||
flog.Debugf("Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, originchannel, dest.Account, channel.Name)
|
||||
msg.Channel = channel.Name
|
||||
msg.Avatar = gw.modifyAvatar(origmsg, dest)
|
||||
msg.Username = gw.modifyUsername(origmsg, dest)
|
||||
@ -183,7 +228,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
|
||||
}
|
||||
}
|
||||
@ -198,7 +245,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
|
||||
@ -210,16 +257,19 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
|
||||
return true
|
||||
}
|
||||
if msg.Text == "" {
|
||||
// we have an attachment
|
||||
if msg.Extra != nil && msg.Extra["attachments"] != nil {
|
||||
// we have an attachment or actual bytes
|
||||
if msg.Extra != nil &&
|
||||
(msg.Extra["attachments"] != nil ||
|
||||
len(msg.Extra["file"]) > 0 ||
|
||||
len(msg.Extra[config.EVENT_FILE_FAILURE_SIZE]) > 0) {
|
||||
return false
|
||||
}
|
||||
log.Debugf("ignoring empty message %#v from %s", msg, msg.Account)
|
||||
flog.Debugf("ignoring empty message %#v from %s", msg, msg.Account)
|
||||
return true
|
||||
}
|
||||
for _, entry := range strings.Fields(gw.Bridges[msg.Account].Config.IgnoreNicks) {
|
||||
if msg.Username == entry {
|
||||
log.Debugf("ignoring %s from %s", msg.Username, msg.Account)
|
||||
flog.Debugf("ignoring %s from %s", msg.Username, msg.Account)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -228,11 +278,11 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
|
||||
if entry != "" {
|
||||
re, err := regexp.Compile(entry)
|
||||
if err != nil {
|
||||
log.Errorf("incorrect regexp %s for %s", entry, msg.Account)
|
||||
flog.Errorf("incorrect regexp %s for %s", entry, msg.Account)
|
||||
continue
|
||||
}
|
||||
if re.MatchString(msg.Text) {
|
||||
log.Debugf("matching %s. ignoring %s from %s", entry, msg.Text, msg.Account)
|
||||
flog.Debugf("matching %s. ignoring %s from %s", entry, msg.Text, msg.Account)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -243,10 +293,28 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
|
||||
func (gw *Gateway) modifyUsername(msg config.Message, dest *bridge.Bridge) string {
|
||||
br := gw.Bridges[msg.Account]
|
||||
msg.Protocol = br.Protocol
|
||||
nick := gw.Config.General.RemoteNickFormat
|
||||
if nick == "" {
|
||||
nick = dest.Config.RemoteNickFormat
|
||||
if gw.Config.General.StripNick || dest.Config.StripNick {
|
||||
re := regexp.MustCompile("[^a-zA-Z0-9]+")
|
||||
msg.Username = re.ReplaceAllString(msg.Username, "")
|
||||
}
|
||||
nick := dest.Config.RemoteNickFormat
|
||||
if nick == "" {
|
||||
nick = gw.Config.General.RemoteNickFormat
|
||||
}
|
||||
|
||||
// loop to replace nicks
|
||||
for _, outer := range br.Config.ReplaceNicks {
|
||||
search := outer[0]
|
||||
replace := outer[1]
|
||||
// TODO move compile to bridge init somewhere
|
||||
re, err := regexp.Compile(search)
|
||||
if err != nil {
|
||||
flog.Errorf("regexp in %s failed: %s", msg.Account, err)
|
||||
break
|
||||
}
|
||||
msg.Username = re.ReplaceAllString(msg.Username, replace)
|
||||
}
|
||||
|
||||
if len(msg.Username) > 0 {
|
||||
// fix utf-8 issue #193
|
||||
i := 0
|
||||
@ -259,9 +327,10 @@ 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, "{LABEL}", br.Config.Label, -1)
|
||||
nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
|
||||
return nick
|
||||
}
|
||||
|
||||
@ -280,7 +349,55 @@ func (gw *Gateway) modifyAvatar(msg config.Message, dest *bridge.Bridge) string
|
||||
func (gw *Gateway) modifyMessage(msg *config.Message) {
|
||||
// replace :emoji: to unicode
|
||||
msg.Text = emojilib.Replace(msg.Text)
|
||||
br := gw.Bridges[msg.Account]
|
||||
// loop to replace messages
|
||||
for _, outer := range br.Config.ReplaceMessages {
|
||||
search := outer[0]
|
||||
replace := outer[1]
|
||||
// TODO move compile to bridge init somewhere
|
||||
re, err := regexp.Compile(search)
|
||||
if err != nil {
|
||||
flog.Errorf("regexp in %s failed: %s", msg.Account, err)
|
||||
break
|
||||
}
|
||||
msg.Text = re.ReplaceAllString(msg.Text, replace)
|
||||
}
|
||||
|
||||
// messages from api have Gateway specified, don't overwrite
|
||||
if msg.Protocol != "api" {
|
||||
msg.Gateway = gw.Name
|
||||
}
|
||||
}
|
||||
|
||||
func (gw *Gateway) handleFiles(msg *config.Message) {
|
||||
if msg.Extra == nil || gw.Config.General.MediaServerUpload == "" {
|
||||
return
|
||||
}
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 5,
|
||||
}
|
||||
for i, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
sha1sum := fmt.Sprintf("%x", sha1.Sum(*fi.Data))
|
||||
reader := bytes.NewReader(*fi.Data)
|
||||
url := gw.Config.General.MediaServerUpload + "/" + sha1sum + "/" + fi.Name
|
||||
durl := gw.Config.General.MediaServerDownload + "/" + sha1sum + "/" + fi.Name
|
||||
extra := msg.Extra["file"][i].(config.FileInfo)
|
||||
extra.URL = durl
|
||||
req, _ := http.NewRequest("PUT", url, reader)
|
||||
req.Header.Set("Content-Type", "binary/octet-stream")
|
||||
_, err := client.Do(req)
|
||||
if err != nil {
|
||||
flog.Errorf("mediaserver upload failed: %#v", err)
|
||||
continue
|
||||
}
|
||||
flog.Debugf("mediaserver download URL = %s", durl)
|
||||
// we uploaded the file successfully. Add the SHA
|
||||
extra.SHA = sha1sum
|
||||
msg.Extra["file"][i] = extra
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getChannelID(msg config.Message) string {
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"github.com/42wim/matterbridge/bridge"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/42wim/matterbridge/gateway/samechannel"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
//log "github.com/sirupsen/logrus"
|
||||
// "github.com/davecgh/go-spew/spew"
|
||||
"time"
|
||||
)
|
||||
@ -42,12 +42,13 @@ func NewRouter(cfg *config.Config) (*Router, error) {
|
||||
func (r *Router) Start() error {
|
||||
m := make(map[string]*bridge.Bridge)
|
||||
for _, gw := range r.Gateways {
|
||||
flog.Infof("Parsing gateway %s", gw.Name)
|
||||
for _, br := range gw.Bridges {
|
||||
m[br.Account] = br
|
||||
}
|
||||
}
|
||||
for _, br := range m {
|
||||
log.Infof("Starting bridge: %s ", br.Account)
|
||||
flog.Infof("Starting bridge: %s ", br.Account)
|
||||
err := br.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Bridge %s failed to start: %v", br.Account, err)
|
||||
@ -99,6 +100,7 @@ func (r *Router) handleReceive() {
|
||||
if !gw.ignoreMessage(&msg) {
|
||||
msg.Timestamp = time.Now()
|
||||
gw.modifyMessage(&msg)
|
||||
gw.handleFiles(&msg)
|
||||
for _, br := range gw.Bridges {
|
||||
msgIDs = append(msgIDs, gw.handleMessage(msg, br)...)
|
||||
}
|
||||
|
@ -5,21 +5,21 @@ import (
|
||||
"fmt"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/42wim/matterbridge/gateway"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/google/gops/agent"
|
||||
log "github.com/sirupsen/logrus"
|
||||
prefixed "github.com/x-cray/logrus-prefixed-formatter"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
version = "1.3.0"
|
||||
version = "1.8.0"
|
||||
githash string
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFormatter(&prefixed.TextFormatter{PrefixPadding: 13, DisableColors: true, FullTimestamp: true})
|
||||
flog := log.WithFields(log.Fields{"prefix": "main"})
|
||||
flagConfig := flag.String("conf", "matterbridge.toml", "config file")
|
||||
flagDebug := flag.Bool("debug", false, "enable debug")
|
||||
flagVersion := flag.Bool("version", false, "show version")
|
||||
@ -33,23 +33,25 @@ func main() {
|
||||
fmt.Printf("version: %s %s\n", version, githash)
|
||||
return
|
||||
}
|
||||
if *flagDebug {
|
||||
log.Info("Enabling debug")
|
||||
if *flagDebug || os.Getenv("DEBUG") == "1" {
|
||||
log.SetFormatter(&prefixed.TextFormatter{PrefixPadding: 13, DisableColors: true, FullTimestamp: false})
|
||||
flog.Info("Enabling debug")
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
log.Printf("Running version %s %s", version, githash)
|
||||
flog.Printf("Running version %s %s", version, githash)
|
||||
if strings.Contains(version, "-dev") {
|
||||
log.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.")
|
||||
flog.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)
|
||||
flog.Fatalf("Starting gateway failed: %s", err)
|
||||
}
|
||||
err = r.Start()
|
||||
if err != nil {
|
||||
log.Fatalf("Starting gateway failed: %s", err)
|
||||
flog.Fatalf("Starting gateway failed: %s", err)
|
||||
}
|
||||
log.Printf("Gateway(s) started succesfully. Now relaying messages")
|
||||
flog.Printf("Gateway(s) started succesfully. Now relaying messages")
|
||||
select {}
|
||||
}
|
||||
|
@ -80,6 +80,15 @@ MessageQueue=30
|
||||
#OPTIONAL (default 400)
|
||||
MessageLength=400
|
||||
|
||||
#Split messages on MessageLength instead of showing the <message clipped>
|
||||
#WARNING: this could lead to flooding
|
||||
#OPTIONAL (default false)
|
||||
MessageSplit=false
|
||||
|
||||
#Delay in seconds to rejoin a channel when kicked
|
||||
#OPTIONAL (default 0)
|
||||
RejoinDelay=0
|
||||
|
||||
#Nicks you want to ignore.
|
||||
#Messages from those users will not be sent to other bridges.
|
||||
#OPTIONAL
|
||||
@ -91,19 +100,51 @@ IgnoreNicks="ircspammer1 ircspammer2"
|
||||
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||
IgnoreMessages="^~~ badword"
|
||||
|
||||
#messages you want to replace.
|
||||
#it replaces outgoing messages from the bridge.
|
||||
#so you need to place it by the sending bridge definition.
|
||||
#regular expressions supported
|
||||
#some examples:
|
||||
#this replaces cat => dog and sleep => awake
|
||||
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
|
||||
#this replaces every number with number. 123 => numbernumbernumber
|
||||
#replacemessages=[ ["[0-9]","number"] ]
|
||||
#optional (default empty)
|
||||
ReplaceMessages=[ ["cat","dog"] ]
|
||||
|
||||
#nicks you want to replace.
|
||||
#see replacemessages for syntaxa
|
||||
#optional (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
|
||||
#RemoteNickFormat defines how remote users appear on this bridge
|
||||
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
|
||||
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
|
||||
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
|
||||
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||
#The string "{NOPINGNICK}" (case sensitive) will be replaced by the actual nick / username, but with a ZWSP inside the nick, so the irc user with the same nick won't get pinged. See https://github.com/42wim/matterbridge/issues/175 for more information
|
||||
#OPTIONAL (default empty)
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||
#It will strip other characters from the nick
|
||||
#OPTIONAL (default false)
|
||||
StripNick=false
|
||||
|
||||
#Enable to show topic changes from other bridges
|
||||
#Only works hiding/show topic changes from slack bridge for now
|
||||
#OPTIONAL (default false)
|
||||
ShowTopicChange=false
|
||||
|
||||
###################################################################
|
||||
#XMPP section
|
||||
###################################################################
|
||||
@ -149,18 +190,49 @@ IgnoreNicks="ircspammer1 ircspammer2"
|
||||
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||
IgnoreMessages="^~~ badword"
|
||||
|
||||
#Messages you want to replace.
|
||||
#It replaces outgoing messages from the bridge.
|
||||
#So you need to place it by the sending bridge definition.
|
||||
#Regular expressions supported
|
||||
#Some examples:
|
||||
#This replaces cat => dog and sleep => awake
|
||||
#ReplaceMessages=[ ["cat","dog"], ["sleep","awake"] ]
|
||||
#This Replaces every number with number. 123 => numbernumbernumber
|
||||
#ReplaceMessages=[ ["[0-9]","number"] ]
|
||||
#OPTIONAL (default empty)
|
||||
ReplaceMessages=[ ["cat","dog"] ]
|
||||
|
||||
#Nicks you want to replace.
|
||||
#See ReplaceMessages for syntaxA
|
||||
#OPTIONAL (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
|
||||
#RemoteNickFormat defines how remote users appear on this bridge
|
||||
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
|
||||
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
|
||||
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
|
||||
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||
#OPTIONAL (default empty)
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||
#It will strip other characters from the nick
|
||||
#OPTIONAL (default false)
|
||||
StripNick=false
|
||||
|
||||
#Enable to show topic changes from other bridges
|
||||
#Only works hiding/show topic changes from slack bridge for now
|
||||
#OPTIONAL (default false)
|
||||
ShowTopicChange=false
|
||||
|
||||
###################################################################
|
||||
#hipchat section
|
||||
@ -199,18 +271,49 @@ IgnoreNicks="spammer1 spammer2"
|
||||
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||
IgnoreMessages="^~~ badword"
|
||||
|
||||
#messages you want to replace.
|
||||
#it replaces outgoing messages from the bridge.
|
||||
#so you need to place it by the sending bridge definition.
|
||||
#regular expressions supported
|
||||
#some examples:
|
||||
#this replaces cat => dog and sleep => awake
|
||||
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
|
||||
#this replaces every number with number. 123 => numbernumbernumber
|
||||
#replacemessages=[ ["[0-9]","number"] ]
|
||||
#optional (default empty)
|
||||
ReplaceMessages=[ ["cat","dog"] ]
|
||||
|
||||
#nicks you want to replace.
|
||||
#see replacemessages for syntaxa
|
||||
#optional (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
|
||||
#RemoteNickFormat defines how remote users appear on this bridge
|
||||
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
|
||||
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
|
||||
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
|
||||
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||
#OPTIONAL (default empty)
|
||||
RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||
#It will strip other characters from the nick
|
||||
#OPTIONAL (default false)
|
||||
StripNick=false
|
||||
|
||||
#Enable to show topic changes from other bridges
|
||||
#Only works hiding/show topic changes from slack bridge for now
|
||||
#OPTIONAL (default false)
|
||||
ShowTopicChange=false
|
||||
|
||||
###################################################################
|
||||
#mattermost section
|
||||
@ -309,18 +412,50 @@ IgnoreNicks="ircspammer1 ircspammer2"
|
||||
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||
IgnoreMessages="^~~ badword"
|
||||
|
||||
#messages you want to replace.
|
||||
#it replaces outgoing messages from the bridge.
|
||||
#so you need to place it by the sending bridge definition.
|
||||
#regular expressions supported
|
||||
#some examples:
|
||||
#this replaces cat => dog and sleep => awake
|
||||
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
|
||||
#this replaces every number with number. 123 => numbernumbernumber
|
||||
#replacemessages=[ ["[0-9]","number"] ]
|
||||
#optional (default empty)
|
||||
ReplaceMessages=[ ["cat","dog"] ]
|
||||
|
||||
#nicks you want to replace.
|
||||
#see replacemessages for syntaxa
|
||||
#optional (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
|
||||
#RemoteNickFormat defines how remote users appear on this bridge
|
||||
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
|
||||
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
|
||||
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
|
||||
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||
#OPTIONAL (default empty)
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||
#It will strip other characters from the nick
|
||||
#OPTIONAL (default false)
|
||||
StripNick=false
|
||||
|
||||
#Enable to show topic changes from other bridges
|
||||
#Only works hiding/show topic changes from slack bridge for now
|
||||
#OPTIONAL (default false)
|
||||
ShowTopicChange=false
|
||||
|
||||
###################################################################
|
||||
#Gitter section
|
||||
#Best to make a dedicated gitter account for the bot.
|
||||
@ -348,18 +483,50 @@ IgnoreNicks="ircspammer1 ircspammer2"
|
||||
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||
IgnoreMessages="^~~ badword"
|
||||
|
||||
#messages you want to replace.
|
||||
#it replaces outgoing messages from the bridge.
|
||||
#so you need to place it by the sending bridge definition.
|
||||
#regular expressions supported
|
||||
#some examples:
|
||||
#this replaces cat => dog and sleep => awake
|
||||
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
|
||||
#this replaces every number with number. 123 => numbernumbernumber
|
||||
#replacemessages=[ ["[0-9]","number"] ]
|
||||
#optional (default empty)
|
||||
ReplaceMessages=[ ["cat","dog"] ]
|
||||
|
||||
#nicks you want to replace.
|
||||
#see replacemessages for syntaxa
|
||||
#optional (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
|
||||
#RemoteNickFormat defines how remote users appear on this bridge
|
||||
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
|
||||
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
|
||||
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
|
||||
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||
#OPTIONAL (default empty)
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||
#It will strip other characters from the nick
|
||||
#OPTIONAL (default false)
|
||||
StripNick=false
|
||||
|
||||
#Enable to show topic changes from other bridges
|
||||
#Only works hiding/show topic changes from slack bridge for now
|
||||
#OPTIONAL (default false)
|
||||
ShowTopicChange=false
|
||||
|
||||
###################################################################
|
||||
#slack section
|
||||
###################################################################
|
||||
@ -395,6 +562,7 @@ WebhookBindAddress="0.0.0.0:9999"
|
||||
#Icon that will be showed in slack
|
||||
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
|
||||
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
|
||||
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
|
||||
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||
#OPTIONAL
|
||||
IconURL="https://robohash.org/{NICK}.png?size=48x48"
|
||||
@ -434,18 +602,50 @@ IgnoreNicks="ircspammer1 ircspammer2"
|
||||
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||
IgnoreMessages="^~~ badword"
|
||||
|
||||
#messages you want to replace.
|
||||
#it replaces outgoing messages from the bridge.
|
||||
#so you need to place it by the sending bridge definition.
|
||||
#regular expressions supported
|
||||
#some examples:
|
||||
#this replaces cat => dog and sleep => awake
|
||||
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
|
||||
#this replaces every number with number. 123 => numbernumbernumber
|
||||
#replacemessages=[ ["[0-9]","number"] ]
|
||||
#optional (default empty)
|
||||
ReplaceMessages=[ ["cat","dog"] ]
|
||||
|
||||
#nicks you want to replace.
|
||||
#see replacemessages for syntaxa
|
||||
#optional (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
|
||||
#RemoteNickFormat defines how remote users appear on this bridge
|
||||
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
|
||||
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
|
||||
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
|
||||
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||
#OPTIONAL (default empty)
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||
#It will strip other characters from the nick
|
||||
#OPTIONAL (default false)
|
||||
StripNick=false
|
||||
|
||||
#Enable to show topic changes from other bridges
|
||||
#Only works hiding/show topic changes from slack bridge for now
|
||||
#OPTIONAL (default false)
|
||||
ShowTopicChange=false
|
||||
|
||||
###################################################################
|
||||
#discord section
|
||||
###################################################################
|
||||
@ -497,18 +697,50 @@ IgnoreNicks="ircspammer1 ircspammer2"
|
||||
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||
IgnoreMessages="^~~ badword"
|
||||
|
||||
#messages you want to replace.
|
||||
#it replaces outgoing messages from the bridge.
|
||||
#so you need to place it by the sending bridge definition.
|
||||
#regular expressions supported
|
||||
#some examples:
|
||||
#this replaces cat => dog and sleep => awake
|
||||
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
|
||||
#this replaces every number with number. 123 => numbernumbernumber
|
||||
#replacemessages=[ ["[0-9]","number"] ]
|
||||
#optional (default empty)
|
||||
ReplaceMessages=[ ["cat","dog"] ]
|
||||
|
||||
#nicks you want to replace.
|
||||
#see replacemessages for syntaxa
|
||||
#optional (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
|
||||
#RemoteNickFormat defines how remote users appear on this bridge
|
||||
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
|
||||
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
|
||||
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
|
||||
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||
#OPTIONAL (default empty)
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||
#It will strip other characters from the nick
|
||||
#OPTIONAL (default false)
|
||||
StripNick=false
|
||||
|
||||
#Enable to show topic changes from other bridges
|
||||
#Only works hiding/show topic changes from slack bridge for now
|
||||
#OPTIONAL (default false)
|
||||
ShowTopicChange=false
|
||||
|
||||
###################################################################
|
||||
#telegram section
|
||||
###################################################################
|
||||
@ -559,18 +791,49 @@ IgnoreNicks="spammer1 spammer2"
|
||||
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||
IgnoreMessages="^~~ badword"
|
||||
|
||||
#messages you want to replace.
|
||||
#it replaces outgoing messages from the bridge.
|
||||
#so you need to place it by the sending bridge definition.
|
||||
#regular expressions supported
|
||||
#some examples:
|
||||
#this replaces cat => dog and sleep => awake
|
||||
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
|
||||
#this replaces every number with number. 123 => numbernumbernumber
|
||||
#replacemessages=[ ["[0-9]","number"] ]
|
||||
#optional (default empty)
|
||||
ReplaceMessages=[ ["cat","dog"] ]
|
||||
|
||||
#nicks you want to replace.
|
||||
#see replacemessages for syntaxa
|
||||
#optional (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
|
||||
#RemoteNickFormat defines how remote users appear on this bridge
|
||||
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
|
||||
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
|
||||
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
|
||||
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||
#OPTIONAL (default empty)
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||
#It will strip other characters from the nick
|
||||
#OPTIONAL (default false)
|
||||
StripNick=false
|
||||
|
||||
#Enable to show topic changes from other bridges
|
||||
#Only works hiding/show topic changes from slack bridge for now
|
||||
#OPTIONAL (default false)
|
||||
ShowTopicChange=false
|
||||
|
||||
###################################################################
|
||||
#rocketchat section
|
||||
@ -623,18 +886,50 @@ IgnoreNicks="ircspammer1 ircspammer2"
|
||||
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||
IgnoreMessages="^~~ badword"
|
||||
|
||||
#messages you want to replace.
|
||||
#it replaces outgoing messages from the bridge.
|
||||
#so you need to place it by the sending bridge definition.
|
||||
#regular expressions supported
|
||||
#some examples:
|
||||
#this replaces cat => dog and sleep => awake
|
||||
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
|
||||
#this replaces every number with number. 123 => numbernumbernumber
|
||||
#replacemessages=[ ["[0-9]","number"] ]
|
||||
#optional (default empty)
|
||||
ReplaceMessages=[ ["cat","dog"] ]
|
||||
|
||||
#nicks you want to replace.
|
||||
#see replacemessages for syntaxa
|
||||
#optional (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
|
||||
#RemoteNickFormat defines how remote users appear on this bridge
|
||||
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
|
||||
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
|
||||
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
|
||||
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||
#OPTIONAL (default empty)
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||
#It will strip other characters from the nick
|
||||
#OPTIONAL (default false)
|
||||
StripNick=false
|
||||
|
||||
#Enable to show topic changes from other bridges
|
||||
#Only works hiding/show topic changes from slack bridge for now
|
||||
#OPTIONAL (default false)
|
||||
ShowTopicChange=false
|
||||
|
||||
###################################################################
|
||||
#matrix section
|
||||
###################################################################
|
||||
@ -678,18 +973,50 @@ IgnoreNicks="spammer1 spammer2"
|
||||
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||
IgnoreMessages="^~~ badword"
|
||||
|
||||
#messages you want to replace.
|
||||
#it replaces outgoing messages from the bridge.
|
||||
#so you need to place it by the sending bridge definition.
|
||||
#regular expressions supported
|
||||
#some examples:
|
||||
#this replaces cat => dog and sleep => awake
|
||||
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
|
||||
#this replaces every number with number. 123 => numbernumbernumber
|
||||
#replacemessages=[ ["[0-9]","number"] ]
|
||||
#optional (default empty)
|
||||
ReplaceMessages=[ ["cat","dog"] ]
|
||||
|
||||
#nicks you want to replace.
|
||||
#see replacemessages for syntaxa
|
||||
#optional (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
|
||||
#RemoteNickFormat defines how remote users appear on this bridge
|
||||
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
|
||||
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
|
||||
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
|
||||
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||
#OPTIONAL (default empty)
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||
#It will strip other characters from the nick
|
||||
#OPTIONAL (default false)
|
||||
StripNick=false
|
||||
|
||||
#Enable to show topic changes from other bridges
|
||||
#Only works hiding/show topic changes from slack bridge for now
|
||||
#OPTIONAL (default false)
|
||||
ShowTopicChange=false
|
||||
|
||||
###################################################################
|
||||
#steam section
|
||||
###################################################################
|
||||
@ -727,18 +1054,49 @@ IgnoreNicks="spammer1 spammer2"
|
||||
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||
IgnoreMessages="^~~ badword"
|
||||
|
||||
#messages you want to replace.
|
||||
#it replaces outgoing messages from the bridge.
|
||||
#so you need to place it by the sending bridge definition.
|
||||
#regular expressions supported
|
||||
#some examples:
|
||||
#this replaces cat => dog and sleep => awake
|
||||
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
|
||||
#this replaces every number with number. 123 => numbernumbernumber
|
||||
#replacemessages=[ ["[0-9]","number"] ]
|
||||
#optional (default empty)
|
||||
ReplaceMessages=[ ["cat","dog"] ]
|
||||
|
||||
#nicks you want to replace.
|
||||
#see replacemessages for syntaxa
|
||||
#optional (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
|
||||
#RemoteNickFormat defines how remote users appear on this bridge
|
||||
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
|
||||
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
|
||||
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
|
||||
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||
#OPTIONAL (default empty)
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||
#It will strip other characters from the nick
|
||||
#OPTIONAL (default false)
|
||||
StripNick=false
|
||||
|
||||
#Enable to show topic changes from other bridges
|
||||
#Only works hiding/show topic changes from slack bridge for now
|
||||
#OPTIONAL (default false)
|
||||
ShowTopicChange=false
|
||||
|
||||
###################################################################
|
||||
#API
|
||||
@ -761,9 +1119,14 @@ Buffer=1000
|
||||
#OPTIONAL (no authorization if token is empty)
|
||||
Token="mytoken"
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
|
||||
#RemoteNickFormat defines how remote users appear on this bridge
|
||||
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
|
||||
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
|
||||
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
|
||||
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||
#OPTIONAL (default empty)
|
||||
RemoteNickFormat="{NICK}"
|
||||
@ -773,15 +1136,45 @@ RemoteNickFormat="{NICK}"
|
||||
###################################################################
|
||||
#General configuration
|
||||
###################################################################
|
||||
#Settings here override specific settings for each protocol
|
||||
# Settings here are defaults that each protocol can override
|
||||
[general]
|
||||
#RemoteNickFormat defines how remote users appear on this bridge
|
||||
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
|
||||
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
|
||||
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
|
||||
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||
#OPTIONAL (default empty)
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||
#It will strip other characters from the nick
|
||||
#OPTIONAL (default false)
|
||||
StripNick=false
|
||||
|
||||
|
||||
#MediaServerUpload and MediaServerDownload are used for uploading images/files/video to
|
||||
#a remote "mediaserver" (a webserver like caddy for example).
|
||||
#When configured images/files uploaded on bridges like mattermost,slack, telegram will be downloaded
|
||||
#and uploaded again to MediaServerUpload URL
|
||||
#The MediaServerDownload will be used so that bridges without native uploading support:
|
||||
#gitter, irc and xmpp will be shown links to the files on MediaServerDownload
|
||||
#
|
||||
#More information https://github.com/42wim/matterbridge/wiki/Mediaserver-setup-%5Badvanced%5D
|
||||
#OPTIONAL (default empty)
|
||||
MediaServerUpload="https://user:pass@yourserver.com/upload"
|
||||
#OPTIONAL (default empty)
|
||||
MediaServerDownload="https://youserver.com/download"
|
||||
|
||||
#MediaDownloadSize is the maximum size of attachments, videos, images
|
||||
#matterbridge will download and upload this file to bridges that also support uploading files.
|
||||
#eg downloading from slack to upload it to mattermost
|
||||
#
|
||||
#It will only download from bridges that don't have public links available, which are for the moment
|
||||
#slack, telegram, matrix and mattermost
|
||||
#
|
||||
#Optional (default 1000000 (1 megabyte))
|
||||
MediaDownloadSize=1000000
|
||||
|
||||
###################################################################
|
||||
#Gateway configuration
|
||||
###################################################################
|
||||
@ -817,7 +1210,7 @@ enable=true
|
||||
#mattermost - channel (the channel name as seen in the URL, not the displayname)
|
||||
#gitter - username/room
|
||||
#xmpp - channel
|
||||
#slack - channel (the channel name as seen in the URL, not the displayname)
|
||||
#slack - channel (without the #)
|
||||
#discord - channel (without the #)
|
||||
# - ID:123456789 (where 123456789 is the channel ID)
|
||||
# (https://github.com/42wim/matterbridge/issues/57)
|
||||
|
@ -13,7 +13,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
prefixed "github.com/x-cray/logrus-prefixed-formatter"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/hashicorp/golang-lru"
|
||||
@ -73,12 +74,16 @@ type MMClient struct {
|
||||
func New(login, pass, team, server string) *MMClient {
|
||||
cred := &Credentials{Login: login, Pass: pass, Team: team, Server: server}
|
||||
mmclient := &MMClient{Credentials: cred, MessageChan: make(chan *Message, 100), Users: make(map[string]*model.User)}
|
||||
mmclient.log = log.WithFields(log.Fields{"module": "matterclient"})
|
||||
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
|
||||
log.SetFormatter(&prefixed.TextFormatter{PrefixPadding: 13, DisableColors: true})
|
||||
mmclient.log = log.WithFields(log.Fields{"prefix": "matterclient"})
|
||||
mmclient.lruCache, _ = lru.New(500)
|
||||
return mmclient
|
||||
}
|
||||
|
||||
func (m *MMClient) SetDebugLog() {
|
||||
log.SetFormatter(&prefixed.TextFormatter{PrefixPadding: 13, DisableColors: true, FullTimestamp: false})
|
||||
}
|
||||
|
||||
func (m *MMClient) SetLogLevel(level string) {
|
||||
l, err := log.ParseLevel(level)
|
||||
if err != nil {
|
||||
@ -585,9 +590,9 @@ func (m *MMClient) UpdateChannelHeader(channelId string, header string) {
|
||||
func (m *MMClient) UpdateLastViewed(channelId string) {
|
||||
m.log.Debugf("posting lastview %#v", channelId)
|
||||
view := &model.ChannelView{ChannelId: channelId}
|
||||
res, _ := m.Client.ViewChannel(m.User.Id, view)
|
||||
if !res {
|
||||
m.log.Errorf("ChannelView update for %s failed", channelId)
|
||||
_, resp := m.Client.ViewChannel(m.User.Id, view)
|
||||
if resp.Error != nil {
|
||||
m.log.Errorf("ChannelView update for %s failed: %s", channelId, resp.Error)
|
||||
}
|
||||
}
|
||||
|
||||
@ -817,9 +822,14 @@ func (m *MMClient) StatusLoop() {
|
||||
backoff = time.Second * 60
|
||||
case <-time.After(time.Second * 5):
|
||||
if retries > 3 {
|
||||
m.log.Debug("StatusLoop() timeout")
|
||||
m.Logout()
|
||||
m.WsQuit = false
|
||||
m.Login()
|
||||
err := m.Login()
|
||||
if err != nil {
|
||||
log.Errorf("Login failed: %#v", err)
|
||||
break
|
||||
}
|
||||
if m.OnWsConnect != nil {
|
||||
m.OnWsConnect()
|
||||
}
|
||||
@ -895,9 +905,7 @@ func supportedVersion(version string) bool {
|
||||
if strings.HasPrefix(version, "3.8.0") ||
|
||||
strings.HasPrefix(version, "3.9.0") ||
|
||||
strings.HasPrefix(version, "3.10.0") ||
|
||||
strings.HasPrefix(version, "4.0") ||
|
||||
strings.HasPrefix(version, "4.1") ||
|
||||
strings.HasPrefix(version, "4.2") {
|
||||
strings.HasPrefix(version, "4.") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
30
vendor/github.com/Sirupsen/logrus/examples/hook/hook.go
generated
vendored
30
vendor/github.com/Sirupsen/logrus/examples/hook/hook.go
generated
vendored
@ -1,30 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/Sirupsen/logrus"
|
||||
"gopkg.in/gemnasium/logrus-airbrake-hook.v2"
|
||||
)
|
||||
|
||||
var log = logrus.New()
|
||||
|
||||
func init() {
|
||||
log.Formatter = new(logrus.TextFormatter) // default
|
||||
log.Hooks.Add(airbrake.NewHook(123, "xyz", "development"))
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.WithFields(logrus.Fields{
|
||||
"animal": "walrus",
|
||||
"size": 10,
|
||||
}).Info("A group of walrus emerges from the ocean")
|
||||
|
||||
log.WithFields(logrus.Fields{
|
||||
"omg": true,
|
||||
"number": 122,
|
||||
}).Warn("The group's number increased tremendously!")
|
||||
|
||||
log.WithFields(logrus.Fields{
|
||||
"omg": true,
|
||||
"number": 100,
|
||||
}).Fatal("The ice breaks!")
|
||||
}
|
67
vendor/github.com/Sirupsen/logrus/hooks/test/test.go
generated
vendored
67
vendor/github.com/Sirupsen/logrus/hooks/test/test.go
generated
vendored
@ -1,67 +0,0 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
// test.Hook is a hook designed for dealing with logs in test scenarios.
|
||||
type Hook struct {
|
||||
Entries []*logrus.Entry
|
||||
}
|
||||
|
||||
// Installs a test hook for the global logger.
|
||||
func NewGlobal() *Hook {
|
||||
|
||||
hook := new(Hook)
|
||||
logrus.AddHook(hook)
|
||||
|
||||
return hook
|
||||
|
||||
}
|
||||
|
||||
// Installs a test hook for a given local logger.
|
||||
func NewLocal(logger *logrus.Logger) *Hook {
|
||||
|
||||
hook := new(Hook)
|
||||
logger.Hooks.Add(hook)
|
||||
|
||||
return hook
|
||||
|
||||
}
|
||||
|
||||
// Creates a discarding logger and installs the test hook.
|
||||
func NewNullLogger() (*logrus.Logger, *Hook) {
|
||||
|
||||
logger := logrus.New()
|
||||
logger.Out = ioutil.Discard
|
||||
|
||||
return logger, NewLocal(logger)
|
||||
|
||||
}
|
||||
|
||||
func (t *Hook) Fire(e *logrus.Entry) error {
|
||||
t.Entries = append(t.Entries, e)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Hook) Levels() []logrus.Level {
|
||||
return logrus.AllLevels
|
||||
}
|
||||
|
||||
// LastEntry returns the last entry that was logged or nil.
|
||||
func (t *Hook) LastEntry() (l *logrus.Entry) {
|
||||
|
||||
if i := len(t.Entries) - 1; i < 0 {
|
||||
return nil
|
||||
} else {
|
||||
return t.Entries[i]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Reset removes all Entries from this test hook.
|
||||
func (t *Hook) Reset() {
|
||||
t.Entries = make([]*logrus.Entry, 0)
|
||||
}
|
10
vendor/github.com/Sirupsen/logrus/terminal_appengine.go
generated
vendored
10
vendor/github.com/Sirupsen/logrus/terminal_appengine.go
generated
vendored
@ -1,10 +0,0 @@
|
||||
// +build appengine
|
||||
|
||||
package logrus
|
||||
|
||||
import "io"
|
||||
|
||||
// IsTerminal returns true if stderr's file descriptor is a terminal.
|
||||
func IsTerminal(f io.Writer) bool {
|
||||
return true
|
||||
}
|
10
vendor/github.com/Sirupsen/logrus/terminal_bsd.go
generated
vendored
10
vendor/github.com/Sirupsen/logrus/terminal_bsd.go
generated
vendored
@ -1,10 +0,0 @@
|
||||
// +build darwin freebsd openbsd netbsd dragonfly
|
||||
// +build !appengine
|
||||
|
||||
package logrus
|
||||
|
||||
import "syscall"
|
||||
|
||||
const ioctlReadTermios = syscall.TIOCGETA
|
||||
|
||||
type Termios syscall.Termios
|
28
vendor/github.com/Sirupsen/logrus/terminal_notwindows.go
generated
vendored
28
vendor/github.com/Sirupsen/logrus/terminal_notwindows.go
generated
vendored
@ -1,28 +0,0 @@
|
||||
// Based on ssh/terminal:
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux darwin freebsd openbsd netbsd dragonfly
|
||||
// +build !appengine
|
||||
|
||||
package logrus
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// IsTerminal returns true if stderr's file descriptor is a terminal.
|
||||
func IsTerminal(f io.Writer) bool {
|
||||
var termios Termios
|
||||
switch v := f.(type) {
|
||||
case *os.File:
|
||||
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(v.Fd()), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
|
||||
return err == 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
21
vendor/github.com/Sirupsen/logrus/terminal_solaris.go
generated
vendored
21
vendor/github.com/Sirupsen/logrus/terminal_solaris.go
generated
vendored
@ -1,21 +0,0 @@
|
||||
// +build solaris,!appengine
|
||||
|
||||
package logrus
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||
func IsTerminal(f io.Writer) bool {
|
||||
switch v := f.(type) {
|
||||
case *os.File:
|
||||
_, err := unix.IoctlGetTermios(int(v.Fd()), unix.TCGETA)
|
||||
return err == nil
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
33
vendor/github.com/Sirupsen/logrus/terminal_windows.go
generated
vendored
33
vendor/github.com/Sirupsen/logrus/terminal_windows.go
generated
vendored
@ -1,33 +0,0 @@
|
||||
// Based on ssh/terminal:
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build windows,!appengine
|
||||
|
||||
package logrus
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
|
||||
var (
|
||||
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
|
||||
)
|
||||
|
||||
// IsTerminal returns true if stderr's file descriptor is a terminal.
|
||||
func IsTerminal(f io.Writer) bool {
|
||||
switch v := f.(type) {
|
||||
case *os.File:
|
||||
var st uint32
|
||||
r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(v.Fd()), uintptr(unsafe.Pointer(&st)), 0)
|
||||
return r != 0 && e == 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
4
vendor/github.com/bwmarrin/discordgo/discord.go
generated
vendored
4
vendor/github.com/bwmarrin/discordgo/discord.go
generated
vendored
@ -21,7 +21,7 @@ import (
|
||||
)
|
||||
|
||||
// VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/)
|
||||
const VERSION = "0.17.0"
|
||||
const VERSION = "0.18.0"
|
||||
|
||||
// ErrMFA will be risen by New when the user has 2FA.
|
||||
var ErrMFA = errors.New("account has 2FA enabled")
|
||||
@ -50,7 +50,7 @@ func New(args ...interface{}) (s *Session, err error) {
|
||||
// Create an empty Session interface.
|
||||
s = &Session{
|
||||
State: NewState(),
|
||||
ratelimiter: NewRatelimiter(),
|
||||
Ratelimiter: NewRatelimiter(),
|
||||
StateEnabled: true,
|
||||
Compress: true,
|
||||
ShouldReconnectOnError: true,
|
||||
|
5
vendor/github.com/bwmarrin/discordgo/endpoints.go
generated
vendored
5
vendor/github.com/bwmarrin/discordgo/endpoints.go
generated
vendored
@ -71,7 +71,6 @@ var (
|
||||
EndpointUserNotes = func(uID string) string { return EndpointUsers + "@me/notes/" + uID }
|
||||
|
||||
EndpointGuild = func(gID string) string { return EndpointGuilds + gID }
|
||||
EndpointGuildInivtes = func(gID string) string { return EndpointGuilds + gID + "/invites" }
|
||||
EndpointGuildChannels = func(gID string) string { return EndpointGuilds + gID + "/channels" }
|
||||
EndpointGuildMembers = func(gID string) string { return EndpointGuilds + gID + "/members" }
|
||||
EndpointGuildMember = func(gID, uID string) string { return EndpointGuilds + gID + "/members/" + uID }
|
||||
@ -98,7 +97,7 @@ var (
|
||||
EndpointChannelMessages = func(cID string) string { return EndpointChannels + cID + "/messages" }
|
||||
EndpointChannelMessage = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID }
|
||||
EndpointChannelMessageAck = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID + "/ack" }
|
||||
EndpointChannelMessagesBulkDelete = func(cID string) string { return EndpointChannel(cID) + "/messages/bulk_delete" }
|
||||
EndpointChannelMessagesBulkDelete = func(cID string) string { return EndpointChannel(cID) + "/messages/bulk-delete" }
|
||||
EndpointChannelMessagesPins = func(cID string) string { return EndpointChannel(cID) + "/pins" }
|
||||
EndpointChannelMessagePin = func(cID, mID string) string { return EndpointChannel(cID) + "/pins/" + mID }
|
||||
|
||||
@ -122,6 +121,8 @@ var (
|
||||
EndpointRelationship = func(uID string) string { return EndpointRelationships() + "/" + uID }
|
||||
EndpointRelationshipsMutual = func(uID string) string { return EndpointUsers + uID + "/relationships" }
|
||||
|
||||
EndpointGuildCreate = EndpointAPI + "guilds"
|
||||
|
||||
EndpointInvite = func(iID string) string { return EndpointAPI + "invite/" + iID }
|
||||
|
||||
EndpointIntegrationsJoin = func(iID string) string { return EndpointAPI + "integrations/" + iID + "/join" }
|
||||
|
2
vendor/github.com/bwmarrin/discordgo/event.go
generated
vendored
2
vendor/github.com/bwmarrin/discordgo/event.go
generated
vendored
@ -6,7 +6,7 @@ type EventHandler interface {
|
||||
Type() string
|
||||
|
||||
// Handle is called whenever an event of Type() happens.
|
||||
// It is the recievers responsibility to type assert that the interface
|
||||
// It is the receivers responsibility to type assert that the interface
|
||||
// is the expected struct.
|
||||
Handle(*Session, interface{})
|
||||
}
|
||||
|
2
vendor/github.com/bwmarrin/discordgo/examples/appmaker/main.go
generated
vendored
2
vendor/github.com/bwmarrin/discordgo/examples/appmaker/main.go
generated
vendored
@ -79,7 +79,7 @@ func main() {
|
||||
ap.Name = Name
|
||||
ap, err = dg.ApplicationCreate(ap)
|
||||
if err != nil {
|
||||
fmt.Println("error creating new applicaiton,", err)
|
||||
fmt.Println("error creating new application,", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
12
vendor/github.com/bwmarrin/discordgo/logging.go
generated
vendored
12
vendor/github.com/bwmarrin/discordgo/logging.go
generated
vendored
@ -23,7 +23,7 @@ const (
|
||||
LogError int = iota
|
||||
|
||||
// LogWarning level is used for very abnormal events and errors that are
|
||||
// also returend to a calling function.
|
||||
// also returned to a calling function.
|
||||
LogWarning
|
||||
|
||||
// LogInformational level is used for normal non-error activity
|
||||
@ -34,14 +34,21 @@ const (
|
||||
LogDebug
|
||||
)
|
||||
|
||||
// Logger can be used to replace the standard logging for discordgo
|
||||
var Logger func(msgL, caller int, format string, a ...interface{})
|
||||
|
||||
// msglog provides package wide logging consistancy for discordgo
|
||||
// the format, a... portion this command follows that of fmt.Printf
|
||||
// msgL : LogLevel of the message
|
||||
// caller : 1 + the number of callers away from the message source
|
||||
// format : Printf style message format
|
||||
// a ... : comma seperated list of values to pass
|
||||
// a ... : comma separated list of values to pass
|
||||
func msglog(msgL, caller int, format string, a ...interface{}) {
|
||||
|
||||
if Logger != nil {
|
||||
Logger(msgL, caller, format, a...)
|
||||
} else {
|
||||
|
||||
pc, file, line, _ := runtime.Caller(caller)
|
||||
|
||||
files := strings.Split(file, "/")
|
||||
@ -54,6 +61,7 @@ func msglog(msgL, caller int, format string, a ...interface{}) {
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
|
||||
log.Printf("[DG%d] %s:%d:%s() %s\n", msgL, file, line, name, msg)
|
||||
}
|
||||
}
|
||||
|
||||
// helper function that wraps msglog for the Session struct
|
||||
|
2
vendor/github.com/bwmarrin/discordgo/message.go
generated
vendored
2
vendor/github.com/bwmarrin/discordgo/message.go
generated
vendored
@ -237,7 +237,7 @@ func (m *Message) ContentWithMoreMentionsReplaced(s *Session) (content string, e
|
||||
continue
|
||||
}
|
||||
|
||||
content = strings.Replace(content, "<&"+role.ID+">", "@"+role.Name, -1)
|
||||
content = strings.Replace(content, "<@&"+role.ID+">", "@"+role.Name, -1)
|
||||
}
|
||||
|
||||
content = patternChannels.ReplaceAllStringFunc(content, func(mention string) string {
|
||||
|
49
vendor/github.com/bwmarrin/discordgo/ratelimit.go
generated
vendored
49
vendor/github.com/bwmarrin/discordgo/ratelimit.go
generated
vendored
@ -41,8 +41,8 @@ func NewRatelimiter() *RateLimiter {
|
||||
}
|
||||
}
|
||||
|
||||
// getBucket retrieves or creates a bucket
|
||||
func (r *RateLimiter) getBucket(key string) *Bucket {
|
||||
// GetBucket retrieves or creates a bucket
|
||||
func (r *RateLimiter) GetBucket(key string) *Bucket {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
@ -51,7 +51,7 @@ func (r *RateLimiter) getBucket(key string) *Bucket {
|
||||
}
|
||||
|
||||
b := &Bucket{
|
||||
remaining: 1,
|
||||
Remaining: 1,
|
||||
Key: key,
|
||||
global: r.global,
|
||||
}
|
||||
@ -68,27 +68,37 @@ func (r *RateLimiter) getBucket(key string) *Bucket {
|
||||
return b
|
||||
}
|
||||
|
||||
// LockBucket Locks until a request can be made
|
||||
func (r *RateLimiter) LockBucket(bucketID string) *Bucket {
|
||||
|
||||
b := r.getBucket(bucketID)
|
||||
|
||||
b.Lock()
|
||||
|
||||
// GetWaitTime returns the duration you should wait for a Bucket
|
||||
func (r *RateLimiter) GetWaitTime(b *Bucket, minRemaining int) time.Duration {
|
||||
// If we ran out of calls and the reset time is still ahead of us
|
||||
// then we need to take it easy and relax a little
|
||||
if b.remaining < 1 && b.reset.After(time.Now()) {
|
||||
time.Sleep(b.reset.Sub(time.Now()))
|
||||
|
||||
if b.Remaining < minRemaining && b.reset.After(time.Now()) {
|
||||
return b.reset.Sub(time.Now())
|
||||
}
|
||||
|
||||
// Check for global ratelimits
|
||||
sleepTo := time.Unix(0, atomic.LoadInt64(r.global))
|
||||
if now := time.Now(); now.Before(sleepTo) {
|
||||
time.Sleep(sleepTo.Sub(now))
|
||||
return sleepTo.Sub(now)
|
||||
}
|
||||
|
||||
b.remaining--
|
||||
return 0
|
||||
}
|
||||
|
||||
// LockBucket Locks until a request can be made
|
||||
func (r *RateLimiter) LockBucket(bucketID string) *Bucket {
|
||||
return r.LockBucketObject(r.GetBucket(bucketID))
|
||||
}
|
||||
|
||||
// LockBucketObject Locks an already resolved bucket until a request can be made
|
||||
func (r *RateLimiter) LockBucketObject(b *Bucket) *Bucket {
|
||||
b.Lock()
|
||||
|
||||
if wait := r.GetWaitTime(b, 1); wait > 0 {
|
||||
time.Sleep(wait)
|
||||
}
|
||||
|
||||
b.Remaining--
|
||||
return b
|
||||
}
|
||||
|
||||
@ -96,13 +106,14 @@ func (r *RateLimiter) LockBucket(bucketID string) *Bucket {
|
||||
type Bucket struct {
|
||||
sync.Mutex
|
||||
Key string
|
||||
remaining int
|
||||
Remaining int
|
||||
limit int
|
||||
reset time.Time
|
||||
global *int64
|
||||
|
||||
lastReset time.Time
|
||||
customRateLimit *customRateLimit
|
||||
Userdata interface{}
|
||||
}
|
||||
|
||||
// Release unlocks the bucket and reads the headers to update the buckets ratelimit info
|
||||
@ -113,10 +124,10 @@ func (b *Bucket) Release(headers http.Header) error {
|
||||
// Check if the bucket uses a custom ratelimiter
|
||||
if rl := b.customRateLimit; rl != nil {
|
||||
if time.Now().Sub(b.lastReset) >= rl.reset {
|
||||
b.remaining = rl.requests - 1
|
||||
b.Remaining = rl.requests - 1
|
||||
b.lastReset = time.Now()
|
||||
}
|
||||
if b.remaining < 1 {
|
||||
if b.Remaining < 1 {
|
||||
b.reset = time.Now().Add(rl.reset)
|
||||
}
|
||||
return nil
|
||||
@ -176,7 +187,7 @@ func (b *Bucket) Release(headers http.Header) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.remaining = int(parsedRemaining)
|
||||
b.Remaining = int(parsedRemaining)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
48
vendor/github.com/bwmarrin/discordgo/restapi.go
generated
vendored
48
vendor/github.com/bwmarrin/discordgo/restapi.go
generated
vendored
@ -65,9 +65,11 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, bucketID
|
||||
if bucketID == "" {
|
||||
bucketID = strings.SplitN(urlStr, "?", 2)[0]
|
||||
}
|
||||
return s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucket(bucketID), sequence)
|
||||
}
|
||||
|
||||
bucket := s.ratelimiter.LockBucket(bucketID)
|
||||
|
||||
// RequestWithLockedBucket makes a request using a bucket that's already been locked
|
||||
func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b []byte, bucket *Bucket, sequence int) (response []byte, err error) {
|
||||
if s.Debug {
|
||||
log.Printf("API REQUEST %8s :: %s\n", method, urlStr)
|
||||
log.Printf("API REQUEST PAYLOAD :: [%s]\n", string(b))
|
||||
@ -139,7 +141,7 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, bucketID
|
||||
if sequence < s.MaxRestRetries {
|
||||
|
||||
s.log(LogInformational, "%s Failed (%s), Retrying...", urlStr, resp.Status)
|
||||
response, err = s.request(method, urlStr, contentType, b, bucketID, sequence+1)
|
||||
response, err = s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucketObject(bucket), sequence+1)
|
||||
} else {
|
||||
err = fmt.Errorf("Exceeded Max retries HTTP %s, %s", resp.Status, response)
|
||||
}
|
||||
@ -158,7 +160,7 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, bucketID
|
||||
// we can make the above smarter
|
||||
// this method can cause longer delays than required
|
||||
|
||||
response, err = s.request(method, urlStr, contentType, b, bucketID, sequence)
|
||||
response, err = s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucketObject(bucket), sequence)
|
||||
|
||||
default: // Error condition
|
||||
err = newRestError(req, resp, response)
|
||||
@ -585,7 +587,7 @@ func (s *Session) GuildCreate(name string) (st *Guild, err error) {
|
||||
Name string `json:"name"`
|
||||
}{name}
|
||||
|
||||
body, err := s.RequestWithBucketID("POST", EndpointGuilds, data, EndpointGuilds)
|
||||
body, err := s.RequestWithBucketID("POST", EndpointGuildCreate, data, EndpointGuildCreate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -907,7 +909,7 @@ func (s *Session) GuildChannelsReorder(guildID string, channels []*Channel) (err
|
||||
// GuildInvites returns an array of Invite structures for the given guild
|
||||
// guildID : The ID of a Guild.
|
||||
func (s *Session) GuildInvites(guildID string) (st []*Invite, err error) {
|
||||
body, err := s.RequestWithBucketID("GET", EndpointGuildInvites(guildID), nil, EndpointGuildInivtes(guildID))
|
||||
body, err := s.RequestWithBucketID("GET", EndpointGuildInvites(guildID), nil, EndpointGuildInvites(guildID))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -957,6 +959,7 @@ func (s *Session) GuildRoleEdit(guildID, roleID, name string, color int, hoist b
|
||||
// Prevent sending a color int that is too big.
|
||||
if color > 0xFFFFFF {
|
||||
err = fmt.Errorf("color value cannot be larger than 0xFFFFFF")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := struct {
|
||||
@ -1020,6 +1023,9 @@ func (s *Session) GuildPruneCount(guildID string, days uint32) (count uint32, er
|
||||
|
||||
uri := EndpointGuildPrune(guildID) + fmt.Sprintf("?days=%d", days)
|
||||
body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildPrune(guildID))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = unmarshal(body, &p)
|
||||
if err != nil {
|
||||
@ -1204,7 +1210,7 @@ func (s *Session) GuildEmbedEdit(guildID string, enabled bool, channelID string)
|
||||
// Functions specific to Discord Channels
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
// Channel returns a Channel strucutre of a specific Channel.
|
||||
// Channel returns a Channel structure of a specific Channel.
|
||||
// channelID : The ID of the Channel you want returned.
|
||||
func (s *Session) Channel(channelID string) (st *Channel, err error) {
|
||||
body, err := s.RequestWithBucketID("GET", EndpointChannel(channelID), nil, EndpointChannel(channelID))
|
||||
@ -1219,12 +1225,16 @@ func (s *Session) Channel(channelID string) (st *Channel, err error) {
|
||||
// ChannelEdit edits the given channel
|
||||
// channelID : The ID of a Channel
|
||||
// name : The new name to assign the channel.
|
||||
func (s *Session) ChannelEdit(channelID, name string) (st *Channel, err error) {
|
||||
|
||||
data := struct {
|
||||
Name string `json:"name"`
|
||||
}{name}
|
||||
func (s *Session) ChannelEdit(channelID, name string) (*Channel, error) {
|
||||
return s.ChannelEditComplex(channelID, &ChannelEdit{
|
||||
Name: name,
|
||||
})
|
||||
}
|
||||
|
||||
// ChannelEditComplex edits an existing channel, replacing the parameters entirely with ChannelEdit struct
|
||||
// channelID : The ID of a Channel
|
||||
// data : The channel struct to send
|
||||
func (s *Session) ChannelEditComplex(channelID string, data *ChannelEdit) (st *Channel, err error) {
|
||||
body, err := s.RequestWithBucketID("PATCH", EndpointChannel(channelID), data, EndpointChannel(channelID))
|
||||
if err != nil {
|
||||
return
|
||||
@ -1476,7 +1486,7 @@ func (s *Session) ChannelMessageDelete(channelID, messageID string) (err error)
|
||||
}
|
||||
|
||||
// ChannelMessagesBulkDelete bulk deletes the messages from the channel for the provided messageIDs.
|
||||
// If only one messageID is in the slice call channelMessageDelete funciton.
|
||||
// If only one messageID is in the slice call channelMessageDelete function.
|
||||
// If the slice is empty do nothing.
|
||||
// channelID : The ID of the channel for the messages to delete.
|
||||
// messages : The IDs of the messages to be deleted. A slice of string IDs. A maximum of 100 messages.
|
||||
@ -1569,16 +1579,14 @@ func (s *Session) ChannelInvites(channelID string) (st []*Invite, err error) {
|
||||
|
||||
// ChannelInviteCreate creates a new invite for the given channel.
|
||||
// channelID : The ID of a Channel
|
||||
// i : An Invite struct with the values MaxAge, MaxUses, Temporary,
|
||||
// and XkcdPass defined.
|
||||
// i : An Invite struct with the values MaxAge, MaxUses and Temporary defined.
|
||||
func (s *Session) ChannelInviteCreate(channelID string, i Invite) (st *Invite, err error) {
|
||||
|
||||
data := struct {
|
||||
MaxAge int `json:"max_age"`
|
||||
MaxUses int `json:"max_uses"`
|
||||
Temporary bool `json:"temporary"`
|
||||
XKCDPass string `json:"xkcdpass"`
|
||||
}{i.MaxAge, i.MaxUses, i.Temporary, i.XkcdPass}
|
||||
}{i.MaxAge, i.MaxUses, i.Temporary}
|
||||
|
||||
body, err := s.RequestWithBucketID("POST", EndpointChannelInvites(channelID), data, EndpointChannelInvites(channelID))
|
||||
if err != nil {
|
||||
@ -1618,7 +1626,7 @@ func (s *Session) ChannelPermissionDelete(channelID, targetID string) (err error
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
// Invite returns an Invite structure of the given invite
|
||||
// inviteID : The invite code (or maybe xkcdpass?)
|
||||
// inviteID : The invite code
|
||||
func (s *Session) Invite(inviteID string) (st *Invite, err error) {
|
||||
|
||||
body, err := s.RequestWithBucketID("GET", EndpointInvite(inviteID), nil, EndpointInvite(""))
|
||||
@ -1631,7 +1639,7 @@ func (s *Session) Invite(inviteID string) (st *Invite, err error) {
|
||||
}
|
||||
|
||||
// InviteDelete deletes an existing invite
|
||||
// inviteID : the code (or maybe xkcdpass?) of an invite
|
||||
// inviteID : the code of an invite
|
||||
func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) {
|
||||
|
||||
body, err := s.RequestWithBucketID("DELETE", EndpointInvite(inviteID), nil, EndpointInvite(""))
|
||||
@ -1644,7 +1652,7 @@ func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) {
|
||||
}
|
||||
|
||||
// InviteAccept accepts an Invite to a Guild or Channel
|
||||
// inviteID : The invite code (or maybe xkcdpass?)
|
||||
// inviteID : The invite code
|
||||
func (s *Session) InviteAccept(inviteID string) (st *Invite, err error) {
|
||||
|
||||
body, err := s.RequestWithBucketID("POST", EndpointInvite(inviteID), nil, EndpointInvite(""))
|
||||
|
9
vendor/github.com/bwmarrin/discordgo/state.go
generated
vendored
9
vendor/github.com/bwmarrin/discordgo/state.go
generated
vendored
@ -531,7 +531,7 @@ func (s *State) PrivateChannel(channelID string) (*Channel, error) {
|
||||
return s.Channel(channelID)
|
||||
}
|
||||
|
||||
// Channel gets a channel by ID, it will look in all guilds an private channels.
|
||||
// Channel gets a channel by ID, it will look in all guilds and private channels.
|
||||
func (s *State) Channel(channelID string) (*Channel, error) {
|
||||
if s == nil {
|
||||
return nil, ErrNilState
|
||||
@ -816,6 +816,13 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) {
|
||||
if s.TrackMembers {
|
||||
err = s.MemberRemove(t.Member)
|
||||
}
|
||||
case *GuildMembersChunk:
|
||||
if s.TrackMembers {
|
||||
for i := range t.Members {
|
||||
t.Members[i].GuildID = t.GuildID
|
||||
err = s.MemberAdd(t.Members[i])
|
||||
}
|
||||
}
|
||||
case *GuildRoleCreate:
|
||||
if s.TrackRoles {
|
||||
err = s.RoleAdd(t.GuildID, t.Role)
|
||||
|
92
vendor/github.com/bwmarrin/discordgo/structs.go
generated
vendored
92
vendor/github.com/bwmarrin/discordgo/structs.go
generated
vendored
@ -14,7 +14,6 @@ package discordgo
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@ -85,6 +84,9 @@ type Session struct {
|
||||
// Stores the last HeartbeatAck that was recieved (in UTC)
|
||||
LastHeartbeatAck time.Time
|
||||
|
||||
// used to deal with rate limits
|
||||
Ratelimiter *RateLimiter
|
||||
|
||||
// Event handlers
|
||||
handlersMu sync.RWMutex
|
||||
handlers map[string][]*eventHandlerInstance
|
||||
@ -96,9 +98,6 @@ type Session struct {
|
||||
// When nil, the session is not listening.
|
||||
listening chan interface{}
|
||||
|
||||
// used to deal with rate limits
|
||||
ratelimiter *RateLimiter
|
||||
|
||||
// sequence tracks the current gateway api websocket sequence number
|
||||
sequence *int64
|
||||
|
||||
@ -143,9 +142,9 @@ type Invite struct {
|
||||
MaxAge int `json:"max_age"`
|
||||
Uses int `json:"uses"`
|
||||
MaxUses int `json:"max_uses"`
|
||||
XkcdPass string `json:"xkcdpass"`
|
||||
Revoked bool `json:"revoked"`
|
||||
Temporary bool `json:"temporary"`
|
||||
Unique bool `json:"unique"`
|
||||
}
|
||||
|
||||
// ChannelType is the type of a Channel
|
||||
@ -171,9 +170,22 @@ type Channel struct {
|
||||
NSFW bool `json:"nsfw"`
|
||||
Position int `json:"position"`
|
||||
Bitrate int `json:"bitrate"`
|
||||
Recipients []*User `json:"recipient"`
|
||||
Recipients []*User `json:"recipients"`
|
||||
Messages []*Message `json:"-"`
|
||||
PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"`
|
||||
ParentID string `json:"parent_id"`
|
||||
}
|
||||
|
||||
// A ChannelEdit holds Channel Feild data for a channel edit.
|
||||
type ChannelEdit struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Topic string `json:"topic,omitempty"`
|
||||
NSFW bool `json:"nsfw,omitempty"`
|
||||
Position int `json:"position"`
|
||||
Bitrate int `json:"bitrate,omitempty"`
|
||||
UserLimit int `json:"user_limit,omitempty"`
|
||||
PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"`
|
||||
ParentID string `json:"parent_id,omitempty"`
|
||||
}
|
||||
|
||||
// A PermissionOverwrite holds permission overwrite data for a Channel
|
||||
@ -191,6 +203,7 @@ type Emoji struct {
|
||||
Roles []string `json:"roles"`
|
||||
Managed bool `json:"managed"`
|
||||
RequireColons bool `json:"require_colons"`
|
||||
Animated bool `json:"animated"`
|
||||
}
|
||||
|
||||
// APIName returns an correctly formatted API name for use in the MessageReactions endpoints.
|
||||
@ -204,7 +217,7 @@ func (e *Emoji) APIName() string {
|
||||
return e.ID
|
||||
}
|
||||
|
||||
// VerificationLevel type defination
|
||||
// VerificationLevel type definition
|
||||
type VerificationLevel int
|
||||
|
||||
// Constants for VerificationLevel levels from 0 to 3 inclusive
|
||||
@ -314,43 +327,56 @@ type Presence struct {
|
||||
Since *int `json:"since"`
|
||||
}
|
||||
|
||||
// GameType is the type of "game" (see GameType* consts) in the Game struct
|
||||
type GameType int
|
||||
|
||||
// Valid GameType values
|
||||
const (
|
||||
GameTypeGame GameType = iota
|
||||
GameTypeStreaming
|
||||
)
|
||||
|
||||
// A Game struct holds the name of the "playing .." game for a user
|
||||
type Game struct {
|
||||
Name string `json:"name"`
|
||||
Type int `json:"type"`
|
||||
Type GameType `json:"type"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
TimeStamps TimeStamps `json:"timestamps,omitempty"`
|
||||
Assets Assets `json:"assets,omitempty"`
|
||||
ApplicationID string `json:"application_id,omitempty"`
|
||||
Instance int8 `json:"instance,omitempty"`
|
||||
// TODO: Party and Secrets (unknown structure)
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals json to Game struct
|
||||
func (g *Game) UnmarshalJSON(bytes []byte) error {
|
||||
temp := &struct {
|
||||
Name json.Number `json:"name"`
|
||||
Type json.RawMessage `json:"type"`
|
||||
URL string `json:"url"`
|
||||
// A TimeStamps struct contains start and end times used in the rich presence "playing .." Game
|
||||
type TimeStamps struct {
|
||||
EndTimestamp int64 `json:"end,omitempty"`
|
||||
StartTimestamp int64 `json:"start,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals JSON into TimeStamps struct
|
||||
func (t *TimeStamps) UnmarshalJSON(b []byte) error {
|
||||
temp := struct {
|
||||
End float64 `json:"end,omitempty"`
|
||||
Start float64 `json:"start,omitempty"`
|
||||
}{}
|
||||
err := json.Unmarshal(bytes, temp)
|
||||
err := json.Unmarshal(b, &temp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
g.URL = temp.URL
|
||||
g.Name = temp.Name.String()
|
||||
|
||||
if temp.Type != nil {
|
||||
err = json.Unmarshal(temp.Type, &g.Type)
|
||||
if err == nil {
|
||||
t.EndTimestamp = int64(temp.End)
|
||||
t.StartTimestamp = int64(temp.Start)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
s := ""
|
||||
err = json.Unmarshal(temp.Type, &s)
|
||||
if err == nil {
|
||||
g.Type, err = strconv.Atoi(s)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
// An Assets struct contains assets and labels used in the rich presence "playing .." Game
|
||||
type Assets struct {
|
||||
LargeImageID string `json:"large_image,omitempty"`
|
||||
SmallImageID string `json:"small_image,omitempty"`
|
||||
LargeText string `json:"large_text,omitempty"`
|
||||
SmallText string `json:"small_text,omitempty"`
|
||||
}
|
||||
|
||||
// A Member stores user information for Guild members.
|
||||
@ -383,7 +409,7 @@ type Settings struct {
|
||||
DeveloperMode bool `json:"developer_mode"`
|
||||
}
|
||||
|
||||
// Status type defination
|
||||
// Status type definition
|
||||
type Status string
|
||||
|
||||
// Constants for Status with the different current available status
|
||||
|
5
vendor/github.com/bwmarrin/discordgo/user.go
generated
vendored
5
vendor/github.com/bwmarrin/discordgo/user.go
generated
vendored
@ -30,6 +30,8 @@ func (u *User) Mention() string {
|
||||
|
||||
// AvatarURL returns a URL to the user's avatar.
|
||||
// size: The size of the user's avatar as a power of two
|
||||
// if size is an empty string, no size parameter will
|
||||
// be added to the URL.
|
||||
func (u *User) AvatarURL(size string) string {
|
||||
var URL string
|
||||
if strings.HasPrefix(u.Avatar, "a_") {
|
||||
@ -38,5 +40,8 @@ func (u *User) AvatarURL(size string) string {
|
||||
URL = EndpointUserAvatar(u.ID, u.Avatar)
|
||||
}
|
||||
|
||||
if size != "" {
|
||||
return URL + "?size=" + size
|
||||
}
|
||||
return URL
|
||||
}
|
||||
|
11
vendor/github.com/bwmarrin/discordgo/voice.go
generated
vendored
11
vendor/github.com/bwmarrin/discordgo/voice.go
generated
vendored
@ -13,7 +13,6 @@ import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -69,7 +68,7 @@ type VoiceConnection struct {
|
||||
voiceSpeakingUpdateHandlers []VoiceSpeakingUpdateHandler
|
||||
}
|
||||
|
||||
// VoiceSpeakingUpdateHandler type provides a function defination for the
|
||||
// VoiceSpeakingUpdateHandler type provides a function definition for the
|
||||
// VoiceSpeakingUpdate event
|
||||
type VoiceSpeakingUpdateHandler func(vc *VoiceConnection, vs *VoiceSpeakingUpdate)
|
||||
|
||||
@ -104,7 +103,7 @@ func (v *VoiceConnection) Speaking(b bool) (err error) {
|
||||
defer v.Unlock()
|
||||
if err != nil {
|
||||
v.speaking = false
|
||||
log.Println("Speaking() write json error:", err)
|
||||
v.log(LogError, "Speaking() write json error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -181,7 +180,7 @@ func (v *VoiceConnection) Close() {
|
||||
v.log(LogInformational, "closing udp")
|
||||
err := v.udpConn.Close()
|
||||
if err != nil {
|
||||
log.Println("error closing udp connection: ", err)
|
||||
v.log(LogError, "error closing udp connection: ", err)
|
||||
}
|
||||
v.udpConn = nil
|
||||
}
|
||||
@ -247,7 +246,7 @@ type voiceOP2 struct {
|
||||
}
|
||||
|
||||
// WaitUntilConnected waits for the Voice Connection to
|
||||
// become ready, if it does not become ready it retuns an err
|
||||
// become ready, if it does not become ready it returns an err
|
||||
func (v *VoiceConnection) waitUntilConnected() error {
|
||||
|
||||
v.log(LogInformational, "called")
|
||||
@ -858,7 +857,7 @@ func (v *VoiceConnection) reconnect() {
|
||||
}
|
||||
|
||||
if v.session.DataReady == false || v.session.wsConn == nil {
|
||||
v.log(LogInformational, "cannot reconenct to channel %s with unready session", v.ChannelID)
|
||||
v.log(LogInformational, "cannot reconnect to channel %s with unready session", v.ChannelID)
|
||||
continue
|
||||
}
|
||||
|
||||
|
255
vendor/github.com/bwmarrin/discordgo/wsapi.go
generated
vendored
255
vendor/github.com/bwmarrin/discordgo/wsapi.go
generated
vendored
@ -15,6 +15,7 @@ import (
|
||||
"compress/zlib"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime"
|
||||
@ -45,19 +46,114 @@ type resumePacket struct {
|
||||
} `json:"d"`
|
||||
}
|
||||
|
||||
// Open opens a websocket connection to Discord.
|
||||
func (s *Session) Open() (err error) {
|
||||
|
||||
// Open creates a websocket connection to Discord.
|
||||
// See: https://discordapp.com/developers/docs/topics/gateway#connecting
|
||||
func (s *Session) Open() error {
|
||||
s.log(LogInformational, "called")
|
||||
|
||||
var err error
|
||||
|
||||
// Prevent Open or other major Session functions from
|
||||
// being called while Open is still running.
|
||||
s.Lock()
|
||||
defer func() {
|
||||
defer s.Unlock()
|
||||
|
||||
// If the websock is already open, bail out here.
|
||||
if s.wsConn != nil {
|
||||
return ErrWSAlreadyOpen
|
||||
}
|
||||
|
||||
// Get the gateway to use for the Websocket connection
|
||||
if s.gateway == "" {
|
||||
s.gateway, err = s.Gateway()
|
||||
if err != nil {
|
||||
s.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// Add the version and encoding to the URL
|
||||
s.gateway = s.gateway + "?v=" + APIVersion + "&encoding=json"
|
||||
}
|
||||
|
||||
// Connect to the Gateway
|
||||
s.log(LogInformational, "connecting to gateway %s", s.gateway)
|
||||
header := http.Header{}
|
||||
header.Add("accept-encoding", "zlib")
|
||||
s.wsConn, _, err = websocket.DefaultDialer.Dial(s.gateway, header)
|
||||
if err != nil {
|
||||
s.log(LogWarning, "error connecting to gateway %s, %s", s.gateway, err)
|
||||
s.gateway = "" // clear cached gateway
|
||||
s.wsConn = nil // Just to be safe.
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// because of this, all code below must set err to the error
|
||||
// when exiting with an error :) Maybe someone has a better
|
||||
// way :)
|
||||
if err != nil {
|
||||
s.wsConn.Close()
|
||||
s.wsConn = nil
|
||||
}
|
||||
}()
|
||||
|
||||
// The first response from Discord should be an Op 10 (Hello) Packet.
|
||||
// When processed by onEvent the heartbeat goroutine will be started.
|
||||
mt, m, err := s.wsConn.ReadMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e, err := s.onEvent(mt, m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.Operation != 10 {
|
||||
err = fmt.Errorf("expecting Op 10, got Op %d instead", e.Operation)
|
||||
return err
|
||||
}
|
||||
s.log(LogInformational, "Op 10 Hello Packet received from Discord")
|
||||
s.LastHeartbeatAck = time.Now().UTC()
|
||||
var h helloOp
|
||||
if err = json.Unmarshal(e.RawData, &h); err != nil {
|
||||
err = fmt.Errorf("error unmarshalling helloOp, %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Now we send either an Op 2 Identity if this is a brand new
|
||||
// connection or Op 6 Resume if we are resuming an existing connection.
|
||||
sequence := atomic.LoadInt64(s.sequence)
|
||||
if s.sessionID == "" && sequence == 0 {
|
||||
|
||||
// Send Op 2 Identity Packet
|
||||
err = s.identify()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error sending identify packet to gateway, %s, %s", s.gateway, err)
|
||||
return err
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// Send Op 6 Resume Packet
|
||||
p := resumePacket{}
|
||||
p.Op = 6
|
||||
p.Data.Token = s.Token
|
||||
p.Data.SessionID = s.sessionID
|
||||
p.Data.Sequence = sequence
|
||||
|
||||
s.log(LogInformational, "sending resume packet to gateway")
|
||||
s.wsMutex.Lock()
|
||||
err = s.wsConn.WriteJSON(p)
|
||||
s.wsMutex.Unlock()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error sending gateway resume packet, %s, %s", s.gateway, err)
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// A basic state is a hard requirement for Voice.
|
||||
// We create it here so the below READY/RESUMED packet can populate
|
||||
// the state :)
|
||||
// XXX: Move to New() func?
|
||||
if s.State == nil {
|
||||
state := NewState()
|
||||
state.TrackChannels = false
|
||||
@ -68,77 +164,42 @@ func (s *Session) Open() (err error) {
|
||||
s.State = state
|
||||
}
|
||||
|
||||
if s.wsConn != nil {
|
||||
err = ErrWSAlreadyOpen
|
||||
return
|
||||
// Now Discord should send us a READY or RESUMED packet.
|
||||
mt, m, err = s.wsConn.ReadMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e, err = s.onEvent(mt, m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.Type != `READY` && e.Type != `RESUMED` {
|
||||
// This is not fatal, but it does not follow their API documentation.
|
||||
s.log(LogWarning, "Expected READY/RESUMED, instead got:\n%#v\n", e)
|
||||
}
|
||||
s.log(LogInformational, "First Packet:\n%#v\n", e)
|
||||
|
||||
s.log(LogInformational, "We are now connected to Discord, emitting connect event")
|
||||
s.handleEvent(connectEventType, &Connect{})
|
||||
|
||||
// A VoiceConnections map is a hard requirement for Voice.
|
||||
// XXX: can this be moved to when opening a voice connection?
|
||||
if s.VoiceConnections == nil {
|
||||
s.log(LogInformational, "creating new VoiceConnections map")
|
||||
s.VoiceConnections = make(map[string]*VoiceConnection)
|
||||
}
|
||||
|
||||
// Get the gateway to use for the Websocket connection
|
||||
if s.gateway == "" {
|
||||
s.gateway, err = s.Gateway()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Add the version and encoding to the URL
|
||||
s.gateway = s.gateway + "?v=" + APIVersion + "&encoding=json"
|
||||
}
|
||||
|
||||
header := http.Header{}
|
||||
header.Add("accept-encoding", "zlib")
|
||||
|
||||
s.log(LogInformational, "connecting to gateway %s", s.gateway)
|
||||
s.wsConn, _, err = websocket.DefaultDialer.Dial(s.gateway, header)
|
||||
if err != nil {
|
||||
s.log(LogWarning, "error connecting to gateway %s, %s", s.gateway, err)
|
||||
s.gateway = "" // clear cached gateway
|
||||
// TODO: should we add a retry block here?
|
||||
return
|
||||
}
|
||||
|
||||
sequence := atomic.LoadInt64(s.sequence)
|
||||
if s.sessionID != "" && sequence > 0 {
|
||||
|
||||
p := resumePacket{}
|
||||
p.Op = 6
|
||||
p.Data.Token = s.Token
|
||||
p.Data.SessionID = s.sessionID
|
||||
p.Data.Sequence = sequence
|
||||
|
||||
s.log(LogInformational, "sending resume packet to gateway")
|
||||
err = s.wsConn.WriteJSON(p)
|
||||
if err != nil {
|
||||
s.log(LogWarning, "error sending gateway resume packet, %s, %s", s.gateway, err)
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
err = s.identify()
|
||||
if err != nil {
|
||||
s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Create listening outside of listen, as it needs to happen inside the mutex
|
||||
// lock.
|
||||
// Create listening chan outside of listen, as it needs to happen inside the
|
||||
// mutex lock and needs to exist before calling heartbeat and listen
|
||||
// go rountines.
|
||||
s.listening = make(chan interface{})
|
||||
|
||||
// Start sending heartbeats and reading messages from Discord.
|
||||
go s.heartbeat(s.wsConn, s.listening, h.HeartbeatInterval)
|
||||
go s.listen(s.wsConn, s.listening)
|
||||
s.LastHeartbeatAck = time.Now().UTC()
|
||||
|
||||
s.Unlock()
|
||||
|
||||
s.log(LogInformational, "emit connect event")
|
||||
s.handleEvent(connectEventType, &Connect{})
|
||||
|
||||
s.log(LogInformational, "exiting")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// listen polls the websocket connection for events, it will stop when the
|
||||
@ -249,7 +310,8 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}
|
||||
}
|
||||
}
|
||||
|
||||
type updateStatusData struct {
|
||||
// UpdateStatusData ia provided to UpdateStatusComplex()
|
||||
type UpdateStatusData struct {
|
||||
IdleSince *int `json:"since"`
|
||||
Game *Game `json:"game"`
|
||||
AFK bool `json:"afk"`
|
||||
@ -258,7 +320,7 @@ type updateStatusData struct {
|
||||
|
||||
type updateStatusOp struct {
|
||||
Op int `json:"op"`
|
||||
Data updateStatusData `json:"d"`
|
||||
Data UpdateStatusData `json:"d"`
|
||||
}
|
||||
|
||||
// UpdateStreamingStatus is used to update the user's streaming status.
|
||||
@ -270,13 +332,7 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err
|
||||
|
||||
s.log(LogInformational, "called")
|
||||
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
if s.wsConn == nil {
|
||||
return ErrWSNotFound
|
||||
}
|
||||
|
||||
usd := updateStatusData{
|
||||
usd := UpdateStatusData{
|
||||
Status: "online",
|
||||
}
|
||||
|
||||
@ -285,9 +341,9 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err
|
||||
}
|
||||
|
||||
if game != "" {
|
||||
gameType := 0
|
||||
gameType := GameTypeGame
|
||||
if url != "" {
|
||||
gameType = 1
|
||||
gameType = GameTypeStreaming
|
||||
}
|
||||
usd.Game = &Game{
|
||||
Name: game,
|
||||
@ -296,6 +352,18 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err
|
||||
}
|
||||
}
|
||||
|
||||
return s.UpdateStatusComplex(usd)
|
||||
}
|
||||
|
||||
// UpdateStatusComplex allows for sending the raw status update data untouched by discordgo.
|
||||
func (s *Session) UpdateStatusComplex(usd UpdateStatusData) (err error) {
|
||||
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
if s.wsConn == nil {
|
||||
return ErrWSNotFound
|
||||
}
|
||||
|
||||
s.wsMutex.Lock()
|
||||
err = s.wsConn.WriteJSON(updateStatusOp{3, usd})
|
||||
s.wsMutex.Unlock()
|
||||
@ -357,9 +425,7 @@ func (s *Session) RequestGuildMembers(guildID, query string, limit int) (err err
|
||||
//
|
||||
// If you use the AddHandler() function to register a handler for the
|
||||
// "OnEvent" event then all events will be passed to that handler.
|
||||
//
|
||||
// TODO: You may also register a custom event handler entirely using...
|
||||
func (s *Session) onEvent(messageType int, message []byte) {
|
||||
func (s *Session) onEvent(messageType int, message []byte) (*Event, error) {
|
||||
|
||||
var err error
|
||||
var reader io.Reader
|
||||
@ -371,7 +437,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
|
||||
z, err2 := zlib.NewReader(reader)
|
||||
if err2 != nil {
|
||||
s.log(LogError, "error uncompressing websocket message, %s", err)
|
||||
return
|
||||
return nil, err2
|
||||
}
|
||||
|
||||
defer func() {
|
||||
@ -389,7 +455,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
|
||||
decoder := json.NewDecoder(reader)
|
||||
if err = decoder.Decode(&e); err != nil {
|
||||
s.log(LogError, "error decoding websocket message, %s", err)
|
||||
return
|
||||
return e, err
|
||||
}
|
||||
|
||||
s.log(LogDebug, "Op: %d, Seq: %d, Type: %s, Data: %s\n\n", e.Operation, e.Sequence, e.Type, string(e.RawData))
|
||||
@ -403,10 +469,10 @@ func (s *Session) onEvent(messageType int, message []byte) {
|
||||
s.wsMutex.Unlock()
|
||||
if err != nil {
|
||||
s.log(LogError, "error sending heartbeat in response to Op1")
|
||||
return
|
||||
return e, err
|
||||
}
|
||||
|
||||
return
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// Reconnect
|
||||
@ -415,7 +481,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
|
||||
s.log(LogInformational, "Closing and reconnecting in response to Op7")
|
||||
s.Close()
|
||||
s.reconnect()
|
||||
return
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// Invalid Session
|
||||
@ -427,20 +493,15 @@ func (s *Session) onEvent(messageType int, message []byte) {
|
||||
err = s.identify()
|
||||
if err != nil {
|
||||
s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err)
|
||||
return
|
||||
return e, err
|
||||
}
|
||||
|
||||
return
|
||||
return e, nil
|
||||
}
|
||||
|
||||
if e.Operation == 10 {
|
||||
var h helloOp
|
||||
if err = json.Unmarshal(e.RawData, &h); err != nil {
|
||||
s.log(LogError, "error unmarshalling helloOp, %s", err)
|
||||
} else {
|
||||
go s.heartbeat(s.wsConn, s.listening, h.HeartbeatInterval)
|
||||
}
|
||||
return
|
||||
// Op10 is handled by Open()
|
||||
return e, nil
|
||||
}
|
||||
|
||||
if e.Operation == 11 {
|
||||
@ -448,7 +509,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
|
||||
s.LastHeartbeatAck = time.Now().UTC()
|
||||
s.Unlock()
|
||||
s.log(LogInformational, "got heartbeat ACK")
|
||||
return
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// Do not try to Dispatch a non-Dispatch Message
|
||||
@ -456,7 +517,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
|
||||
// But we probably should be doing something with them.
|
||||
// TEMP
|
||||
s.log(LogWarning, "unknown Op: %d, Seq: %d, Type: %s, Data: %s, message: %s", e.Operation, e.Sequence, e.Type, string(e.RawData), string(message))
|
||||
return
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// Store the message sequence
|
||||
@ -485,6 +546,8 @@ func (s *Session) onEvent(messageType int, message []byte) {
|
||||
|
||||
// For legacy reasons, we send the raw event also, this could be useful for handling unknown events.
|
||||
s.handleEvent(eventEventType, e)
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
@ -610,7 +673,7 @@ func (s *Session) onVoiceServerUpdate(st *VoiceServerUpdate) {
|
||||
voice.GuildID = st.GuildID
|
||||
voice.Unlock()
|
||||
|
||||
// Open a conenction to the voice server
|
||||
// Open a connection to the voice server
|
||||
err := voice.open()
|
||||
if err != nil {
|
||||
s.log(LogError, "onVoiceServerUpdate voice.open, %s", err)
|
||||
|
77
vendor/github.com/gorilla/websocket/proxy.go
generated
vendored
Normal file
77
vendor/github.com/gorilla/websocket/proxy.go
generated
vendored
Normal file
@ -0,0 +1,77 @@
|
||||
// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type netDialerFunc func(netowrk, addr string) (net.Conn, error)
|
||||
|
||||
func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) {
|
||||
return fn(network, addr)
|
||||
}
|
||||
|
||||
func init() {
|
||||
proxy_RegisterDialerType("http", func(proxyURL *url.URL, forwardDialer proxy_Dialer) (proxy_Dialer, error) {
|
||||
return &httpProxyDialer{proxyURL: proxyURL, fowardDial: forwardDialer.Dial}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type httpProxyDialer struct {
|
||||
proxyURL *url.URL
|
||||
fowardDial func(network, addr string) (net.Conn, error)
|
||||
}
|
||||
|
||||
func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error) {
|
||||
hostPort, _ := hostPortNoPort(hpd.proxyURL)
|
||||
conn, err := hpd.fowardDial(network, hostPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
connectHeader := make(http.Header)
|
||||
if user := hpd.proxyURL.User; user != nil {
|
||||
proxyUser := user.Username()
|
||||
if proxyPassword, passwordSet := user.Password(); passwordSet {
|
||||
credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword))
|
||||
connectHeader.Set("Proxy-Authorization", "Basic "+credential)
|
||||
}
|
||||
}
|
||||
|
||||
connectReq := &http.Request{
|
||||
Method: "CONNECT",
|
||||
URL: &url.URL{Opaque: addr},
|
||||
Host: addr,
|
||||
Header: connectHeader,
|
||||
}
|
||||
|
||||
if err := connectReq.Write(conn); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read response. It's OK to use and discard buffered reader here becaue
|
||||
// the remote server does not speak until spoken to.
|
||||
br := bufio.NewReader(conn)
|
||||
resp, err := http.ReadResponse(br, connectReq)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
conn.Close()
|
||||
f := strings.SplitN(resp.Status, " ", 2)
|
||||
return nil, errors.New(f[1])
|
||||
}
|
||||
return conn, nil
|
||||
}
|
473
vendor/github.com/gorilla/websocket/x_net_proxy.go
generated
vendored
Normal file
473
vendor/github.com/gorilla/websocket/x_net_proxy.go
generated
vendored
Normal file
@ -0,0 +1,473 @@
|
||||
// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT.
|
||||
//go:generate bundle -o x_net_proxy.go golang.org/x/net/proxy
|
||||
|
||||
// Package proxy provides support for a variety of protocols to proxy network
|
||||
// data.
|
||||
//
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type proxy_direct struct{}
|
||||
|
||||
// Direct is a direct proxy: one that makes network connections directly.
|
||||
var proxy_Direct = proxy_direct{}
|
||||
|
||||
func (proxy_direct) Dial(network, addr string) (net.Conn, error) {
|
||||
return net.Dial(network, addr)
|
||||
}
|
||||
|
||||
// A PerHost directs connections to a default Dialer unless the host name
|
||||
// requested matches one of a number of exceptions.
|
||||
type proxy_PerHost struct {
|
||||
def, bypass proxy_Dialer
|
||||
|
||||
bypassNetworks []*net.IPNet
|
||||
bypassIPs []net.IP
|
||||
bypassZones []string
|
||||
bypassHosts []string
|
||||
}
|
||||
|
||||
// NewPerHost returns a PerHost Dialer that directs connections to either
|
||||
// defaultDialer or bypass, depending on whether the connection matches one of
|
||||
// the configured rules.
|
||||
func proxy_NewPerHost(defaultDialer, bypass proxy_Dialer) *proxy_PerHost {
|
||||
return &proxy_PerHost{
|
||||
def: defaultDialer,
|
||||
bypass: bypass,
|
||||
}
|
||||
}
|
||||
|
||||
// Dial connects to the address addr on the given network through either
|
||||
// defaultDialer or bypass.
|
||||
func (p *proxy_PerHost) Dial(network, addr string) (c net.Conn, err error) {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p.dialerForRequest(host).Dial(network, addr)
|
||||
}
|
||||
|
||||
func (p *proxy_PerHost) dialerForRequest(host string) proxy_Dialer {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
for _, net := range p.bypassNetworks {
|
||||
if net.Contains(ip) {
|
||||
return p.bypass
|
||||
}
|
||||
}
|
||||
for _, bypassIP := range p.bypassIPs {
|
||||
if bypassIP.Equal(ip) {
|
||||
return p.bypass
|
||||
}
|
||||
}
|
||||
return p.def
|
||||
}
|
||||
|
||||
for _, zone := range p.bypassZones {
|
||||
if strings.HasSuffix(host, zone) {
|
||||
return p.bypass
|
||||
}
|
||||
if host == zone[1:] {
|
||||
// For a zone ".example.com", we match "example.com"
|
||||
// too.
|
||||
return p.bypass
|
||||
}
|
||||
}
|
||||
for _, bypassHost := range p.bypassHosts {
|
||||
if bypassHost == host {
|
||||
return p.bypass
|
||||
}
|
||||
}
|
||||
return p.def
|
||||
}
|
||||
|
||||
// AddFromString parses a string that contains comma-separated values
|
||||
// specifying hosts that should use the bypass proxy. Each value is either an
|
||||
// IP address, a CIDR range, a zone (*.example.com) or a host name
|
||||
// (localhost). A best effort is made to parse the string and errors are
|
||||
// ignored.
|
||||
func (p *proxy_PerHost) AddFromString(s string) {
|
||||
hosts := strings.Split(s, ",")
|
||||
for _, host := range hosts {
|
||||
host = strings.TrimSpace(host)
|
||||
if len(host) == 0 {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(host, "/") {
|
||||
// We assume that it's a CIDR address like 127.0.0.0/8
|
||||
if _, net, err := net.ParseCIDR(host); err == nil {
|
||||
p.AddNetwork(net)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
p.AddIP(ip)
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(host, "*.") {
|
||||
p.AddZone(host[1:])
|
||||
continue
|
||||
}
|
||||
p.AddHost(host)
|
||||
}
|
||||
}
|
||||
|
||||
// AddIP specifies an IP address that will use the bypass proxy. Note that
|
||||
// this will only take effect if a literal IP address is dialed. A connection
|
||||
// to a named host will never match an IP.
|
||||
func (p *proxy_PerHost) AddIP(ip net.IP) {
|
||||
p.bypassIPs = append(p.bypassIPs, ip)
|
||||
}
|
||||
|
||||
// AddNetwork specifies an IP range that will use the bypass proxy. Note that
|
||||
// this will only take effect if a literal IP address is dialed. A connection
|
||||
// to a named host will never match.
|
||||
func (p *proxy_PerHost) AddNetwork(net *net.IPNet) {
|
||||
p.bypassNetworks = append(p.bypassNetworks, net)
|
||||
}
|
||||
|
||||
// AddZone specifies a DNS suffix that will use the bypass proxy. A zone of
|
||||
// "example.com" matches "example.com" and all of its subdomains.
|
||||
func (p *proxy_PerHost) AddZone(zone string) {
|
||||
if strings.HasSuffix(zone, ".") {
|
||||
zone = zone[:len(zone)-1]
|
||||
}
|
||||
if !strings.HasPrefix(zone, ".") {
|
||||
zone = "." + zone
|
||||
}
|
||||
p.bypassZones = append(p.bypassZones, zone)
|
||||
}
|
||||
|
||||
// AddHost specifies a host name that will use the bypass proxy.
|
||||
func (p *proxy_PerHost) AddHost(host string) {
|
||||
if strings.HasSuffix(host, ".") {
|
||||
host = host[:len(host)-1]
|
||||
}
|
||||
p.bypassHosts = append(p.bypassHosts, host)
|
||||
}
|
||||
|
||||
// A Dialer is a means to establish a connection.
|
||||
type proxy_Dialer interface {
|
||||
// Dial connects to the given address via the proxy.
|
||||
Dial(network, addr string) (c net.Conn, err error)
|
||||
}
|
||||
|
||||
// Auth contains authentication parameters that specific Dialers may require.
|
||||
type proxy_Auth struct {
|
||||
User, Password string
|
||||
}
|
||||
|
||||
// FromEnvironment returns the dialer specified by the proxy related variables in
|
||||
// the environment.
|
||||
func proxy_FromEnvironment() proxy_Dialer {
|
||||
allProxy := proxy_allProxyEnv.Get()
|
||||
if len(allProxy) == 0 {
|
||||
return proxy_Direct
|
||||
}
|
||||
|
||||
proxyURL, err := url.Parse(allProxy)
|
||||
if err != nil {
|
||||
return proxy_Direct
|
||||
}
|
||||
proxy, err := proxy_FromURL(proxyURL, proxy_Direct)
|
||||
if err != nil {
|
||||
return proxy_Direct
|
||||
}
|
||||
|
||||
noProxy := proxy_noProxyEnv.Get()
|
||||
if len(noProxy) == 0 {
|
||||
return proxy
|
||||
}
|
||||
|
||||
perHost := proxy_NewPerHost(proxy, proxy_Direct)
|
||||
perHost.AddFromString(noProxy)
|
||||
return perHost
|
||||
}
|
||||
|
||||
// proxySchemes is a map from URL schemes to a function that creates a Dialer
|
||||
// from a URL with such a scheme.
|
||||
var proxy_proxySchemes map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error)
|
||||
|
||||
// RegisterDialerType takes a URL scheme and a function to generate Dialers from
|
||||
// a URL with that scheme and a forwarding Dialer. Registered schemes are used
|
||||
// by FromURL.
|
||||
func proxy_RegisterDialerType(scheme string, f func(*url.URL, proxy_Dialer) (proxy_Dialer, error)) {
|
||||
if proxy_proxySchemes == nil {
|
||||
proxy_proxySchemes = make(map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error))
|
||||
}
|
||||
proxy_proxySchemes[scheme] = f
|
||||
}
|
||||
|
||||
// FromURL returns a Dialer given a URL specification and an underlying
|
||||
// Dialer for it to make network requests.
|
||||
func proxy_FromURL(u *url.URL, forward proxy_Dialer) (proxy_Dialer, error) {
|
||||
var auth *proxy_Auth
|
||||
if u.User != nil {
|
||||
auth = new(proxy_Auth)
|
||||
auth.User = u.User.Username()
|
||||
if p, ok := u.User.Password(); ok {
|
||||
auth.Password = p
|
||||
}
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "socks5":
|
||||
return proxy_SOCKS5("tcp", u.Host, auth, forward)
|
||||
}
|
||||
|
||||
// If the scheme doesn't match any of the built-in schemes, see if it
|
||||
// was registered by another package.
|
||||
if proxy_proxySchemes != nil {
|
||||
if f, ok := proxy_proxySchemes[u.Scheme]; ok {
|
||||
return f(u, forward)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("proxy: unknown scheme: " + u.Scheme)
|
||||
}
|
||||
|
||||
var (
|
||||
proxy_allProxyEnv = &proxy_envOnce{
|
||||
names: []string{"ALL_PROXY", "all_proxy"},
|
||||
}
|
||||
proxy_noProxyEnv = &proxy_envOnce{
|
||||
names: []string{"NO_PROXY", "no_proxy"},
|
||||
}
|
||||
)
|
||||
|
||||
// envOnce looks up an environment variable (optionally by multiple
|
||||
// names) once. It mitigates expensive lookups on some platforms
|
||||
// (e.g. Windows).
|
||||
// (Borrowed from net/http/transport.go)
|
||||
type proxy_envOnce struct {
|
||||
names []string
|
||||
once sync.Once
|
||||
val string
|
||||
}
|
||||
|
||||
func (e *proxy_envOnce) Get() string {
|
||||
e.once.Do(e.init)
|
||||
return e.val
|
||||
}
|
||||
|
||||
func (e *proxy_envOnce) init() {
|
||||
for _, n := range e.names {
|
||||
e.val = os.Getenv(n)
|
||||
if e.val != "" {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SOCKS5 returns a Dialer that makes SOCKSv5 connections to the given address
|
||||
// with an optional username and password. See RFC 1928 and RFC 1929.
|
||||
func proxy_SOCKS5(network, addr string, auth *proxy_Auth, forward proxy_Dialer) (proxy_Dialer, error) {
|
||||
s := &proxy_socks5{
|
||||
network: network,
|
||||
addr: addr,
|
||||
forward: forward,
|
||||
}
|
||||
if auth != nil {
|
||||
s.user = auth.User
|
||||
s.password = auth.Password
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
type proxy_socks5 struct {
|
||||
user, password string
|
||||
network, addr string
|
||||
forward proxy_Dialer
|
||||
}
|
||||
|
||||
const proxy_socks5Version = 5
|
||||
|
||||
const (
|
||||
proxy_socks5AuthNone = 0
|
||||
proxy_socks5AuthPassword = 2
|
||||
)
|
||||
|
||||
const proxy_socks5Connect = 1
|
||||
|
||||
const (
|
||||
proxy_socks5IP4 = 1
|
||||
proxy_socks5Domain = 3
|
||||
proxy_socks5IP6 = 4
|
||||
)
|
||||
|
||||
var proxy_socks5Errors = []string{
|
||||
"",
|
||||
"general failure",
|
||||
"connection forbidden",
|
||||
"network unreachable",
|
||||
"host unreachable",
|
||||
"connection refused",
|
||||
"TTL expired",
|
||||
"command not supported",
|
||||
"address type not supported",
|
||||
}
|
||||
|
||||
// Dial connects to the address addr on the given network via the SOCKS5 proxy.
|
||||
func (s *proxy_socks5) Dial(network, addr string) (net.Conn, error) {
|
||||
switch network {
|
||||
case "tcp", "tcp6", "tcp4":
|
||||
default:
|
||||
return nil, errors.New("proxy: no support for SOCKS5 proxy connections of type " + network)
|
||||
}
|
||||
|
||||
conn, err := s.forward.Dial(s.network, s.addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.connect(conn, addr); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// connect takes an existing connection to a socks5 proxy server,
|
||||
// and commands the server to extend that connection to target,
|
||||
// which must be a canonical address with a host and port.
|
||||
func (s *proxy_socks5) connect(conn net.Conn, target string) error {
|
||||
host, portStr, err := net.SplitHostPort(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return errors.New("proxy: failed to parse port number: " + portStr)
|
||||
}
|
||||
if port < 1 || port > 0xffff {
|
||||
return errors.New("proxy: port number out of range: " + portStr)
|
||||
}
|
||||
|
||||
// the size here is just an estimate
|
||||
buf := make([]byte, 0, 6+len(host))
|
||||
|
||||
buf = append(buf, proxy_socks5Version)
|
||||
if len(s.user) > 0 && len(s.user) < 256 && len(s.password) < 256 {
|
||||
buf = append(buf, 2 /* num auth methods */, proxy_socks5AuthNone, proxy_socks5AuthPassword)
|
||||
} else {
|
||||
buf = append(buf, 1 /* num auth methods */, proxy_socks5AuthNone)
|
||||
}
|
||||
|
||||
if _, err := conn.Write(buf); err != nil {
|
||||
return errors.New("proxy: failed to write greeting to SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
|
||||
return errors.New("proxy: failed to read greeting from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
if buf[0] != 5 {
|
||||
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " has unexpected version " + strconv.Itoa(int(buf[0])))
|
||||
}
|
||||
if buf[1] == 0xff {
|
||||
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " requires authentication")
|
||||
}
|
||||
|
||||
// See RFC 1929
|
||||
if buf[1] == proxy_socks5AuthPassword {
|
||||
buf = buf[:0]
|
||||
buf = append(buf, 1 /* password protocol version */)
|
||||
buf = append(buf, uint8(len(s.user)))
|
||||
buf = append(buf, s.user...)
|
||||
buf = append(buf, uint8(len(s.password)))
|
||||
buf = append(buf, s.password...)
|
||||
|
||||
if _, err := conn.Write(buf); err != nil {
|
||||
return errors.New("proxy: failed to write authentication request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
|
||||
return errors.New("proxy: failed to read authentication reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
if buf[1] != 0 {
|
||||
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " rejected username/password")
|
||||
}
|
||||
}
|
||||
|
||||
buf = buf[:0]
|
||||
buf = append(buf, proxy_socks5Version, proxy_socks5Connect, 0 /* reserved */)
|
||||
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
buf = append(buf, proxy_socks5IP4)
|
||||
ip = ip4
|
||||
} else {
|
||||
buf = append(buf, proxy_socks5IP6)
|
||||
}
|
||||
buf = append(buf, ip...)
|
||||
} else {
|
||||
if len(host) > 255 {
|
||||
return errors.New("proxy: destination host name too long: " + host)
|
||||
}
|
||||
buf = append(buf, proxy_socks5Domain)
|
||||
buf = append(buf, byte(len(host)))
|
||||
buf = append(buf, host...)
|
||||
}
|
||||
buf = append(buf, byte(port>>8), byte(port))
|
||||
|
||||
if _, err := conn.Write(buf); err != nil {
|
||||
return errors.New("proxy: failed to write connect request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:4]); err != nil {
|
||||
return errors.New("proxy: failed to read connect reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
failure := "unknown error"
|
||||
if int(buf[1]) < len(proxy_socks5Errors) {
|
||||
failure = proxy_socks5Errors[buf[1]]
|
||||
}
|
||||
|
||||
if len(failure) > 0 {
|
||||
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " failed to connect: " + failure)
|
||||
}
|
||||
|
||||
bytesToDiscard := 0
|
||||
switch buf[3] {
|
||||
case proxy_socks5IP4:
|
||||
bytesToDiscard = net.IPv4len
|
||||
case proxy_socks5IP6:
|
||||
bytesToDiscard = net.IPv6len
|
||||
case proxy_socks5Domain:
|
||||
_, err := io.ReadFull(conn, buf[:1])
|
||||
if err != nil {
|
||||
return errors.New("proxy: failed to read domain length from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
bytesToDiscard = int(buf[0])
|
||||
default:
|
||||
return errors.New("proxy: got unknown address type " + strconv.Itoa(int(buf[3])) + " from SOCKS5 proxy at " + s.addr)
|
||||
}
|
||||
|
||||
if cap(buf) < bytesToDiscard {
|
||||
buf = make([]byte, bytesToDiscard)
|
||||
} else {
|
||||
buf = buf[:bytesToDiscard]
|
||||
}
|
||||
if _, err := io.ReadFull(conn, buf); err != nil {
|
||||
return errors.New("proxy: failed to read address from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
// Also need to discard the port number
|
||||
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
|
||||
return errors.New("proxy: failed to read port from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
18
vendor/github.com/labstack/echo/context.go
generated
vendored
18
vendor/github.com/labstack/echo/context.go
generated
vendored
@ -274,13 +274,6 @@ func (c *context) Param(name string) string {
|
||||
if n == name {
|
||||
return c.pvalues[i]
|
||||
}
|
||||
|
||||
// Param name with aliases
|
||||
for _, p := range strings.Split(n, ",") {
|
||||
if p == name {
|
||||
return c.pvalues[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
@ -494,14 +487,9 @@ func (c *context) Stream(code int, contentType string, r io.Reader) (err error)
|
||||
}
|
||||
|
||||
func (c *context) File(file string) (err error) {
|
||||
file, err = url.QueryUnescape(file) // Issue #839
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return ErrNotFound
|
||||
return NotFoundHandler(c)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
@ -510,7 +498,7 @@ func (c *context) File(file string) (err error) {
|
||||
file = filepath.Join(file, indexPage)
|
||||
f, err = os.Open(file)
|
||||
if err != nil {
|
||||
return ErrNotFound
|
||||
return NotFoundHandler(c)
|
||||
}
|
||||
defer f.Close()
|
||||
if fi, err = f.Stat(); err != nil {
|
||||
@ -530,7 +518,7 @@ func (c *context) Inline(file, name string) (err error) {
|
||||
}
|
||||
|
||||
func (c *context) contentDisposition(file, name, dispositionType string) (err error) {
|
||||
c.response.Header().Set(HeaderContentDisposition, fmt.Sprintf("%s; filename=%s", dispositionType, name))
|
||||
c.response.Header().Set(HeaderContentDisposition, fmt.Sprintf("%s; filename=%q", dispositionType, name))
|
||||
c.File(file)
|
||||
return
|
||||
}
|
||||
|
137
vendor/github.com/labstack/echo/echo.go
generated
vendored
137
vendor/github.com/labstack/echo/echo.go
generated
vendored
@ -72,14 +72,15 @@ type (
|
||||
TLSServer *http.Server
|
||||
Listener net.Listener
|
||||
TLSListener net.Listener
|
||||
AutoTLSManager autocert.Manager
|
||||
DisableHTTP2 bool
|
||||
Debug bool
|
||||
HideBanner bool
|
||||
HidePort bool
|
||||
HTTPErrorHandler HTTPErrorHandler
|
||||
Binder Binder
|
||||
Validator Validator
|
||||
Renderer Renderer
|
||||
AutoTLSManager autocert.Manager
|
||||
// Mutex sync.RWMutex
|
||||
Logger Logger
|
||||
}
|
||||
@ -88,13 +89,14 @@ type (
|
||||
Route struct {
|
||||
Method string `json:"method"`
|
||||
Path string `json:"path"`
|
||||
Handler string `json:"handler"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// HTTPError represents an error that occurred while handling a request.
|
||||
HTTPError struct {
|
||||
Code int
|
||||
Message interface{}
|
||||
Inner error // Stores the error returned by an external dependency
|
||||
}
|
||||
|
||||
// MiddlewareFunc defines a function to process middleware.
|
||||
@ -121,7 +123,7 @@ type (
|
||||
|
||||
// i is the interface for Echo and Group.
|
||||
i interface {
|
||||
GET(string, HandlerFunc, ...MiddlewareFunc)
|
||||
GET(string, HandlerFunc, ...MiddlewareFunc) *Route
|
||||
}
|
||||
)
|
||||
|
||||
@ -212,7 +214,7 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
version = "3.1.0"
|
||||
version = "3.2.6"
|
||||
website = "https://echo.labstack.com"
|
||||
// http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo
|
||||
banner = `
|
||||
@ -282,7 +284,7 @@ func New() (e *Echo) {
|
||||
e.TLSServer.Handler = e
|
||||
e.HTTPErrorHandler = e.DefaultHTTPErrorHandler
|
||||
e.Binder = &DefaultBinder{}
|
||||
e.Logger.SetLevel(log.OFF)
|
||||
e.Logger.SetLevel(log.ERROR)
|
||||
e.stdLogger = stdLog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0)
|
||||
e.pool.New = func() interface{} {
|
||||
return e.NewContext(nil, nil)
|
||||
@ -319,6 +321,9 @@ func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) {
|
||||
if he, ok := err.(*HTTPError); ok {
|
||||
code = he.Code
|
||||
msg = he.Message
|
||||
if he.Inner != nil {
|
||||
msg = fmt.Sprintf("%v, %v", err, he.Inner)
|
||||
}
|
||||
} else if e.Debug {
|
||||
msg = err.Error()
|
||||
} else {
|
||||
@ -328,19 +333,19 @@ func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) {
|
||||
msg = Map{"message": msg}
|
||||
}
|
||||
|
||||
e.Logger.Error(err)
|
||||
|
||||
// Send response
|
||||
if !c.Response().Committed {
|
||||
if c.Request().Method == HEAD { // Issue #608
|
||||
if err := c.NoContent(code); err != nil {
|
||||
goto ERROR
|
||||
}
|
||||
err = c.NoContent(code)
|
||||
} else {
|
||||
if err := c.JSON(code, msg); err != nil {
|
||||
goto ERROR
|
||||
err = c.JSON(code, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
ERROR:
|
||||
if err != nil {
|
||||
e.Logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pre adds middleware to the chain which is run before router.
|
||||
@ -355,104 +360,114 @@ func (e *Echo) Use(middleware ...MiddlewareFunc) {
|
||||
|
||||
// CONNECT registers a new CONNECT route for a path with matching handler in the
|
||||
// router with optional route-level middleware.
|
||||
func (e *Echo) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) {
|
||||
e.add(CONNECT, path, h, m...)
|
||||
func (e *Echo) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
|
||||
return e.Add(CONNECT, path, h, m...)
|
||||
}
|
||||
|
||||
// DELETE registers a new DELETE route for a path with matching handler in the router
|
||||
// with optional route-level middleware.
|
||||
func (e *Echo) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) {
|
||||
e.add(DELETE, path, h, m...)
|
||||
func (e *Echo) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
|
||||
return e.Add(DELETE, path, h, m...)
|
||||
}
|
||||
|
||||
// GET registers a new GET route for a path with matching handler in the router
|
||||
// with optional route-level middleware.
|
||||
func (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) {
|
||||
e.add(GET, path, h, m...)
|
||||
func (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
|
||||
return e.Add(GET, path, h, m...)
|
||||
}
|
||||
|
||||
// HEAD registers a new HEAD route for a path with matching handler in the
|
||||
// router with optional route-level middleware.
|
||||
func (e *Echo) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) {
|
||||
e.add(HEAD, path, h, m...)
|
||||
func (e *Echo) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
|
||||
return e.Add(HEAD, path, h, m...)
|
||||
}
|
||||
|
||||
// OPTIONS registers a new OPTIONS route for a path with matching handler in the
|
||||
// router with optional route-level middleware.
|
||||
func (e *Echo) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) {
|
||||
e.add(OPTIONS, path, h, m...)
|
||||
func (e *Echo) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
|
||||
return e.Add(OPTIONS, path, h, m...)
|
||||
}
|
||||
|
||||
// PATCH registers a new PATCH route for a path with matching handler in the
|
||||
// router with optional route-level middleware.
|
||||
func (e *Echo) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) {
|
||||
e.add(PATCH, path, h, m...)
|
||||
func (e *Echo) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
|
||||
return e.Add(PATCH, path, h, m...)
|
||||
}
|
||||
|
||||
// POST registers a new POST route for a path with matching handler in the
|
||||
// router with optional route-level middleware.
|
||||
func (e *Echo) POST(path string, h HandlerFunc, m ...MiddlewareFunc) {
|
||||
e.add(POST, path, h, m...)
|
||||
func (e *Echo) POST(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
|
||||
return e.Add(POST, path, h, m...)
|
||||
}
|
||||
|
||||
// PUT registers a new PUT route for a path with matching handler in the
|
||||
// router with optional route-level middleware.
|
||||
func (e *Echo) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) {
|
||||
e.add(PUT, path, h, m...)
|
||||
func (e *Echo) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
|
||||
return e.Add(PUT, path, h, m...)
|
||||
}
|
||||
|
||||
// TRACE registers a new TRACE route for a path with matching handler in the
|
||||
// router with optional route-level middleware.
|
||||
func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) {
|
||||
e.add(TRACE, path, h, m...)
|
||||
func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
|
||||
return e.Add(TRACE, path, h, m...)
|
||||
}
|
||||
|
||||
// Any registers a new route for all HTTP methods and path with matching handler
|
||||
// in the router with optional route-level middleware.
|
||||
func (e *Echo) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) {
|
||||
for _, m := range methods {
|
||||
e.add(m, path, handler, middleware...)
|
||||
func (e *Echo) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
|
||||
routes := make([]*Route, len(methods))
|
||||
for i, m := range methods {
|
||||
routes[i] = e.Add(m, path, handler, middleware...)
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
// Match registers a new route for multiple HTTP methods and path with matching
|
||||
// handler in the router with optional route-level middleware.
|
||||
func (e *Echo) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) {
|
||||
for _, m := range methods {
|
||||
e.add(m, path, handler, middleware...)
|
||||
func (e *Echo) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
|
||||
routes := make([]*Route, len(methods))
|
||||
for i, m := range methods {
|
||||
routes[i] = e.Add(m, path, handler, middleware...)
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
// Static registers a new route with path prefix to serve static files from the
|
||||
// provided root directory.
|
||||
func (e *Echo) Static(prefix, root string) {
|
||||
func (e *Echo) Static(prefix, root string) *Route {
|
||||
if root == "" {
|
||||
root = "." // For security we want to restrict to CWD.
|
||||
}
|
||||
static(e, prefix, root)
|
||||
return static(e, prefix, root)
|
||||
}
|
||||
|
||||
func static(i i, prefix, root string) {
|
||||
func static(i i, prefix, root string) *Route {
|
||||
h := func(c Context) error {
|
||||
name := filepath.Join(root, path.Clean("/"+c.Param("*"))) // "/"+ for security
|
||||
p, err := PathUnescape(c.Param("*"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := filepath.Join(root, path.Clean("/"+p)) // "/"+ for security
|
||||
return c.File(name)
|
||||
}
|
||||
i.GET(prefix, h)
|
||||
if prefix == "/" {
|
||||
i.GET(prefix+"*", h)
|
||||
} else {
|
||||
i.GET(prefix+"/*", h)
|
||||
return i.GET(prefix+"*", h)
|
||||
}
|
||||
|
||||
return i.GET(prefix+"/*", h)
|
||||
}
|
||||
|
||||
// File registers a new route with path to serve a static file.
|
||||
func (e *Echo) File(path, file string) {
|
||||
e.GET(path, func(c Context) error {
|
||||
func (e *Echo) File(path, file string) *Route {
|
||||
return e.GET(path, func(c Context) error {
|
||||
return c.File(file)
|
||||
})
|
||||
}
|
||||
|
||||
func (e *Echo) add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) {
|
||||
// Add registers a new route for an HTTP method and path with matching handler
|
||||
// in the router with optional route-level middleware.
|
||||
func (e *Echo) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
|
||||
name := handlerName(handler)
|
||||
e.router.Add(method, path, func(c Context) error {
|
||||
h := handler
|
||||
@ -465,9 +480,10 @@ func (e *Echo) add(method, path string, handler HandlerFunc, middleware ...Middl
|
||||
r := &Route{
|
||||
Method: method,
|
||||
Path: path,
|
||||
Handler: name,
|
||||
Name: name,
|
||||
}
|
||||
e.router.routes[method+path] = r
|
||||
return r
|
||||
}
|
||||
|
||||
// Group creates a new router group with prefix and optional group-level middleware.
|
||||
@ -479,12 +495,22 @@ func (e *Echo) Group(prefix string, m ...MiddlewareFunc) (g *Group) {
|
||||
|
||||
// URI generates a URI from handler.
|
||||
func (e *Echo) URI(handler HandlerFunc, params ...interface{}) string {
|
||||
name := handlerName(handler)
|
||||
return e.Reverse(name, params...)
|
||||
}
|
||||
|
||||
// URL is an alias for `URI` function.
|
||||
func (e *Echo) URL(h HandlerFunc, params ...interface{}) string {
|
||||
return e.URI(h, params...)
|
||||
}
|
||||
|
||||
// Reverse generates an URL from route name and provided parameters.
|
||||
func (e *Echo) Reverse(name string, params ...interface{}) string {
|
||||
uri := new(bytes.Buffer)
|
||||
ln := len(params)
|
||||
n := 0
|
||||
name := handlerName(handler)
|
||||
for _, r := range e.router.routes {
|
||||
if r.Handler == name {
|
||||
if r.Name == name {
|
||||
for i, l := 0, len(r.Path); i < l; i++ {
|
||||
if r.Path[i] == ':' && n < ln {
|
||||
for ; i < l && r.Path[i] != '/'; i++ {
|
||||
@ -502,11 +528,6 @@ func (e *Echo) URI(handler HandlerFunc, params ...interface{}) string {
|
||||
return uri.String()
|
||||
}
|
||||
|
||||
// URL is an alias for `URI` function.
|
||||
func (e *Echo) URL(h HandlerFunc, params ...interface{}) string {
|
||||
return e.URI(h, params...)
|
||||
}
|
||||
|
||||
// Routes returns the registered routes.
|
||||
func (e *Echo) Routes() []*Route {
|
||||
routes := []*Route{}
|
||||
@ -624,7 +645,7 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !e.HideBanner {
|
||||
if !e.HidePort {
|
||||
e.colorer.Printf("⇨ http server started on %s\n", e.colorer.Green(e.Listener.Addr()))
|
||||
}
|
||||
return s.Serve(e.Listener)
|
||||
@ -636,7 +657,7 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
|
||||
}
|
||||
e.TLSListener = tls.NewListener(l, s.TLSConfig)
|
||||
}
|
||||
if !e.HideBanner {
|
||||
if !e.HidePort {
|
||||
e.colorer.Printf("⇨ https server started on %s\n", e.colorer.Green(e.TLSListener.Addr()))
|
||||
}
|
||||
return s.Serve(e.TLSListener)
|
||||
@ -653,7 +674,7 @@ func NewHTTPError(code int, message ...interface{}) *HTTPError {
|
||||
|
||||
// Error makes it compatible with `error` interface.
|
||||
func (he *HTTPError) Error() string {
|
||||
return fmt.Sprintf("code=%d, message=%s", he.Code, he.Message)
|
||||
return fmt.Sprintf("code=%d, message=%v", he.Code, he.Message)
|
||||
}
|
||||
|
||||
// WrapHandler wraps `http.Handler` into `echo.HandlerFunc`.
|
||||
|
63
vendor/github.com/labstack/echo/group.go
generated
vendored
63
vendor/github.com/labstack/echo/group.go
generated
vendored
@ -20,68 +20,74 @@ func (g *Group) Use(middleware ...MiddlewareFunc) {
|
||||
g.middleware = append(g.middleware, middleware...)
|
||||
// Allow all requests to reach the group as they might get dropped if router
|
||||
// doesn't find a match, making none of the group middleware process.
|
||||
g.echo.Any(path.Clean(g.prefix+"/*"), func(c Context) error {
|
||||
return ErrNotFound
|
||||
for _, p := range []string{"", "/*"} {
|
||||
g.echo.Any(path.Clean(g.prefix+p), func(c Context) error {
|
||||
return NotFoundHandler(c)
|
||||
}, g.middleware...)
|
||||
}
|
||||
}
|
||||
|
||||
// CONNECT implements `Echo#CONNECT()` for sub-routes within the Group.
|
||||
func (g *Group) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) {
|
||||
g.add(CONNECT, path, h, m...)
|
||||
func (g *Group) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
|
||||
return g.Add(CONNECT, path, h, m...)
|
||||
}
|
||||
|
||||
// DELETE implements `Echo#DELETE()` for sub-routes within the Group.
|
||||
func (g *Group) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) {
|
||||
g.add(DELETE, path, h, m...)
|
||||
func (g *Group) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
|
||||
return g.Add(DELETE, path, h, m...)
|
||||
}
|
||||
|
||||
// GET implements `Echo#GET()` for sub-routes within the Group.
|
||||
func (g *Group) GET(path string, h HandlerFunc, m ...MiddlewareFunc) {
|
||||
g.add(GET, path, h, m...)
|
||||
func (g *Group) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
|
||||
return g.Add(GET, path, h, m...)
|
||||
}
|
||||
|
||||
// HEAD implements `Echo#HEAD()` for sub-routes within the Group.
|
||||
func (g *Group) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) {
|
||||
g.add(HEAD, path, h, m...)
|
||||
func (g *Group) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
|
||||
return g.Add(HEAD, path, h, m...)
|
||||
}
|
||||
|
||||
// OPTIONS implements `Echo#OPTIONS()` for sub-routes within the Group.
|
||||
func (g *Group) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) {
|
||||
g.add(OPTIONS, path, h, m...)
|
||||
func (g *Group) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
|
||||
return g.Add(OPTIONS, path, h, m...)
|
||||
}
|
||||
|
||||
// PATCH implements `Echo#PATCH()` for sub-routes within the Group.
|
||||
func (g *Group) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) {
|
||||
g.add(PATCH, path, h, m...)
|
||||
func (g *Group) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
|
||||
return g.Add(PATCH, path, h, m...)
|
||||
}
|
||||
|
||||
// POST implements `Echo#POST()` for sub-routes within the Group.
|
||||
func (g *Group) POST(path string, h HandlerFunc, m ...MiddlewareFunc) {
|
||||
g.add(POST, path, h, m...)
|
||||
func (g *Group) POST(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
|
||||
return g.Add(POST, path, h, m...)
|
||||
}
|
||||
|
||||
// PUT implements `Echo#PUT()` for sub-routes within the Group.
|
||||
func (g *Group) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) {
|
||||
g.add(PUT, path, h, m...)
|
||||
func (g *Group) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
|
||||
return g.Add(PUT, path, h, m...)
|
||||
}
|
||||
|
||||
// TRACE implements `Echo#TRACE()` for sub-routes within the Group.
|
||||
func (g *Group) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) {
|
||||
g.add(TRACE, path, h, m...)
|
||||
func (g *Group) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
|
||||
return g.Add(TRACE, path, h, m...)
|
||||
}
|
||||
|
||||
// Any implements `Echo#Any()` for sub-routes within the Group.
|
||||
func (g *Group) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) {
|
||||
for _, m := range methods {
|
||||
g.add(m, path, handler, middleware...)
|
||||
func (g *Group) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
|
||||
routes := make([]*Route, len(methods))
|
||||
for i, m := range methods {
|
||||
routes[i] = g.Add(m, path, handler, middleware...)
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
// Match implements `Echo#Match()` for sub-routes within the Group.
|
||||
func (g *Group) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) {
|
||||
for _, m := range methods {
|
||||
g.add(m, path, handler, middleware...)
|
||||
func (g *Group) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
|
||||
routes := make([]*Route, len(methods))
|
||||
for i, m := range methods {
|
||||
routes[i] = g.Add(m, path, handler, middleware...)
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
// Group creates a new sub-group with prefix and optional sub-group-level middleware.
|
||||
@ -102,12 +108,13 @@ func (g *Group) File(path, file string) {
|
||||
g.echo.File(g.prefix+path, file)
|
||||
}
|
||||
|
||||
func (g *Group) add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) {
|
||||
// Add implements `Echo#Add()` for sub-routes within the Group.
|
||||
func (g *Group) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
|
||||
// Combine into a new slice to avoid accidentally passing the same slice for
|
||||
// multiple routes, which would lead to later add() calls overwriting the
|
||||
// middleware from earlier calls.
|
||||
m := []MiddlewareFunc{}
|
||||
m = append(m, g.middleware...)
|
||||
m = append(m, middleware...)
|
||||
g.echo.add(method, g.prefix+path, handler, m...)
|
||||
return g.echo.Add(method, g.prefix+path, handler, m...)
|
||||
}
|
||||
|
8
vendor/github.com/labstack/echo/middleware/basic_auth.go
generated
vendored
8
vendor/github.com/labstack/echo/middleware/basic_auth.go
generated
vendored
@ -3,6 +3,7 @@ package middleware
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
)
|
||||
@ -27,7 +28,7 @@ type (
|
||||
)
|
||||
|
||||
const (
|
||||
basic = "Basic"
|
||||
basic = "basic"
|
||||
defaultRealm = "Restricted"
|
||||
)
|
||||
|
||||
@ -54,7 +55,7 @@ func BasicAuth(fn BasicAuthValidator) echo.MiddlewareFunc {
|
||||
func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {
|
||||
// Defaults
|
||||
if config.Validator == nil {
|
||||
panic("basic-auth middleware requires a validator function")
|
||||
panic("echo: basic-auth middleware requires a validator function")
|
||||
}
|
||||
if config.Skipper == nil {
|
||||
config.Skipper = DefaultBasicAuthConfig.Skipper
|
||||
@ -72,7 +73,7 @@ func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {
|
||||
auth := c.Request().Header.Get(echo.HeaderAuthorization)
|
||||
l := len(basic)
|
||||
|
||||
if len(auth) > l+1 && auth[:l] == basic {
|
||||
if len(auth) > l+1 && strings.ToLower(auth[:l]) == basic {
|
||||
b, err := base64.StdEncoding.DecodeString(auth[l+1:])
|
||||
if err != nil {
|
||||
return err
|
||||
@ -87,6 +88,7 @@ func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {
|
||||
} else if valid {
|
||||
return next(c)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
112
vendor/github.com/labstack/echo/middleware/body_dump.go
generated
vendored
Normal file
112
vendor/github.com/labstack/echo/middleware/body_dump.go
generated
vendored
Normal file
@ -0,0 +1,112 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"io"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
)
|
||||
|
||||
type (
|
||||
// BodyDumpConfig defines the config for BodyDump middleware.
|
||||
BodyDumpConfig struct {
|
||||
// Skipper defines a function to skip middleware.
|
||||
Skipper Skipper
|
||||
|
||||
// Handler receives request and response payload.
|
||||
// Required.
|
||||
Handler BodyDumpHandler
|
||||
}
|
||||
|
||||
// BodyDumpHandler receives the request and response payload.
|
||||
BodyDumpHandler func(echo.Context, []byte, []byte)
|
||||
|
||||
bodyDumpResponseWriter struct {
|
||||
io.Writer
|
||||
http.ResponseWriter
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultBodyDumpConfig is the default BodyDump middleware config.
|
||||
DefaultBodyDumpConfig = BodyDumpConfig{
|
||||
Skipper: DefaultSkipper,
|
||||
}
|
||||
)
|
||||
|
||||
// BodyDump returns a BodyDump middleware.
|
||||
//
|
||||
// BodyLimit middleware captures the request and response payload and calls the
|
||||
// registered handler.
|
||||
func BodyDump(handler BodyDumpHandler) echo.MiddlewareFunc {
|
||||
c := DefaultBodyDumpConfig
|
||||
c.Handler = handler
|
||||
return BodyDumpWithConfig(c)
|
||||
}
|
||||
|
||||
// BodyDumpWithConfig returns a BodyDump middleware with config.
|
||||
// See: `BodyDump()`.
|
||||
func BodyDumpWithConfig(config BodyDumpConfig) echo.MiddlewareFunc {
|
||||
// Defaults
|
||||
if config.Handler == nil {
|
||||
panic("echo: body-dump middleware requires a handler function")
|
||||
}
|
||||
if config.Skipper == nil {
|
||||
config.Skipper = DefaultBodyDumpConfig.Skipper
|
||||
}
|
||||
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) (err error) {
|
||||
if config.Skipper(c) {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
// Request
|
||||
reqBody := []byte{}
|
||||
if c.Request().Body != nil { // Read
|
||||
reqBody, _ = ioutil.ReadAll(c.Request().Body)
|
||||
}
|
||||
c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(reqBody)) // Reset
|
||||
|
||||
// Response
|
||||
resBody := new(bytes.Buffer)
|
||||
mw := io.MultiWriter(c.Response().Writer, resBody)
|
||||
writer := &bodyDumpResponseWriter{Writer: mw, ResponseWriter: c.Response().Writer}
|
||||
c.Response().Writer = writer
|
||||
|
||||
if err = next(c); err != nil {
|
||||
c.Error(err)
|
||||
}
|
||||
|
||||
// Callback
|
||||
config.Handler(c, reqBody, resBody.Bytes())
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *bodyDumpResponseWriter) WriteHeader(code int) {
|
||||
w.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func (w *bodyDumpResponseWriter) Write(b []byte) (int, error) {
|
||||
return w.Writer.Write(b)
|
||||
}
|
||||
|
||||
func (w *bodyDumpResponseWriter) Flush() {
|
||||
w.ResponseWriter.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
func (w *bodyDumpResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
return w.ResponseWriter.(http.Hijacker).Hijack()
|
||||
}
|
||||
|
||||
func (w *bodyDumpResponseWriter) CloseNotify() <-chan bool {
|
||||
return w.ResponseWriter.(http.CloseNotifier).CloseNotify()
|
||||
}
|
6
vendor/github.com/labstack/echo/middleware/body_limit.go
generated
vendored
6
vendor/github.com/labstack/echo/middleware/body_limit.go
generated
vendored
@ -17,7 +17,7 @@ type (
|
||||
|
||||
// Maximum allowed size for a request body, it can be specified
|
||||
// as `4x` or `4xB`, where x is one of the multiple from K, M, G, T or P.
|
||||
Limit string `json:"limit"`
|
||||
Limit string `yaml:"limit"`
|
||||
limit int64
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ type (
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultBodyLimitConfig is the default Gzip middleware config.
|
||||
// DefaultBodyLimitConfig is the default BodyLimit middleware config.
|
||||
DefaultBodyLimitConfig = BodyLimitConfig{
|
||||
Skipper: DefaultSkipper,
|
||||
}
|
||||
@ -60,7 +60,7 @@ func BodyLimitWithConfig(config BodyLimitConfig) echo.MiddlewareFunc {
|
||||
|
||||
limit, err := bytes.Parse(config.Limit)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("invalid body-limit=%s", config.Limit))
|
||||
panic(fmt.Errorf("echo: invalid body-limit=%s", config.Limit))
|
||||
}
|
||||
config.limit = limit
|
||||
pool := limitedReaderPool(config)
|
||||
|
5
vendor/github.com/labstack/echo/middleware/compress.go
generated
vendored
5
vendor/github.com/labstack/echo/middleware/compress.go
generated
vendored
@ -20,7 +20,7 @@ type (
|
||||
|
||||
// Gzip compression level.
|
||||
// Optional. Default value -1.
|
||||
Level int `json:"level"`
|
||||
Level int `yaml:"level"`
|
||||
}
|
||||
|
||||
gzipResponseWriter struct {
|
||||
@ -67,7 +67,7 @@ func GzipWithConfig(config GzipConfig) echo.MiddlewareFunc {
|
||||
res := c.Response()
|
||||
res.Header().Add(echo.HeaderVary, echo.HeaderAcceptEncoding)
|
||||
if strings.Contains(c.Request().Header.Get(echo.HeaderAcceptEncoding), gzipScheme) {
|
||||
res.Header().Add(echo.HeaderContentEncoding, gzipScheme) // Issue #806
|
||||
res.Header().Set(echo.HeaderContentEncoding, gzipScheme) // Issue #806
|
||||
rw := res.Writer
|
||||
w, err := gzip.NewWriterLevel(rw, config.Level)
|
||||
if err != nil {
|
||||
@ -98,6 +98,7 @@ func (w *gzipResponseWriter) WriteHeader(code int) {
|
||||
if code == http.StatusNoContent { // Issue #489
|
||||
w.ResponseWriter.Header().Del(echo.HeaderContentEncoding)
|
||||
}
|
||||
w.Header().Del(echo.HeaderContentLength) // Issue #444
|
||||
w.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
|
12
vendor/github.com/labstack/echo/middleware/cors.go
generated
vendored
12
vendor/github.com/labstack/echo/middleware/cors.go
generated
vendored
@ -16,34 +16,34 @@ type (
|
||||
|
||||
// AllowOrigin defines a list of origins that may access the resource.
|
||||
// Optional. Default value []string{"*"}.
|
||||
AllowOrigins []string `json:"allow_origins"`
|
||||
AllowOrigins []string `yaml:"allow_origins"`
|
||||
|
||||
// AllowMethods defines a list methods allowed when accessing the resource.
|
||||
// This is used in response to a preflight request.
|
||||
// Optional. Default value DefaultCORSConfig.AllowMethods.
|
||||
AllowMethods []string `json:"allow_methods"`
|
||||
AllowMethods []string `yaml:"allow_methods"`
|
||||
|
||||
// AllowHeaders defines a list of request headers that can be used when
|
||||
// making the actual request. This in response to a preflight request.
|
||||
// Optional. Default value []string{}.
|
||||
AllowHeaders []string `json:"allow_headers"`
|
||||
AllowHeaders []string `yaml:"allow_headers"`
|
||||
|
||||
// AllowCredentials indicates whether or not the response to the request
|
||||
// can be exposed when the credentials flag is true. When used as part of
|
||||
// a response to a preflight request, this indicates whether or not the
|
||||
// actual request can be made using credentials.
|
||||
// Optional. Default value false.
|
||||
AllowCredentials bool `json:"allow_credentials"`
|
||||
AllowCredentials bool `yaml:"allow_credentials"`
|
||||
|
||||
// ExposeHeaders defines a whitelist headers that clients are allowed to
|
||||
// access.
|
||||
// Optional. Default value []string{}.
|
||||
ExposeHeaders []string `json:"expose_headers"`
|
||||
ExposeHeaders []string `yaml:"expose_headers"`
|
||||
|
||||
// MaxAge indicates how long (in seconds) the results of a preflight request
|
||||
// can be cached.
|
||||
// Optional. Default value 0.
|
||||
MaxAge int `json:"max_age"`
|
||||
MaxAge int `yaml:"max_age"`
|
||||
}
|
||||
)
|
||||
|
||||
|
18
vendor/github.com/labstack/echo/middleware/csrf.go
generated
vendored
18
vendor/github.com/labstack/echo/middleware/csrf.go
generated
vendored
@ -18,7 +18,7 @@ type (
|
||||
Skipper Skipper
|
||||
|
||||
// TokenLength is the length of the generated token.
|
||||
TokenLength uint8 `json:"token_length"`
|
||||
TokenLength uint8 `yaml:"token_length"`
|
||||
// Optional. Default value 32.
|
||||
|
||||
// TokenLookup is a string in the form of "<source>:<key>" that is used
|
||||
@ -28,35 +28,35 @@ type (
|
||||
// - "header:<name>"
|
||||
// - "form:<name>"
|
||||
// - "query:<name>"
|
||||
TokenLookup string `json:"token_lookup"`
|
||||
TokenLookup string `yaml:"token_lookup"`
|
||||
|
||||
// Context key to store generated CSRF token into context.
|
||||
// Optional. Default value "csrf".
|
||||
ContextKey string `json:"context_key"`
|
||||
ContextKey string `yaml:"context_key"`
|
||||
|
||||
// Name of the CSRF cookie. This cookie will store CSRF token.
|
||||
// Optional. Default value "csrf".
|
||||
CookieName string `json:"cookie_name"`
|
||||
CookieName string `yaml:"cookie_name"`
|
||||
|
||||
// Domain of the CSRF cookie.
|
||||
// Optional. Default value none.
|
||||
CookieDomain string `json:"cookie_domain"`
|
||||
CookieDomain string `yaml:"cookie_domain"`
|
||||
|
||||
// Path of the CSRF cookie.
|
||||
// Optional. Default value none.
|
||||
CookiePath string `json:"cookie_path"`
|
||||
CookiePath string `yaml:"cookie_path"`
|
||||
|
||||
// Max age (in seconds) of the CSRF cookie.
|
||||
// Optional. Default value 86400 (24hr).
|
||||
CookieMaxAge int `json:"cookie_max_age"`
|
||||
CookieMaxAge int `yaml:"cookie_max_age"`
|
||||
|
||||
// Indicates if CSRF cookie is secure.
|
||||
// Optional. Default value false.
|
||||
CookieSecure bool `json:"cookie_secure"`
|
||||
CookieSecure bool `yaml:"cookie_secure"`
|
||||
|
||||
// Indicates if CSRF cookie is HTTP only.
|
||||
// Optional. Default value false.
|
||||
CookieHTTPOnly bool `json:"cookie_http_only"`
|
||||
CookieHTTPOnly bool `yaml:"cookie_http_only"`
|
||||
}
|
||||
|
||||
// csrfTokenExtractor defines a function that takes `echo.Context` and returns
|
||||
|
26
vendor/github.com/labstack/echo/middleware/jwt.go
generated
vendored
26
vendor/github.com/labstack/echo/middleware/jwt.go
generated
vendored
@ -1,7 +1,6 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
@ -57,6 +56,12 @@ const (
|
||||
AlgorithmHS256 = "HS256"
|
||||
)
|
||||
|
||||
// Errors
|
||||
var (
|
||||
ErrJWTMissing = echo.NewHTTPError(http.StatusBadRequest, "Missing or malformed jwt")
|
||||
ErrJWTInvalid = echo.NewHTTPError(http.StatusUnauthorized, "Invalid or expired jwt")
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultJWTConfig is the default JWT auth middleware config.
|
||||
DefaultJWTConfig = JWTConfig{
|
||||
@ -77,7 +82,7 @@ var (
|
||||
//
|
||||
// See: https://jwt.io/introduction
|
||||
// See `JWTConfig.TokenLookup`
|
||||
func JWT(key []byte) echo.MiddlewareFunc {
|
||||
func JWT(key interface{}) echo.MiddlewareFunc {
|
||||
c := DefaultJWTConfig
|
||||
c.SigningKey = key
|
||||
return JWTWithConfig(c)
|
||||
@ -134,14 +139,15 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
|
||||
|
||||
auth, err := extractor(c)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
return err
|
||||
}
|
||||
token := new(jwt.Token)
|
||||
// Issue #647, #656
|
||||
if _, ok := config.Claims.(jwt.MapClaims); ok {
|
||||
token, err = jwt.Parse(auth, config.keyFunc)
|
||||
} else {
|
||||
claims := reflect.ValueOf(config.Claims).Interface().(jwt.Claims)
|
||||
t := reflect.ValueOf(config.Claims).Type().Elem()
|
||||
claims := reflect.New(t).Interface().(jwt.Claims)
|
||||
token, err = jwt.ParseWithClaims(auth, claims, config.keyFunc)
|
||||
}
|
||||
if err == nil && token.Valid {
|
||||
@ -149,7 +155,11 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
|
||||
c.Set(config.ContextKey, token)
|
||||
return next(c)
|
||||
}
|
||||
return echo.ErrUnauthorized
|
||||
return &echo.HTTPError{
|
||||
Code: ErrJWTInvalid.Code,
|
||||
Message: ErrJWTInvalid.Message,
|
||||
Inner: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -162,7 +172,7 @@ func jwtFromHeader(header string, authScheme string) jwtExtractor {
|
||||
if len(auth) > l+1 && auth[:l] == authScheme {
|
||||
return auth[l+1:], nil
|
||||
}
|
||||
return "", errors.New("Missing or invalid jwt in the request header")
|
||||
return "", ErrJWTMissing
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,7 +181,7 @@ func jwtFromQuery(param string) jwtExtractor {
|
||||
return func(c echo.Context) (string, error) {
|
||||
token := c.QueryParam(param)
|
||||
if token == "" {
|
||||
return "", errors.New("Missing jwt in the query string")
|
||||
return "", ErrJWTMissing
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
@ -182,7 +192,7 @@ func jwtFromCookie(name string) jwtExtractor {
|
||||
return func(c echo.Context) (string, error) {
|
||||
cookie, err := c.Cookie(name)
|
||||
if err != nil {
|
||||
return "", errors.New("Missing jwt in the cookie")
|
||||
return "", ErrJWTMissing
|
||||
}
|
||||
return cookie.Value, nil
|
||||
}
|
||||
|
18
vendor/github.com/labstack/echo/middleware/key_auth.go
generated
vendored
18
vendor/github.com/labstack/echo/middleware/key_auth.go
generated
vendored
@ -20,7 +20,8 @@ type (
|
||||
// Possible values:
|
||||
// - "header:<name>"
|
||||
// - "query:<name>"
|
||||
KeyLookup string `json:"key_lookup"`
|
||||
// - "form:<name>"
|
||||
KeyLookup string `yaml:"key_lookup"`
|
||||
|
||||
// AuthScheme to be used in the Authorization header.
|
||||
// Optional. Default value "Bearer".
|
||||
@ -72,7 +73,7 @@ func KeyAuthWithConfig(config KeyAuthConfig) echo.MiddlewareFunc {
|
||||
config.KeyLookup = DefaultKeyAuthConfig.KeyLookup
|
||||
}
|
||||
if config.Validator == nil {
|
||||
panic("key-auth middleware requires a validator function")
|
||||
panic("echo: key-auth middleware requires a validator function")
|
||||
}
|
||||
|
||||
// Initialize
|
||||
@ -81,6 +82,8 @@ func KeyAuthWithConfig(config KeyAuthConfig) echo.MiddlewareFunc {
|
||||
switch parts[0] {
|
||||
case "query":
|
||||
extractor = keyFromQuery(parts[1])
|
||||
case "form":
|
||||
extractor = keyFromForm(parts[1])
|
||||
}
|
||||
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
@ -134,3 +137,14 @@ func keyFromQuery(param string) keyExtractor {
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
|
||||
// keyFromForm returns a `keyExtractor` that extracts key from the form.
|
||||
func keyFromForm(param string) keyExtractor {
|
||||
return func(c echo.Context) (string, error) {
|
||||
key := c.FormValue(param)
|
||||
if key == "" {
|
||||
return "", errors.New("Missing key in the form")
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
|
9
vendor/github.com/labstack/echo/middleware/logger.go
generated
vendored
9
vendor/github.com/labstack/echo/middleware/logger.go
generated
vendored
@ -26,6 +26,7 @@ type (
|
||||
// - time_unix_nano
|
||||
// - time_rfc3339
|
||||
// - time_rfc3339_nano
|
||||
// - time_custom
|
||||
// - id (Request ID)
|
||||
// - remote_ip
|
||||
// - uri
|
||||
@ -46,7 +47,10 @@ type (
|
||||
// Example "${remote_ip} ${status}"
|
||||
//
|
||||
// Optional. Default value DefaultLoggerConfig.Format.
|
||||
Format string `json:"format"`
|
||||
Format string `yaml:"format"`
|
||||
|
||||
// Optional. Default value DefaultLoggerConfig.CustomTimeFormat.
|
||||
CustomTimeFormat string `yaml:"custom_time_format"`
|
||||
|
||||
// Output is a writer where logs in JSON format are written.
|
||||
// Optional. Default value os.Stdout.
|
||||
@ -66,6 +70,7 @@ var (
|
||||
`"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` +
|
||||
`"latency_human":"${latency_human}","bytes_in":${bytes_in},` +
|
||||
`"bytes_out":${bytes_out}}` + "\n",
|
||||
CustomTimeFormat:"2006-01-02 15:04:05.00000",
|
||||
Output: os.Stdout,
|
||||
colorer: color.New(),
|
||||
}
|
||||
@ -126,6 +131,8 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
|
||||
return buf.WriteString(time.Now().Format(time.RFC3339))
|
||||
case "time_rfc3339_nano":
|
||||
return buf.WriteString(time.Now().Format(time.RFC3339Nano))
|
||||
case "time_custom":
|
||||
return buf.WriteString(time.Now().Format(config.CustomTimeFormat))
|
||||
case "id":
|
||||
id := req.Header.Get(echo.HeaderXRequestID)
|
||||
if id == "" {
|
||||
|
23
vendor/github.com/labstack/echo/middleware/middleware.go
generated
vendored
23
vendor/github.com/labstack/echo/middleware/middleware.go
generated
vendored
@ -1,6 +1,12 @@
|
||||
package middleware
|
||||
|
||||
import "github.com/labstack/echo"
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
)
|
||||
|
||||
type (
|
||||
// Skipper defines a function to skip middleware. Returning true skips processing
|
||||
@ -8,6 +14,21 @@ type (
|
||||
Skipper func(c echo.Context) bool
|
||||
)
|
||||
|
||||
func captureTokens(pattern *regexp.Regexp, input string) *strings.Replacer {
|
||||
groups := pattern.FindAllStringSubmatch(input, -1)
|
||||
if groups == nil {
|
||||
return nil
|
||||
}
|
||||
values := groups[0][1:]
|
||||
replace := make([]string, 2*len(values))
|
||||
for i, v := range values {
|
||||
j := 2 * i
|
||||
replace[j] = "$" + strconv.Itoa(i+1)
|
||||
replace[j+1] = v
|
||||
}
|
||||
return strings.NewReplacer(replace...)
|
||||
}
|
||||
|
||||
// DefaultSkipper returns false which processes the middleware.
|
||||
func DefaultSkipper(echo.Context) bool {
|
||||
return false
|
||||
|
166
vendor/github.com/labstack/echo/middleware/proxy.go
generated
vendored
166
vendor/github.com/labstack/echo/middleware/proxy.go
generated
vendored
@ -1,7 +1,6 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
@ -9,6 +8,9 @@ import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@ -25,33 +27,56 @@ type (
|
||||
|
||||
// Balancer defines a load balancing technique.
|
||||
// Required.
|
||||
// Possible values:
|
||||
// - RandomBalancer
|
||||
// - RoundRobinBalancer
|
||||
Balancer ProxyBalancer
|
||||
|
||||
// Rewrite defines URL path rewrite rules. The values captured in asterisk can be
|
||||
// retrieved by index e.g. $1, $2 and so on.
|
||||
// Examples:
|
||||
// "/old": "/new",
|
||||
// "/api/*": "/$1",
|
||||
// "/js/*": "/public/javascripts/$1",
|
||||
// "/users/*/orders/*": "/user/$1/order/$2",
|
||||
Rewrite map[string]string
|
||||
|
||||
rewriteRegex map[*regexp.Regexp]string
|
||||
}
|
||||
|
||||
// ProxyTarget defines the upstream target.
|
||||
ProxyTarget struct {
|
||||
Name string
|
||||
URL *url.URL
|
||||
}
|
||||
|
||||
// RandomBalancer implements a random load balancing technique.
|
||||
RandomBalancer struct {
|
||||
Targets []*ProxyTarget
|
||||
random *rand.Rand
|
||||
}
|
||||
|
||||
// RoundRobinBalancer implements a round-robin load balancing technique.
|
||||
RoundRobinBalancer struct {
|
||||
Targets []*ProxyTarget
|
||||
i uint32
|
||||
}
|
||||
|
||||
// ProxyBalancer defines an interface to implement a load balancing technique.
|
||||
ProxyBalancer interface {
|
||||
AddTarget(*ProxyTarget) bool
|
||||
RemoveTarget(string) bool
|
||||
Next() *ProxyTarget
|
||||
}
|
||||
|
||||
commonBalancer struct {
|
||||
targets []*ProxyTarget
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// RandomBalancer implements a random load balancing technique.
|
||||
randomBalancer struct {
|
||||
*commonBalancer
|
||||
random *rand.Rand
|
||||
}
|
||||
|
||||
// RoundRobinBalancer implements a round-robin load balancing technique.
|
||||
roundRobinBalancer struct {
|
||||
*commonBalancer
|
||||
i uint32
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultProxyConfig is the default Proxy middleware config.
|
||||
DefaultProxyConfig = ProxyConfig{
|
||||
Skipper: DefaultSkipper,
|
||||
}
|
||||
)
|
||||
|
||||
func proxyHTTP(t *ProxyTarget) http.Handler {
|
||||
@ -60,29 +85,25 @@ func proxyHTTP(t *ProxyTarget) http.Handler {
|
||||
|
||||
func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
c.Error(errors.New("proxy raw, not a hijacker"))
|
||||
return
|
||||
}
|
||||
in, _, err := h.Hijack()
|
||||
in, _, err := c.Response().Hijack()
|
||||
if err != nil {
|
||||
c.Error(fmt.Errorf("proxy raw, hijack error=%v, url=%s", r.URL, err))
|
||||
c.Error(fmt.Errorf("proxy raw, hijack error=%v, url=%s", t.URL, err))
|
||||
return
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
out, err := net.Dial("tcp", t.URL.Host)
|
||||
if err != nil {
|
||||
he := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, dial error=%v, url=%s", r.URL, err))
|
||||
he := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, dial error=%v, url=%s", t.URL, err))
|
||||
c.Error(he)
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Write header
|
||||
err = r.Write(out)
|
||||
if err != nil {
|
||||
he := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, request copy error=%v, url=%s", r.URL, err))
|
||||
he := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, request header copy error=%v, url=%s", t.URL, err))
|
||||
c.Error(he)
|
||||
return
|
||||
}
|
||||
@ -97,29 +118,81 @@ func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler {
|
||||
go cp(in, out)
|
||||
err = <-errc
|
||||
if err != nil && err != io.EOF {
|
||||
c.Logger().Errorf("proxy raw, error=%v, url=%s", r.URL, err)
|
||||
c.Logger().Errorf("proxy raw, copy body error=%v, url=%s", t.URL, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Next randomly returns an upstream target.
|
||||
func (r *RandomBalancer) Next() *ProxyTarget {
|
||||
if r.random == nil {
|
||||
r.random = rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
|
||||
// NewRandomBalancer returns a random proxy balancer.
|
||||
func NewRandomBalancer(targets []*ProxyTarget) ProxyBalancer {
|
||||
b := &randomBalancer{commonBalancer: new(commonBalancer)}
|
||||
b.targets = targets
|
||||
return b
|
||||
}
|
||||
|
||||
// NewRoundRobinBalancer returns a round-robin proxy balancer.
|
||||
func NewRoundRobinBalancer(targets []*ProxyTarget) ProxyBalancer {
|
||||
b := &roundRobinBalancer{commonBalancer: new(commonBalancer)}
|
||||
b.targets = targets
|
||||
return b
|
||||
}
|
||||
|
||||
// AddTarget adds an upstream target to the list.
|
||||
func (b *commonBalancer) AddTarget(target *ProxyTarget) bool {
|
||||
for _, t := range b.targets {
|
||||
if t.Name == target.Name {
|
||||
return false
|
||||
}
|
||||
return r.Targets[r.random.Intn(len(r.Targets))]
|
||||
}
|
||||
b.mutex.Lock()
|
||||
defer b.mutex.Unlock()
|
||||
b.targets = append(b.targets, target)
|
||||
return true
|
||||
}
|
||||
|
||||
// RemoveTarget removes an upstream target from the list.
|
||||
func (b *commonBalancer) RemoveTarget(name string) bool {
|
||||
b.mutex.Lock()
|
||||
defer b.mutex.Unlock()
|
||||
for i, t := range b.targets {
|
||||
if t.Name == name {
|
||||
b.targets = append(b.targets[:i], b.targets[i+1:]...)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Next randomly returns an upstream target.
|
||||
func (b *randomBalancer) Next() *ProxyTarget {
|
||||
if b.random == nil {
|
||||
b.random = rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
|
||||
}
|
||||
b.mutex.RLock()
|
||||
defer b.mutex.RUnlock()
|
||||
return b.targets[b.random.Intn(len(b.targets))]
|
||||
}
|
||||
|
||||
// Next returns an upstream target using round-robin technique.
|
||||
func (r *RoundRobinBalancer) Next() *ProxyTarget {
|
||||
r.i = r.i % uint32(len(r.Targets))
|
||||
t := r.Targets[r.i]
|
||||
atomic.AddUint32(&r.i, 1)
|
||||
func (b *roundRobinBalancer) Next() *ProxyTarget {
|
||||
b.i = b.i % uint32(len(b.targets))
|
||||
t := b.targets[b.i]
|
||||
atomic.AddUint32(&b.i, 1)
|
||||
return t
|
||||
}
|
||||
|
||||
// Proxy returns an HTTP/WebSocket reverse proxy middleware.
|
||||
func Proxy(config ProxyConfig) echo.MiddlewareFunc {
|
||||
// Proxy returns a Proxy middleware.
|
||||
//
|
||||
// Proxy middleware forwards the request to upstream server using a configured load balancing technique.
|
||||
func Proxy(balancer ProxyBalancer) echo.MiddlewareFunc {
|
||||
c := DefaultProxyConfig
|
||||
c.Balancer = balancer
|
||||
return ProxyWithConfig(c)
|
||||
}
|
||||
|
||||
// ProxyWithConfig returns a Proxy middleware with config.
|
||||
// See: `Proxy()`
|
||||
func ProxyWithConfig(config ProxyConfig) echo.MiddlewareFunc {
|
||||
// Defaults
|
||||
if config.Skipper == nil {
|
||||
config.Skipper = DefaultLoggerConfig.Skipper
|
||||
@ -127,13 +200,32 @@ func Proxy(config ProxyConfig) echo.MiddlewareFunc {
|
||||
if config.Balancer == nil {
|
||||
panic("echo: proxy middleware requires balancer")
|
||||
}
|
||||
config.rewriteRegex = map[*regexp.Regexp]string{}
|
||||
|
||||
// Initialize
|
||||
for k, v := range config.Rewrite {
|
||||
k = strings.Replace(k, "*", "(\\S*)", -1)
|
||||
config.rewriteRegex[regexp.MustCompile(k)] = v
|
||||
}
|
||||
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) (err error) {
|
||||
if config.Skipper(c) {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
req := c.Request()
|
||||
res := c.Response()
|
||||
tgt := config.Balancer.Next()
|
||||
|
||||
// Rewrite
|
||||
for k, v := range config.rewriteRegex {
|
||||
replacer := captureTokens(k, req.URL.Path)
|
||||
if replacer != nil {
|
||||
req.URL.Path = replacer.Replace(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Fix header
|
||||
if req.Header.Get(echo.HeaderXRealIP) == "" {
|
||||
req.Header.Set(echo.HeaderXRealIP, c.RealIP())
|
||||
|
16
vendor/github.com/labstack/echo/middleware/recover.go
generated
vendored
16
vendor/github.com/labstack/echo/middleware/recover.go
generated
vendored
@ -5,7 +5,6 @@ import (
|
||||
"runtime"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/labstack/gommon/color"
|
||||
)
|
||||
|
||||
type (
|
||||
@ -16,16 +15,16 @@ type (
|
||||
|
||||
// Size of the stack to be printed.
|
||||
// Optional. Default value 4KB.
|
||||
StackSize int `json:"stack_size"`
|
||||
StackSize int `yaml:"stack_size"`
|
||||
|
||||
// DisableStackAll disables formatting stack traces of all other goroutines
|
||||
// into buffer after the trace for the current goroutine.
|
||||
// Optional. Default value false.
|
||||
DisableStackAll bool `json:"disable_stack_all"`
|
||||
DisableStackAll bool `yaml:"disable_stack_all"`
|
||||
|
||||
// DisablePrintStack disables printing stack trace.
|
||||
// Optional. Default value as false.
|
||||
DisablePrintStack bool `json:"disable_print_stack"`
|
||||
DisablePrintStack bool `yaml:"disable_print_stack"`
|
||||
}
|
||||
)
|
||||
|
||||
@ -64,17 +63,14 @@ func RecoverWithConfig(config RecoverConfig) echo.MiddlewareFunc {
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
var err error
|
||||
switch r := r.(type) {
|
||||
case error:
|
||||
err = r
|
||||
default:
|
||||
err, ok := r.(error)
|
||||
if !ok {
|
||||
err = fmt.Errorf("%v", r)
|
||||
}
|
||||
stack := make([]byte, config.StackSize)
|
||||
length := runtime.Stack(stack, !config.DisableStackAll)
|
||||
if !config.DisablePrintStack {
|
||||
c.Logger().Printf("[%s] %s %s\n", color.Red("PANIC RECOVER"), err, stack[:length])
|
||||
c.Logger().Printf("[PANIC RECOVER] %v %s\n", err, stack[:length])
|
||||
}
|
||||
c.Error(err)
|
||||
}
|
||||
|
158
vendor/github.com/labstack/echo/middleware/redirect.go
generated
vendored
158
vendor/github.com/labstack/echo/middleware/redirect.go
generated
vendored
@ -6,29 +6,28 @@ import (
|
||||
"github.com/labstack/echo"
|
||||
)
|
||||
|
||||
type (
|
||||
// RedirectConfig defines the config for Redirect middleware.
|
||||
RedirectConfig struct {
|
||||
// RedirectConfig defines the config for Redirect middleware.
|
||||
type RedirectConfig struct {
|
||||
// Skipper defines a function to skip middleware.
|
||||
Skipper Skipper
|
||||
Skipper
|
||||
|
||||
// Status code to be used when redirecting the request.
|
||||
// Optional. Default value http.StatusMovedPermanently.
|
||||
Code int `json:"code"`
|
||||
}
|
||||
)
|
||||
Code int `yaml:"code"`
|
||||
}
|
||||
|
||||
const (
|
||||
www = "www"
|
||||
)
|
||||
// redirectLogic represents a function that given a scheme, host and uri
|
||||
// can both: 1) determine if redirect is needed (will set ok accordingly) and
|
||||
// 2) return the appropriate redirect url.
|
||||
type redirectLogic func(scheme, host, uri string) (ok bool, url string)
|
||||
|
||||
var (
|
||||
// DefaultRedirectConfig is the default Redirect middleware config.
|
||||
DefaultRedirectConfig = RedirectConfig{
|
||||
const www = "www"
|
||||
|
||||
// DefaultRedirectConfig is the default Redirect middleware config.
|
||||
var DefaultRedirectConfig = RedirectConfig{
|
||||
Skipper: DefaultSkipper,
|
||||
Code: http.StatusMovedPermanently,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// HTTPSRedirect redirects http requests to https.
|
||||
// For example, http://labstack.com will be redirect to https://labstack.com.
|
||||
@ -41,29 +40,12 @@ func HTTPSRedirect() echo.MiddlewareFunc {
|
||||
// HTTPSRedirectWithConfig returns an HTTPSRedirect middleware with config.
|
||||
// See `HTTPSRedirect()`.
|
||||
func HTTPSRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
|
||||
// Defaults
|
||||
if config.Skipper == nil {
|
||||
config.Skipper = DefaultTrailingSlashConfig.Skipper
|
||||
}
|
||||
if config.Code == 0 {
|
||||
config.Code = DefaultRedirectConfig.Code
|
||||
}
|
||||
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
if config.Skipper(c) {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
req := c.Request()
|
||||
host := req.Host
|
||||
uri := req.RequestURI
|
||||
if !c.IsTLS() {
|
||||
return c.Redirect(config.Code, "https://"+host+uri)
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
|
||||
if ok = scheme != "https"; ok {
|
||||
url = "https://" + host + uri
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// HTTPSWWWRedirect redirects http requests to https www.
|
||||
@ -77,29 +59,12 @@ func HTTPSWWWRedirect() echo.MiddlewareFunc {
|
||||
// HTTPSWWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
|
||||
// See `HTTPSWWWRedirect()`.
|
||||
func HTTPSWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
|
||||
// Defaults
|
||||
if config.Skipper == nil {
|
||||
config.Skipper = DefaultTrailingSlashConfig.Skipper
|
||||
}
|
||||
if config.Code == 0 {
|
||||
config.Code = DefaultRedirectConfig.Code
|
||||
}
|
||||
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
if config.Skipper(c) {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
req := c.Request()
|
||||
host := req.Host
|
||||
uri := req.RequestURI
|
||||
if !c.IsTLS() && host[:3] != www {
|
||||
return c.Redirect(config.Code, "https://www."+host+uri)
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
|
||||
if ok = scheme != "https" && host[:3] != www; ok {
|
||||
url = "https://www." + host + uri
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// HTTPSNonWWWRedirect redirects http requests to https non www.
|
||||
@ -113,32 +78,15 @@ func HTTPSNonWWWRedirect() echo.MiddlewareFunc {
|
||||
// HTTPSNonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
|
||||
// See `HTTPSNonWWWRedirect()`.
|
||||
func HTTPSNonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
|
||||
// Defaults
|
||||
if config.Skipper == nil {
|
||||
config.Skipper = DefaultTrailingSlashConfig.Skipper
|
||||
}
|
||||
if config.Code == 0 {
|
||||
config.Code = DefaultRedirectConfig.Code
|
||||
}
|
||||
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
if config.Skipper(c) {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
req := c.Request()
|
||||
host := req.Host
|
||||
uri := req.RequestURI
|
||||
if !c.IsTLS() {
|
||||
return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
|
||||
if ok = scheme != "https"; ok {
|
||||
if host[:3] == www {
|
||||
return c.Redirect(config.Code, "https://"+host[4:]+uri)
|
||||
}
|
||||
return c.Redirect(config.Code, "https://"+host+uri)
|
||||
}
|
||||
return next(c)
|
||||
host = host[4:]
|
||||
}
|
||||
url = "https://" + host + uri
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// WWWRedirect redirects non www requests to www.
|
||||
@ -152,30 +100,12 @@ func WWWRedirect() echo.MiddlewareFunc {
|
||||
// WWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
|
||||
// See `WWWRedirect()`.
|
||||
func WWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
|
||||
// Defaults
|
||||
if config.Skipper == nil {
|
||||
config.Skipper = DefaultTrailingSlashConfig.Skipper
|
||||
}
|
||||
if config.Code == 0 {
|
||||
config.Code = DefaultRedirectConfig.Code
|
||||
}
|
||||
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
if config.Skipper(c) {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
req := c.Request()
|
||||
scheme := c.Scheme()
|
||||
host := req.Host
|
||||
if host[:3] != www {
|
||||
uri := req.RequestURI
|
||||
return c.Redirect(config.Code, scheme+"://www."+host+uri)
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
|
||||
if ok = host[:3] != www; ok {
|
||||
url = scheme + "://www." + host + uri
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// NonWWWRedirect redirects www requests to non www.
|
||||
@ -189,6 +119,15 @@ func NonWWWRedirect() echo.MiddlewareFunc {
|
||||
// NonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
|
||||
// See `NonWWWRedirect()`.
|
||||
func NonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
|
||||
return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
|
||||
if ok = host[:3] == www; ok {
|
||||
url = scheme + "://" + host[4:] + uri
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func redirect(config RedirectConfig, cb redirectLogic) echo.MiddlewareFunc {
|
||||
if config.Skipper == nil {
|
||||
config.Skipper = DefaultTrailingSlashConfig.Skipper
|
||||
}
|
||||
@ -202,13 +141,12 @@ func NonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
req := c.Request()
|
||||
scheme := c.Scheme()
|
||||
req, scheme := c.Request(), c.Scheme()
|
||||
host := req.Host
|
||||
if host[:3] == www {
|
||||
uri := req.RequestURI
|
||||
return c.Redirect(config.Code, scheme+"://"+host[4:]+uri)
|
||||
if ok, url := cb(scheme, host, req.RequestURI); ok {
|
||||
return c.Redirect(config.Code, url)
|
||||
}
|
||||
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
83
vendor/github.com/labstack/echo/middleware/rewrite.go
generated
vendored
Normal file
83
vendor/github.com/labstack/echo/middleware/rewrite.go
generated
vendored
Normal file
@ -0,0 +1,83 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
)
|
||||
|
||||
type (
|
||||
// RewriteConfig defines the config for Rewrite middleware.
|
||||
RewriteConfig struct {
|
||||
// Skipper defines a function to skip middleware.
|
||||
Skipper Skipper
|
||||
|
||||
// Rules defines the URL path rewrite rules. The values captured in asterisk can be
|
||||
// retrieved by index e.g. $1, $2 and so on.
|
||||
// Example:
|
||||
// "/old": "/new",
|
||||
// "/api/*": "/$1",
|
||||
// "/js/*": "/public/javascripts/$1",
|
||||
// "/users/*/orders/*": "/user/$1/order/$2",
|
||||
// Required.
|
||||
Rules map[string]string `yaml:"rules"`
|
||||
|
||||
rulesRegex map[*regexp.Regexp]string
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultRewriteConfig is the default Rewrite middleware config.
|
||||
DefaultRewriteConfig = RewriteConfig{
|
||||
Skipper: DefaultSkipper,
|
||||
}
|
||||
)
|
||||
|
||||
// Rewrite returns a Rewrite middleware.
|
||||
//
|
||||
// Rewrite middleware rewrites the URL path based on the provided rules.
|
||||
func Rewrite(rules map[string]string) echo.MiddlewareFunc {
|
||||
c := DefaultRewriteConfig
|
||||
c.Rules = rules
|
||||
return RewriteWithConfig(c)
|
||||
}
|
||||
|
||||
// RewriteWithConfig returns a Rewrite middleware with config.
|
||||
// See: `Rewrite()`.
|
||||
func RewriteWithConfig(config RewriteConfig) echo.MiddlewareFunc {
|
||||
// Defaults
|
||||
if config.Rules == nil {
|
||||
panic("echo: rewrite middleware requires url path rewrite rules")
|
||||
}
|
||||
if config.Skipper == nil {
|
||||
config.Skipper = DefaultBodyDumpConfig.Skipper
|
||||
}
|
||||
config.rulesRegex = map[*regexp.Regexp]string{}
|
||||
|
||||
// Initialize
|
||||
for k, v := range config.Rules {
|
||||
k = strings.Replace(k, "*", "(\\S*)", -1)
|
||||
config.rulesRegex[regexp.MustCompile(k)] = v
|
||||
}
|
||||
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) (err error) {
|
||||
if config.Skipper(c) {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
req := c.Request()
|
||||
|
||||
// Rewrite
|
||||
for k, v := range config.rulesRegex {
|
||||
replacer := captureTokens(k, req.URL.Path)
|
||||
if replacer != nil {
|
||||
req.URL.Path = replacer.Replace(v)
|
||||
}
|
||||
}
|
||||
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
12
vendor/github.com/labstack/echo/middleware/secure.go
generated
vendored
12
vendor/github.com/labstack/echo/middleware/secure.go
generated
vendored
@ -15,12 +15,12 @@ type (
|
||||
// XSSProtection provides protection against cross-site scripting attack (XSS)
|
||||
// by setting the `X-XSS-Protection` header.
|
||||
// Optional. Default value "1; mode=block".
|
||||
XSSProtection string `json:"xss_protection"`
|
||||
XSSProtection string `yaml:"xss_protection"`
|
||||
|
||||
// ContentTypeNosniff provides protection against overriding Content-Type
|
||||
// header by setting the `X-Content-Type-Options` header.
|
||||
// Optional. Default value "nosniff".
|
||||
ContentTypeNosniff string `json:"content_type_nosniff"`
|
||||
ContentTypeNosniff string `yaml:"content_type_nosniff"`
|
||||
|
||||
// XFrameOptions can be used to indicate whether or not a browser should
|
||||
// be allowed to render a page in a <frame>, <iframe> or <object> .
|
||||
@ -32,27 +32,27 @@ type (
|
||||
// - "SAMEORIGIN" - The page can only be displayed in a frame on the same origin as the page itself.
|
||||
// - "DENY" - The page cannot be displayed in a frame, regardless of the site attempting to do so.
|
||||
// - "ALLOW-FROM uri" - The page can only be displayed in a frame on the specified origin.
|
||||
XFrameOptions string `json:"x_frame_options"`
|
||||
XFrameOptions string `yaml:"x_frame_options"`
|
||||
|
||||
// HSTSMaxAge sets the `Strict-Transport-Security` header to indicate how
|
||||
// long (in seconds) browsers should remember that this site is only to
|
||||
// be accessed using HTTPS. This reduces your exposure to some SSL-stripping
|
||||
// man-in-the-middle (MITM) attacks.
|
||||
// Optional. Default value 0.
|
||||
HSTSMaxAge int `json:"hsts_max_age"`
|
||||
HSTSMaxAge int `yaml:"hsts_max_age"`
|
||||
|
||||
// HSTSExcludeSubdomains won't include subdomains tag in the `Strict Transport Security`
|
||||
// header, excluding all subdomains from security policy. It has no effect
|
||||
// unless HSTSMaxAge is set to a non-zero value.
|
||||
// Optional. Default value false.
|
||||
HSTSExcludeSubdomains bool `json:"hsts_exclude_subdomains"`
|
||||
HSTSExcludeSubdomains bool `yaml:"hsts_exclude_subdomains"`
|
||||
|
||||
// ContentSecurityPolicy sets the `Content-Security-Policy` header providing
|
||||
// security against cross-site scripting (XSS), clickjacking and other code
|
||||
// injection attacks resulting from execution of malicious content in the
|
||||
// trusted web page context.
|
||||
// Optional. Default value "".
|
||||
ContentSecurityPolicy string `json:"content_security_policy"`
|
||||
ContentSecurityPolicy string `yaml:"content_security_policy"`
|
||||
}
|
||||
)
|
||||
|
||||
|
2
vendor/github.com/labstack/echo/middleware/slash.go
generated
vendored
2
vendor/github.com/labstack/echo/middleware/slash.go
generated
vendored
@ -12,7 +12,7 @@ type (
|
||||
|
||||
// Status code to be used when redirecting the request.
|
||||
// Optional, but when provided the request is redirected using this code.
|
||||
RedirectCode int `json:"redirect_code"`
|
||||
RedirectCode int `yaml:"redirect_code"`
|
||||
}
|
||||
)
|
||||
|
||||
|
39
vendor/github.com/labstack/echo/middleware/static.go
generated
vendored
39
vendor/github.com/labstack/echo/middleware/static.go
generated
vendored
@ -2,6 +2,7 @@ package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@ -18,20 +19,20 @@ type (
|
||||
|
||||
// Root directory from where the static content is served.
|
||||
// Required.
|
||||
Root string `json:"root"`
|
||||
Root string `yaml:"root"`
|
||||
|
||||
// Index file for serving a directory.
|
||||
// Optional. Default value "index.html".
|
||||
Index string `json:"index"`
|
||||
Index string `yaml:"index"`
|
||||
|
||||
// Enable HTML5 mode by forwarding all not-found requests to root so that
|
||||
// SPA (single-page application) can handle the routing.
|
||||
// Optional. Default value false.
|
||||
HTML5 bool `json:"html5"`
|
||||
HTML5 bool `yaml:"html5"`
|
||||
|
||||
// Enable directory browsing.
|
||||
// Optional. Default value false.
|
||||
Browse bool `json:"browse"`
|
||||
Browse bool `yaml:"browse"`
|
||||
}
|
||||
)
|
||||
|
||||
@ -66,7 +67,7 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
|
||||
}
|
||||
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return func(c echo.Context) (err error) {
|
||||
if config.Skipper(c) {
|
||||
return next(c)
|
||||
}
|
||||
@ -75,17 +76,25 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
|
||||
if strings.HasSuffix(c.Path(), "*") { // When serving from a group, e.g. `/static*`.
|
||||
p = c.Param("*")
|
||||
}
|
||||
p, err = echo.PathUnescape(p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
name := filepath.Join(config.Root, path.Clean("/"+p)) // "/"+ for security
|
||||
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if config.HTML5 && path.Ext(p) == "" {
|
||||
if err = next(c); err != nil {
|
||||
if he, ok := err.(*echo.HTTPError); ok {
|
||||
if config.HTML5 && he.Code == http.StatusNotFound {
|
||||
return c.File(filepath.Join(config.Root, config.Index))
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
return err
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
@ -99,7 +108,7 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
|
||||
if os.IsNotExist(err) {
|
||||
return next(c)
|
||||
}
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
return c.File(index)
|
||||
@ -110,20 +119,20 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func listDir(name string, res *echo.Response) error {
|
||||
func listDir(name string, res *echo.Response) (err error) {
|
||||
dir, err := os.Open(name)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
dirs, err := dir.Readdir(-1)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
// Create a directory index
|
||||
res.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8)
|
||||
if _, err = fmt.Fprintf(res, "<pre>\n"); err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
for _, d := range dirs {
|
||||
name := d.Name()
|
||||
@ -133,9 +142,9 @@ func listDir(name string, res *echo.Response) error {
|
||||
name += "/"
|
||||
}
|
||||
if _, err = fmt.Fprintf(res, "<a href=\"%s\" style=\"color: %s;\">%s</a>\n", name, color, name); err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
}
|
||||
_, err = fmt.Fprintf(res, "</pre>\n")
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
29
vendor/github.com/labstack/echo/response.go
generated
vendored
29
vendor/github.com/labstack/echo/response.go
generated
vendored
@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type (
|
||||
@ -11,11 +12,14 @@ type (
|
||||
// by an HTTP handler to construct an HTTP response.
|
||||
// See: https://golang.org/pkg/net/http/#ResponseWriter
|
||||
Response struct {
|
||||
echo *Echo
|
||||
contentLength int64
|
||||
beforeFuncs []func()
|
||||
afterFuncs []func()
|
||||
Writer http.ResponseWriter
|
||||
Status int
|
||||
Size int64
|
||||
Committed bool
|
||||
echo *Echo
|
||||
}
|
||||
)
|
||||
|
||||
@ -34,6 +38,17 @@ func (r *Response) Header() http.Header {
|
||||
return r.Writer.Header()
|
||||
}
|
||||
|
||||
// Before registers a function which is called just before the response is written.
|
||||
func (r *Response) Before(fn func()) {
|
||||
r.beforeFuncs = append(r.beforeFuncs, fn)
|
||||
}
|
||||
|
||||
// After registers a function which is called just after the response is written.
|
||||
// If the `Content-Length` is unknown, none of the after function is executed.
|
||||
func (r *Response) After(fn func()) {
|
||||
r.afterFuncs = append(r.afterFuncs, fn)
|
||||
}
|
||||
|
||||
// WriteHeader sends an HTTP response header with status code. If WriteHeader is
|
||||
// not called explicitly, the first call to Write will trigger an implicit
|
||||
// WriteHeader(http.StatusOK). Thus explicit calls to WriteHeader are mainly
|
||||
@ -43,9 +58,13 @@ func (r *Response) WriteHeader(code int) {
|
||||
r.echo.Logger.Warn("response already committed")
|
||||
return
|
||||
}
|
||||
for _, fn := range r.beforeFuncs {
|
||||
fn()
|
||||
}
|
||||
r.Status = code
|
||||
r.Writer.WriteHeader(code)
|
||||
r.Committed = true
|
||||
r.contentLength, _ = strconv.ParseInt(r.Header().Get(HeaderContentLength), 10, 0)
|
||||
}
|
||||
|
||||
// Write writes the data to the connection as part of an HTTP reply.
|
||||
@ -55,6 +74,11 @@ func (r *Response) Write(b []byte) (n int, err error) {
|
||||
}
|
||||
n, err = r.Writer.Write(b)
|
||||
r.Size += int64(n)
|
||||
if r.Size == r.contentLength {
|
||||
for _, fn := range r.afterFuncs {
|
||||
fn()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -82,6 +106,9 @@ func (r *Response) CloseNotify() <-chan bool {
|
||||
}
|
||||
|
||||
func (r *Response) reset(w http.ResponseWriter) {
|
||||
r.contentLength = 0
|
||||
r.beforeFuncs = nil
|
||||
r.afterFuncs = nil
|
||||
r.Writer = w
|
||||
r.Size = 0
|
||||
r.Status = http.StatusOK
|
||||
|
10
vendor/github.com/labstack/echo/router.go
generated
vendored
10
vendor/github.com/labstack/echo/router.go
generated
vendored
@ -1,7 +1,5 @@
|
||||
package echo
|
||||
|
||||
import "strings"
|
||||
|
||||
type (
|
||||
// Router is the registry of all registered routes for an `Echo` instance for
|
||||
// request matching and URL path parameter parsing.
|
||||
@ -175,12 +173,6 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
|
||||
if len(cn.pnames) == 0 { // Issue #729
|
||||
cn.pnames = pnames
|
||||
}
|
||||
for i, n := range pnames {
|
||||
// Param name aliases
|
||||
if i < len(cn.pnames) && !strings.Contains(cn.pnames[i], n) {
|
||||
cn.pnames[i] += "," + n
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
@ -394,7 +386,7 @@ func (r *Router) Find(method, path string, c Context) {
|
||||
if cn = cn.findChildByKind(akind); cn == nil {
|
||||
if nn != nil {
|
||||
cn = nn
|
||||
nn = nil // Next
|
||||
nn = cn.parent // Next (Issue #954)
|
||||
search = ns
|
||||
if nk == pkind {
|
||||
goto Param
|
||||
|
12
vendor/github.com/labstack/echo/util_go17.go
generated
vendored
Normal file
12
vendor/github.com/labstack/echo/util_go17.go
generated
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
// +build go1.7, !go1.8
|
||||
|
||||
package echo
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// PathUnescape is wraps `url.QueryUnescape`
|
||||
func PathUnescape(s string) (string, error) {
|
||||
return url.QueryUnescape(s)
|
||||
}
|
10
vendor/github.com/labstack/echo/util_go18.go
generated
vendored
Normal file
10
vendor/github.com/labstack/echo/util_go18.go
generated
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
// +build go1.8
|
||||
|
||||
package echo
|
||||
|
||||
import "net/url"
|
||||
|
||||
// PathUnescape is wraps `url.PathUnescape`
|
||||
func PathUnescape(s string) (string, error) {
|
||||
return url.PathUnescape(s)
|
||||
}
|
21
vendor/github.com/lrstanley/girc/LICENSE
generated
vendored
Normal file
21
vendor/github.com/lrstanley/girc/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 Liam Stanley <me@liamstanley.io>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
516
vendor/github.com/lrstanley/girc/builtin.go
generated
vendored
Normal file
516
vendor/github.com/lrstanley/girc/builtin.go
generated
vendored
Normal file
@ -0,0 +1,516 @@
|
||||
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||
// of this source code is governed by the MIT license that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
package girc
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// registerBuiltin sets up built-in handlers, based on client
|
||||
// configuration.
|
||||
func (c *Client) registerBuiltins() {
|
||||
c.debug.Print("registering built-in handlers")
|
||||
c.Handlers.mu.Lock()
|
||||
|
||||
// Built-in things that should always be supported.
|
||||
c.Handlers.register(true, true, RPL_WELCOME, HandlerFunc(handleConnect))
|
||||
c.Handlers.register(true, false, PING, HandlerFunc(handlePING))
|
||||
c.Handlers.register(true, false, PONG, HandlerFunc(handlePONG))
|
||||
|
||||
if !c.Config.disableTracking {
|
||||
// Joins/parts/anything that may add/remove/rename users.
|
||||
c.Handlers.register(true, false, JOIN, HandlerFunc(handleJOIN))
|
||||
c.Handlers.register(true, false, PART, HandlerFunc(handlePART))
|
||||
c.Handlers.register(true, false, KICK, HandlerFunc(handleKICK))
|
||||
c.Handlers.register(true, false, QUIT, HandlerFunc(handleQUIT))
|
||||
c.Handlers.register(true, false, NICK, HandlerFunc(handleNICK))
|
||||
c.Handlers.register(true, false, RPL_NAMREPLY, HandlerFunc(handleNAMES))
|
||||
|
||||
// Modes.
|
||||
c.Handlers.register(true, false, MODE, HandlerFunc(handleMODE))
|
||||
c.Handlers.register(true, false, RPL_CHANNELMODEIS, HandlerFunc(handleMODE))
|
||||
|
||||
// WHO/WHOX responses.
|
||||
c.Handlers.register(true, false, RPL_WHOREPLY, HandlerFunc(handleWHO))
|
||||
c.Handlers.register(true, false, RPL_WHOSPCRPL, HandlerFunc(handleWHO))
|
||||
|
||||
// Other misc. useful stuff.
|
||||
c.Handlers.register(true, false, TOPIC, HandlerFunc(handleTOPIC))
|
||||
c.Handlers.register(true, false, RPL_TOPIC, HandlerFunc(handleTOPIC))
|
||||
c.Handlers.register(true, false, RPL_MYINFO, HandlerFunc(handleMYINFO))
|
||||
c.Handlers.register(true, false, RPL_ISUPPORT, HandlerFunc(handleISUPPORT))
|
||||
c.Handlers.register(true, false, RPL_MOTDSTART, HandlerFunc(handleMOTD))
|
||||
c.Handlers.register(true, false, RPL_MOTD, HandlerFunc(handleMOTD))
|
||||
|
||||
// Keep users lastactive times up to date.
|
||||
c.Handlers.register(true, false, PRIVMSG, HandlerFunc(updateLastActive))
|
||||
c.Handlers.register(true, false, NOTICE, HandlerFunc(updateLastActive))
|
||||
c.Handlers.register(true, false, TOPIC, HandlerFunc(updateLastActive))
|
||||
c.Handlers.register(true, false, KICK, HandlerFunc(updateLastActive))
|
||||
|
||||
// CAP IRCv3-specific tracking and functionality.
|
||||
c.Handlers.register(true, false, CAP, HandlerFunc(handleCAP))
|
||||
c.Handlers.register(true, false, CAP_CHGHOST, HandlerFunc(handleCHGHOST))
|
||||
c.Handlers.register(true, false, CAP_AWAY, HandlerFunc(handleAWAY))
|
||||
c.Handlers.register(true, false, CAP_ACCOUNT, HandlerFunc(handleACCOUNT))
|
||||
c.Handlers.register(true, false, ALL_EVENTS, HandlerFunc(handleTags))
|
||||
|
||||
// SASL IRCv3 support.
|
||||
c.Handlers.register(true, false, AUTHENTICATE, HandlerFunc(handleSASL))
|
||||
c.Handlers.register(true, false, RPL_SASLSUCCESS, HandlerFunc(handleSASL))
|
||||
c.Handlers.register(true, false, RPL_NICKLOCKED, HandlerFunc(handleSASLError))
|
||||
c.Handlers.register(true, false, ERR_SASLFAIL, HandlerFunc(handleSASLError))
|
||||
c.Handlers.register(true, false, ERR_SASLTOOLONG, HandlerFunc(handleSASLError))
|
||||
c.Handlers.register(true, false, ERR_SASLABORTED, HandlerFunc(handleSASLError))
|
||||
c.Handlers.register(true, false, RPL_SASLMECHS, HandlerFunc(handleSASLError))
|
||||
}
|
||||
|
||||
// Nickname collisions.
|
||||
c.Handlers.register(true, false, ERR_NICKNAMEINUSE, HandlerFunc(nickCollisionHandler))
|
||||
c.Handlers.register(true, false, ERR_NICKCOLLISION, HandlerFunc(nickCollisionHandler))
|
||||
c.Handlers.register(true, false, ERR_UNAVAILRESOURCE, HandlerFunc(nickCollisionHandler))
|
||||
|
||||
c.Handlers.mu.Unlock()
|
||||
}
|
||||
|
||||
// handleConnect is a helper function which lets the client know that enough
|
||||
// time has passed and now they can send commands.
|
||||
//
|
||||
// Should always run in separate thread due to blocking delay.
|
||||
func handleConnect(c *Client, e Event) {
|
||||
// This should be the nick that the server gives us. 99% of the time, it's
|
||||
// the one we supplied during connection, but some networks will rename
|
||||
// users on connect.
|
||||
if len(e.Params) > 0 {
|
||||
c.state.Lock()
|
||||
c.state.nick = e.Params[0]
|
||||
c.state.Unlock()
|
||||
|
||||
c.state.notify(c, UPDATE_GENERAL)
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
c.RunHandlers(&Event{Command: CONNECTED, Trailing: c.Server()})
|
||||
}
|
||||
|
||||
// nickCollisionHandler helps prevent the client from having conflicting
|
||||
// nicknames with another bot, user, etc.
|
||||
func nickCollisionHandler(c *Client, e Event) {
|
||||
if c.Config.HandleNickCollide == nil {
|
||||
c.Cmd.Nick(c.GetNick() + "_")
|
||||
return
|
||||
}
|
||||
|
||||
c.Cmd.Nick(c.Config.HandleNickCollide(c.GetNick()))
|
||||
}
|
||||
|
||||
// handlePING helps respond to ping requests from the server.
|
||||
func handlePING(c *Client, e Event) {
|
||||
c.Cmd.Pong(e.Trailing)
|
||||
}
|
||||
|
||||
func handlePONG(c *Client, e Event) {
|
||||
c.conn.lastPong = time.Now()
|
||||
}
|
||||
|
||||
// handleJOIN ensures that the state has updated users and channels.
|
||||
func handleJOIN(c *Client, e Event) {
|
||||
if e.Source == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var channelName string
|
||||
if len(e.Params) > 0 {
|
||||
channelName = e.Params[0]
|
||||
} else {
|
||||
channelName = e.Trailing
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
|
||||
channel := c.state.lookupChannel(channelName)
|
||||
if channel == nil {
|
||||
if ok := c.state.createChannel(channelName); !ok {
|
||||
c.state.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
channel = c.state.lookupChannel(channelName)
|
||||
}
|
||||
|
||||
user := c.state.lookupUser(e.Source.Name)
|
||||
if user == nil {
|
||||
if ok := c.state.createUser(e.Source.Name); !ok {
|
||||
c.state.Unlock()
|
||||
return
|
||||
}
|
||||
user = c.state.lookupUser(e.Source.Name)
|
||||
}
|
||||
|
||||
defer c.state.notify(c, UPDATE_STATE)
|
||||
|
||||
channel.addUser(user.Nick)
|
||||
user.addChannel(channel.Name)
|
||||
|
||||
// Assume extended-join (ircv3).
|
||||
if len(e.Params) == 2 {
|
||||
if e.Params[1] != "*" {
|
||||
user.Extras.Account = e.Params[1]
|
||||
}
|
||||
|
||||
if len(e.Trailing) > 0 {
|
||||
user.Extras.Name = e.Trailing
|
||||
}
|
||||
}
|
||||
c.state.Unlock()
|
||||
|
||||
if e.Source.Name == c.GetNick() {
|
||||
// If it's us, don't just add our user to the list. Run a WHO which
|
||||
// will tell us who exactly is in the entire channel.
|
||||
c.Send(&Event{Command: WHO, Params: []string{channelName, "%tacuhnr,1"}})
|
||||
|
||||
// Also send a MODE to obtain the list of channel modes.
|
||||
c.Send(&Event{Command: MODE, Params: []string{channelName}})
|
||||
|
||||
// Update our ident and host too, in state -- since there is no
|
||||
// cleaner method to do this.
|
||||
c.state.Lock()
|
||||
c.state.ident = e.Source.Ident
|
||||
c.state.host = e.Source.Host
|
||||
c.state.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Only WHO the user, which is more efficient.
|
||||
c.Send(&Event{Command: WHO, Params: []string{e.Source.Name, "%tacuhnr,1"}})
|
||||
}
|
||||
|
||||
// handlePART ensures that the state is clean of old user and channel entries.
|
||||
func handlePART(c *Client, e Event) {
|
||||
if e.Source == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var channel string
|
||||
if len(e.Params) > 0 {
|
||||
channel = e.Params[0]
|
||||
} else {
|
||||
channel = e.Trailing
|
||||
}
|
||||
|
||||
if channel == "" {
|
||||
return
|
||||
}
|
||||
|
||||
defer c.state.notify(c, UPDATE_STATE)
|
||||
|
||||
if e.Source.Name == c.GetNick() {
|
||||
c.state.Lock()
|
||||
c.state.deleteChannel(channel)
|
||||
c.state.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
c.state.deleteUser(channel, e.Source.Name)
|
||||
c.state.Unlock()
|
||||
}
|
||||
|
||||
// handleTOPIC handles incoming TOPIC events and keeps channel tracking info
|
||||
// updated with the latest channel topic.
|
||||
func handleTOPIC(c *Client, e Event) {
|
||||
var name string
|
||||
switch len(e.Params) {
|
||||
case 0:
|
||||
return
|
||||
case 1:
|
||||
name = e.Params[0]
|
||||
default:
|
||||
name = e.Params[len(e.Params)-1]
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
channel := c.state.lookupChannel(name)
|
||||
if channel == nil {
|
||||
c.state.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
channel.Topic = e.Trailing
|
||||
c.state.Unlock()
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
}
|
||||
|
||||
// handlWHO updates our internal tracking of users/channels with WHO/WHOX
|
||||
// information.
|
||||
func handleWHO(c *Client, e Event) {
|
||||
var ident, host, nick, account, realname string
|
||||
|
||||
// Assume WHOX related.
|
||||
if e.Command == RPL_WHOSPCRPL {
|
||||
if len(e.Params) != 7 {
|
||||
// Assume there was some form of error or invalid WHOX response.
|
||||
return
|
||||
}
|
||||
|
||||
if e.Params[1] != "1" {
|
||||
// We should always be sending 1, and we should receive 1. If this
|
||||
// is anything but, then we didn't send the request and we can
|
||||
// ignore it.
|
||||
return
|
||||
}
|
||||
|
||||
ident, host, nick, account = e.Params[3], e.Params[4], e.Params[5], e.Params[6]
|
||||
realname = e.Trailing
|
||||
} else {
|
||||
// Assume RPL_WHOREPLY.
|
||||
ident, host, nick = e.Params[2], e.Params[3], e.Params[5]
|
||||
if len(e.Trailing) > 2 {
|
||||
realname = e.Trailing[2:]
|
||||
}
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
user := c.state.lookupUser(nick)
|
||||
if user == nil {
|
||||
c.state.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
user.Host = host
|
||||
user.Ident = ident
|
||||
user.Extras.Name = realname
|
||||
|
||||
if account != "0" {
|
||||
user.Extras.Account = account
|
||||
}
|
||||
|
||||
c.state.Unlock()
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
}
|
||||
|
||||
// handleKICK ensures that users are cleaned up after being kicked from the
|
||||
// channel
|
||||
func handleKICK(c *Client, e Event) {
|
||||
if len(e.Params) < 2 {
|
||||
// Needs at least channel and user.
|
||||
return
|
||||
}
|
||||
|
||||
defer c.state.notify(c, UPDATE_STATE)
|
||||
|
||||
if e.Params[1] == c.GetNick() {
|
||||
c.state.Lock()
|
||||
c.state.deleteChannel(e.Params[0])
|
||||
c.state.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Assume it's just another user.
|
||||
c.state.Lock()
|
||||
c.state.deleteUser(e.Params[0], e.Params[1])
|
||||
c.state.Unlock()
|
||||
}
|
||||
|
||||
// handleNICK ensures that users are renamed in state, or the client name is
|
||||
// up to date.
|
||||
func handleNICK(c *Client, e Event) {
|
||||
if e.Source == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
// renameUser updates the LastActive time automatically.
|
||||
if len(e.Params) == 1 {
|
||||
c.state.renameUser(e.Source.Name, e.Params[0])
|
||||
} else if len(e.Trailing) > 0 {
|
||||
c.state.renameUser(e.Source.Name, e.Trailing)
|
||||
}
|
||||
c.state.Unlock()
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
}
|
||||
|
||||
// handleQUIT handles users that are quitting from the network.
|
||||
func handleQUIT(c *Client, e Event) {
|
||||
if e.Source == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if e.Source.Name == c.GetNick() {
|
||||
return
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
c.state.deleteUser("", e.Source.Name)
|
||||
c.state.Unlock()
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
}
|
||||
|
||||
// handleMYINFO handles incoming MYINFO events -- these are commonly used
|
||||
// to tell us what the server name is, what version of software is being used
|
||||
// as well as what channel and user modes are being used on the server.
|
||||
func handleMYINFO(c *Client, e Event) {
|
||||
// Malformed or odd output. As this can differ strongly between networks,
|
||||
// just skip it.
|
||||
if len(e.Params) < 3 {
|
||||
return
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
c.state.serverOptions["SERVER"] = e.Params[1]
|
||||
c.state.serverOptions["VERSION"] = e.Params[2]
|
||||
c.state.Unlock()
|
||||
c.state.notify(c, UPDATE_GENERAL)
|
||||
}
|
||||
|
||||
// handleISUPPORT handles incoming RPL_ISUPPORT (also known as RPL_PROTOCTL)
|
||||
// events. These commonly contain the server capabilities and limitations.
|
||||
// For example, things like max channel name length, or nickname length.
|
||||
func handleISUPPORT(c *Client, e Event) {
|
||||
// Must be a ISUPPORT-based message. 005 is also used for server bounce
|
||||
// related things, so this handler may be triggered during other
|
||||
// situations.
|
||||
|
||||
// Also known as RPL_PROTOCTL.
|
||||
if !strings.HasSuffix(e.Trailing, "this server") {
|
||||
return
|
||||
}
|
||||
|
||||
// Must have at least one configuration.
|
||||
if len(e.Params) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
// Skip the first parameter, as it's our nickname.
|
||||
for i := 1; i < len(e.Params); i++ {
|
||||
j := strings.IndexByte(e.Params[i], '=')
|
||||
|
||||
if j < 1 || (j+1) == len(e.Params[i]) {
|
||||
c.state.serverOptions[e.Params[i]] = ""
|
||||
continue
|
||||
}
|
||||
|
||||
name := e.Params[i][0:j]
|
||||
val := e.Params[i][j+1:]
|
||||
c.state.serverOptions[name] = val
|
||||
}
|
||||
c.state.Unlock()
|
||||
c.state.notify(c, UPDATE_GENERAL)
|
||||
}
|
||||
|
||||
// handleMOTD handles incoming MOTD messages and buffers them up for use with
|
||||
// Client.ServerMOTD().
|
||||
func handleMOTD(c *Client, e Event) {
|
||||
c.state.Lock()
|
||||
|
||||
defer c.state.notify(c, UPDATE_GENERAL)
|
||||
|
||||
// Beginning of the MOTD.
|
||||
if e.Command == RPL_MOTDSTART {
|
||||
c.state.motd = ""
|
||||
|
||||
c.state.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, assume we're getting sent the MOTD line-by-line.
|
||||
if len(c.state.motd) != 0 {
|
||||
e.Trailing = "\n" + e.Trailing
|
||||
}
|
||||
|
||||
c.state.motd += e.Trailing
|
||||
c.state.Unlock()
|
||||
}
|
||||
|
||||
// handleNAMES handles incoming NAMES queries, of which lists all users in
|
||||
// a given channel. Optionally also obtains ident/host values, as well as
|
||||
// permissions for each user, depending on what capabilities are enabled.
|
||||
func handleNAMES(c *Client, e Event) {
|
||||
if len(e.Params) < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
channel := c.state.lookupChannel(e.Params[len(e.Params)-1])
|
||||
if channel == nil {
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Split(e.Trailing, " ")
|
||||
|
||||
var host, ident, modes, nick string
|
||||
var ok bool
|
||||
|
||||
c.state.Lock()
|
||||
for i := 0; i < len(parts); i++ {
|
||||
modes, nick, ok = parseUserPrefix(parts[i])
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// If userhost-in-names.
|
||||
if strings.Contains(nick, "@") {
|
||||
s := ParseSource(nick)
|
||||
if s == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
host = s.Host
|
||||
nick = s.Name
|
||||
ident = s.Ident
|
||||
}
|
||||
|
||||
if !IsValidNick(nick) {
|
||||
continue
|
||||
}
|
||||
|
||||
c.state.createUser(nick)
|
||||
user := c.state.lookupUser(nick)
|
||||
if user == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
user.addChannel(channel.Name)
|
||||
channel.addUser(nick)
|
||||
|
||||
// Add necessary userhost-in-names data into the user.
|
||||
if host != "" {
|
||||
user.Host = host
|
||||
}
|
||||
if ident != "" {
|
||||
user.Ident = ident
|
||||
}
|
||||
|
||||
// Don't append modes, overwrite them.
|
||||
perms, _ := user.Perms.Lookup(channel.Name)
|
||||
perms.set(modes, false)
|
||||
user.Perms.set(channel.Name, perms)
|
||||
}
|
||||
c.state.Unlock()
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
}
|
||||
|
||||
// updateLastActive is a wrapper for any event which the source author
|
||||
// should have it's LastActive time updated. This is useful for things like
|
||||
// a KICK where we know they are active, as they just kicked another user,
|
||||
// even though they may not be talking.
|
||||
func updateLastActive(c *Client, e Event) {
|
||||
if e.Source == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
|
||||
// Update the users last active time, if they exist.
|
||||
user := c.state.lookupUser(e.Source.Name)
|
||||
if user == nil {
|
||||
c.state.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
user.LastActive = time.Now()
|
||||
c.state.Unlock()
|
||||
}
|
639
vendor/github.com/lrstanley/girc/cap.go
generated
vendored
Normal file
639
vendor/github.com/lrstanley/girc/cap.go
generated
vendored
Normal file
@ -0,0 +1,639 @@
|
||||
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||
// of this source code is governed by the MIT license that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
package girc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var possibleCap = map[string][]string{
|
||||
"account-notify": nil,
|
||||
"account-tag": nil,
|
||||
"away-notify": nil,
|
||||
"batch": nil,
|
||||
"cap-notify": nil,
|
||||
"chghost": nil,
|
||||
"extended-join": nil,
|
||||
"invite-notify": nil,
|
||||
"message-tags": nil,
|
||||
"multi-prefix": nil,
|
||||
"userhost-in-names": nil,
|
||||
}
|
||||
|
||||
func (c *Client) listCAP() {
|
||||
if !c.Config.disableTracking {
|
||||
c.write(&Event{Command: CAP, Params: []string{CAP_LS, "302"}})
|
||||
}
|
||||
}
|
||||
|
||||
func possibleCapList(c *Client) map[string][]string {
|
||||
out := make(map[string][]string)
|
||||
|
||||
if c.Config.SASL != nil {
|
||||
out["sasl"] = nil
|
||||
}
|
||||
|
||||
for k := range c.Config.SupportedCaps {
|
||||
out[k] = c.Config.SupportedCaps[k]
|
||||
}
|
||||
|
||||
for k := range possibleCap {
|
||||
out[k] = possibleCap[k]
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func parseCap(raw string) map[string][]string {
|
||||
out := make(map[string][]string)
|
||||
parts := strings.Split(raw, " ")
|
||||
|
||||
var val int
|
||||
|
||||
for i := 0; i < len(parts); i++ {
|
||||
val = strings.IndexByte(parts[i], prefixTagValue) // =
|
||||
|
||||
// No value splitter, or has splitter but no trailing value.
|
||||
if val < 1 || len(parts[i]) < val+1 {
|
||||
// The capability doesn't contain a value.
|
||||
out[parts[i]] = nil
|
||||
continue
|
||||
}
|
||||
|
||||
out[parts[i][:val]] = strings.Split(parts[i][val+1:], ",")
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// handleCAP attempts to find out what IRCv3 capabilities the server supports.
|
||||
// This will lock further registration until we have acknowledged the
|
||||
// capabilities.
|
||||
func handleCAP(c *Client, e Event) {
|
||||
if len(e.Params) >= 2 && (e.Params[1] == CAP_NEW || e.Params[1] == CAP_DEL) {
|
||||
c.listCAP()
|
||||
return
|
||||
}
|
||||
|
||||
// We can assume there was a failure attempting to enable a capability.
|
||||
if len(e.Params) == 2 && e.Params[1] == CAP_NAK {
|
||||
// Let the server know that we're done.
|
||||
c.write(&Event{Command: CAP, Params: []string{CAP_END}})
|
||||
return
|
||||
}
|
||||
|
||||
possible := possibleCapList(c)
|
||||
|
||||
if len(e.Params) >= 2 && len(e.Trailing) > 1 && e.Params[1] == CAP_LS {
|
||||
c.state.Lock()
|
||||
|
||||
caps := parseCap(e.Trailing)
|
||||
|
||||
for k := range caps {
|
||||
if _, ok := possible[k]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(possible[k]) == 0 || len(caps[k]) == 0 {
|
||||
c.state.tmpCap = append(c.state.tmpCap, k)
|
||||
continue
|
||||
}
|
||||
|
||||
var contains bool
|
||||
for i := 0; i < len(caps[k]); i++ {
|
||||
for j := 0; j < len(possible[k]); j++ {
|
||||
if caps[k][i] == possible[k][j] {
|
||||
// Assume we have a matching split value.
|
||||
contains = true
|
||||
goto checkcontains
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkcontains:
|
||||
if !contains {
|
||||
continue
|
||||
}
|
||||
|
||||
c.state.tmpCap = append(c.state.tmpCap, k)
|
||||
}
|
||||
c.state.Unlock()
|
||||
|
||||
// Indicates if this is a multi-line LS. (2 args means it's the
|
||||
// last LS).
|
||||
if len(e.Params) == 2 {
|
||||
// If we support no caps, just ack the CAP message and END.
|
||||
if len(c.state.tmpCap) == 0 {
|
||||
c.write(&Event{Command: CAP, Params: []string{CAP_END}})
|
||||
return
|
||||
}
|
||||
|
||||
// Let them know which ones we'd like to enable.
|
||||
c.write(&Event{Command: CAP, Params: []string{CAP_REQ}, Trailing: strings.Join(c.state.tmpCap, " "), EmptyTrailing: true})
|
||||
|
||||
// Re-initialize the tmpCap, so if we get multiple 'CAP LS' requests
|
||||
// due to cap-notify, we can re-evaluate what we can support.
|
||||
c.state.Lock()
|
||||
c.state.tmpCap = []string{}
|
||||
c.state.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
if len(e.Params) == 2 && len(e.Trailing) > 1 && e.Params[1] == CAP_ACK {
|
||||
c.state.Lock()
|
||||
c.state.enabledCap = strings.Split(e.Trailing, " ")
|
||||
|
||||
// Do we need to do sasl auth?
|
||||
wantsSASL := false
|
||||
for i := 0; i < len(c.state.enabledCap); i++ {
|
||||
if c.state.enabledCap[i] == "sasl" {
|
||||
wantsSASL = true
|
||||
break
|
||||
}
|
||||
}
|
||||
c.state.Unlock()
|
||||
|
||||
if wantsSASL {
|
||||
c.write(&Event{Command: AUTHENTICATE, Params: []string{c.Config.SASL.Method()}})
|
||||
// Don't "CAP END", since we want to authenticate.
|
||||
return
|
||||
}
|
||||
|
||||
// Let the server know that we're done.
|
||||
c.write(&Event{Command: CAP, Params: []string{CAP_END}})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SASLMech is an representation of what a SASL mechanism should support.
|
||||
// See SASLExternal and SASLPlain for implementations of this.
|
||||
type SASLMech interface {
|
||||
// Method returns the uppercase version of the SASL mechanism name.
|
||||
Method() string
|
||||
// Encode returns the response that the SASL mechanism wants to use. If
|
||||
// the returned string is empty (e.g. the mechanism gives up), the handler
|
||||
// will attempt to panic, as expectation is that if SASL authentication
|
||||
// fails, the client will disconnect.
|
||||
Encode(params []string) (output string)
|
||||
}
|
||||
|
||||
// SASLExternal implements the "EXTERNAL" SASL type.
|
||||
type SASLExternal struct {
|
||||
// Identity is an optional field which allows the client to specify
|
||||
// pre-authentication identification. This means that EXTERNAL will
|
||||
// supply this in the initial response. This usually isn't needed (e.g.
|
||||
// CertFP).
|
||||
Identity string `json:"identity"`
|
||||
}
|
||||
|
||||
// Method identifies what type of SASL this implements.
|
||||
func (sasl *SASLExternal) Method() string {
|
||||
return "EXTERNAL"
|
||||
}
|
||||
|
||||
// Encode for external SALS authentication should really only return a "+",
|
||||
// unless the user has specified pre-authentication or identification data.
|
||||
// See https://tools.ietf.org/html/rfc4422#appendix-A for more info.
|
||||
func (sasl *SASLExternal) Encode(params []string) string {
|
||||
if len(params) != 1 || params[0] != "+" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if sasl.Identity != "" {
|
||||
return sasl.Identity
|
||||
}
|
||||
|
||||
return "+"
|
||||
}
|
||||
|
||||
// SASLPlain contains the user and password needed for PLAIN SASL authentication.
|
||||
type SASLPlain struct {
|
||||
User string `json:"user"` // User is the username for SASL.
|
||||
Pass string `json:"pass"` // Pass is the password for SASL.
|
||||
}
|
||||
|
||||
// Method identifies what type of SASL this implements.
|
||||
func (sasl *SASLPlain) Method() string {
|
||||
return "PLAIN"
|
||||
}
|
||||
|
||||
// Encode encodes the plain user+password into a SASL PLAIN implementation.
|
||||
// See https://tools.ietf.org/rfc/rfc4422.txt for more info.
|
||||
func (sasl *SASLPlain) Encode(params []string) string {
|
||||
if len(params) != 1 || params[0] != "+" {
|
||||
return ""
|
||||
}
|
||||
|
||||
in := []byte(sasl.User)
|
||||
|
||||
in = append(in, 0x0)
|
||||
in = append(in, []byte(sasl.User)...)
|
||||
in = append(in, 0x0)
|
||||
in = append(in, []byte(sasl.Pass)...)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(in)
|
||||
}
|
||||
|
||||
const saslChunkSize = 400
|
||||
|
||||
func handleSASL(c *Client, e Event) {
|
||||
if e.Command == RPL_SASLSUCCESS || e.Command == ERR_SASLALREADY {
|
||||
// Let the server know that we're done.
|
||||
c.write(&Event{Command: CAP, Params: []string{CAP_END}})
|
||||
return
|
||||
}
|
||||
|
||||
// Assume they want us to handle sending auth.
|
||||
auth := c.Config.SASL.Encode(e.Params)
|
||||
|
||||
if auth == "" {
|
||||
// Assume the SASL authentication method doesn't want to respond for
|
||||
// some reason. The SASL spec and IRCv3 spec do not define a clear
|
||||
// way to abort a SASL exchange, other than to disconnect, or proceed
|
||||
// with CAP END.
|
||||
c.rx <- &Event{Command: ERROR, Trailing: fmt.Sprintf(
|
||||
"closing connection: invalid %s SASL configuration provided: %s",
|
||||
c.Config.SASL.Method(), e.Trailing,
|
||||
)}
|
||||
return
|
||||
}
|
||||
|
||||
// Send in "saslChunkSize"-length byte chunks. If the last chuck is
|
||||
// exactly "saslChunkSize" bytes, send a "AUTHENTICATE +" 0-byte
|
||||
// acknowledgement response to let the server know that we're done.
|
||||
for {
|
||||
if len(auth) > saslChunkSize {
|
||||
c.write(&Event{Command: AUTHENTICATE, Params: []string{auth[0 : saslChunkSize-1]}, Sensitive: true})
|
||||
auth = auth[saslChunkSize:]
|
||||
continue
|
||||
}
|
||||
|
||||
if len(auth) <= saslChunkSize {
|
||||
c.write(&Event{Command: AUTHENTICATE, Params: []string{auth}, Sensitive: true})
|
||||
|
||||
if len(auth) == 400 {
|
||||
c.write(&Event{Command: AUTHENTICATE, Params: []string{"+"}})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func handleSASLError(c *Client, e Event) {
|
||||
if c.Config.SASL == nil {
|
||||
c.write(&Event{Command: CAP, Params: []string{CAP_END}})
|
||||
return
|
||||
}
|
||||
|
||||
// Authentication failed. The SASL spec and IRCv3 spec do not define a
|
||||
// clear way to abort a SASL exchange, other than to disconnect, or
|
||||
// proceed with CAP END.
|
||||
c.rx <- &Event{Command: ERROR, Trailing: "closing connection: " + e.Trailing}
|
||||
}
|
||||
|
||||
// handleCHGHOST handles incoming IRCv3 hostname change events. CHGHOST is
|
||||
// what occurs (when enabled) when a servers services change the hostname of
|
||||
// a user. Traditionally, this was simply resolved with a quick QUIT and JOIN,
|
||||
// however CHGHOST resolves this in a much cleaner fashion.
|
||||
func handleCHGHOST(c *Client, e Event) {
|
||||
if len(e.Params) != 2 {
|
||||
return
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
user := c.state.lookupUser(e.Source.Name)
|
||||
if user != nil {
|
||||
user.Ident = e.Params[0]
|
||||
user.Host = e.Params[1]
|
||||
}
|
||||
c.state.Unlock()
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
}
|
||||
|
||||
// handleAWAY handles incoming IRCv3 AWAY events, for which are sent both
|
||||
// when users are no longer away, or when they are away.
|
||||
func handleAWAY(c *Client, e Event) {
|
||||
c.state.Lock()
|
||||
user := c.state.lookupUser(e.Source.Name)
|
||||
if user != nil {
|
||||
user.Extras.Away = e.Trailing
|
||||
}
|
||||
c.state.Unlock()
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
}
|
||||
|
||||
// handleACCOUNT handles incoming IRCv3 ACCOUNT events. ACCOUNT is sent when
|
||||
// a user logs into an account, logs out of their account, or logs into a
|
||||
// different account. The account backend is handled server-side, so this
|
||||
// could be NickServ, X (undernet?), etc.
|
||||
func handleACCOUNT(c *Client, e Event) {
|
||||
if len(e.Params) != 1 {
|
||||
return
|
||||
}
|
||||
|
||||
account := e.Params[0]
|
||||
if account == "*" {
|
||||
account = ""
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
user := c.state.lookupUser(e.Source.Name)
|
||||
if user != nil {
|
||||
user.Extras.Account = account
|
||||
}
|
||||
c.state.Unlock()
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
}
|
||||
|
||||
// handleTags handles any messages that have tags that will affect state. (e.g.
|
||||
// 'account' tags.)
|
||||
func handleTags(c *Client, e Event) {
|
||||
if len(e.Tags) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
account, ok := e.Tags.Get("account")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
user := c.state.lookupUser(e.Source.Name)
|
||||
if user != nil {
|
||||
user.Extras.Account = account
|
||||
}
|
||||
c.state.Unlock()
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
}
|
||||
|
||||
const (
|
||||
prefixTag byte = '@'
|
||||
prefixTagValue byte = '='
|
||||
prefixUserTag byte = '+'
|
||||
tagSeparator byte = ';'
|
||||
maxTagLength int = 511 // 510 + @ and " " (space), though space usually not included.
|
||||
)
|
||||
|
||||
// Tags represents the key-value pairs in IRCv3 message tags. The map contains
|
||||
// the encoded message-tag values. If the tag is present, it may still be
|
||||
// empty. See Tags.Get() and Tags.Set() for use with getting/setting
|
||||
// information within the tags.
|
||||
//
|
||||
// Note that retrieving and setting tags are not concurrent safe. If this is
|
||||
// necessary, you will need to implement it yourself.
|
||||
type Tags map[string]string
|
||||
|
||||
// ParseTags parses out the key-value map of tags. raw should only be the tag
|
||||
// data, not a full message. For example:
|
||||
// @aaa=bbb;ccc;example.com/ddd=eee
|
||||
// NOT:
|
||||
// @aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello
|
||||
func ParseTags(raw string) (t Tags) {
|
||||
t = make(Tags)
|
||||
|
||||
if len(raw) > 0 && raw[0] == prefixTag {
|
||||
raw = raw[1:]
|
||||
}
|
||||
|
||||
parts := strings.Split(raw, string(tagSeparator))
|
||||
var hasValue int
|
||||
|
||||
for i := 0; i < len(parts); i++ {
|
||||
hasValue = strings.IndexByte(parts[i], prefixTagValue)
|
||||
|
||||
// The tag doesn't contain a value or has a splitter with no value.
|
||||
if hasValue < 1 || len(parts[i]) < hasValue+1 {
|
||||
if !validTag(parts[i]) {
|
||||
continue
|
||||
}
|
||||
|
||||
t[parts[i]] = ""
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if tag key or decoded value are invalid.
|
||||
if !validTag(parts[i][:hasValue]) || !validTagValue(tagDecoder.Replace(parts[i][hasValue+1:])) {
|
||||
continue
|
||||
}
|
||||
|
||||
t[parts[i][:hasValue]] = parts[i][hasValue+1:]
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// Len determines the length of the bytes representation of this tag map. This
|
||||
// does not include the trailing space required when creating an event, but
|
||||
// does include the tag prefix ("@").
|
||||
func (t Tags) Len() (length int) {
|
||||
if t == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return len(t.Bytes())
|
||||
}
|
||||
|
||||
// Count finds how many total tags that there are.
|
||||
func (t Tags) Count() int {
|
||||
if t == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return len(t)
|
||||
}
|
||||
|
||||
// Bytes returns a []byte representation of this tag map, including the tag
|
||||
// prefix ("@"). Note that this will return the tags sorted, regardless of
|
||||
// the order of how they were originally parsed.
|
||||
func (t Tags) Bytes() []byte {
|
||||
if t == nil {
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
max := len(t)
|
||||
if max == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
buffer := new(bytes.Buffer)
|
||||
buffer.WriteByte(prefixTag)
|
||||
|
||||
var current int
|
||||
|
||||
// Sort the writing of tags so we can at least guarantee that they will
|
||||
// be in order, and testable.
|
||||
var names []string
|
||||
for tagName := range t {
|
||||
names = append(names, tagName)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
for i := 0; i < len(names); i++ {
|
||||
// Trim at max allowed chars.
|
||||
if (buffer.Len() + len(names[i]) + len(t[names[i]]) + 2) > maxTagLength {
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
buffer.WriteString(names[i])
|
||||
|
||||
// Write the value as necessary.
|
||||
if len(t[names[i]]) > 0 {
|
||||
buffer.WriteByte(prefixTagValue)
|
||||
buffer.WriteString(t[names[i]])
|
||||
}
|
||||
|
||||
// add the separator ";" between tags.
|
||||
if current < max-1 {
|
||||
buffer.WriteByte(tagSeparator)
|
||||
}
|
||||
|
||||
current++
|
||||
}
|
||||
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
// String returns a string representation of this tag map.
|
||||
func (t Tags) String() string {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(t.Bytes())
|
||||
}
|
||||
|
||||
// writeTo writes the necessary tag bytes to an io.Writer, including a trailing
|
||||
// space-separator.
|
||||
func (t Tags) writeTo(w io.Writer) (n int, err error) {
|
||||
b := t.Bytes()
|
||||
if len(b) == 0 {
|
||||
return n, err
|
||||
}
|
||||
|
||||
n, err = w.Write(b)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
var j int
|
||||
j, err = w.Write([]byte{eventSpace})
|
||||
n += j
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
// tagDecode are encoded -> decoded pairs for replacement to decode.
|
||||
var tagDecode = []string{
|
||||
"\\:", ";",
|
||||
"\\s", " ",
|
||||
"\\\\", "\\",
|
||||
"\\r", "\r",
|
||||
"\\n", "\n",
|
||||
}
|
||||
var tagDecoder = strings.NewReplacer(tagDecode...)
|
||||
|
||||
// tagEncode are decoded -> encoded pairs for replacement to decode.
|
||||
var tagEncode = []string{
|
||||
";", "\\:",
|
||||
" ", "\\s",
|
||||
"\\", "\\\\",
|
||||
"\r", "\\r",
|
||||
"\n", "\\n",
|
||||
}
|
||||
var tagEncoder = strings.NewReplacer(tagEncode...)
|
||||
|
||||
// Get returns the unescaped value of given tag key. Note that this is not
|
||||
// concurrent safe.
|
||||
func (t Tags) Get(key string) (tag string, success bool) {
|
||||
if t == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if _, ok := t[key]; ok {
|
||||
tag = tagDecoder.Replace(t[key])
|
||||
success = true
|
||||
}
|
||||
|
||||
return tag, success
|
||||
}
|
||||
|
||||
// Set escapes given value and saves it as the value for given key. Note that
|
||||
// this is not concurrent safe.
|
||||
func (t Tags) Set(key, value string) error {
|
||||
if t == nil {
|
||||
t = make(Tags)
|
||||
}
|
||||
|
||||
if !validTag(key) {
|
||||
return fmt.Errorf("tag key %q is invalid", key)
|
||||
}
|
||||
|
||||
value = tagEncoder.Replace(value)
|
||||
|
||||
if len(value) > 0 && !validTagValue(value) {
|
||||
return fmt.Errorf("tag value %q of key %q is invalid", value, key)
|
||||
}
|
||||
|
||||
// Check to make sure it's not too long here.
|
||||
if (t.Len() + len(key) + len(value) + 2) > maxTagLength {
|
||||
return fmt.Errorf("unable to set tag %q [value %q]: tags too long for message", key, value)
|
||||
}
|
||||
|
||||
t[key] = value
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove deletes the tag frwom the tag map.
|
||||
func (t Tags) Remove(key string) (success bool) {
|
||||
if t == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, success = t[key]; success {
|
||||
delete(t, key)
|
||||
}
|
||||
|
||||
return success
|
||||
}
|
||||
|
||||
// validTag validates an IRC tag.
|
||||
func validTag(name string) bool {
|
||||
if len(name) < 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Allow user tags to be passed to validTag.
|
||||
if len(name) >= 2 && name[0] == prefixUserTag {
|
||||
name = name[1:]
|
||||
}
|
||||
|
||||
for i := 0; i < len(name); i++ {
|
||||
// A-Z, a-z, 0-9, -/._
|
||||
if (name[i] < 'A' || name[i] > 'Z') && (name[i] < 'a' || name[i] > 'z') && (name[i] < '-' || name[i] > '9') && name[i] != '_' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// validTagValue valids a decoded IRC tag value. If the value is not decoded
|
||||
// with tagDecoder first, it may be seen as invalid.
|
||||
func validTagValue(value string) bool {
|
||||
for i := 0; i < len(value); i++ {
|
||||
// Don't allow any invisible chars within the tag, or semicolons.
|
||||
if value[i] < '!' || value[i] > '~' || value[i] == ';' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
668
vendor/github.com/lrstanley/girc/client.go
generated
vendored
Normal file
668
vendor/github.com/lrstanley/girc/client.go
generated
vendored
Normal file
@ -0,0 +1,668 @@
|
||||
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||
// of this source code is governed by the MIT license that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
package girc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client contains all of the information necessary to run a single IRC
|
||||
// client.
|
||||
type Client struct {
|
||||
// Config represents the configuration. Please take extra caution in that
|
||||
// entries in this are not edited while the client is connected, to prevent
|
||||
// data races. This is NOT concurrent safe to update.
|
||||
Config Config
|
||||
// rx is a buffer of events waiting to be processed.
|
||||
rx chan *Event
|
||||
// tx is a buffer of events waiting to be sent.
|
||||
tx chan *Event
|
||||
// state represents the throw-away state for the irc session.
|
||||
state *state
|
||||
// initTime represents the creation time of the client.
|
||||
initTime time.Time
|
||||
// Handlers is a handler which manages internal and external handlers.
|
||||
Handlers *Caller
|
||||
// CTCP is a handler which manages internal and external CTCP handlers.
|
||||
CTCP *CTCP
|
||||
// Cmd contains various helper methods to interact with the server.
|
||||
Cmd *Commands
|
||||
// mu is the mux used for connections/disconnections from the server,
|
||||
// so multiple threads aren't trying to connect at the same time, and
|
||||
// vice versa.
|
||||
mu sync.RWMutex
|
||||
// stop is used to communicate with Connect(), letting it know that the
|
||||
// client wishes to cancel/close.
|
||||
stop context.CancelFunc
|
||||
// conn is a net.Conn reference to the IRC server. If this is nil, it is
|
||||
// safe to assume that we're not connected. If this is not nil, this
|
||||
// means we're either connected, connecting, or cleaning up. This should
|
||||
// be guarded with Client.mu.
|
||||
conn *ircConn
|
||||
// debug is used if a writer is supplied for Client.Config.Debugger.
|
||||
debug *log.Logger
|
||||
}
|
||||
|
||||
// Config contains configuration options for an IRC client
|
||||
type Config struct {
|
||||
// Server is a host/ip of the server you want to connect to. This only
|
||||
// has an affect during the dial process
|
||||
Server string
|
||||
// ServerPass is the server password used to authenticate. This only has
|
||||
// an affect during the dial process.
|
||||
ServerPass string
|
||||
// Port is the port that will be used during server connection. This only
|
||||
// has an affect during the dial process.
|
||||
Port int
|
||||
// Nick is an rfc-valid nickname used during connection. This only has an
|
||||
// affect during the dial process.
|
||||
Nick string
|
||||
// User is the username/ident to use on connect. Ignored if an identd
|
||||
// server is used. This only has an affect during the dial process.
|
||||
User string
|
||||
// Name is the "realname" that's used during connection. This only has an
|
||||
// affect during the dial process.
|
||||
Name string
|
||||
// SASL contains the necessary authentication data to authenticate
|
||||
// with SASL. See the documentation for SASLMech for what is currently
|
||||
// supported. Capability tracking must be enabled for this to work, as
|
||||
// this requires IRCv3 CAP handling.
|
||||
SASL SASLMech
|
||||
// Bind is used to bind to a specific host or ip during the dial process
|
||||
// when connecting to the server. This can be a hostname, however it must
|
||||
// resolve to an IPv4/IPv6 address bindable on your system. Otherwise,
|
||||
// you can simply use a IPv4/IPv6 address directly. This only has an
|
||||
// affect during the dial process and will not work with DialerConnect().
|
||||
Bind string
|
||||
// SSL allows dialing via TLS. See TLSConfig to set your own TLS
|
||||
// configuration (e.g. to not force hostname checking). This only has an
|
||||
// affect during the dial process.
|
||||
SSL bool
|
||||
// TLSConfig is an optional user-supplied tls configuration, used during
|
||||
// socket creation to the server. SSL must be enabled for this to be used.
|
||||
// This only has an affect during the dial process.
|
||||
TLSConfig *tls.Config
|
||||
// AllowFlood allows the client to bypass the rate limit of outbound
|
||||
// messages.
|
||||
AllowFlood bool
|
||||
// GlobalFormat enables passing through all events which have trailing
|
||||
// text through the color Fmt() function, so you don't have to wrap
|
||||
// every response in the Fmt() method.
|
||||
//
|
||||
// Note that this only actually applies to PRIVMSG, NOTICE and TOPIC
|
||||
// events, to ensure it doesn't clobber unwanted events.
|
||||
GlobalFormat bool
|
||||
// Debug is an optional, user supplied location to log the raw lines
|
||||
// sent from the server, or other useful debug logs. Defaults to
|
||||
// ioutil.Discard. For quick debugging, this could be set to os.Stdout.
|
||||
Debug io.Writer
|
||||
// Out is used to write out a prettified version of incoming events. For
|
||||
// example, channel JOIN/PART, PRIVMSG/NOTICE, KICk, etc. Useful to get
|
||||
// a brief output of the activity of the client. If you are looking to
|
||||
// log raw messages, look at a handler and girc.ALLEVENTS and the relevant
|
||||
// Event.Bytes() or Event.String() methods.
|
||||
Out io.Writer
|
||||
// RecoverFunc is called when a handler throws a panic. If RecoverFunc is
|
||||
// set, the panic will be considered recovered, otherwise the client will
|
||||
// panic. Set this to DefaultRecoverHandler if you don't want the client
|
||||
// to panic, however you don't want to handle the panic yourself.
|
||||
// DefaultRecoverHandler will log the panic to Debug or os.Stdout if
|
||||
// Debug is unset.
|
||||
RecoverFunc func(c *Client, e *HandlerError)
|
||||
// SupportedCaps are the IRCv3 capabilities you would like the client to
|
||||
// support on top of the ones which the client already supports (see
|
||||
// cap.go for which ones the client enables by default). Only use this
|
||||
// if you have not called DisableTracking(). The keys value gets passed
|
||||
// to the server if supported.
|
||||
SupportedCaps map[string][]string
|
||||
// Version is the application version information that will be used in
|
||||
// response to a CTCP VERSION, if default CTCP replies have not been
|
||||
// overwritten or a VERSION handler was already supplied.
|
||||
Version string
|
||||
// PingDelay is the frequency between when the client sends a keep-alive
|
||||
// PING to the server, and awaits a response (and times out if the server
|
||||
// doesn't respond in time). This should be between 20-600 seconds. See
|
||||
// Client.Lag() if you want to determine the delay between the server
|
||||
// and the client. If this is set to -1, the client will not attempt to
|
||||
// send client -> server PING requests.
|
||||
PingDelay time.Duration
|
||||
|
||||
// disableTracking disables all channel and user-level tracking. Useful
|
||||
// for highly embedded scripts with single purposes. This has an exported
|
||||
// method which enables this and ensures prop cleanup, see
|
||||
// Client.DisableTracking().
|
||||
disableTracking bool
|
||||
// HandleNickCollide when set, allows the client to handle nick collisions
|
||||
// in a custom way. If unset, the client will attempt to append a
|
||||
// underscore to the end of the nickname, in order to bypass using
|
||||
// an invalid nickname. For example, if "test" is already in use, or is
|
||||
// blocked by the network/a service, the client will try and use "test_",
|
||||
// then it will attempt "test__", "test___", and so on.
|
||||
HandleNickCollide func(oldNick string) (newNick string)
|
||||
}
|
||||
|
||||
// ErrInvalidConfig is returned when the configuration passed to the client
|
||||
// is invalid.
|
||||
type ErrInvalidConfig struct {
|
||||
Conf Config // Conf is the configuration that was not valid.
|
||||
err error
|
||||
}
|
||||
|
||||
func (e ErrInvalidConfig) Error() string { return "invalid configuration: " + e.err.Error() }
|
||||
|
||||
// isValid checks some basic settings to ensure the config is valid.
|
||||
func (conf *Config) isValid() error {
|
||||
if conf.Server == "" {
|
||||
return &ErrInvalidConfig{Conf: *conf, err: errors.New("empty server")}
|
||||
}
|
||||
|
||||
// Default port to 6667 (the standard IRC port).
|
||||
if conf.Port == 0 {
|
||||
conf.Port = 6667
|
||||
}
|
||||
|
||||
if conf.Port < 21 || conf.Port > 65535 {
|
||||
return &ErrInvalidConfig{Conf: *conf, err: errors.New("port outside valid range (21-65535)")}
|
||||
}
|
||||
|
||||
if !IsValidNick(conf.Nick) {
|
||||
return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad nickname specified")}
|
||||
}
|
||||
if !IsValidUser(conf.User) {
|
||||
return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad user/ident specified")}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ErrNotConnected is returned if a method is used when the client isn't
|
||||
// connected.
|
||||
var ErrNotConnected = errors.New("client is not connected to server")
|
||||
|
||||
// New creates a new IRC client with the specified server, name and config.
|
||||
func New(config Config) *Client {
|
||||
c := &Client{
|
||||
Config: config,
|
||||
rx: make(chan *Event, 25),
|
||||
tx: make(chan *Event, 25),
|
||||
CTCP: newCTCP(),
|
||||
initTime: time.Now(),
|
||||
}
|
||||
|
||||
c.Cmd = &Commands{c: c}
|
||||
|
||||
if c.Config.PingDelay >= 0 && c.Config.PingDelay < (20*time.Second) {
|
||||
c.Config.PingDelay = 20 * time.Second
|
||||
} else if c.Config.PingDelay > (600 * time.Second) {
|
||||
c.Config.PingDelay = 600 * time.Second
|
||||
}
|
||||
|
||||
if c.Config.Debug == nil {
|
||||
c.debug = log.New(ioutil.Discard, "", 0)
|
||||
} else {
|
||||
c.debug = log.New(c.Config.Debug, "debug:", log.Ltime|log.Lshortfile)
|
||||
c.debug.Print("initializing debugging")
|
||||
}
|
||||
|
||||
// Setup the caller.
|
||||
c.Handlers = newCaller(c.debug)
|
||||
|
||||
// Give ourselves a new state.
|
||||
c.state = &state{}
|
||||
c.state.reset()
|
||||
|
||||
// Register builtin handlers.
|
||||
c.registerBuiltins()
|
||||
|
||||
// Register default CTCP responses.
|
||||
c.CTCP.addDefaultHandlers()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// String returns a brief description of the current client state.
|
||||
func (c *Client) String() string {
|
||||
connected := c.IsConnected()
|
||||
|
||||
return fmt.Sprintf(
|
||||
"<Client init:%q handlers:%d connected:%t>", c.initTime.String(), c.Handlers.Len(), connected,
|
||||
)
|
||||
}
|
||||
|
||||
// TLSConnectionState returns the TLS connection state from tls.Conn{}, which
|
||||
// is useful to return needed TLS fingerprint info, certificates, verify cert
|
||||
// expiration dates, etc. Will only return an error if the underlying
|
||||
// connection wasn't established using TLS (see ErrConnNotTLS), or if the
|
||||
// client isn't connected.
|
||||
func (c *Client) TLSConnectionState() (*tls.ConnectionState, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
if c.conn == nil {
|
||||
return nil, ErrNotConnected
|
||||
}
|
||||
|
||||
c.conn.mu.RLock()
|
||||
defer c.conn.mu.RUnlock()
|
||||
|
||||
if !c.conn.connected {
|
||||
return nil, ErrNotConnected
|
||||
}
|
||||
|
||||
if tlsConn, ok := c.conn.sock.(*tls.Conn); ok {
|
||||
cs := tlsConn.ConnectionState()
|
||||
return &cs, nil
|
||||
}
|
||||
|
||||
return nil, ErrConnNotTLS
|
||||
}
|
||||
|
||||
// ErrConnNotTLS is returned when Client.TLSConnectionState() is called, and
|
||||
// the connection to the server wasn't made with TLS.
|
||||
var ErrConnNotTLS = errors.New("underlying connection is not tls")
|
||||
|
||||
// Close closes the network connection to the server, and sends a STOPPED
|
||||
// event. This should cause Connect() to return with nil. This should be
|
||||
// safe to call multiple times. See Connect()'s documentation on how
|
||||
// handlers and goroutines are handled when disconnected from the server.
|
||||
func (c *Client) Close() {
|
||||
c.mu.RLock()
|
||||
if c.stop != nil {
|
||||
c.debug.Print("requesting client to stop")
|
||||
c.stop()
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
}
|
||||
|
||||
// ErrEvent is an error returned when the server (or library) sends an ERROR
|
||||
// message response. The string returned contains the trailing text from the
|
||||
// message.
|
||||
type ErrEvent struct {
|
||||
Event *Event
|
||||
}
|
||||
|
||||
func (e *ErrEvent) Error() string {
|
||||
if e.Event == nil {
|
||||
return "unknown error occurred"
|
||||
}
|
||||
|
||||
return e.Event.Trailing
|
||||
}
|
||||
|
||||
func (c *Client) execLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
|
||||
c.debug.Print("starting execLoop")
|
||||
defer c.debug.Print("closing execLoop")
|
||||
|
||||
var event *Event
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// We've been told to exit, however we shouldn't bail on the
|
||||
// current events in the queue that should be processed, as one
|
||||
// may want to handle an ERROR, QUIT, etc.
|
||||
c.debug.Printf("received signal to close, flushing %d events and executing", len(c.rx))
|
||||
for {
|
||||
select {
|
||||
case event = <-c.rx:
|
||||
c.RunHandlers(event)
|
||||
default:
|
||||
goto done
|
||||
}
|
||||
}
|
||||
|
||||
done:
|
||||
wg.Done()
|
||||
return
|
||||
case event = <-c.rx:
|
||||
if event != nil && event.Command == ERROR {
|
||||
// Handles incoming ERROR responses. These are only ever sent
|
||||
// by the server (with the exception that this library may use
|
||||
// them as a lower level way of signalling to disconnect due
|
||||
// to some other client-choosen error), and should always be
|
||||
// followed up by the server disconnecting the client. If for
|
||||
// some reason the server doesn't disconnect the client, or
|
||||
// if this library is the source of the error, this should
|
||||
// signal back up to the main connect loop, to disconnect.
|
||||
errs <- &ErrEvent{Event: event}
|
||||
|
||||
// Make sure to not actually exit, so we can let any handlers
|
||||
// actually handle the ERROR event.
|
||||
}
|
||||
|
||||
c.RunHandlers(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DisableTracking disables all channel/user-level/CAP tracking, and clears
|
||||
// all internal handlers. Useful for highly embedded scripts with single
|
||||
// purposes. This cannot be un-done on a client.
|
||||
func (c *Client) DisableTracking() {
|
||||
c.debug.Print("disabling tracking")
|
||||
c.Config.disableTracking = true
|
||||
c.Handlers.clearInternal()
|
||||
|
||||
c.state.Lock()
|
||||
c.state.channels = nil
|
||||
c.state.Unlock()
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
|
||||
c.registerBuiltins()
|
||||
}
|
||||
|
||||
// Server returns the string representation of host+port pair for net.Conn.
|
||||
func (c *Client) Server() string {
|
||||
return fmt.Sprintf("%s:%d", c.Config.Server, c.Config.Port)
|
||||
}
|
||||
|
||||
// Lifetime returns the amount of time that has passed since the client was
|
||||
// created.
|
||||
func (c *Client) Lifetime() time.Duration {
|
||||
return time.Since(c.initTime)
|
||||
}
|
||||
|
||||
// Uptime is the time at which the client successfully connected to the
|
||||
// server.
|
||||
func (c *Client) Uptime() (up *time.Time, err error) {
|
||||
if !c.IsConnected() {
|
||||
return nil, ErrNotConnected
|
||||
}
|
||||
|
||||
c.mu.RLock()
|
||||
c.conn.mu.RLock()
|
||||
up = c.conn.connTime
|
||||
c.conn.mu.RUnlock()
|
||||
c.mu.RUnlock()
|
||||
|
||||
return up, nil
|
||||
}
|
||||
|
||||
// ConnSince is the duration that has past since the client successfully
|
||||
// connected to the server.
|
||||
func (c *Client) ConnSince() (since *time.Duration, err error) {
|
||||
if !c.IsConnected() {
|
||||
return nil, ErrNotConnected
|
||||
}
|
||||
|
||||
c.mu.RLock()
|
||||
c.conn.mu.RLock()
|
||||
timeSince := time.Since(*c.conn.connTime)
|
||||
c.conn.mu.RUnlock()
|
||||
c.mu.RUnlock()
|
||||
|
||||
return &timeSince, nil
|
||||
}
|
||||
|
||||
// IsConnected returns true if the client is connected to the server.
|
||||
func (c *Client) IsConnected() bool {
|
||||
c.mu.RLock()
|
||||
if c.conn == nil {
|
||||
c.mu.RUnlock()
|
||||
return false
|
||||
}
|
||||
|
||||
c.conn.mu.RLock()
|
||||
connected := c.conn.connected
|
||||
c.conn.mu.RUnlock()
|
||||
c.mu.RUnlock()
|
||||
|
||||
return connected
|
||||
}
|
||||
|
||||
// GetNick returns the current nickname of the active connection. Panics if
|
||||
// tracking is disabled.
|
||||
func (c *Client) GetNick() string {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
defer c.state.RUnlock()
|
||||
|
||||
if c.state.nick == "" {
|
||||
return c.Config.Nick
|
||||
}
|
||||
|
||||
return c.state.nick
|
||||
}
|
||||
|
||||
// GetIdent returns the current ident of the active connection. Panics if
|
||||
// tracking is disabled. May be empty, as this is obtained from when we join
|
||||
// a channel, as there is no other more efficient method to return this info.
|
||||
func (c *Client) GetIdent() string {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
defer c.state.RUnlock()
|
||||
|
||||
if c.state.ident == "" {
|
||||
return c.Config.User
|
||||
}
|
||||
|
||||
return c.state.ident
|
||||
}
|
||||
|
||||
// GetHost returns the current host of the active connection. Panics if
|
||||
// tracking is disabled. May be empty, as this is obtained from when we join
|
||||
// a channel, as there is no other more efficient method to return this info.
|
||||
func (c *Client) GetHost() string {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
defer c.state.RUnlock()
|
||||
|
||||
return c.state.host
|
||||
}
|
||||
|
||||
// ChannelList returns the active list of channel names that the client is in.
|
||||
// Panics if tracking is disabled.
|
||||
func (c *Client) ChannelList() []string {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
channels := make([]string, len(c.state.channels))
|
||||
var i int
|
||||
for channel := range c.state.channels {
|
||||
channels[i] = c.state.channels[channel].Name
|
||||
i++
|
||||
}
|
||||
c.state.RUnlock()
|
||||
sort.Strings(channels)
|
||||
|
||||
return channels
|
||||
}
|
||||
|
||||
// Channels returns the active channels that the client is in. Panics if
|
||||
// tracking is disabled.
|
||||
func (c *Client) Channels() []*Channel {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
channels := make([]*Channel, len(c.state.channels))
|
||||
var i int
|
||||
for channel := range c.state.channels {
|
||||
channels[i] = c.state.channels[channel].Copy()
|
||||
i++
|
||||
}
|
||||
c.state.RUnlock()
|
||||
|
||||
return channels
|
||||
}
|
||||
|
||||
// UserList returns the active list of nicknames that the client is tracking
|
||||
// across all networks. Panics if tracking is disabled.
|
||||
func (c *Client) UserList() []string {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
users := make([]string, len(c.state.users))
|
||||
var i int
|
||||
for user := range c.state.users {
|
||||
users[i] = c.state.users[user].Nick
|
||||
i++
|
||||
}
|
||||
c.state.RUnlock()
|
||||
sort.Strings(users)
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
// Users returns the active users that the client is tracking across all
|
||||
// networks. Panics if tracking is disabled.
|
||||
func (c *Client) Users() []*User {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
users := make([]*User, len(c.state.users))
|
||||
var i int
|
||||
for user := range c.state.users {
|
||||
users[i] = c.state.users[user].Copy()
|
||||
i++
|
||||
}
|
||||
c.state.RUnlock()
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
// LookupChannel looks up a given channel in state. If the channel doesn't
|
||||
// exist, nil is returned. Panics if tracking is disabled.
|
||||
func (c *Client) LookupChannel(name string) *Channel {
|
||||
c.panicIfNotTracking()
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.state.RLock()
|
||||
defer c.state.RUnlock()
|
||||
|
||||
channel := c.state.lookupChannel(name)
|
||||
if channel == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return channel.Copy()
|
||||
}
|
||||
|
||||
// LookupUser looks up a given user in state. If the user doesn't exist, nil
|
||||
// is returned. Panics if tracking is disabled.
|
||||
func (c *Client) LookupUser(nick string) *User {
|
||||
c.panicIfNotTracking()
|
||||
if nick == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.state.RLock()
|
||||
defer c.state.RUnlock()
|
||||
|
||||
user := c.state.lookupUser(nick)
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return user.Copy()
|
||||
}
|
||||
|
||||
// IsInChannel returns true if the client is in channel. Panics if tracking
|
||||
// is disabled.
|
||||
func (c *Client) IsInChannel(channel string) bool {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
_, inChannel := c.state.channels[ToRFC1459(channel)]
|
||||
c.state.RUnlock()
|
||||
|
||||
return inChannel
|
||||
}
|
||||
|
||||
// GetServerOption retrieves a server capability setting that was retrieved
|
||||
// during client connection. This is also known as ISUPPORT (or RPL_PROTOCTL).
|
||||
// Will panic if used when tracking has been disabled. Examples of usage:
|
||||
//
|
||||
// nickLen, success := GetServerOption("MAXNICKLEN")
|
||||
//
|
||||
func (c *Client) GetServerOption(key string) (result string, ok bool) {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
result, ok = c.state.serverOptions[key]
|
||||
c.state.RUnlock()
|
||||
|
||||
return result, ok
|
||||
}
|
||||
|
||||
// NetworkName returns the network identifier. E.g. "EsperNet", "ByteIRC".
|
||||
// May be empty if the server does not support RPL_ISUPPORT (or RPL_PROTOCTL).
|
||||
// Will panic if used when tracking has been disabled.
|
||||
func (c *Client) NetworkName() (name string) {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
name, _ = c.GetServerOption("NETWORK")
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
// ServerVersion returns the server software version, if the server has
|
||||
// supplied this information during connection. May be empty if the server
|
||||
// does not support RPL_MYINFO. Will panic if used when tracking has been
|
||||
// disabled.
|
||||
func (c *Client) ServerVersion() string {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
version, _ := c.GetServerOption("VERSION")
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
// ServerMOTD returns the servers message of the day, if the server has sent
|
||||
// it upon connect. Will panic if used when tracking has been disabled.
|
||||
func (c *Client) ServerMOTD() string {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
motd := c.state.motd
|
||||
c.state.RUnlock()
|
||||
|
||||
return motd
|
||||
}
|
||||
|
||||
// Latency is the latency between the server and the client. This is measured
|
||||
// by determining the difference in time between when we ping the server, and
|
||||
// when we receive a pong.
|
||||
func (c *Client) Latency() time.Duration {
|
||||
c.mu.RLock()
|
||||
c.conn.mu.RLock()
|
||||
delta := c.conn.lastPong.Sub(c.conn.lastPing)
|
||||
c.conn.mu.RUnlock()
|
||||
c.mu.RUnlock()
|
||||
|
||||
if delta < 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return delta
|
||||
}
|
||||
|
||||
// panicIfNotTracking will throw a panic when it's called, and tracking is
|
||||
// disabled. Adds useful info like what function specifically, and where it
|
||||
// was called from.
|
||||
func (c *Client) panicIfNotTracking() {
|
||||
if !c.Config.disableTracking {
|
||||
return
|
||||
}
|
||||
|
||||
pc, _, _, _ := runtime.Caller(1)
|
||||
fn := runtime.FuncForPC(pc)
|
||||
_, file, line, _ := runtime.Caller(2)
|
||||
|
||||
panic(fmt.Sprintf("%s used when tracking is disabled (caller %s:%d)", fn.Name(), file, line))
|
||||
}
|
199
vendor/github.com/lrstanley/girc/cmdhandler/cmd.go
generated
vendored
Normal file
199
vendor/github.com/lrstanley/girc/cmdhandler/cmd.go
generated
vendored
Normal file
@ -0,0 +1,199 @@
|
||||
package cmdhandler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/lrstanley/girc"
|
||||
)
|
||||
|
||||
// Input is a wrapper for events, based around private messages.
|
||||
type Input struct {
|
||||
Origin *girc.Event
|
||||
Args []string
|
||||
RawArgs string
|
||||
}
|
||||
|
||||
// Command is an IRC command, supporting aliases, help documentation and easy
|
||||
// wrapping for message inputs.
|
||||
type Command struct {
|
||||
// Name of command, e.g. "search" or "ping".
|
||||
Name string
|
||||
// Aliases for the above command, e.g. "s" for search, or "p" for "ping".
|
||||
Aliases []string
|
||||
// Help documentation. Should be in the format "<arg> <arg> [arg] --
|
||||
// something useful here"
|
||||
Help string
|
||||
// MinArgs is the minimum required arguments for the command. Defaults to
|
||||
// 0, which means multiple, or no arguments can be supplied. If set
|
||||
// above 0, this means that the command handler will throw an error asking
|
||||
// the person to check "<prefix>help <command>" for more info.
|
||||
MinArgs int
|
||||
// Fn is the function which is executed when the command is ran from a
|
||||
// private message, or channel.
|
||||
Fn func(*girc.Client, *Input)
|
||||
}
|
||||
|
||||
func (c *Command) genHelp(prefix string) string {
|
||||
out := "{b}" + prefix + c.Name + "{b}"
|
||||
|
||||
if c.Aliases != nil && len(c.Aliases) > 0 {
|
||||
out += " ({b}" + prefix + strings.Join(c.Aliases, "{b}, {b}"+prefix) + "{b})"
|
||||
}
|
||||
|
||||
out += " :: " + c.Help
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// CmdHandler is an irc command parser and execution format which you could
|
||||
// use as an example for building your own version/bot.
|
||||
//
|
||||
// An example of how you would register this with girc:
|
||||
//
|
||||
// ch, err := cmdhandler.New("!")
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
//
|
||||
// ch.Add(&cmdhandler.Command{
|
||||
// Name: "ping",
|
||||
// Help: "Sends a pong reply back to the original user.",
|
||||
// Fn: func(c *girc.Client, input *cmdhandler.Input) {
|
||||
// c.Commands.ReplyTo(*input.Origin, "pong!")
|
||||
// },
|
||||
// })
|
||||
//
|
||||
// client.Handlers.AddHandler(girc.PRIVMSG, ch)
|
||||
type CmdHandler struct {
|
||||
prefix string
|
||||
re *regexp.Regexp
|
||||
|
||||
mu sync.Mutex
|
||||
cmds map[string]*Command
|
||||
}
|
||||
|
||||
var cmdMatch = `^%s([a-z0-9-_]{1,20})(?: (.*))?$`
|
||||
|
||||
// New returns a new CmdHandler based on the specified command prefix. A good
|
||||
// prefix is a single character, and easy to remember/use. E.g. "!", or ".".
|
||||
func New(prefix string) (*CmdHandler, error) {
|
||||
re, err := regexp.Compile(fmt.Sprintf(cmdMatch, regexp.QuoteMeta(prefix)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CmdHandler{prefix: prefix, re: re, cmds: make(map[string]*Command)}, nil
|
||||
}
|
||||
|
||||
var validName = regexp.MustCompile(`^[a-z0-9-_]{1,20}$`)
|
||||
|
||||
// Add registers a new command to the handler. Note that you cannot remove
|
||||
// commands once added, unless you add another CmdHandler to the client.
|
||||
func (ch *CmdHandler) Add(cmd *Command) error {
|
||||
if cmd == nil {
|
||||
return errors.New("nil command provided to CmdHandler")
|
||||
}
|
||||
|
||||
cmd.Name = strings.ToLower(cmd.Name)
|
||||
if !validName.MatchString(cmd.Name) {
|
||||
return fmt.Errorf("invalid command name: %q (req: %q)", cmd.Name, validName.String())
|
||||
}
|
||||
|
||||
if cmd.Aliases != nil {
|
||||
for i := 0; i < len(cmd.Aliases); i++ {
|
||||
cmd.Aliases[i] = strings.ToLower(cmd.Aliases[i])
|
||||
if !validName.MatchString(cmd.Aliases[i]) {
|
||||
return fmt.Errorf("invalid command name: %q (req: %q)", cmd.Aliases[i], validName.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.MinArgs < 0 {
|
||||
cmd.MinArgs = 0
|
||||
}
|
||||
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
|
||||
if _, ok := ch.cmds[cmd.Name]; ok {
|
||||
return fmt.Errorf("command already registered: %s", cmd.Name)
|
||||
}
|
||||
|
||||
ch.cmds[cmd.Name] = cmd
|
||||
|
||||
// Since we'd be storing pointers, duplicates do not matter.
|
||||
for i := 0; i < len(cmd.Aliases); i++ {
|
||||
if _, ok := ch.cmds[cmd.Aliases[i]]; ok {
|
||||
return fmt.Errorf("alias already registered: %s", cmd.Aliases[i])
|
||||
}
|
||||
|
||||
ch.cmds[cmd.Aliases[i]] = cmd
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Execute satisfies the girc.Handler interface.
|
||||
func (ch *CmdHandler) Execute(client *girc.Client, event girc.Event) {
|
||||
if event.Source == nil || event.Command != girc.PRIVMSG {
|
||||
return
|
||||
}
|
||||
|
||||
parsed := ch.re.FindStringSubmatch(event.Trailing)
|
||||
if len(parsed) != 3 {
|
||||
return
|
||||
}
|
||||
|
||||
invCmd := strings.ToLower(parsed[1])
|
||||
args := strings.Split(parsed[2], " ")
|
||||
if len(args) == 1 && args[0] == "" {
|
||||
args = []string{}
|
||||
}
|
||||
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
|
||||
if invCmd == "help" {
|
||||
if len(args) == 0 {
|
||||
client.Cmd.ReplyTo(event, girc.Fmt("type '{b}!help {blue}<command>{c}{b}' to optionally get more info about a specific command."))
|
||||
return
|
||||
}
|
||||
|
||||
args[0] = strings.ToLower(args[0])
|
||||
|
||||
if _, ok := ch.cmds[args[0]]; !ok {
|
||||
client.Cmd.ReplyTof(event, girc.Fmt("unknown command {b}%q{b}."), args[0])
|
||||
return
|
||||
}
|
||||
|
||||
if ch.cmds[args[0]].Help == "" {
|
||||
client.Cmd.ReplyTof(event, girc.Fmt("there is no help documentation for {b}%q{b}"), args[0])
|
||||
return
|
||||
}
|
||||
|
||||
client.Cmd.ReplyTo(event, girc.Fmt(ch.cmds[args[0]].genHelp(ch.prefix)))
|
||||
return
|
||||
}
|
||||
|
||||
cmd, ok := ch.cmds[invCmd]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if len(args) < cmd.MinArgs {
|
||||
client.Cmd.ReplyTof(event, girc.Fmt("not enough arguments supplied for {b}%q{b}. try '{b}%shelp %s{b}'?"), invCmd, ch.prefix, invCmd)
|
||||
return
|
||||
}
|
||||
|
||||
in := &Input{
|
||||
Origin: &event,
|
||||
Args: args,
|
||||
RawArgs: parsed[2],
|
||||
}
|
||||
|
||||
go cmd.Fn(client, in)
|
||||
}
|
361
vendor/github.com/lrstanley/girc/commands.go
generated
vendored
Normal file
361
vendor/github.com/lrstanley/girc/commands.go
generated
vendored
Normal file
@ -0,0 +1,361 @@
|
||||
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||
// of this source code is governed by the MIT license that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
package girc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Commands holds a large list of useful methods to interact with the server,
|
||||
// and wrappers for common events.
|
||||
type Commands struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// Nick changes the client nickname.
|
||||
func (cmd *Commands) Nick(name string) {
|
||||
cmd.c.Send(&Event{Command: NICK, Params: []string{name}})
|
||||
}
|
||||
|
||||
// Join attempts to enter a list of IRC channels, at bulk if possible to
|
||||
// prevent sending extensive JOIN commands.
|
||||
func (cmd *Commands) Join(channels ...string) {
|
||||
// We can join multiple channels at once, however we need to ensure that
|
||||
// we are not exceeding the line length. (see maxLength)
|
||||
max := maxLength - len(JOIN) - 1
|
||||
|
||||
var buffer string
|
||||
|
||||
for i := 0; i < len(channels); i++ {
|
||||
if len(buffer+","+channels[i]) > max {
|
||||
cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}})
|
||||
buffer = ""
|
||||
continue
|
||||
}
|
||||
|
||||
if len(buffer) == 0 {
|
||||
buffer = channels[i]
|
||||
} else {
|
||||
buffer += "," + channels[i]
|
||||
}
|
||||
|
||||
if i == len(channels)-1 {
|
||||
cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// JoinKey attempts to enter an IRC channel with a password.
|
||||
func (cmd *Commands) JoinKey(channel, password string) {
|
||||
cmd.c.Send(&Event{Command: JOIN, Params: []string{channel, password}})
|
||||
}
|
||||
|
||||
// Part leaves an IRC channel.
|
||||
func (cmd *Commands) Part(channels ...string) {
|
||||
for i := 0; i < len(channels); i++ {
|
||||
cmd.c.Send(&Event{Command: PART, Params: []string{channels[i]}})
|
||||
}
|
||||
}
|
||||
|
||||
// PartMessage leaves an IRC channel with a specified leave message.
|
||||
func (cmd *Commands) PartMessage(channel, message string) {
|
||||
cmd.c.Send(&Event{Command: PART, Params: []string{channel}, Trailing: message, EmptyTrailing: true})
|
||||
}
|
||||
|
||||
// SendCTCP sends a CTCP request to target. Note that this method uses
|
||||
// PRIVMSG specifically. ctcpType is the CTCP command, e.g. "FINGER", "TIME",
|
||||
// "VERSION", etc.
|
||||
func (cmd *Commands) SendCTCP(target, ctcpType, message string) {
|
||||
out := encodeCTCPRaw(ctcpType, message)
|
||||
if out == "" {
|
||||
panic(fmt.Sprintf("invalid CTCP: %s -> %s: %s", target, ctcpType, message))
|
||||
}
|
||||
|
||||
cmd.Message(target, out)
|
||||
}
|
||||
|
||||
// SendCTCPf sends a CTCP request to target using a specific format. Note that
|
||||
// this method uses PRIVMSG specifically. ctcpType is the CTCP command, e.g.
|
||||
// "FINGER", "TIME", "VERSION", etc.
|
||||
func (cmd *Commands) SendCTCPf(target, ctcpType, format string, a ...interface{}) {
|
||||
cmd.SendCTCP(target, ctcpType, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// SendCTCPReplyf sends a CTCP response to target using a specific format.
|
||||
// Note that this method uses NOTICE specifically. ctcpType is the CTCP
|
||||
// command, e.g. "FINGER", "TIME", "VERSION", etc.
|
||||
func (cmd *Commands) SendCTCPReplyf(target, ctcpType, format string, a ...interface{}) {
|
||||
cmd.SendCTCPReply(target, ctcpType, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// SendCTCPReply sends a CTCP response to target. Note that this method uses
|
||||
// NOTICE specifically.
|
||||
func (cmd *Commands) SendCTCPReply(target, ctcpType, message string) {
|
||||
out := encodeCTCPRaw(ctcpType, message)
|
||||
if out == "" {
|
||||
panic(fmt.Sprintf("invalid CTCP: %s -> %s: %s", target, ctcpType, message))
|
||||
}
|
||||
|
||||
cmd.Notice(target, out)
|
||||
}
|
||||
|
||||
// Message sends a PRIVMSG to target (either channel, service, or user).
|
||||
func (cmd *Commands) Message(target, message string) {
|
||||
cmd.c.Send(&Event{Command: PRIVMSG, Params: []string{target}, Trailing: message, EmptyTrailing: true})
|
||||
}
|
||||
|
||||
// Messagef sends a formated PRIVMSG to target (either channel, service, or
|
||||
// user).
|
||||
func (cmd *Commands) Messagef(target, format string, a ...interface{}) {
|
||||
cmd.Message(target, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// ErrInvalidSource is returned when a method needs to know the origin of an
|
||||
// event, however Event.Source is unknown (e.g. sent by the user, not the
|
||||
// server.)
|
||||
var ErrInvalidSource = errors.New("event has nil or invalid source address")
|
||||
|
||||
// Reply sends a reply to channel or user, based on where the supplied event
|
||||
// originated from. See also ReplyTo(). Panics if the incoming event has no
|
||||
// source.
|
||||
func (cmd *Commands) Reply(event Event, message string) {
|
||||
if event.Source == nil {
|
||||
panic(ErrInvalidSource)
|
||||
}
|
||||
|
||||
if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
|
||||
cmd.Message(event.Params[0], message)
|
||||
return
|
||||
}
|
||||
|
||||
cmd.Message(event.Source.Name, message)
|
||||
}
|
||||
|
||||
// Replyf sends a reply to channel or user with a format string, based on
|
||||
// where the supplied event originated from. See also ReplyTof(). Panics if
|
||||
// the incoming event has no source.
|
||||
func (cmd *Commands) Replyf(event Event, format string, a ...interface{}) {
|
||||
cmd.Reply(event, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// ReplyTo sends a reply to a channel or user, based on where the supplied
|
||||
// event originated from. ReplyTo(), when originating from a channel will
|
||||
// default to replying with "<user>, <message>". See also Reply(). Panics if
|
||||
// the incoming event has no source.
|
||||
func (cmd *Commands) ReplyTo(event Event, message string) {
|
||||
if event.Source == nil {
|
||||
panic(ErrInvalidSource)
|
||||
}
|
||||
|
||||
if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
|
||||
cmd.Message(event.Params[0], event.Source.Name+", "+message)
|
||||
return
|
||||
}
|
||||
|
||||
cmd.Message(event.Source.Name, message)
|
||||
}
|
||||
|
||||
// ReplyTof sends a reply to a channel or user with a format string, based
|
||||
// on where the supplied event originated from. ReplyTo(), when originating
|
||||
// from a channel will default to replying with "<user>, <message>". See
|
||||
// also Replyf(). Panics if the incoming event has no source.
|
||||
func (cmd *Commands) ReplyTof(event Event, format string, a ...interface{}) {
|
||||
cmd.ReplyTo(event, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// Action sends a PRIVMSG ACTION (/me) to target (either channel, service,
|
||||
// or user).
|
||||
func (cmd *Commands) Action(target, message string) {
|
||||
cmd.c.Send(&Event{
|
||||
Command: PRIVMSG,
|
||||
Params: []string{target},
|
||||
Trailing: fmt.Sprintf("\001ACTION %s\001", message),
|
||||
})
|
||||
}
|
||||
|
||||
// Actionf sends a formated PRIVMSG ACTION (/me) to target (either channel,
|
||||
// service, or user).
|
||||
func (cmd *Commands) Actionf(target, format string, a ...interface{}) {
|
||||
cmd.Action(target, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// Notice sends a NOTICE to target (either channel, service, or user).
|
||||
func (cmd *Commands) Notice(target, message string) {
|
||||
cmd.c.Send(&Event{Command: NOTICE, Params: []string{target}, Trailing: message, EmptyTrailing: true})
|
||||
}
|
||||
|
||||
// Noticef sends a formated NOTICE to target (either channel, service, or
|
||||
// user).
|
||||
func (cmd *Commands) Noticef(target, format string, a ...interface{}) {
|
||||
cmd.Notice(target, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// SendRaw sends a raw string (or multiple) to the server, without carriage
|
||||
// returns or newlines. Returns an error if one of the raw strings cannot be
|
||||
// properly parsed.
|
||||
func (cmd *Commands) SendRaw(raw ...string) error {
|
||||
var event *Event
|
||||
|
||||
for i := 0; i < len(raw); i++ {
|
||||
event = ParseEvent(raw[i])
|
||||
if event == nil {
|
||||
return errors.New("invalid event: " + raw[i])
|
||||
}
|
||||
|
||||
cmd.c.Send(event)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendRawf sends a formated string back to the server, without carriage
|
||||
// returns or newlines.
|
||||
func (cmd *Commands) SendRawf(format string, a ...interface{}) error {
|
||||
return cmd.SendRaw(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// Topic sets the topic of channel to message. Does not verify the length
|
||||
// of the topic.
|
||||
func (cmd *Commands) Topic(channel, message string) {
|
||||
cmd.c.Send(&Event{Command: TOPIC, Params: []string{channel}, Trailing: message, EmptyTrailing: true})
|
||||
}
|
||||
|
||||
// Who sends a WHO query to the server, which will attempt WHOX by default.
|
||||
// See http://faerion.sourceforge.net/doc/irc/whox.var for more details. This
|
||||
// sends "%tcuhnr,2" per default. Do not use "1" as this will conflict with
|
||||
// girc's builtin tracking functionality.
|
||||
func (cmd *Commands) Who(users ...string) {
|
||||
for i := 0; i < len(users); i++ {
|
||||
cmd.c.Send(&Event{Command: WHO, Params: []string{users[i], "%tcuhnr,2"}})
|
||||
}
|
||||
}
|
||||
|
||||
// Whois sends a WHOIS query to the server, targeted at a specific user (or
|
||||
// set of users). As WHOIS is a bit slower, you may want to use WHO for brief
|
||||
// user info.
|
||||
func (cmd *Commands) Whois(users ...string) {
|
||||
for i := 0; i < len(users); i++ {
|
||||
cmd.c.Send(&Event{Command: WHOIS, Params: []string{users[i]}})
|
||||
}
|
||||
}
|
||||
|
||||
// Ping sends a PING query to the server, with a specific identifier that
|
||||
// the server should respond with.
|
||||
func (cmd *Commands) Ping(id string) {
|
||||
cmd.c.write(&Event{Command: PING, Params: []string{id}})
|
||||
}
|
||||
|
||||
// Pong sends a PONG query to the server, with an identifier which was
|
||||
// received from a previous PING query received by the client.
|
||||
func (cmd *Commands) Pong(id string) {
|
||||
cmd.c.write(&Event{Command: PONG, Params: []string{id}})
|
||||
}
|
||||
|
||||
// Oper sends a OPER authentication query to the server, with a username
|
||||
// and password.
|
||||
func (cmd *Commands) Oper(user, pass string) {
|
||||
cmd.c.Send(&Event{Command: OPER, Params: []string{user, pass}, Sensitive: true})
|
||||
}
|
||||
|
||||
// Kick sends a KICK query to the server, attempting to kick nick from
|
||||
// channel, with reason. If reason is blank, one will not be sent to the
|
||||
// server.
|
||||
func (cmd *Commands) Kick(channel, user, reason string) {
|
||||
if reason != "" {
|
||||
cmd.c.Send(&Event{Command: KICK, Params: []string{channel, user}, Trailing: reason, EmptyTrailing: true})
|
||||
}
|
||||
|
||||
cmd.c.Send(&Event{Command: KICK, Params: []string{channel, user}})
|
||||
}
|
||||
|
||||
// Ban adds the +b mode on the given mask on a channel.
|
||||
func (cmd *Commands) Ban(channel, mask string) {
|
||||
cmd.Mode(channel, "+b", mask)
|
||||
}
|
||||
|
||||
// Unban removes the +b mode on the given mask on a channel.
|
||||
func (cmd *Commands) Unban(channel, mask string) {
|
||||
cmd.Mode(channel, "-b", mask)
|
||||
}
|
||||
|
||||
// Mode sends a mode change to the server which should be applied to target
|
||||
// (usually a channel or user), along with a set of modes (generally "+m",
|
||||
// "+mmmm", or "-m", where "m" is the mode you want to change). Params is only
|
||||
// needed if the mode change requires a parameter (ban or invite-only exclude.)
|
||||
func (cmd *Commands) Mode(target, modes string, params ...string) {
|
||||
out := []string{target, modes}
|
||||
out = append(out, params...)
|
||||
|
||||
cmd.c.Send(&Event{Command: MODE, Params: out})
|
||||
}
|
||||
|
||||
// Invite sends a INVITE query to the server, to invite nick to channel.
|
||||
func (cmd *Commands) Invite(channel string, users ...string) {
|
||||
for i := 0; i < len(users); i++ {
|
||||
cmd.c.Send(&Event{Command: INVITE, Params: []string{users[i], channel}})
|
||||
}
|
||||
}
|
||||
|
||||
// Away sends a AWAY query to the server, suggesting that the client is no
|
||||
// longer active. If reason is blank, Client.Back() is called. Also see
|
||||
// Client.Back().
|
||||
func (cmd *Commands) Away(reason string) {
|
||||
if reason == "" {
|
||||
cmd.Back()
|
||||
return
|
||||
}
|
||||
|
||||
cmd.c.Send(&Event{Command: AWAY, Params: []string{reason}})
|
||||
}
|
||||
|
||||
// Back sends a AWAY query to the server, however the query is blank,
|
||||
// suggesting that the client is active once again. Also see Client.Away().
|
||||
func (cmd *Commands) Back() {
|
||||
cmd.c.Send(&Event{Command: AWAY})
|
||||
}
|
||||
|
||||
// List sends a LIST query to the server, which will list channels and topics.
|
||||
// Supports multiple channels at once, in hopes it will reduce extensive
|
||||
// LIST queries to the server. Supply no channels to run a list against the
|
||||
// entire server (warning, that may mean LOTS of channels!)
|
||||
func (cmd *Commands) List(channels ...string) {
|
||||
if len(channels) == 0 {
|
||||
cmd.c.Send(&Event{Command: LIST})
|
||||
return
|
||||
}
|
||||
|
||||
// We can LIST multiple channels at once, however we need to ensure that
|
||||
// we are not exceeding the line length. (see maxLength)
|
||||
max := maxLength - len(JOIN) - 1
|
||||
|
||||
var buffer string
|
||||
|
||||
for i := 0; i < len(channels); i++ {
|
||||
if len(buffer+","+channels[i]) > max {
|
||||
cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}})
|
||||
buffer = ""
|
||||
continue
|
||||
}
|
||||
|
||||
if len(buffer) == 0 {
|
||||
buffer = channels[i]
|
||||
} else {
|
||||
buffer += "," + channels[i]
|
||||
}
|
||||
|
||||
if i == len(channels)-1 {
|
||||
cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Whowas sends a WHOWAS query to the server. amount is the amount of results
|
||||
// you want back.
|
||||
func (cmd *Commands) Whowas(user string, amount int) {
|
||||
cmd.c.Send(&Event{Command: WHOWAS, Params: []string{user, string(amount)}})
|
||||
}
|
566
vendor/github.com/lrstanley/girc/conn.go
generated
vendored
Normal file
566
vendor/github.com/lrstanley/girc/conn.go
generated
vendored
Normal file
@ -0,0 +1,566 @@
|
||||
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||
// of this source code is governed by the MIT license that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
package girc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Messages are delimited with CR and LF line endings, we're using the last
|
||||
// one to split the stream. Both are removed during parsing of the message.
|
||||
const delim byte = '\n'
|
||||
|
||||
var endline = []byte("\r\n")
|
||||
|
||||
// ircConn represents an IRC network protocol connection, it consists of an
|
||||
// Encoder and Decoder to manage i/o.
|
||||
type ircConn struct {
|
||||
io *bufio.ReadWriter
|
||||
sock net.Conn
|
||||
|
||||
mu sync.RWMutex
|
||||
// lastWrite is used to keep track of when we last wrote to the server.
|
||||
lastWrite time.Time
|
||||
// lastActive is the last time the client was interacting with the server,
|
||||
// excluding a few background commands (PING, PONG, WHO, etc).
|
||||
lastActive time.Time
|
||||
// writeDelay is used to keep track of rate limiting of events sent to
|
||||
// the server.
|
||||
writeDelay time.Duration
|
||||
// connected is true if we're actively connected to a server.
|
||||
connected bool
|
||||
// connTime is the time at which the client has connected to a server.
|
||||
connTime *time.Time
|
||||
// lastPing is the last time that we pinged the server.
|
||||
lastPing time.Time
|
||||
// lastPong is the last successful time that we pinged the server and
|
||||
// received a successful pong back.
|
||||
lastPong time.Time
|
||||
pingDelay time.Duration
|
||||
}
|
||||
|
||||
// Dialer is an interface implementation of net.Dialer. Use this if you would
|
||||
// like to implement your own dialer which the client will use when connecting.
|
||||
type Dialer interface {
|
||||
// Dial takes two arguments. Network, which should be similar to "tcp",
|
||||
// "tdp6", "udp", etc -- as well as address, which is the hostname or ip
|
||||
// of the network. Note that network can be ignored if your transport
|
||||
// doesn't take advantage of network types.
|
||||
Dial(network, address string) (net.Conn, error)
|
||||
}
|
||||
|
||||
// newConn sets up and returns a new connection to the server.
|
||||
func newConn(conf Config, dialer Dialer, addr string) (*ircConn, error) {
|
||||
if err := conf.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var conn net.Conn
|
||||
var err error
|
||||
|
||||
if dialer == nil {
|
||||
netDialer := &net.Dialer{Timeout: 5 * time.Second}
|
||||
|
||||
if conf.Bind != "" {
|
||||
var local *net.TCPAddr
|
||||
local, err = net.ResolveTCPAddr("tcp", conf.Bind+":0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
netDialer.LocalAddr = local
|
||||
}
|
||||
|
||||
dialer = netDialer
|
||||
}
|
||||
|
||||
if conn, err = dialer.Dial("tcp", addr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if conf.SSL {
|
||||
var tlsConn net.Conn
|
||||
tlsConn, err = tlsHandshake(conn, conf.TLSConfig, conf.Server, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn = tlsConn
|
||||
}
|
||||
|
||||
ctime := time.Now()
|
||||
|
||||
c := &ircConn{
|
||||
sock: conn,
|
||||
connTime: &ctime,
|
||||
connected: true,
|
||||
}
|
||||
c.newReadWriter()
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func newMockConn(conn net.Conn) *ircConn {
|
||||
ctime := time.Now()
|
||||
c := &ircConn{
|
||||
sock: conn,
|
||||
connTime: &ctime,
|
||||
connected: true,
|
||||
}
|
||||
c.newReadWriter()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// ErrParseEvent is returned when an event cannot be parsed with ParseEvent().
|
||||
type ErrParseEvent struct {
|
||||
Line string
|
||||
}
|
||||
|
||||
func (e ErrParseEvent) Error() string { return "unable to parse event: " + e.Line }
|
||||
|
||||
func (c *ircConn) decode() (event *Event, err error) {
|
||||
line, err := c.io.ReadString(delim)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if event = ParseEvent(line); event == nil {
|
||||
return nil, ErrParseEvent{line}
|
||||
}
|
||||
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func (c *ircConn) encode(event *Event) error {
|
||||
if _, err := c.io.Write(event.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.io.Write(endline); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.io.Flush()
|
||||
}
|
||||
|
||||
func (c *ircConn) newReadWriter() {
|
||||
c.io = bufio.NewReadWriter(bufio.NewReader(c.sock), bufio.NewWriter(c.sock))
|
||||
}
|
||||
|
||||
func tlsHandshake(conn net.Conn, conf *tls.Config, server string, validate bool) (net.Conn, error) {
|
||||
if conf == nil {
|
||||
conf = &tls.Config{ServerName: server, InsecureSkipVerify: !validate}
|
||||
}
|
||||
|
||||
tlsConn := tls.Client(conn, conf)
|
||||
return net.Conn(tlsConn), nil
|
||||
}
|
||||
|
||||
// Close closes the underlying socket.
|
||||
func (c *ircConn) Close() error {
|
||||
return c.sock.Close()
|
||||
}
|
||||
|
||||
// Connect attempts to connect to the given IRC server. Returns only when
|
||||
// an error has occurred, or a disconnect was requested with Close(). Connect
|
||||
// will only return once all client-based goroutines have been closed to
|
||||
// ensure there are no long-running routines becoming backed up.
|
||||
//
|
||||
// Connect will wait for all non-goroutine handlers to complete on error/quit,
|
||||
// however it will not wait for goroutine-based handlers.
|
||||
//
|
||||
// If this returns nil, this means that the client requested to be closed
|
||||
// (e.g. Client.Close()). Connect will panic if called when the last call has
|
||||
// not completed.
|
||||
func (c *Client) Connect() error {
|
||||
return c.internalConnect(nil, nil)
|
||||
}
|
||||
|
||||
// DialerConnect allows you to specify your own custom dialer which implements
|
||||
// the Dialer interface.
|
||||
//
|
||||
// An example of using this library would be to take advantage of the
|
||||
// golang.org/x/net/proxy library:
|
||||
//
|
||||
// proxyUrl, _ := proxyURI, err = url.Parse("socks5://1.2.3.4:8888")
|
||||
// dialer, _ := proxy.FromURL(proxyURI, &net.Dialer{Timeout: 5 * time.Second})
|
||||
// _ := girc.DialerConnect(dialer)
|
||||
func (c *Client) DialerConnect(dialer Dialer) error {
|
||||
return c.internalConnect(nil, dialer)
|
||||
}
|
||||
|
||||
// MockConnect is used to implement mocking with an IRC server. Supply a net.Conn
|
||||
// that will be used to spoof the server. A useful way to do this is to so
|
||||
// net.Pipe(), pass one end into MockConnect(), and the other end into
|
||||
// bufio.NewReader().
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// client := girc.New(girc.Config{
|
||||
// Server: "dummy.int",
|
||||
// Port: 6667,
|
||||
// Nick: "test",
|
||||
// User: "test",
|
||||
// Name: "Testing123",
|
||||
// })
|
||||
//
|
||||
// in, out := net.Pipe()
|
||||
// defer in.Close()
|
||||
// defer out.Close()
|
||||
// b := bufio.NewReader(in)
|
||||
//
|
||||
// go func() {
|
||||
// if err := client.MockConnect(out); err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// }()
|
||||
//
|
||||
// defer client.Close(false)
|
||||
//
|
||||
// for {
|
||||
// in.SetReadDeadline(time.Now().Add(300 * time.Second))
|
||||
// line, err := b.ReadString(byte('\n'))
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
//
|
||||
// event := girc.ParseEvent(line)
|
||||
//
|
||||
// if event == nil {
|
||||
// continue
|
||||
// }
|
||||
//
|
||||
// // Do stuff with event here.
|
||||
// }
|
||||
func (c *Client) MockConnect(conn net.Conn) error {
|
||||
return c.internalConnect(conn, nil)
|
||||
}
|
||||
|
||||
func (c *Client) internalConnect(mock net.Conn, dialer Dialer) error {
|
||||
// We want to be the only one handling connects/disconnects right now.
|
||||
c.mu.Lock()
|
||||
|
||||
if c.conn != nil {
|
||||
panic("use of connect more than once")
|
||||
}
|
||||
|
||||
// Reset the state.
|
||||
c.state.reset()
|
||||
|
||||
if mock == nil {
|
||||
// Validate info, and actually make the connection.
|
||||
c.debug.Printf("connecting to %s...", c.Server())
|
||||
conn, err := newConn(c.Config, dialer, c.Server())
|
||||
if err != nil {
|
||||
c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
c.conn = conn
|
||||
} else {
|
||||
c.conn = newMockConn(mock)
|
||||
}
|
||||
|
||||
var ctx context.Context
|
||||
ctx, c.stop = context.WithCancel(context.Background())
|
||||
c.mu.Unlock()
|
||||
|
||||
errs := make(chan error, 4)
|
||||
var wg sync.WaitGroup
|
||||
// 4 being the number of goroutines we need to finish when this function
|
||||
// returns.
|
||||
wg.Add(4)
|
||||
go c.execLoop(ctx, errs, &wg)
|
||||
go c.readLoop(ctx, errs, &wg)
|
||||
go c.sendLoop(ctx, errs, &wg)
|
||||
go c.pingLoop(ctx, errs, &wg)
|
||||
|
||||
// Passwords first.
|
||||
if c.Config.ServerPass != "" {
|
||||
c.write(&Event{Command: PASS, Params: []string{c.Config.ServerPass}, Sensitive: true})
|
||||
}
|
||||
|
||||
// List the IRCv3 capabilities, specifically with the max protocol we
|
||||
// support. The IRCv3 specification doesn't directly state if this should
|
||||
// be called directly before registration, or if it should be called
|
||||
// after NICK/USER requests. It looks like non-supporting networks
|
||||
// should ignore this, and some IRCv3 capable networks require this to
|
||||
// occur before NICK/USER registration.
|
||||
c.listCAP()
|
||||
|
||||
// Then nickname.
|
||||
c.write(&Event{Command: NICK, Params: []string{c.Config.Nick}})
|
||||
|
||||
// Then username and realname.
|
||||
if c.Config.Name == "" {
|
||||
c.Config.Name = c.Config.User
|
||||
}
|
||||
|
||||
c.write(&Event{Command: USER, Params: []string{c.Config.User, "*", "*"}, Trailing: c.Config.Name})
|
||||
|
||||
// Send a virtual event allowing hooks for successful socket connection.
|
||||
c.RunHandlers(&Event{Command: INITIALIZED, Trailing: c.Server()})
|
||||
|
||||
// Wait for the first error.
|
||||
var result error
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
c.debug.Print("received request to close, beginning clean up")
|
||||
c.RunHandlers(&Event{Command: STOPPED, Trailing: c.Server()})
|
||||
case err := <-errs:
|
||||
c.debug.Print("received error, beginning clean up")
|
||||
result = err
|
||||
}
|
||||
|
||||
// Make sure that the connection is closed if not already.
|
||||
c.mu.RLock()
|
||||
if c.stop != nil {
|
||||
c.stop()
|
||||
}
|
||||
c.conn.mu.Lock()
|
||||
c.conn.connected = false
|
||||
_ = c.conn.Close()
|
||||
c.conn.mu.Unlock()
|
||||
c.mu.RUnlock()
|
||||
|
||||
// Once we have our error/result, let all other functions know we're done.
|
||||
c.debug.Print("waiting for all routines to finish")
|
||||
|
||||
// Wait for all goroutines to finish.
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
|
||||
// This helps ensure that the end user isn't improperly using the client
|
||||
// more than once. If they want to do this, they should be using multiple
|
||||
// clients, not multiple instances of Connect().
|
||||
c.mu.Lock()
|
||||
c.conn = nil
|
||||
c.mu.Unlock()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// readLoop sets a timeout of 300 seconds, and then attempts to read from the
|
||||
// IRC server. If there is an error, it calls Reconnect.
|
||||
func (c *Client) readLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
|
||||
c.debug.Print("starting readLoop")
|
||||
defer c.debug.Print("closing readLoop")
|
||||
|
||||
var event *Event
|
||||
var err error
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
wg.Done()
|
||||
return
|
||||
default:
|
||||
_ = c.conn.sock.SetReadDeadline(time.Now().Add(300 * time.Second))
|
||||
event, err = c.conn.decode()
|
||||
if err != nil {
|
||||
errs <- err
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
|
||||
c.rx <- event
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send sends an event to the server. Use Client.RunHandlers() if you are
|
||||
// simply looking to trigger handlers with an event.
|
||||
func (c *Client) Send(event *Event) {
|
||||
if !c.Config.AllowFlood {
|
||||
<-time.After(c.conn.rate(event.Len()))
|
||||
}
|
||||
|
||||
if c.Config.GlobalFormat && event.Trailing != "" &&
|
||||
(event.Command == PRIVMSG || event.Command == TOPIC || event.Command == NOTICE) {
|
||||
event.Trailing = Fmt(event.Trailing)
|
||||
}
|
||||
|
||||
c.write(event)
|
||||
}
|
||||
|
||||
// write is the lower level function to write an event. It does not have a
|
||||
// write-delay when sending events.
|
||||
func (c *Client) write(event *Event) {
|
||||
c.tx <- event
|
||||
}
|
||||
|
||||
// rate allows limiting events based on how frequent the event is being sent,
|
||||
// as well as how many characters each event has.
|
||||
func (c *ircConn) rate(chars int) time.Duration {
|
||||
_time := time.Second + ((time.Duration(chars) * time.Second) / 100)
|
||||
|
||||
c.mu.Lock()
|
||||
if c.writeDelay += _time - time.Now().Sub(c.lastWrite); c.writeDelay < 0 {
|
||||
c.writeDelay = 0
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
if c.writeDelay > (8 * time.Second) {
|
||||
return _time
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
|
||||
c.debug.Print("starting sendLoop")
|
||||
defer c.debug.Print("closing sendLoop")
|
||||
|
||||
var err error
|
||||
|
||||
for {
|
||||
select {
|
||||
case event := <-c.tx:
|
||||
// Check if tags exist on the event. If they do, and message-tags
|
||||
// isn't a supported capability, remove them from the event.
|
||||
if event.Tags != nil {
|
||||
c.state.RLock()
|
||||
var in bool
|
||||
for i := 0; i < len(c.state.enabledCap); i++ {
|
||||
if c.state.enabledCap[i] == "message-tags" {
|
||||
in = true
|
||||
break
|
||||
}
|
||||
}
|
||||
c.state.RUnlock()
|
||||
|
||||
if !in {
|
||||
event.Tags = Tags{}
|
||||
}
|
||||
}
|
||||
|
||||
// Log the event.
|
||||
if event.Sensitive {
|
||||
c.debug.Printf("> %s ***redacted***", event.Command)
|
||||
} else {
|
||||
c.debug.Print("> ", StripRaw(event.String()))
|
||||
}
|
||||
if c.Config.Out != nil {
|
||||
if pretty, ok := event.Pretty(); ok {
|
||||
fmt.Fprintln(c.Config.Out, StripRaw(pretty))
|
||||
}
|
||||
}
|
||||
|
||||
c.conn.mu.Lock()
|
||||
c.conn.lastWrite = time.Now()
|
||||
|
||||
if event.Command != PING && event.Command != PONG && event.Command != WHO {
|
||||
c.conn.lastActive = c.conn.lastWrite
|
||||
}
|
||||
c.conn.mu.Unlock()
|
||||
|
||||
// Write the raw line.
|
||||
_, err = c.conn.io.Write(event.Bytes())
|
||||
if err == nil {
|
||||
// And the \r\n.
|
||||
_, err = c.conn.io.Write(endline)
|
||||
if err == nil {
|
||||
// Lastly, flush everything to the socket.
|
||||
err = c.conn.io.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errs <- err
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
case <-ctx.Done():
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ErrTimedOut is returned when we attempt to ping the server, and timed out
|
||||
// before receiving a PONG back.
|
||||
type ErrTimedOut struct {
|
||||
// TimeSinceSuccess is how long ago we received a successful pong.
|
||||
TimeSinceSuccess time.Duration
|
||||
// LastPong is the time we received our last successful pong.
|
||||
LastPong time.Time
|
||||
// LastPong is the last time we sent a pong request.
|
||||
LastPing time.Time
|
||||
// Delay is the configured delay between how often we send a ping request.
|
||||
Delay time.Duration
|
||||
}
|
||||
|
||||
func (ErrTimedOut) Error() string { return "timed out during ping to server" }
|
||||
|
||||
func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
|
||||
// Don't run the pingLoop if they want to disable it.
|
||||
if c.Config.PingDelay <= 0 {
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
|
||||
c.debug.Print("starting pingLoop")
|
||||
defer c.debug.Print("closing pingLoop")
|
||||
|
||||
c.conn.mu.Lock()
|
||||
c.conn.lastPing = time.Now()
|
||||
c.conn.lastPong = time.Now()
|
||||
c.conn.mu.Unlock()
|
||||
|
||||
tick := time.NewTicker(c.Config.PingDelay)
|
||||
defer tick.Stop()
|
||||
|
||||
started := time.Now()
|
||||
past := false
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
// Delay during connect to wait for the client to register, otherwise
|
||||
// some ircd's will not respond (e.g. during SASL negotiation).
|
||||
if !past {
|
||||
if time.Since(started) < 30*time.Second {
|
||||
continue
|
||||
}
|
||||
|
||||
past = true
|
||||
}
|
||||
|
||||
c.conn.mu.RLock()
|
||||
if time.Since(c.conn.lastPong) > c.Config.PingDelay+(60*time.Second) {
|
||||
// It's 60 seconds over what out ping delay is, connection
|
||||
// has probably dropped.
|
||||
errs <- ErrTimedOut{
|
||||
TimeSinceSuccess: time.Since(c.conn.lastPong),
|
||||
LastPong: c.conn.lastPong,
|
||||
LastPing: c.conn.lastPing,
|
||||
Delay: c.Config.PingDelay,
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
c.conn.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
c.conn.mu.RUnlock()
|
||||
|
||||
c.conn.mu.Lock()
|
||||
c.conn.lastPing = time.Now()
|
||||
c.conn.mu.Unlock()
|
||||
|
||||
c.Cmd.Ping(fmt.Sprintf("%d", time.Now().UnixNano()))
|
||||
case <-ctx.Done():
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
338
vendor/github.com/lrstanley/girc/contants.go
generated
vendored
Normal file
338
vendor/github.com/lrstanley/girc/contants.go
generated
vendored
Normal file
@ -0,0 +1,338 @@
|
||||
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||
// of this source code is governed by the MIT license that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
package girc
|
||||
|
||||
// Standard CTCP based constants.
|
||||
const (
|
||||
CTCP_PING = "PING"
|
||||
CTCP_PONG = "PONG"
|
||||
CTCP_VERSION = "VERSION"
|
||||
CTCP_USERINFO = "USERINFO"
|
||||
CTCP_CLIENTINFO = "CLIENTINFO"
|
||||
CTCP_SOURCE = "SOURCE"
|
||||
CTCP_TIME = "TIME"
|
||||
CTCP_FINGER = "FINGER"
|
||||
CTCP_ERRMSG = "ERRMSG"
|
||||
)
|
||||
|
||||
// Emulated event commands used to allow easier hooks into the changing
|
||||
// state of the client.
|
||||
const (
|
||||
UPDATE_STATE = "CLIENT_STATE_UPDATED" // when channel/user state is updated.
|
||||
UPDATE_GENERAL = "CLIENT_GENERAL_UPDATED" // when general state (client nick, server name, etc) is updated.
|
||||
ALL_EVENTS = "*" // trigger on all events
|
||||
CONNECTED = "CLIENT_CONNECTED" // when it's safe to send arbitrary commands (joins, list, who, etc), trailing is host:port
|
||||
INITIALIZED = "CLIENT_INIT" // verifies successful socket connection, trailing is host:port
|
||||
DISCONNECTED = "CLIENT_DISCONNECTED" // occurs when we're disconnected from the server (user-requested or not)
|
||||
STOPPED = "CLIENT_STOPPED" // occurs when Client.Stop() has been called
|
||||
)
|
||||
|
||||
// User/channel prefixes :: RFC1459.
|
||||
const (
|
||||
DefaultPrefixes = "(ov)@+" // the most common default prefixes
|
||||
ModeAddPrefix = "+" // modes are being added
|
||||
ModeDelPrefix = "-" // modes are being removed
|
||||
|
||||
ChannelPrefix = "#" // regular channel
|
||||
DistributedPrefix = "&" // distributed channel
|
||||
OwnerPrefix = "~" // user owner +q (non-rfc)
|
||||
AdminPrefix = "&" // user admin +a (non-rfc)
|
||||
HalfOperatorPrefix = "%" // user half operator +h (non-rfc)
|
||||
OperatorPrefix = "@" // user operator +o
|
||||
VoicePrefix = "+" // user has voice +v
|
||||
)
|
||||
|
||||
// User modes :: RFC1459; section 4.2.3.2.
|
||||
const (
|
||||
UserModeInvisible = "i" // invisible
|
||||
UserModeOperator = "o" // server operator
|
||||
UserModeServerNotices = "s" // user wants to receive server notices
|
||||
UserModeWallops = "w" // user wants to receive wallops
|
||||
)
|
||||
|
||||
// Channel modes :: RFC1459; section 4.2.3.1.
|
||||
const (
|
||||
ModeDefaults = "beI,k,l,imnpst" // the most common default modes
|
||||
|
||||
ModeInviteOnly = "i" // only join with an invite
|
||||
ModeKey = "k" // channel password
|
||||
ModeLimit = "l" // user limit
|
||||
ModeModerated = "m" // only voiced users and operators can talk
|
||||
ModeOperator = "o" // operator
|
||||
ModePrivate = "p" // private
|
||||
ModeSecret = "s" // secret
|
||||
ModeTopic = "t" // must be op to set topic
|
||||
ModeVoice = "v" // speak during moderation mode
|
||||
|
||||
ModeOwner = "q" // owner privileges (non-rfc)
|
||||
ModeAdmin = "a" // admin privileges (non-rfc)
|
||||
ModeHalfOperator = "h" // half-operator privileges (non-rfc)
|
||||
)
|
||||
|
||||
// IRC commands :: RFC2812; section 3 :: RFC2813; section 4.
|
||||
const (
|
||||
ADMIN = "ADMIN"
|
||||
AWAY = "AWAY"
|
||||
CONNECT = "CONNECT"
|
||||
DIE = "DIE"
|
||||
ERROR = "ERROR"
|
||||
INFO = "INFO"
|
||||
INVITE = "INVITE"
|
||||
ISON = "ISON"
|
||||
JOIN = "JOIN"
|
||||
KICK = "KICK"
|
||||
KILL = "KILL"
|
||||
LINKS = "LINKS"
|
||||
LIST = "LIST"
|
||||
LUSERS = "LUSERS"
|
||||
MODE = "MODE"
|
||||
MOTD = "MOTD"
|
||||
NAMES = "NAMES"
|
||||
NICK = "NICK"
|
||||
NJOIN = "NJOIN"
|
||||
NOTICE = "NOTICE"
|
||||
OPER = "OPER"
|
||||
PART = "PART"
|
||||
PASS = "PASS"
|
||||
PING = "PING"
|
||||
PONG = "PONG"
|
||||
PRIVMSG = "PRIVMSG"
|
||||
QUIT = "QUIT"
|
||||
REHASH = "REHASH"
|
||||
RESTART = "RESTART"
|
||||
SERVER = "SERVER"
|
||||
SERVICE = "SERVICE"
|
||||
SERVLIST = "SERVLIST"
|
||||
SQUERY = "SQUERY"
|
||||
SQUIT = "SQUIT"
|
||||
STATS = "STATS"
|
||||
SUMMON = "SUMMON"
|
||||
TIME = "TIME"
|
||||
TOPIC = "TOPIC"
|
||||
TRACE = "TRACE"
|
||||
USER = "USER"
|
||||
USERHOST = "USERHOST"
|
||||
USERS = "USERS"
|
||||
VERSION = "VERSION"
|
||||
WALLOPS = "WALLOPS"
|
||||
WHO = "WHO"
|
||||
WHOIS = "WHOIS"
|
||||
WHOWAS = "WHOWAS"
|
||||
)
|
||||
|
||||
// Numeric IRC reply mapping :: RFC2812; section 5.
|
||||
const (
|
||||
RPL_WELCOME = "001"
|
||||
RPL_YOURHOST = "002"
|
||||
RPL_CREATED = "003"
|
||||
RPL_MYINFO = "004"
|
||||
RPL_BOUNCE = "005"
|
||||
RPL_ISUPPORT = "005"
|
||||
RPL_USERHOST = "302"
|
||||
RPL_ISON = "303"
|
||||
RPL_AWAY = "301"
|
||||
RPL_UNAWAY = "305"
|
||||
RPL_NOWAWAY = "306"
|
||||
RPL_WHOISUSER = "311"
|
||||
RPL_WHOISSERVER = "312"
|
||||
RPL_WHOISOPERATOR = "313"
|
||||
RPL_WHOISIDLE = "317"
|
||||
RPL_ENDOFWHOIS = "318"
|
||||
RPL_WHOISCHANNELS = "319"
|
||||
RPL_WHOWASUSER = "314"
|
||||
RPL_ENDOFWHOWAS = "369"
|
||||
RPL_LISTSTART = "321"
|
||||
RPL_LIST = "322"
|
||||
RPL_LISTEND = "323"
|
||||
RPL_UNIQOPIS = "325"
|
||||
RPL_CHANNELMODEIS = "324"
|
||||
RPL_NOTOPIC = "331"
|
||||
RPL_TOPIC = "332"
|
||||
RPL_INVITING = "341"
|
||||
RPL_SUMMONING = "342"
|
||||
RPL_INVITELIST = "346"
|
||||
RPL_ENDOFINVITELIST = "347"
|
||||
RPL_EXCEPTLIST = "348"
|
||||
RPL_ENDOFEXCEPTLIST = "349"
|
||||
RPL_VERSION = "351"
|
||||
RPL_WHOREPLY = "352"
|
||||
RPL_ENDOFWHO = "315"
|
||||
RPL_NAMREPLY = "353"
|
||||
RPL_ENDOFNAMES = "366"
|
||||
RPL_LINKS = "364"
|
||||
RPL_ENDOFLINKS = "365"
|
||||
RPL_BANLIST = "367"
|
||||
RPL_ENDOFBANLIST = "368"
|
||||
RPL_INFO = "371"
|
||||
RPL_ENDOFINFO = "374"
|
||||
RPL_MOTDSTART = "375"
|
||||
RPL_MOTD = "372"
|
||||
RPL_ENDOFMOTD = "376"
|
||||
RPL_YOUREOPER = "381"
|
||||
RPL_REHASHING = "382"
|
||||
RPL_YOURESERVICE = "383"
|
||||
RPL_TIME = "391"
|
||||
RPL_USERSSTART = "392"
|
||||
RPL_USERS = "393"
|
||||
RPL_ENDOFUSERS = "394"
|
||||
RPL_NOUSERS = "395"
|
||||
RPL_TRACELINK = "200"
|
||||
RPL_TRACECONNECTING = "201"
|
||||
RPL_TRACEHANDSHAKE = "202"
|
||||
RPL_TRACEUNKNOWN = "203"
|
||||
RPL_TRACEOPERATOR = "204"
|
||||
RPL_TRACEUSER = "205"
|
||||
RPL_TRACESERVER = "206"
|
||||
RPL_TRACESERVICE = "207"
|
||||
RPL_TRACENEWTYPE = "208"
|
||||
RPL_TRACECLASS = "209"
|
||||
RPL_TRACERECONNECT = "210"
|
||||
RPL_TRACELOG = "261"
|
||||
RPL_TRACEEND = "262"
|
||||
RPL_STATSLINKINFO = "211"
|
||||
RPL_STATSCOMMANDS = "212"
|
||||
RPL_ENDOFSTATS = "219"
|
||||
RPL_STATSUPTIME = "242"
|
||||
RPL_STATSOLINE = "243"
|
||||
RPL_UMODEIS = "221"
|
||||
RPL_SERVLIST = "234"
|
||||
RPL_SERVLISTEND = "235"
|
||||
RPL_LUSERCLIENT = "251"
|
||||
RPL_LUSEROP = "252"
|
||||
RPL_LUSERUNKNOWN = "253"
|
||||
RPL_LUSERCHANNELS = "254"
|
||||
RPL_LUSERME = "255"
|
||||
RPL_ADMINME = "256"
|
||||
RPL_ADMINLOC1 = "257"
|
||||
RPL_ADMINLOC2 = "258"
|
||||
RPL_ADMINEMAIL = "259"
|
||||
RPL_TRYAGAIN = "263"
|
||||
ERR_NOSUCHNICK = "401"
|
||||
ERR_NOSUCHSERVER = "402"
|
||||
ERR_NOSUCHCHANNEL = "403"
|
||||
ERR_CANNOTSENDTOCHAN = "404"
|
||||
ERR_TOOMANYCHANNELS = "405"
|
||||
ERR_WASNOSUCHNICK = "406"
|
||||
ERR_TOOMANYTARGETS = "407"
|
||||
ERR_NOSUCHSERVICE = "408"
|
||||
ERR_NOORIGIN = "409"
|
||||
ERR_NORECIPIENT = "411"
|
||||
ERR_NOTEXTTOSEND = "412"
|
||||
ERR_NOTOPLEVEL = "413"
|
||||
ERR_WILDTOPLEVEL = "414"
|
||||
ERR_BADMASK = "415"
|
||||
ERR_UNKNOWNCOMMAND = "421"
|
||||
ERR_NOMOTD = "422"
|
||||
ERR_NOADMININFO = "423"
|
||||
ERR_FILEERROR = "424"
|
||||
ERR_NONICKNAMEGIVEN = "431"
|
||||
ERR_ERRONEUSNICKNAME = "432"
|
||||
ERR_NICKNAMEINUSE = "433"
|
||||
ERR_NICKCOLLISION = "436"
|
||||
ERR_UNAVAILRESOURCE = "437"
|
||||
ERR_USERNOTINCHANNEL = "441"
|
||||
ERR_NOTONCHANNEL = "442"
|
||||
ERR_USERONCHANNEL = "443"
|
||||
ERR_NOLOGIN = "444"
|
||||
ERR_SUMMONDISABLED = "445"
|
||||
ERR_USERSDISABLED = "446"
|
||||
ERR_NOTREGISTERED = "451"
|
||||
ERR_NEEDMOREPARAMS = "461"
|
||||
ERR_ALREADYREGISTRED = "462"
|
||||
ERR_NOPERMFORHOST = "463"
|
||||
ERR_PASSWDMISMATCH = "464"
|
||||
ERR_YOUREBANNEDCREEP = "465"
|
||||
ERR_YOUWILLBEBANNED = "466"
|
||||
ERR_KEYSET = "467"
|
||||
ERR_CHANNELISFULL = "471"
|
||||
ERR_UNKNOWNMODE = "472"
|
||||
ERR_INVITEONLYCHAN = "473"
|
||||
ERR_BANNEDFROMCHAN = "474"
|
||||
ERR_BADCHANNELKEY = "475"
|
||||
ERR_BADCHANMASK = "476"
|
||||
ERR_NOCHANMODES = "477"
|
||||
ERR_BANLISTFULL = "478"
|
||||
ERR_NOPRIVILEGES = "481"
|
||||
ERR_CHANOPRIVSNEEDED = "482"
|
||||
ERR_CANTKILLSERVER = "483"
|
||||
ERR_RESTRICTED = "484"
|
||||
ERR_UNIQOPPRIVSNEEDED = "485"
|
||||
ERR_NOOPERHOST = "491"
|
||||
ERR_UMODEUNKNOWNFLAG = "501"
|
||||
ERR_USERSDONTMATCH = "502"
|
||||
)
|
||||
|
||||
// IRCv3 commands and extensions :: http://ircv3.net/irc/.
|
||||
const (
|
||||
AUTHENTICATE = "AUTHENTICATE"
|
||||
STARTTLS = "STARTTLS"
|
||||
|
||||
CAP = "CAP"
|
||||
CAP_ACK = "ACK"
|
||||
CAP_CLEAR = "CLEAR"
|
||||
CAP_END = "END"
|
||||
CAP_LIST = "LIST"
|
||||
CAP_LS = "LS"
|
||||
CAP_NAK = "NAK"
|
||||
CAP_REQ = "REQ"
|
||||
CAP_NEW = "NEW"
|
||||
CAP_DEL = "DEL"
|
||||
|
||||
CAP_CHGHOST = "CHGHOST"
|
||||
CAP_AWAY = "AWAY"
|
||||
CAP_ACCOUNT = "ACCOUNT"
|
||||
)
|
||||
|
||||
// Numeric IRC reply mapping for ircv3 :: http://ircv3.net/irc/.
|
||||
const (
|
||||
RPL_LOGGEDIN = "900"
|
||||
RPL_LOGGEDOUT = "901"
|
||||
RPL_NICKLOCKED = "902"
|
||||
RPL_SASLSUCCESS = "903"
|
||||
ERR_SASLFAIL = "904"
|
||||
ERR_SASLTOOLONG = "905"
|
||||
ERR_SASLABORTED = "906"
|
||||
ERR_SASLALREADY = "907"
|
||||
RPL_SASLMECHS = "908"
|
||||
RPL_STARTTLS = "670"
|
||||
ERR_STARTTLS = "691"
|
||||
)
|
||||
|
||||
// Numeric IRC event mapping :: RFC2812; section 5.3.
|
||||
const (
|
||||
RPL_STATSCLINE = "213"
|
||||
RPL_STATSNLINE = "214"
|
||||
RPL_STATSILINE = "215"
|
||||
RPL_STATSKLINE = "216"
|
||||
RPL_STATSQLINE = "217"
|
||||
RPL_STATSYLINE = "218"
|
||||
RPL_SERVICEINFO = "231"
|
||||
RPL_ENDOFSERVICES = "232"
|
||||
RPL_SERVICE = "233"
|
||||
RPL_STATSVLINE = "240"
|
||||
RPL_STATSLLINE = "241"
|
||||
RPL_STATSHLINE = "244"
|
||||
RPL_STATSSLINE = "245"
|
||||
RPL_STATSPING = "246"
|
||||
RPL_STATSBLINE = "247"
|
||||
RPL_STATSDLINE = "250"
|
||||
RPL_NONE = "300"
|
||||
RPL_WHOISCHANOP = "316"
|
||||
RPL_KILLDONE = "361"
|
||||
RPL_CLOSING = "362"
|
||||
RPL_CLOSEEND = "363"
|
||||
RPL_INFOSTART = "373"
|
||||
RPL_MYPORTIS = "384"
|
||||
ERR_NOSERVICEHOST = "492"
|
||||
)
|
||||
|
||||
// Misc.
|
||||
const (
|
||||
ERR_TOOMANYMATCHES = "416" // IRCNet.
|
||||
RPL_GLOBALUSERS = "266" // aircd/hybrid/bahamut, used on freenode.
|
||||
RPL_LOCALUSERS = "265" // aircd/hybrid/bahamut, used on freenode.
|
||||
RPL_TOPICWHOTIME = "333" // ircu, used on freenode.
|
||||
RPL_WHOSPCRPL = "354" // ircu, used on networks with WHOX support.
|
||||
)
|
288
vendor/github.com/lrstanley/girc/ctcp.go
generated
vendored
Normal file
288
vendor/github.com/lrstanley/girc/ctcp.go
generated
vendored
Normal file
@ -0,0 +1,288 @@
|
||||
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||
// of this source code is governed by the MIT license that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
package girc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ctcpDelim if the delimiter used for CTCP formatted events/messages.
|
||||
const ctcpDelim byte = 0x01 // Prefix and suffix for CTCP messages.
|
||||
|
||||
// CTCPEvent is the necessary information from an IRC message.
|
||||
type CTCPEvent struct {
|
||||
// Origin is the original event that the CTCP event was decoded from.
|
||||
Origin *Event `json:"origin"`
|
||||
// Source is the author of the CTCP event.
|
||||
Source *Source `json:"source"`
|
||||
// Command is the type of CTCP event. E.g. PING, TIME, VERSION.
|
||||
Command string `json:"command"`
|
||||
// Text is the raw arguments following the command.
|
||||
Text string `json:"text"`
|
||||
// Reply is true if the CTCP event is intended to be a reply to a
|
||||
// previous CTCP (e.g, if we sent one).
|
||||
Reply bool `json:"reply"`
|
||||
}
|
||||
|
||||
// decodeCTCP decodes an incoming CTCP event, if it is CTCP. nil is returned
|
||||
// if the incoming event does not match a valid CTCP.
|
||||
func decodeCTCP(e *Event) *CTCPEvent {
|
||||
// http://www.irchelp.org/protocol/ctcpspec.html
|
||||
|
||||
// Must be targeting a user/channel, AND trailing must have
|
||||
// DELIM+TAG+DELIM minimum (at least 3 chars).
|
||||
if len(e.Params) != 1 || len(e.Trailing) < 3 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if (e.Command != PRIVMSG && e.Command != NOTICE) || !IsValidNick(e.Params[0]) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if e.Trailing[0] != ctcpDelim || e.Trailing[len(e.Trailing)-1] != ctcpDelim {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Strip delimiters.
|
||||
text := e.Trailing[1 : len(e.Trailing)-1]
|
||||
|
||||
s := strings.IndexByte(text, eventSpace)
|
||||
|
||||
// Check to see if it only contains a tag.
|
||||
if s < 0 {
|
||||
for i := 0; i < len(text); i++ {
|
||||
// Check for A-Z, 0-9.
|
||||
if (text[i] < 'A' || text[i] > 'Z') && (text[i] < '0' || text[i] > '9') {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return &CTCPEvent{
|
||||
Origin: e,
|
||||
Source: e.Source,
|
||||
Command: text,
|
||||
Reply: e.Command == NOTICE,
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through checking the tag first.
|
||||
for i := 0; i < s; i++ {
|
||||
// Check for A-Z, 0-9.
|
||||
if (text[i] < 'A' || text[i] > 'Z') && (text[i] < '0' || text[i] > '9') {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return &CTCPEvent{
|
||||
Origin: e,
|
||||
Source: e.Source,
|
||||
Command: text[0:s],
|
||||
Text: text[s+1:],
|
||||
Reply: e.Command == NOTICE,
|
||||
}
|
||||
}
|
||||
|
||||
// encodeCTCP encodes a CTCP event into a string, including delimiters.
|
||||
func encodeCTCP(ctcp *CTCPEvent) (out string) {
|
||||
if ctcp == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return encodeCTCPRaw(ctcp.Command, ctcp.Text)
|
||||
}
|
||||
|
||||
// encodeCTCPRaw is much like encodeCTCP, however accepts a raw command and
|
||||
// string as input.
|
||||
func encodeCTCPRaw(cmd, text string) (out string) {
|
||||
if len(cmd) <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
out = string(ctcpDelim) + cmd
|
||||
|
||||
if len(text) > 0 {
|
||||
out += string(eventSpace) + text
|
||||
}
|
||||
|
||||
return out + string(ctcpDelim)
|
||||
}
|
||||
|
||||
// CTCP handles the storage and execution of CTCP handlers against incoming
|
||||
// CTCP events.
|
||||
type CTCP struct {
|
||||
// mu is the mutex that should be used when accessing any ctcp handlers.
|
||||
mu sync.RWMutex
|
||||
// handlers is a map of CTCP message -> functions.
|
||||
handlers map[string]CTCPHandler
|
||||
}
|
||||
|
||||
// newCTCP returns a new clean CTCP handler.
|
||||
func newCTCP() *CTCP {
|
||||
return &CTCP{handlers: map[string]CTCPHandler{}}
|
||||
}
|
||||
|
||||
// call executes the necessary CTCP handler for the incoming event/CTCP
|
||||
// command.
|
||||
func (c *CTCP) call(client *Client, event *CTCPEvent) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
// If they want to catch any panics, add to defer stack.
|
||||
if client.Config.RecoverFunc != nil && event.Origin != nil {
|
||||
defer recoverHandlerPanic(client, event.Origin, "ctcp-"+strings.ToLower(event.Command), 3)
|
||||
}
|
||||
|
||||
// Support wildcard CTCP event handling. Gets executed first before
|
||||
// regular event handlers.
|
||||
if _, ok := c.handlers["*"]; ok {
|
||||
c.handlers["*"](client, *event)
|
||||
}
|
||||
|
||||
if _, ok := c.handlers[event.Command]; !ok {
|
||||
// Send a ERRMSG reply, if we know who sent it.
|
||||
if event.Source != nil && IsValidNick(event.Source.Name) {
|
||||
client.Cmd.SendCTCPReply(event.Source.Name, CTCP_ERRMSG, "that is an unknown CTCP query")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.handlers[event.Command](client, *event)
|
||||
}
|
||||
|
||||
// parseCMD parses a CTCP command/tag, ensuring it's valid. If not, an empty
|
||||
// string is returned.
|
||||
func (c *CTCP) parseCMD(cmd string) string {
|
||||
// TODO: Needs proper testing.
|
||||
// Check if wildcard.
|
||||
if cmd == "*" {
|
||||
return "*"
|
||||
}
|
||||
|
||||
cmd = strings.ToUpper(cmd)
|
||||
|
||||
for i := 0; i < len(cmd); i++ {
|
||||
// Check for A-Z, 0-9.
|
||||
if (cmd[i] < 'A' || cmd[i] > 'Z') && (cmd[i] < '0' || cmd[i] > '9') {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Set saves handler for execution upon a matching incoming CTCP event.
|
||||
// Use SetBg if the handler may take an extended period of time to execute.
|
||||
// If you would like to have a handler which will catch ALL CTCP requests,
|
||||
// simply use "*" in place of the command.
|
||||
func (c *CTCP) Set(cmd string, handler func(client *Client, ctcp CTCPEvent)) {
|
||||
if cmd = c.parseCMD(cmd); cmd == "" {
|
||||
return
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.handlers[cmd] = CTCPHandler(handler)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetBg is much like Set, however the handler is executed in the background,
|
||||
// ensuring that event handling isn't hung during long running tasks. See Set
|
||||
// for more information.
|
||||
func (c *CTCP) SetBg(cmd string, handler func(client *Client, ctcp CTCPEvent)) {
|
||||
c.Set(cmd, func(client *Client, ctcp CTCPEvent) {
|
||||
go handler(client, ctcp)
|
||||
})
|
||||
}
|
||||
|
||||
// Clear removes currently setup handler for cmd, if one is set.
|
||||
func (c *CTCP) Clear(cmd string) {
|
||||
if cmd = c.parseCMD(cmd); cmd == "" {
|
||||
return
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
delete(c.handlers, cmd)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// ClearAll removes all currently setup and re-sets the default handlers.
|
||||
func (c *CTCP) ClearAll() {
|
||||
c.mu.Lock()
|
||||
c.handlers = map[string]CTCPHandler{}
|
||||
c.mu.Unlock()
|
||||
|
||||
// Register necessary handlers.
|
||||
c.addDefaultHandlers()
|
||||
}
|
||||
|
||||
// CTCPHandler is a type that represents the function necessary to
|
||||
// implement a CTCP handler.
|
||||
type CTCPHandler func(client *Client, ctcp CTCPEvent)
|
||||
|
||||
// addDefaultHandlers adds some useful default CTCP response handlers.
|
||||
func (c *CTCP) addDefaultHandlers() {
|
||||
c.SetBg(CTCP_PING, handleCTCPPing)
|
||||
c.SetBg(CTCP_PONG, handleCTCPPong)
|
||||
c.SetBg(CTCP_VERSION, handleCTCPVersion)
|
||||
c.SetBg(CTCP_SOURCE, handleCTCPSource)
|
||||
c.SetBg(CTCP_TIME, handleCTCPTime)
|
||||
c.SetBg(CTCP_FINGER, handleCTCPFinger)
|
||||
}
|
||||
|
||||
// handleCTCPPing replies with a ping and whatever was originally requested.
|
||||
func handleCTCPPing(client *Client, ctcp CTCPEvent) {
|
||||
if ctcp.Reply {
|
||||
return
|
||||
}
|
||||
client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_PING, ctcp.Text)
|
||||
}
|
||||
|
||||
// handleCTCPPong replies with a pong.
|
||||
func handleCTCPPong(client *Client, ctcp CTCPEvent) {
|
||||
if ctcp.Reply {
|
||||
return
|
||||
}
|
||||
client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_PONG, "")
|
||||
}
|
||||
|
||||
// handleCTCPVersion replies with the name of the client, Go version, as well
|
||||
// as the os type (darwin, linux, windows, etc) and architecture type (x86,
|
||||
// arm, etc).
|
||||
func handleCTCPVersion(client *Client, ctcp CTCPEvent) {
|
||||
if client.Config.Version != "" {
|
||||
client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_VERSION, client.Config.Version)
|
||||
return
|
||||
}
|
||||
|
||||
client.Cmd.SendCTCPReplyf(
|
||||
ctcp.Source.Name, CTCP_VERSION,
|
||||
"girc (github.com/lrstanley/girc) using %s (%s, %s)",
|
||||
runtime.Version(), runtime.GOOS, runtime.GOARCH,
|
||||
)
|
||||
}
|
||||
|
||||
// handleCTCPSource replies with the public git location of this library.
|
||||
func handleCTCPSource(client *Client, ctcp CTCPEvent) {
|
||||
client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_SOURCE, "https://github.com/lrstanley/girc")
|
||||
}
|
||||
|
||||
// handleCTCPTime replies with a RFC 1123 (Z) formatted version of Go's
|
||||
// local time.
|
||||
func handleCTCPTime(client *Client, ctcp CTCPEvent) {
|
||||
client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_TIME, ":"+time.Now().Format(time.RFC1123Z))
|
||||
}
|
||||
|
||||
// handleCTCPFinger replies with the realname and idle time of the user. This
|
||||
// is obsoleted by improvements to the IRC protocol, however still supported.
|
||||
func handleCTCPFinger(client *Client, ctcp CTCPEvent) {
|
||||
client.conn.mu.RLock()
|
||||
active := client.conn.lastActive
|
||||
client.conn.mu.RUnlock()
|
||||
|
||||
client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_FINGER, fmt.Sprintf("%s -- idle %s", client.Config.Name, time.Since(active)))
|
||||
}
|
12
vendor/github.com/lrstanley/girc/doc.go
generated
vendored
Normal file
12
vendor/github.com/lrstanley/girc/doc.go
generated
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
// Package girc provides a high level, yet flexible IRC library for use with
|
||||
// interacting with IRC servers. girc has support for user/channel tracking,
|
||||
// as well as a few other neat features (like auto-reconnect).
|
||||
//
|
||||
// Much of what girc can do, can also be disabled. The goal is to provide a
|
||||
// solid API that you don't necessarily have to work with out of the box if
|
||||
// you don't want to.
|
||||
//
|
||||
// See the examples below for a few brief and useful snippets taking
|
||||
// advantage of girc, which should give you a general idea of how the API
|
||||
// works.
|
||||
package girc
|
550
vendor/github.com/lrstanley/girc/event.go
generated
vendored
Normal file
550
vendor/github.com/lrstanley/girc/event.go
generated
vendored
Normal file
@ -0,0 +1,550 @@
|
||||
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||
// of this source code is governed by the MIT license that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
package girc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
eventSpace byte = ' ' // Separator.
|
||||
maxLength = 510 // Maximum length is 510 (2 for line endings).
|
||||
)
|
||||
|
||||
// cutCRFunc is used to trim CR characters from prefixes/messages.
|
||||
func cutCRFunc(r rune) bool {
|
||||
return r == '\r' || r == '\n'
|
||||
}
|
||||
|
||||
// Event represents an IRC protocol message, see RFC1459 section 2.3.1
|
||||
//
|
||||
// <message> :: [':' <prefix> <SPACE>] <command> <params> <crlf>
|
||||
// <prefix> :: <servername> | <nick> ['!' <user>] ['@' <host>]
|
||||
// <command> :: <letter>{<letter>} | <number> <number> <number>
|
||||
// <SPACE> :: ' '{' '}
|
||||
// <params> :: <SPACE> [':' <trailing> | <middle> <params>]
|
||||
// <middle> :: <Any *non-empty* sequence of octets not including SPACE or NUL
|
||||
// or CR or LF, the first of which may not be ':'>
|
||||
// <trailing> :: <Any, possibly empty, sequence of octets not including NUL or
|
||||
// CR or LF>
|
||||
// <crlf> :: CR LF
|
||||
type Event struct {
|
||||
Source *Source `json:"source"` // The source of the event.
|
||||
Tags Tags `json:"tags"` // IRCv3 style message tags. Only use if network supported.
|
||||
Command string `json:"command"` // the IRC command, e.g. JOIN, PRIVMSG, KILL.
|
||||
Params []string `json:"params"` // parameters to the command. Commonly nickname, channel, etc.
|
||||
Trailing string `json:"trailing"` // any trailing data. e.g. with a PRIVMSG, this is the message text.
|
||||
EmptyTrailing bool `json:"empty_trailing"` // if true, trailing prefix (:) will be added even if Event.Trailing is empty.
|
||||
Sensitive bool `json:"sensitive"` // if the message is sensitive (e.g. and should not be logged).
|
||||
}
|
||||
|
||||
// ParseEvent takes a string and attempts to create a Event struct.
|
||||
//
|
||||
// Returns nil if the Event is invalid.
|
||||
func ParseEvent(raw string) (e *Event) {
|
||||
// Ignore empty events.
|
||||
if raw = strings.TrimFunc(raw, cutCRFunc); len(raw) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var i, j int
|
||||
e = &Event{}
|
||||
|
||||
if raw[0] == prefixTag {
|
||||
// Tags end with a space.
|
||||
i = strings.IndexByte(raw, eventSpace)
|
||||
|
||||
if i < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
e.Tags = ParseTags(raw[1:i])
|
||||
raw = raw[i+1:]
|
||||
}
|
||||
|
||||
if raw[0] == messagePrefix {
|
||||
// Prefix ends with a space.
|
||||
i = strings.IndexByte(raw, eventSpace)
|
||||
|
||||
// Prefix string must not be empty if the indicator is present.
|
||||
if i < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
e.Source = ParseSource(raw[1:i])
|
||||
|
||||
// Skip space at the end of the prefix.
|
||||
i++
|
||||
}
|
||||
|
||||
// Find end of command.
|
||||
j = i + strings.IndexByte(raw[i:], eventSpace)
|
||||
|
||||
if j < i {
|
||||
// If there are no proceeding spaces, it's the only thing specified.
|
||||
e.Command = strings.ToUpper(raw[i:])
|
||||
return e
|
||||
}
|
||||
|
||||
e.Command = strings.ToUpper(raw[i:j])
|
||||
|
||||
// Skip the space after the command.
|
||||
j++
|
||||
|
||||
// Check if and where the trailing text is within the incoming line.
|
||||
var lastIndex, trailerIndex int
|
||||
for {
|
||||
// We must loop through, as it's possible that the first message
|
||||
// prefix is not actually what we want. (e.g, colons are commonly
|
||||
// used within ISUPPORT to delegate things like CHANLIMIT or TARGMAX.)
|
||||
lastIndex = trailerIndex
|
||||
trailerIndex = strings.IndexByte(raw[j+lastIndex:], messagePrefix)
|
||||
|
||||
if trailerIndex == -1 {
|
||||
// No trailing argument found, assume the rest is just params.
|
||||
e.Params = strings.Split(raw[j:], string(eventSpace))
|
||||
return e
|
||||
}
|
||||
|
||||
// This means we found a prefix that was proceeded by a space, and
|
||||
// it's good to assume this is the start of trailing text to the line.
|
||||
if raw[j+lastIndex+trailerIndex-1] == eventSpace {
|
||||
i = lastIndex + trailerIndex
|
||||
break
|
||||
}
|
||||
|
||||
// Keep looping through until we either can't find any more prefixes,
|
||||
// or we find the one we want.
|
||||
trailerIndex += lastIndex + 1
|
||||
}
|
||||
|
||||
// Set i to that of the substring we were using before, and where the
|
||||
// trailing prefix is.
|
||||
i = j + i
|
||||
|
||||
// Check if we need to parse arguments. If so, take everything after the
|
||||
// command, and right before the trailing prefix, and cut it up.
|
||||
if i > j {
|
||||
e.Params = strings.Split(raw[j:i-1], string(eventSpace))
|
||||
}
|
||||
|
||||
e.Trailing = raw[i+1:]
|
||||
|
||||
// We need to re-encode the trailing argument even if it was empty.
|
||||
if len(e.Trailing) <= 0 {
|
||||
e.EmptyTrailing = true
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// Copy makes a deep copy of a given event, for use with allowing untrusted
|
||||
// functions/handlers edit the event without causing potential issues with
|
||||
// other handlers.
|
||||
func (e *Event) Copy() *Event {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
newEvent := &Event{
|
||||
Command: e.Command,
|
||||
Trailing: e.Trailing,
|
||||
EmptyTrailing: e.EmptyTrailing,
|
||||
Sensitive: e.Sensitive,
|
||||
}
|
||||
|
||||
// Copy Source field, as it's a pointer and needs to be dereferenced.
|
||||
if e.Source != nil {
|
||||
newEvent.Source = e.Source.Copy()
|
||||
}
|
||||
|
||||
// Copy Params in order to dereference as well.
|
||||
if e.Params != nil {
|
||||
newEvent.Params = make([]string, len(e.Params))
|
||||
copy(newEvent.Params, e.Params)
|
||||
}
|
||||
|
||||
// Copy tags as necessary.
|
||||
if e.Tags != nil {
|
||||
newEvent.Tags = Tags{}
|
||||
for k, v := range e.Tags {
|
||||
newEvent.Tags[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return newEvent
|
||||
}
|
||||
|
||||
// Len calculates the length of the string representation of event. Note that
|
||||
// this will return the true length (even if longer than what IRC supports),
|
||||
// which may be useful if you are trying to check and see if a message is
|
||||
// too long, to trim it down yourself.
|
||||
func (e *Event) Len() (length int) {
|
||||
if e.Tags != nil {
|
||||
// Include tags and trailing space.
|
||||
length = e.Tags.Len() + 1
|
||||
}
|
||||
if e.Source != nil {
|
||||
// Include prefix and trailing space.
|
||||
length += e.Source.Len() + 2
|
||||
}
|
||||
|
||||
length += len(e.Command)
|
||||
|
||||
if len(e.Params) > 0 {
|
||||
length += len(e.Params)
|
||||
|
||||
for i := 0; i < len(e.Params); i++ {
|
||||
length += len(e.Params[i])
|
||||
}
|
||||
}
|
||||
|
||||
if len(e.Trailing) > 0 || e.EmptyTrailing {
|
||||
// Include prefix and space.
|
||||
length += len(e.Trailing) + 2
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Bytes returns a []byte representation of event. Strips all newlines and
|
||||
// carriage returns.
|
||||
//
|
||||
// Per RFC2812 section 2.3, messages should not exceed 512 characters in
|
||||
// length. This method forces that limit by discarding any characters
|
||||
// exceeding the length limit.
|
||||
func (e *Event) Bytes() []byte {
|
||||
buffer := new(bytes.Buffer)
|
||||
|
||||
// Tags.
|
||||
if e.Tags != nil {
|
||||
e.Tags.writeTo(buffer)
|
||||
}
|
||||
|
||||
// Event prefix.
|
||||
if e.Source != nil {
|
||||
buffer.WriteByte(messagePrefix)
|
||||
e.Source.writeTo(buffer)
|
||||
buffer.WriteByte(eventSpace)
|
||||
}
|
||||
|
||||
// Command is required.
|
||||
buffer.WriteString(e.Command)
|
||||
|
||||
// Space separated list of arguments.
|
||||
if len(e.Params) > 0 {
|
||||
buffer.WriteByte(eventSpace)
|
||||
buffer.WriteString(strings.Join(e.Params, string(eventSpace)))
|
||||
}
|
||||
|
||||
if len(e.Trailing) > 0 || e.EmptyTrailing {
|
||||
buffer.WriteByte(eventSpace)
|
||||
buffer.WriteByte(messagePrefix)
|
||||
buffer.WriteString(e.Trailing)
|
||||
}
|
||||
|
||||
// We need the limit the buffer length.
|
||||
if buffer.Len() > (maxLength) {
|
||||
buffer.Truncate(maxLength)
|
||||
}
|
||||
|
||||
out := buffer.Bytes()
|
||||
|
||||
// Strip newlines and carriage returns.
|
||||
for i := 0; i < len(out); i++ {
|
||||
if out[i] == '\n' || out[i] == '\r' {
|
||||
out = append(out[:i], out[i+1:]...)
|
||||
i-- // Decrease the index so we can pick up where we left off.
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// String returns a string representation of this event. Strips all newlines
|
||||
// and carriage returns.
|
||||
func (e *Event) String() string {
|
||||
return string(e.Bytes())
|
||||
}
|
||||
|
||||
// Pretty returns a prettified string of the event. If the event doesn't
|
||||
// support prettification, ok is false. Pretty is not just useful to make
|
||||
// an event prettier, but also to filter out events that most don't visually
|
||||
// see in normal IRC clients. e.g. most clients don't show WHO queries.
|
||||
func (e *Event) Pretty() (out string, ok bool) {
|
||||
if e.Sensitive {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if e.Command == ERROR {
|
||||
return fmt.Sprintf("[*] an error occurred: %s", e.Trailing), true
|
||||
}
|
||||
|
||||
if e.Source == nil {
|
||||
if e.Command != PRIVMSG && e.Command != NOTICE {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if len(e.Params) > 0 && len(e.Trailing) > 0 {
|
||||
return fmt.Sprintf("[>] writing %s [%s]: %s", strings.ToLower(e.Command), strings.Join(e.Params, ", "), e.Trailing), true
|
||||
} else if len(e.Params) > 0 {
|
||||
return fmt.Sprintf("[>] writing %s [%s]", strings.ToLower(e.Command), strings.Join(e.Params, ", ")), true
|
||||
} else if len(e.Trailing) > 0 {
|
||||
return fmt.Sprintf("[>] writing %s: %s", strings.ToLower(e.Command), e.Trailing), true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
if e.Command == INITIALIZED {
|
||||
return fmt.Sprintf("[*] connection to %s initialized", e.Trailing), true
|
||||
}
|
||||
|
||||
if e.Command == CONNECTED {
|
||||
return fmt.Sprintf("[*] successfully connected to %s", e.Trailing), true
|
||||
}
|
||||
|
||||
if (e.Command == PRIVMSG || e.Command == NOTICE) && len(e.Params) > 0 {
|
||||
if ctcp := decodeCTCP(e); ctcp != nil {
|
||||
if ctcp.Reply {
|
||||
return
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[*] CTCP query from %s: %s%s", ctcp.Source.Name, ctcp.Command, " "+ctcp.Text), true
|
||||
}
|
||||
return fmt.Sprintf("[%s] (%s) %s", strings.Join(e.Params, ","), e.Source.Name, e.Trailing), true
|
||||
}
|
||||
|
||||
if e.Command == RPL_MOTD || e.Command == RPL_MOTDSTART ||
|
||||
e.Command == RPL_WELCOME || e.Command == RPL_YOURHOST ||
|
||||
e.Command == RPL_CREATED || e.Command == RPL_LUSERCLIENT {
|
||||
return "[*] " + e.Trailing, true
|
||||
}
|
||||
|
||||
if e.Command == JOIN && len(e.Params) > 0 {
|
||||
return fmt.Sprintf("[*] %s (%s) has joined %s", e.Source.Name, e.Source.Host, e.Params[0]), true
|
||||
}
|
||||
|
||||
if e.Command == PART && len(e.Params) > 0 {
|
||||
return fmt.Sprintf("[*] %s (%s) has left %s (%s)", e.Source.Name, e.Source.Host, e.Params[0], e.Trailing), true
|
||||
}
|
||||
|
||||
if e.Command == QUIT {
|
||||
return fmt.Sprintf("[*] %s has quit (%s)", e.Source.Name, e.Trailing), true
|
||||
}
|
||||
|
||||
if e.Command == KICK && len(e.Params) == 2 {
|
||||
return fmt.Sprintf("[%s] *** %s has kicked %s: %s", e.Params[0], e.Source.Name, e.Params[1], e.Trailing), true
|
||||
}
|
||||
|
||||
if e.Command == NICK && len(e.Params) == 1 {
|
||||
return fmt.Sprintf("[*] %s is now known as %s", e.Source.Name, e.Params[0]), true
|
||||
}
|
||||
|
||||
if e.Command == TOPIC && len(e.Params) > 0 {
|
||||
return fmt.Sprintf("[%s] *** %s has set the topic to: %s", e.Params[len(e.Params)-1], e.Source.Name, e.Trailing), true
|
||||
}
|
||||
|
||||
if e.Command == MODE && len(e.Params) > 2 {
|
||||
return fmt.Sprintf("[%s] *** %s set modes: %s", e.Params[0], e.Source.Name, strings.Join(e.Params[1:], " ")), true
|
||||
}
|
||||
|
||||
if e.Command == CAP_AWAY {
|
||||
if len(e.Trailing) > 0 {
|
||||
return fmt.Sprintf("[*] %s is now away: %s", e.Source.Name, e.Trailing), true
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[*] %s is no longer away", e.Source.Name), true
|
||||
}
|
||||
|
||||
if e.Command == CAP_CHGHOST && len(e.Params) == 2 {
|
||||
return fmt.Sprintf("[*] %s has changed their host to %s (was %s)", e.Source.Name, e.Params[1], e.Source.Host), true
|
||||
}
|
||||
|
||||
if e.Command == CAP_ACCOUNT && len(e.Params) == 1 {
|
||||
if e.Params[0] == "*" {
|
||||
return fmt.Sprintf("[*] %s has become un-authenticated", e.Source.Name), true
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[*] %s has authenticated for account: %s", e.Source.Name, e.Params[0]), true
|
||||
}
|
||||
|
||||
if e.Command == RPL_TOPIC && len(e.Params) > 0 && len(e.Trailing) > 0 {
|
||||
return fmt.Sprintf("[*] topic for %s is: %s", e.Params[len(e.Params)-1], e.Trailing), true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// IsAction checks to see if the event is a PRIVMSG, and is an ACTION (/me).
|
||||
func (e *Event) IsAction() bool {
|
||||
if e.Source == nil || e.Command != PRIVMSG || len(e.Trailing) < 9 {
|
||||
return false
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(e.Trailing, "\001ACTION") || e.Trailing[len(e.Trailing)-1] != ctcpDelim {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsFromChannel checks to see if a message was from a channel (rather than
|
||||
// a private message).
|
||||
func (e *Event) IsFromChannel() bool {
|
||||
if e.Source == nil || e.Command != PRIVMSG || len(e.Params) < 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
if !IsValidChannel(e.Params[0]) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsFromUser checks to see if a message was from a user (rather than a
|
||||
// channel).
|
||||
func (e *Event) IsFromUser() bool {
|
||||
if e.Source == nil || e.Command != PRIVMSG || len(e.Params) < 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
if !IsValidNick(e.Params[0]) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// StripAction returns the stripped version of the action encoding from a
|
||||
// PRIVMSG ACTION (/me).
|
||||
func (e *Event) StripAction() string {
|
||||
if !e.IsAction() {
|
||||
return e.Trailing
|
||||
}
|
||||
|
||||
return e.Trailing[8 : len(e.Trailing)-1]
|
||||
}
|
||||
|
||||
const (
|
||||
messagePrefix byte = ':' // Prefix or last argument.
|
||||
prefixIdent byte = '!' // Username.
|
||||
prefixHost byte = '@' // Hostname.
|
||||
)
|
||||
|
||||
// Source represents the sender of an IRC event, see RFC1459 section 2.3.1.
|
||||
// <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
|
||||
type Source struct {
|
||||
// Name is the nickname, server name, or service name.
|
||||
Name string `json:"name"`
|
||||
// Ident is commonly known as the "user".
|
||||
Ident string `json:"ident"`
|
||||
// Host is the hostname or IP address of the user/service. Is not accurate
|
||||
// due to how IRC servers can spoof hostnames.
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
// Copy returns a deep copy of Source.
|
||||
func (s *Source) Copy() *Source {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
newSource := &Source{
|
||||
Name: s.Name,
|
||||
Ident: s.Ident,
|
||||
Host: s.Host,
|
||||
}
|
||||
|
||||
return newSource
|
||||
}
|
||||
|
||||
// ParseSource takes a string and attempts to create a Source struct.
|
||||
func ParseSource(raw string) (src *Source) {
|
||||
src = new(Source)
|
||||
|
||||
user := strings.IndexByte(raw, prefixIdent)
|
||||
host := strings.IndexByte(raw, prefixHost)
|
||||
|
||||
switch {
|
||||
case user > 0 && host > user:
|
||||
src.Name = raw[:user]
|
||||
src.Ident = raw[user+1 : host]
|
||||
src.Host = raw[host+1:]
|
||||
case user > 0:
|
||||
src.Name = raw[:user]
|
||||
src.Ident = raw[user+1:]
|
||||
case host > 0:
|
||||
src.Name = raw[:host]
|
||||
src.Host = raw[host+1:]
|
||||
default:
|
||||
src.Name = raw
|
||||
}
|
||||
|
||||
return src
|
||||
}
|
||||
|
||||
// Len calculates the length of the string representation of prefix
|
||||
func (s *Source) Len() (length int) {
|
||||
length = len(s.Name)
|
||||
if len(s.Ident) > 0 {
|
||||
length = 1 + length + len(s.Ident)
|
||||
}
|
||||
if len(s.Host) > 0 {
|
||||
length = 1 + length + len(s.Host)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Bytes returns a []byte representation of source.
|
||||
func (s *Source) Bytes() []byte {
|
||||
buffer := new(bytes.Buffer)
|
||||
s.writeTo(buffer)
|
||||
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
// String returns a string representation of source.
|
||||
func (s *Source) String() (out string) {
|
||||
out = s.Name
|
||||
if len(s.Ident) > 0 {
|
||||
out = out + string(prefixIdent) + s.Ident
|
||||
}
|
||||
if len(s.Host) > 0 {
|
||||
out = out + string(prefixHost) + s.Host
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IsHostmask returns true if source looks like a user hostmask.
|
||||
func (s *Source) IsHostmask() bool {
|
||||
return len(s.Ident) > 0 && len(s.Host) > 0
|
||||
}
|
||||
|
||||
// IsServer returns true if this source looks like a server name.
|
||||
func (s *Source) IsServer() bool {
|
||||
return len(s.Ident) <= 0 && len(s.Host) <= 0
|
||||
}
|
||||
|
||||
// writeTo is an utility function to write the source to the bytes.Buffer
|
||||
// in Event.String().
|
||||
func (s *Source) writeTo(buffer *bytes.Buffer) {
|
||||
buffer.WriteString(s.Name)
|
||||
if len(s.Ident) > 0 {
|
||||
buffer.WriteByte(prefixIdent)
|
||||
buffer.WriteString(s.Ident)
|
||||
}
|
||||
if len(s.Host) > 0 {
|
||||
buffer.WriteByte(prefixHost)
|
||||
buffer.WriteString(s.Host)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
352
vendor/github.com/lrstanley/girc/format.go
generated
vendored
Normal file
352
vendor/github.com/lrstanley/girc/format.go
generated
vendored
Normal file
@ -0,0 +1,352 @@
|
||||
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||
// of this source code is governed by the MIT license that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
package girc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
fmtOpenChar = '{'
|
||||
fmtCloseChar = '}'
|
||||
)
|
||||
|
||||
var fmtColors = map[string]int{
|
||||
"white": 0,
|
||||
"black": 1,
|
||||
"blue": 2,
|
||||
"navy": 2,
|
||||
"green": 3,
|
||||
"red": 4,
|
||||
"brown": 5,
|
||||
"maroon": 5,
|
||||
"purple": 6,
|
||||
"gold": 7,
|
||||
"olive": 7,
|
||||
"orange": 7,
|
||||
"yellow": 8,
|
||||
"lightgreen": 9,
|
||||
"lime": 9,
|
||||
"teal": 10,
|
||||
"cyan": 11,
|
||||
"lightblue": 12,
|
||||
"royal": 12,
|
||||
"fuchsia": 13,
|
||||
"lightpurple": 13,
|
||||
"pink": 13,
|
||||
"gray": 14,
|
||||
"grey": 14,
|
||||
"lightgrey": 15,
|
||||
"silver": 15,
|
||||
}
|
||||
|
||||
var fmtCodes = map[string]string{
|
||||
"bold": "\x02",
|
||||
"b": "\x02",
|
||||
"italic": "\x1d",
|
||||
"i": "\x1d",
|
||||
"reset": "\x0f",
|
||||
"r": "\x0f",
|
||||
"clear": "\x03",
|
||||
"c": "\x03", // Clears formatting.
|
||||
"reverse": "\x16",
|
||||
"underline": "\x1f",
|
||||
"ul": "\x1f",
|
||||
"ctcp": "\x01", // CTCP/ACTION delimiter.
|
||||
}
|
||||
|
||||
// Fmt takes format strings like "{red}" or "{red,blue}" (for background
|
||||
// colors) and turns them into the resulting ASCII format/color codes for IRC.
|
||||
// See format.go for the list of supported format codes allowed.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// client.Message("#channel", Fmt("{red}{b}Hello {red,blue}World{c}"))
|
||||
func Fmt(text string) string {
|
||||
var last = -1
|
||||
for i := 0; i < len(text); i++ {
|
||||
if text[i] == fmtOpenChar {
|
||||
last = i
|
||||
continue
|
||||
}
|
||||
|
||||
if text[i] == fmtCloseChar && last > -1 {
|
||||
code := strings.ToLower(text[last+1 : i])
|
||||
|
||||
// Check to see if they're passing in a second (background) color
|
||||
// as {fgcolor,bgcolor}.
|
||||
var secondary string
|
||||
if com := strings.Index(code, ","); com > -1 {
|
||||
secondary = code[com+1:]
|
||||
code = code[:com]
|
||||
}
|
||||
|
||||
var repl string
|
||||
|
||||
if color, ok := fmtColors[code]; ok {
|
||||
repl = fmt.Sprintf("\x03%02d", color)
|
||||
}
|
||||
|
||||
if repl != "" && secondary != "" {
|
||||
if color, ok := fmtColors[secondary]; ok {
|
||||
repl += fmt.Sprintf(",%02d", color)
|
||||
}
|
||||
}
|
||||
|
||||
if repl == "" {
|
||||
if fmtCode, ok := fmtCodes[code]; ok {
|
||||
repl = fmtCode
|
||||
}
|
||||
}
|
||||
|
||||
next := len(text[:last]+repl) - 1
|
||||
text = text[:last] + repl + text[i+1:]
|
||||
last = -1
|
||||
i = next
|
||||
continue
|
||||
}
|
||||
|
||||
if last > -1 {
|
||||
// A-Z, a-z, and ","
|
||||
if text[i] != ',' && (text[i] <= 'A' || text[i] >= 'Z') && (text[i] <= 'a' || text[i] >= 'z') {
|
||||
last = -1
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
// TrimFmt strips all "{fmt}" formatting strings from the input text.
|
||||
// See Fmt() for more information.
|
||||
func TrimFmt(text string) string {
|
||||
for color := range fmtColors {
|
||||
text = strings.Replace(text, string(fmtOpenChar)+color+string(fmtCloseChar), "", -1)
|
||||
}
|
||||
for code := range fmtCodes {
|
||||
text = strings.Replace(text, string(fmtOpenChar)+code+string(fmtCloseChar), "", -1)
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
// This is really the only fastest way of doing this (marginably better than
|
||||
// actually trying to parse it manually.)
|
||||
var reStripColor = regexp.MustCompile(`\x03([019]?[0-9](,[019]?[0-9])?)?`)
|
||||
|
||||
// StripRaw tries to strip all ASCII format codes that are used for IRC.
|
||||
// Primarily, foreground/background colors, and other control bytes like
|
||||
// reset, bold, italic, reverse, etc. This also is done in a specific way
|
||||
// in order to ensure no truncation of other non-irc formatting.
|
||||
func StripRaw(text string) string {
|
||||
text = reStripColor.ReplaceAllString(text, "")
|
||||
|
||||
for _, code := range fmtCodes {
|
||||
text = strings.Replace(text, code, "", -1)
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
// IsValidChannel validates if channel is an RFC complaint channel or not.
|
||||
//
|
||||
// NOTE: If you are using this to validate a channel that contains a channel
|
||||
// ID, (!<channelid>NAME), this only supports the standard 5 character length.
|
||||
//
|
||||
// NOTE: If you do not need to validate against servers that support unicode,
|
||||
// you may want to ensure that all channel chars are within the range of
|
||||
// all ASCII printable chars. This function will NOT do that for
|
||||
// compatibility reasons.
|
||||
//
|
||||
// channel = ( "#" / "+" / ( "!" channelid ) / "&" ) chanstring
|
||||
// [ ":" chanstring ]
|
||||
// chanstring = 0x01-0x07 / 0x08-0x09 / 0x0B-0x0C / 0x0E-0x1F / 0x21-0x2B
|
||||
// chanstring = / 0x2D-0x39 / 0x3B-0xFF
|
||||
// ; any octet except NUL, BELL, CR, LF, " ", "," and ":"
|
||||
// channelid = 5( 0x41-0x5A / digit ) ; 5( A-Z / 0-9 )
|
||||
func IsValidChannel(channel string) bool {
|
||||
if len(channel) <= 1 || len(channel) > 50 {
|
||||
return false
|
||||
}
|
||||
|
||||
// #, +, !<channelid>, ~, or &
|
||||
// Including "*" and "~" in the prefix list, as these are commonly used
|
||||
// (e.g. ZNC.)
|
||||
if bytes.IndexByte([]byte{'!', '#', '&', '*', '~', '+'}, channel[0]) == -1 {
|
||||
return false
|
||||
}
|
||||
|
||||
// !<channelid> -- not very commonly supported, but we'll check it anyway.
|
||||
// The ID must be 5 chars. This means min-channel size should be:
|
||||
// 1 (prefix) + 5 (id) + 1 (+, channel name)
|
||||
// On some networks, this may be extended with ISUPPORT capabilities,
|
||||
// however this is extremely uncommon.
|
||||
if channel[0] == '!' {
|
||||
if len(channel) < 7 {
|
||||
return false
|
||||
}
|
||||
|
||||
// check for valid ID
|
||||
for i := 1; i < 6; i++ {
|
||||
if (channel[i] < '0' || channel[i] > '9') && (channel[i] < 'A' || channel[i] > 'Z') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for invalid octets here.
|
||||
bad := []byte{0x00, 0x07, 0x0D, 0x0A, 0x20, 0x2C, 0x3A}
|
||||
for i := 1; i < len(channel); i++ {
|
||||
if bytes.IndexByte(bad, channel[i]) != -1 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsValidNick validates an IRC nickame. Note that this does not validate
|
||||
// IRC nickname length.
|
||||
//
|
||||
// nickname = ( letter / special ) *8( letter / digit / special / "-" )
|
||||
// letter = 0x41-0x5A / 0x61-0x7A
|
||||
// digit = 0x30-0x39
|
||||
// special = 0x5B-0x60 / 0x7B-0x7D
|
||||
func IsValidNick(nick string) bool {
|
||||
if len(nick) <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check the first index. Some characters aren't allowed for the first
|
||||
// index of an IRC nickname.
|
||||
if (nick[0] < 'A' || nick[0] > '}') && nick[0] != '?' {
|
||||
// a-z, A-Z, '_\[]{}^|', and '?' in the case of znc.
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 1; i < len(nick); i++ {
|
||||
if (nick[i] < 'A' || nick[i] > '}') && (nick[i] < '0' || nick[i] > '9') && nick[i] != '-' {
|
||||
// a-z, A-Z, 0-9, -, and _\[]{}^|
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsValidUser validates an IRC ident/username. Note that this does not
|
||||
// validate IRC ident length.
|
||||
//
|
||||
// The validation checks are much like what characters are allowed with an
|
||||
// IRC nickname (see IsValidNick()), however an ident/username can:
|
||||
//
|
||||
// 1. Must either start with alphanumberic char, or "~" then alphanumberic
|
||||
// char.
|
||||
//
|
||||
// 2. Contain a "." (period), for use with "first.last". Though, this may
|
||||
// not be supported on all networks. Some limit this to only a single period.
|
||||
//
|
||||
// Per RFC:
|
||||
// user = 1*( %x01-09 / %x0B-0C / %x0E-1F / %x21-3F / %x41-FF )
|
||||
// ; any octet except NUL, CR, LF, " " and "@"
|
||||
func IsValidUser(name string) bool {
|
||||
if len(name) <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// "~" is prepended (commonly) if there was no ident server response.
|
||||
if name[0] == '~' {
|
||||
// Means name only contained "~".
|
||||
if len(name) < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
name = name[1:]
|
||||
}
|
||||
|
||||
// Check to see if the first index is alphanumeric.
|
||||
if (name[0] < 'A' || name[0] > 'J') && (name[0] < 'a' || name[0] > 'z') && (name[0] < '0' || name[0] > '9') {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 1; i < len(name); i++ {
|
||||
if (name[i] < 'A' || name[i] > '}') && (name[i] < '0' || name[i] > '9') && name[i] != '-' && name[i] != '.' {
|
||||
// a-z, A-Z, 0-9, -, and _\[]{}^|
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ToRFC1459 converts a string to the stripped down conversion within RFC
|
||||
// 1459. This will do things like replace an "A" with an "a", "[]" with "{}",
|
||||
// and so forth. Useful to compare two nicknames or channels. Note that this
|
||||
// should not be used to normalize nicknames or similar, as this may convert
|
||||
// valid input characters to non-rfc-valid characters. As such, it's main use
|
||||
// is for comparing two nicks.
|
||||
func ToRFC1459(input string) string {
|
||||
var out string
|
||||
|
||||
for i := 0; i < len(input); i++ {
|
||||
if input[i] >= 65 && input[i] <= 94 {
|
||||
out += string(rune(input[i]) + 32)
|
||||
} else {
|
||||
out += string(input[i])
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
const globChar = "*"
|
||||
|
||||
// Glob will test a string pattern, potentially containing globs, against a
|
||||
// string. The glob character is *.
|
||||
func Glob(input, match string) bool {
|
||||
// Empty pattern.
|
||||
if match == "" {
|
||||
return input == match
|
||||
}
|
||||
|
||||
// If a glob, match all.
|
||||
if match == globChar {
|
||||
return true
|
||||
}
|
||||
|
||||
parts := strings.Split(match, globChar)
|
||||
|
||||
if len(parts) == 1 {
|
||||
// No globs, test for equality.
|
||||
return input == match
|
||||
}
|
||||
|
||||
leadingGlob, trailingGlob := strings.HasPrefix(match, globChar), strings.HasSuffix(match, globChar)
|
||||
last := len(parts) - 1
|
||||
|
||||
// Check prefix first.
|
||||
if !leadingGlob && !strings.HasPrefix(input, parts[0]) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check middle section.
|
||||
for i := 1; i < last; i++ {
|
||||
if !strings.Contains(input, parts[i]) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Trim already-evaluated text from input during loop over match
|
||||
// text.
|
||||
idx := strings.Index(input, parts[i]) + len(parts[i])
|
||||
input = input[idx:]
|
||||
}
|
||||
|
||||
// Check suffix last.
|
||||
return trailingGlob || strings.HasSuffix(input, parts[last])
|
||||
}
|
487
vendor/github.com/lrstanley/girc/handler.go
generated
vendored
Normal file
487
vendor/github.com/lrstanley/girc/handler.go
generated
vendored
Normal file
@ -0,0 +1,487 @@
|
||||
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||
// of this source code is governed by the MIT license that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
package girc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RunHandlers manually runs handlers for a given event.
|
||||
func (c *Client) RunHandlers(event *Event) {
|
||||
if event == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Log the event.
|
||||
c.debug.Print("< " + StripRaw(event.String()))
|
||||
if c.Config.Out != nil {
|
||||
if pretty, ok := event.Pretty(); ok {
|
||||
fmt.Fprintln(c.Config.Out, StripRaw(pretty))
|
||||
}
|
||||
}
|
||||
|
||||
// Background handlers first.
|
||||
c.Handlers.exec(ALL_EVENTS, true, c, event.Copy())
|
||||
c.Handlers.exec(event.Command, true, c, event.Copy())
|
||||
|
||||
c.Handlers.exec(ALL_EVENTS, false, c, event.Copy())
|
||||
c.Handlers.exec(event.Command, false, c, event.Copy())
|
||||
|
||||
// Check if it's a CTCP.
|
||||
if ctcp := decodeCTCP(event.Copy()); ctcp != nil {
|
||||
// Execute it.
|
||||
c.CTCP.call(c, ctcp)
|
||||
}
|
||||
}
|
||||
|
||||
// Handler is lower level implementation of a handler. See
|
||||
// Caller.AddHandler()
|
||||
type Handler interface {
|
||||
Execute(*Client, Event)
|
||||
}
|
||||
|
||||
// HandlerFunc is a type that represents the function necessary to
|
||||
// implement Handler.
|
||||
type HandlerFunc func(client *Client, event Event)
|
||||
|
||||
// Execute calls the HandlerFunc with the sender and irc message.
|
||||
func (f HandlerFunc) Execute(client *Client, event Event) {
|
||||
f(client, event)
|
||||
}
|
||||
|
||||
// Caller manages internal and external (user facing) handlers.
|
||||
type Caller struct {
|
||||
// mu is the mutex that should be used when accessing handlers.
|
||||
mu sync.RWMutex
|
||||
|
||||
// external/internal keys are of structure:
|
||||
// map[COMMAND][CUID]Handler
|
||||
// Also of note: "COMMAND" should always be uppercase for normalization.
|
||||
|
||||
// external is a map of user facing handlers.
|
||||
external map[string]map[string]Handler
|
||||
// internal is a map of internally used handlers for the client.
|
||||
internal map[string]map[string]Handler
|
||||
// debug is the clients logger used for debugging.
|
||||
debug *log.Logger
|
||||
}
|
||||
|
||||
// newCaller creates and initializes a new handler.
|
||||
func newCaller(debugOut *log.Logger) *Caller {
|
||||
c := &Caller{
|
||||
external: map[string]map[string]Handler{},
|
||||
internal: map[string]map[string]Handler{},
|
||||
debug: debugOut,
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Len returns the total amount of user-entered registered handlers.
|
||||
func (c *Caller) Len() int {
|
||||
var total int
|
||||
|
||||
c.mu.RLock()
|
||||
for command := range c.external {
|
||||
total += len(c.external[command])
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
// Count is much like Caller.Len(), however it counts the number of
|
||||
// registered handlers for a given command.
|
||||
func (c *Caller) Count(cmd string) int {
|
||||
var total int
|
||||
|
||||
cmd = strings.ToUpper(cmd)
|
||||
|
||||
c.mu.RLock()
|
||||
for command := range c.external {
|
||||
if command == cmd {
|
||||
total += len(c.external[command])
|
||||
}
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
func (c *Caller) String() string {
|
||||
var total int
|
||||
|
||||
c.mu.RLock()
|
||||
for cmd := range c.internal {
|
||||
total += len(c.internal[cmd])
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
return fmt.Sprintf("<Caller external:%d internal:%d>", c.Len(), total)
|
||||
}
|
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
// cuid generates a unique UID string for each handler for ease of removal.
|
||||
func (c *Caller) cuid(cmd string, n int) (cuid, uid string) {
|
||||
b := make([]byte, n)
|
||||
|
||||
for i := range b {
|
||||
b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
|
||||
}
|
||||
|
||||
return cmd + ":" + string(b), string(b)
|
||||
}
|
||||
|
||||
// cuidToID allows easy mapping between a generated cuid and the caller
|
||||
// external/internal handler maps.
|
||||
func (c *Caller) cuidToID(input string) (cmd, uid string) {
|
||||
i := strings.IndexByte(input, ':')
|
||||
if i < 0 {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return input[:i], input[i+1:]
|
||||
}
|
||||
|
||||
type execStack struct {
|
||||
Handler
|
||||
cuid string
|
||||
}
|
||||
|
||||
// exec executes all handlers pertaining to specified event. Internal first,
|
||||
// then external.
|
||||
//
|
||||
// Please note that there is no specific order/priority for which the handlers
|
||||
// are executed.
|
||||
func (c *Caller) exec(command string, bg bool, client *Client, event *Event) {
|
||||
// Build a stack of handlers which can be executed concurrently.
|
||||
var stack []execStack
|
||||
|
||||
c.mu.RLock()
|
||||
// Get internal handlers first.
|
||||
if _, ok := c.internal[command]; ok {
|
||||
for cuid := range c.internal[command] {
|
||||
if (strings.HasSuffix(cuid, ":bg") && !bg) || (!strings.HasSuffix(cuid, ":bg") && bg) {
|
||||
continue
|
||||
}
|
||||
|
||||
stack = append(stack, execStack{c.internal[command][cuid], cuid})
|
||||
}
|
||||
}
|
||||
|
||||
// Then external handlers.
|
||||
if _, ok := c.external[command]; ok {
|
||||
for cuid := range c.external[command] {
|
||||
if (strings.HasSuffix(cuid, ":bg") && !bg) || (!strings.HasSuffix(cuid, ":bg") && bg) {
|
||||
continue
|
||||
}
|
||||
|
||||
stack = append(stack, execStack{c.external[command][cuid], cuid})
|
||||
}
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
// Run all handlers concurrently across the same event. This should
|
||||
// still help prevent mis-ordered events, while speeding up the
|
||||
// execution speed.
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(stack))
|
||||
for i := 0; i < len(stack); i++ {
|
||||
go func(index int) {
|
||||
defer wg.Done()
|
||||
c.debug.Printf("[%d/%d] exec %s => %s", index+1, len(stack), stack[index].cuid, command)
|
||||
start := time.Now()
|
||||
|
||||
if bg {
|
||||
go func() {
|
||||
if client.Config.RecoverFunc != nil {
|
||||
defer recoverHandlerPanic(client, event, stack[index].cuid, 3)
|
||||
}
|
||||
|
||||
stack[index].Execute(client, *event)
|
||||
c.debug.Printf("[%d/%d] done %s == %s", index+1, len(stack), stack[index].cuid, time.Since(start))
|
||||
}()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if client.Config.RecoverFunc != nil {
|
||||
defer recoverHandlerPanic(client, event, stack[index].cuid, 3)
|
||||
}
|
||||
|
||||
stack[index].Execute(client, *event)
|
||||
c.debug.Printf("[%d/%d] done %s == %s", index+1, len(stack), stack[index].cuid, time.Since(start))
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all of the handlers to complete. Not doing this may cause
|
||||
// new events from becoming ahead of older handlers.
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// ClearAll clears all external handlers currently setup within the client.
|
||||
// This ignores internal handlers.
|
||||
func (c *Caller) ClearAll() {
|
||||
c.mu.Lock()
|
||||
c.external = map[string]map[string]Handler{}
|
||||
c.mu.Unlock()
|
||||
|
||||
c.debug.Print("cleared all external handlers")
|
||||
}
|
||||
|
||||
// clearInternal clears all internal handlers currently setup within the
|
||||
// client.
|
||||
func (c *Caller) clearInternal() {
|
||||
c.mu.Lock()
|
||||
c.internal = map[string]map[string]Handler{}
|
||||
c.mu.Unlock()
|
||||
|
||||
c.debug.Print("cleared all internal handlers")
|
||||
}
|
||||
|
||||
// Clear clears all of the handlers for the given event.
|
||||
// This ignores internal handlers.
|
||||
func (c *Caller) Clear(cmd string) {
|
||||
cmd = strings.ToUpper(cmd)
|
||||
|
||||
c.mu.Lock()
|
||||
if _, ok := c.external[cmd]; ok {
|
||||
delete(c.external, cmd)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
c.debug.Printf("cleared external handlers for %s", cmd)
|
||||
}
|
||||
|
||||
// Remove removes the handler with cuid from the handler stack. success
|
||||
// indicates that it existed, and has been removed. If not success, it
|
||||
// wasn't a registered handler.
|
||||
func (c *Caller) Remove(cuid string) (success bool) {
|
||||
c.mu.Lock()
|
||||
success = c.remove(cuid)
|
||||
c.mu.Unlock()
|
||||
|
||||
return success
|
||||
}
|
||||
|
||||
// remove is much like Remove, however is NOT concurrency safe. Lock Caller.mu
|
||||
// on your own.
|
||||
func (c *Caller) remove(cuid string) (success bool) {
|
||||
cmd, uid := c.cuidToID(cuid)
|
||||
if len(cmd) == 0 || len(uid) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the irc command/event has any handlers on it.
|
||||
if _, ok := c.external[cmd]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check to see if it's actually a registered handler.
|
||||
if _, ok := c.external[cmd][uid]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
delete(c.external[cmd], uid)
|
||||
c.debug.Printf("removed handler %s", cuid)
|
||||
|
||||
// Assume success.
|
||||
return true
|
||||
}
|
||||
|
||||
// sregister is much like Caller.register(), except that it safely locks
|
||||
// the Caller mutex.
|
||||
func (c *Caller) sregister(internal, bg bool, cmd string, handler Handler) (cuid string) {
|
||||
c.mu.Lock()
|
||||
cuid = c.register(internal, bg, cmd, handler)
|
||||
c.mu.Unlock()
|
||||
|
||||
return cuid
|
||||
}
|
||||
|
||||
// register will register a handler in the internal tracker. Unsafe (you
|
||||
// must lock c.mu yourself!)
|
||||
func (c *Caller) register(internal, bg bool, cmd string, handler Handler) (cuid string) {
|
||||
var uid string
|
||||
|
||||
cmd = strings.ToUpper(cmd)
|
||||
|
||||
cuid, uid = c.cuid(cmd, 20)
|
||||
if bg {
|
||||
uid += ":bg"
|
||||
cuid += ":bg"
|
||||
}
|
||||
|
||||
if internal {
|
||||
if _, ok := c.internal[cmd]; !ok {
|
||||
c.internal[cmd] = map[string]Handler{}
|
||||
}
|
||||
|
||||
c.internal[cmd][uid] = handler
|
||||
} else {
|
||||
if _, ok := c.external[cmd]; !ok {
|
||||
c.external[cmd] = map[string]Handler{}
|
||||
}
|
||||
|
||||
c.external[cmd][uid] = handler
|
||||
}
|
||||
|
||||
_, file, line, _ := runtime.Caller(3)
|
||||
|
||||
c.debug.Printf("reg %q => %s [int:%t bg:%t] %s:%d", uid, cmd, internal, bg, file, line)
|
||||
|
||||
return cuid
|
||||
}
|
||||
|
||||
// AddHandler registers a handler (matching the handler interface) for the
|
||||
// given event. cuid is the handler uid which can be used to remove the
|
||||
// handler with Caller.Remove().
|
||||
func (c *Caller) AddHandler(cmd string, handler Handler) (cuid string) {
|
||||
return c.sregister(false, false, cmd, handler)
|
||||
}
|
||||
|
||||
// Add registers the handler function for the given event. cuid is the
|
||||
// handler uid which can be used to remove the handler with Caller.Remove().
|
||||
func (c *Caller) Add(cmd string, handler func(client *Client, event Event)) (cuid string) {
|
||||
return c.sregister(false, false, cmd, HandlerFunc(handler))
|
||||
}
|
||||
|
||||
// AddBg registers the handler function for the given event and executes it
|
||||
// in a go-routine. cuid is the handler uid which can be used to remove the
|
||||
// handler with Caller.Remove().
|
||||
func (c *Caller) AddBg(cmd string, handler func(client *Client, event Event)) (cuid string) {
|
||||
return c.sregister(false, true, cmd, HandlerFunc(handler))
|
||||
}
|
||||
|
||||
// AddTmp adds a "temporary" handler, which is good for one-time or few-time
|
||||
// uses. This supports a deadline and/or manual removal, as this differs
|
||||
// much from how normal handlers work. An example of a good use for this
|
||||
// would be to capture the entire output of a multi-response query to the
|
||||
// server. (e.g. LIST, WHOIS, etc)
|
||||
//
|
||||
// The supplied handler is able to return a boolean, which if true, will
|
||||
// remove the handler from the handler stack.
|
||||
//
|
||||
// Additionally, AddTmp has a useful option, deadline. When set to greater
|
||||
// than 0, deadline will be the amount of time that passes before the handler
|
||||
// is removed from the stack, regardless of if the handler returns true or not.
|
||||
// This is useful in that it ensures that the handler is cleaned up if the
|
||||
// server does not respond appropriately, or takes too long to respond.
|
||||
//
|
||||
// Note that handlers supplied with AddTmp are executed in a goroutine to
|
||||
// ensure that they are not blocking other handlers. However, if you are
|
||||
// creating a temporary handler from another handler, it should be a
|
||||
// background handler.
|
||||
//
|
||||
// Use cuid with Caller.Remove() to prematurely remove the handler from the
|
||||
// stack, bypassing the timeout or waiting for the handler to return that it
|
||||
// wants to be removed from the stack.
|
||||
func (c *Caller) AddTmp(cmd string, deadline time.Duration, handler func(client *Client, event Event) bool) (cuid string, done chan struct{}) {
|
||||
done = make(chan struct{})
|
||||
|
||||
cuid = c.sregister(false, true, cmd, HandlerFunc(func(client *Client, event Event) {
|
||||
remove := handler(client, event)
|
||||
if remove {
|
||||
if ok := c.Remove(cuid); ok {
|
||||
close(done)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
if deadline > 0 {
|
||||
go func() {
|
||||
select {
|
||||
case <-time.After(deadline):
|
||||
case <-done:
|
||||
}
|
||||
|
||||
if ok := c.Remove(cuid); ok {
|
||||
close(done)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return cuid, done
|
||||
}
|
||||
|
||||
// recoverHandlerPanic is used to catch all handler panics, and re-route
|
||||
// them if necessary.
|
||||
func recoverHandlerPanic(client *Client, event *Event, id string, skip int) {
|
||||
perr := recover()
|
||||
if perr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var file string
|
||||
var line int
|
||||
var ok bool
|
||||
|
||||
_, file, line, ok = runtime.Caller(skip)
|
||||
|
||||
err := &HandlerError{
|
||||
Event: *event,
|
||||
ID: id,
|
||||
File: file,
|
||||
Line: line,
|
||||
Panic: perr,
|
||||
Stack: debug.Stack(),
|
||||
callOk: ok,
|
||||
}
|
||||
|
||||
client.Config.RecoverFunc(client, err)
|
||||
return
|
||||
}
|
||||
|
||||
// HandlerError is the error returned when a panic is intentionally recovered
|
||||
// from. It contains useful information like the handler identifier (if
|
||||
// applicable), filename, line in file where panic occurred, the call
|
||||
// trace, and original event.
|
||||
type HandlerError struct {
|
||||
Event Event // Event is the event that caused the error.
|
||||
ID string // ID is the CUID of the handler.
|
||||
File string // File is the file from where the panic originated.
|
||||
Line int // Line number where panic originated.
|
||||
Panic interface{} // Panic is the error that was passed to panic().
|
||||
Stack []byte // Stack is the call stack. Note you may have to skip 1 or 2 due to debug functions.
|
||||
callOk bool
|
||||
}
|
||||
|
||||
// Error returns a prettified version of HandlerError, containing ID, file,
|
||||
// line, and basic error string.
|
||||
func (e *HandlerError) Error() string {
|
||||
if e.callOk {
|
||||
return fmt.Sprintf("panic during handler [%s] execution in %s:%d: %s", e.ID, e.File, e.Line, e.Panic)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("panic during handler [%s] execution in unknown: %s", e.ID, e.Panic)
|
||||
}
|
||||
|
||||
// String returns the error that panic returned, as well as the entire call
|
||||
// trace of where it originated.
|
||||
func (e *HandlerError) String() string {
|
||||
return fmt.Sprintf("panic: %s\n\n%s", e.Panic, string(e.Stack))
|
||||
}
|
||||
|
||||
// DefaultRecoverHandler can be used with Config.RecoverFunc as a default
|
||||
// catch-all for panics. This will log the error, and the call trace to the
|
||||
// debug log (see Config.Debug), or os.Stdout if Config.Debug is unset.
|
||||
func DefaultRecoverHandler(client *Client, err *HandlerError) {
|
||||
if client.Config.Debug == nil {
|
||||
fmt.Println(err.Error())
|
||||
fmt.Println(err.String())
|
||||
return
|
||||
}
|
||||
|
||||
client.debug.Println(err.Error())
|
||||
client.debug.Println(err.String())
|
||||
}
|
550
vendor/github.com/lrstanley/girc/modes.go
generated
vendored
Normal file
550
vendor/github.com/lrstanley/girc/modes.go
generated
vendored
Normal file
@ -0,0 +1,550 @@
|
||||
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||
// of this source code is governed by the MIT license that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
package girc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// CMode represents a single step of a given mode change.
|
||||
type CMode struct {
|
||||
add bool // if it's a +, or -.
|
||||
name byte // character representation of the given mode.
|
||||
setting bool // if it's a setting (should be stored) or temporary (op/voice/etc).
|
||||
args string // arguments to the mode, if arguments are supported.
|
||||
}
|
||||
|
||||
// Short returns a short representation of a mode without arguments. E.g. "+a",
|
||||
// or "-b".
|
||||
func (c *CMode) Short() string {
|
||||
var status string
|
||||
if c.add {
|
||||
status = "+"
|
||||
} else {
|
||||
status = "-"
|
||||
}
|
||||
|
||||
return status + string(c.name)
|
||||
}
|
||||
|
||||
// String returns a string representation of a mode, including optional
|
||||
// arguments. E.g. "+b user*!ident@host.*.com"
|
||||
func (c *CMode) String() string {
|
||||
if len(c.args) == 0 {
|
||||
return c.Short()
|
||||
}
|
||||
|
||||
return c.Short() + " " + c.args
|
||||
}
|
||||
|
||||
// CModes is a representation of a set of modes. This may be the given state
|
||||
// of a channel/user, or the given state changes to a given channel/user.
|
||||
type CModes struct {
|
||||
raw string // raw supported modes.
|
||||
modesListArgs string // modes that add/remove users from lists and support args.
|
||||
modesArgs string // modes that support args.
|
||||
modesSetArgs string // modes that support args ONLY when set.
|
||||
modesNoArgs string // modes that do not support args.
|
||||
|
||||
prefixes string // user permission prefixes. these aren't a CMode.setting.
|
||||
modes []CMode // the list of modes for this given state.
|
||||
}
|
||||
|
||||
// Copy returns a deep copy of CModes.
|
||||
func (c *CModes) Copy() (nc CModes) {
|
||||
nc = CModes{}
|
||||
nc = *c
|
||||
|
||||
nc.modes = make([]CMode, len(c.modes))
|
||||
|
||||
// Copy modes.
|
||||
for i := 0; i < len(c.modes); i++ {
|
||||
nc.modes[i] = c.modes[i]
|
||||
}
|
||||
|
||||
return nc
|
||||
}
|
||||
|
||||
// String returns a complete set of modes for this given state (change?). For
|
||||
// example, "+a-b+cde some-arg".
|
||||
func (c *CModes) String() string {
|
||||
var out string
|
||||
var args string
|
||||
|
||||
if len(c.modes) > 0 {
|
||||
out += "+"
|
||||
}
|
||||
|
||||
for i := 0; i < len(c.modes); i++ {
|
||||
out += string(c.modes[i].name)
|
||||
|
||||
if len(c.modes[i].args) > 0 {
|
||||
args += " " + c.modes[i].args
|
||||
}
|
||||
}
|
||||
|
||||
return out + args
|
||||
}
|
||||
|
||||
// HasMode checks if the CModes state has a given mode. E.g. "m", or "I".
|
||||
func (c *CModes) HasMode(mode string) bool {
|
||||
for i := 0; i < len(c.modes); i++ {
|
||||
if string(c.modes[i].name) == mode {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Get returns the arguments for a given mode within this session, if it
|
||||
// supports args.
|
||||
func (c *CModes) Get(mode string) (args string, ok bool) {
|
||||
for i := 0; i < len(c.modes); i++ {
|
||||
if string(c.modes[i].name) == mode {
|
||||
if len(c.modes[i].args) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return c.modes[i].args, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// hasArg checks to see if the mode supports arguments. What ones support this?:
|
||||
// A = Mode that adds or removes a nick or address to a list. Always has a parameter.
|
||||
// B = Mode that changes a setting and always has a parameter.
|
||||
// C = Mode that changes a setting and only has a parameter when set.
|
||||
// D = Mode that changes a setting and never has a parameter.
|
||||
// Note: Modes of type A return the list when there is no parameter present.
|
||||
// Note: Some clients assumes that any mode not listed is of type D.
|
||||
// Note: Modes in PREFIX are not listed but could be considered type B.
|
||||
func (c *CModes) hasArg(set bool, mode byte) (hasArgs, isSetting bool) {
|
||||
if len(c.raw) < 1 {
|
||||
return false, true
|
||||
}
|
||||
|
||||
if strings.IndexByte(c.modesListArgs, mode) > -1 {
|
||||
return true, false
|
||||
}
|
||||
|
||||
if strings.IndexByte(c.modesArgs, mode) > -1 {
|
||||
return true, true
|
||||
}
|
||||
|
||||
if strings.IndexByte(c.modesSetArgs, mode) > -1 {
|
||||
if set {
|
||||
return true, true
|
||||
}
|
||||
|
||||
return false, true
|
||||
}
|
||||
|
||||
if strings.IndexByte(c.prefixes, mode) > -1 {
|
||||
return true, false
|
||||
}
|
||||
|
||||
return false, true
|
||||
}
|
||||
|
||||
// Apply merges two state changes, or one state change into a state of modes.
|
||||
// For example, the latter would mean applying an incoming MODE with the modes
|
||||
// stored for a channel.
|
||||
func (c *CModes) Apply(modes []CMode) {
|
||||
var new []CMode
|
||||
|
||||
for j := 0; j < len(c.modes); j++ {
|
||||
isin := false
|
||||
for i := 0; i < len(modes); i++ {
|
||||
if !modes[i].setting {
|
||||
continue
|
||||
}
|
||||
if c.modes[j].name == modes[i].name && modes[i].add {
|
||||
new = append(new, modes[i])
|
||||
isin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isin {
|
||||
new = append(new, c.modes[j])
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(modes); i++ {
|
||||
if !modes[i].setting || !modes[i].add {
|
||||
continue
|
||||
}
|
||||
|
||||
isin := false
|
||||
for j := 0; j < len(new); j++ {
|
||||
if modes[i].name == new[j].name {
|
||||
isin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isin {
|
||||
new = append(new, modes[i])
|
||||
}
|
||||
}
|
||||
|
||||
c.modes = new
|
||||
}
|
||||
|
||||
// Parse parses a set of flags and args, returning the necessary list of
|
||||
// mappings for the mode flags.
|
||||
func (c *CModes) Parse(flags string, args []string) (out []CMode) {
|
||||
// add is the mode state we're currently in. Adding, or removing modes.
|
||||
add := true
|
||||
var argCount int
|
||||
|
||||
for i := 0; i < len(flags); i++ {
|
||||
if flags[i] == '+' {
|
||||
add = true
|
||||
continue
|
||||
}
|
||||
if flags[i] == '-' {
|
||||
add = false
|
||||
continue
|
||||
}
|
||||
|
||||
mode := CMode{
|
||||
name: flags[i],
|
||||
add: add,
|
||||
}
|
||||
|
||||
hasArgs, isSetting := c.hasArg(add, flags[i])
|
||||
if hasArgs && len(args) >= argCount+1 {
|
||||
mode.args = args[argCount]
|
||||
argCount++
|
||||
}
|
||||
mode.setting = isSetting
|
||||
|
||||
out = append(out, mode)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// NewCModes returns a new CModes reference. channelModes and userPrefixes
|
||||
// would be something you see from the server's "CHANMODES" and "PREFIX"
|
||||
// ISUPPORT capability messages (alternatively, fall back to the standard)
|
||||
// DefaultPrefixes and ModeDefaults.
|
||||
func NewCModes(channelModes, userPrefixes string) CModes {
|
||||
split := strings.SplitN(channelModes, ",", 4)
|
||||
if len(split) != 4 {
|
||||
for i := len(split); i < 4; i++ {
|
||||
split = append(split, "")
|
||||
}
|
||||
}
|
||||
|
||||
return CModes{
|
||||
raw: channelModes,
|
||||
modesListArgs: split[0],
|
||||
modesArgs: split[1],
|
||||
modesSetArgs: split[2],
|
||||
modesNoArgs: split[3],
|
||||
|
||||
prefixes: userPrefixes,
|
||||
modes: []CMode{},
|
||||
}
|
||||
}
|
||||
|
||||
// IsValidChannelMode validates a channel mode (CHANMODES).
|
||||
func IsValidChannelMode(raw string) bool {
|
||||
if len(raw) < 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < len(raw); i++ {
|
||||
// Allowed are: ",", A-Z and a-z.
|
||||
if raw[i] != ',' && (raw[i] < 'A' || raw[i] > 'Z') && (raw[i] < 'a' || raw[i] > 'z') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// isValidUserPrefix validates a list of ISUPPORT-style user prefixes (PREFIX).
|
||||
func isValidUserPrefix(raw string) bool {
|
||||
if len(raw) < 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
if raw[0] != '(' {
|
||||
return false
|
||||
}
|
||||
|
||||
var keys, rep int
|
||||
var passedKeys bool
|
||||
|
||||
// Skip the first one as we know it's (.
|
||||
for i := 1; i < len(raw); i++ {
|
||||
if raw[i] == ')' {
|
||||
passedKeys = true
|
||||
continue
|
||||
}
|
||||
|
||||
if passedKeys {
|
||||
rep++
|
||||
} else {
|
||||
keys++
|
||||
}
|
||||
}
|
||||
|
||||
return keys == rep
|
||||
}
|
||||
|
||||
// parsePrefixes parses the mode character mappings from the symbols of a
|
||||
// ISUPPORT-style user prefixes list (PREFIX).
|
||||
func parsePrefixes(raw string) (modes, prefixes string) {
|
||||
if !isValidUserPrefix(raw) {
|
||||
return modes, prefixes
|
||||
}
|
||||
|
||||
i := strings.Index(raw, ")")
|
||||
if i < 1 {
|
||||
return modes, prefixes
|
||||
}
|
||||
|
||||
return raw[1:i], raw[i+1:]
|
||||
}
|
||||
|
||||
// handleMODE handles incoming MODE messages, and updates the tracking
|
||||
// information for each channel, as well as if any of the modes affect user
|
||||
// permissions.
|
||||
func handleMODE(c *Client, e Event) {
|
||||
// Check if it's a RPL_CHANNELMODEIS.
|
||||
if e.Command == RPL_CHANNELMODEIS && len(e.Params) > 2 {
|
||||
// RPL_CHANNELMODEIS sends the user as the first param, skip it.
|
||||
e.Params = e.Params[1:]
|
||||
}
|
||||
// Should be at least MODE <target> <flags>, to be useful. As well, only
|
||||
// tracking channel modes at the moment.
|
||||
if len(e.Params) < 2 || !IsValidChannel(e.Params[0]) {
|
||||
return
|
||||
}
|
||||
|
||||
c.state.RLock()
|
||||
channel := c.state.lookupChannel(e.Params[0])
|
||||
if channel == nil {
|
||||
c.state.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
flags := e.Params[1]
|
||||
var args []string
|
||||
if len(e.Params) > 2 {
|
||||
args = append(args, e.Params[2:]...)
|
||||
}
|
||||
|
||||
modes := channel.Modes.Parse(flags, args)
|
||||
channel.Modes.Apply(modes)
|
||||
|
||||
// Loop through and update users modes as necessary.
|
||||
for i := 0; i < len(modes); i++ {
|
||||
if modes[i].setting || len(modes[i].args) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
user := c.state.lookupUser(modes[i].args)
|
||||
if user != nil {
|
||||
perms, _ := user.Perms.Lookup(channel.Name)
|
||||
perms.setFromMode(modes[i])
|
||||
user.Perms.set(channel.Name, perms)
|
||||
}
|
||||
}
|
||||
|
||||
c.state.RUnlock()
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
}
|
||||
|
||||
// chanModes returns the ISUPPORT list of server-supported channel modes,
|
||||
// alternatively falling back to ModeDefaults.
|
||||
func (s *state) chanModes() string {
|
||||
if modes, ok := s.serverOptions["CHANMODES"]; ok && IsValidChannelMode(modes) {
|
||||
return modes
|
||||
}
|
||||
|
||||
return ModeDefaults
|
||||
}
|
||||
|
||||
// userPrefixes returns the ISUPPORT list of server-supported user prefixes.
|
||||
// This includes mode characters, as well as user prefix symbols. Falls back
|
||||
// to DefaultPrefixes if not server-supported.
|
||||
func (s *state) userPrefixes() string {
|
||||
if prefix, ok := s.serverOptions["PREFIX"]; ok && isValidUserPrefix(prefix) {
|
||||
return prefix
|
||||
}
|
||||
|
||||
return DefaultPrefixes
|
||||
}
|
||||
|
||||
// UserPerms contains all of the permissions for each channel the user is
|
||||
// in.
|
||||
type UserPerms struct {
|
||||
mu sync.RWMutex
|
||||
channels map[string]Perms
|
||||
}
|
||||
|
||||
// Copy returns a deep copy of the channel permissions.
|
||||
func (p *UserPerms) Copy() (perms *UserPerms) {
|
||||
np := &UserPerms{
|
||||
channels: make(map[string]Perms),
|
||||
}
|
||||
|
||||
p.mu.RLock()
|
||||
for key := range p.channels {
|
||||
np.channels[key] = p.channels[key]
|
||||
}
|
||||
p.mu.RUnlock()
|
||||
|
||||
return np
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (p *UserPerms) MarshalJSON() ([]byte, error) {
|
||||
p.mu.Lock()
|
||||
out, err := json.Marshal(&p.channels)
|
||||
p.mu.Unlock()
|
||||
|
||||
return out, err
|
||||
}
|
||||
|
||||
// Lookup looks up the users permissions for a given channel. ok is false
|
||||
// if the user is not in the given channel.
|
||||
func (p *UserPerms) Lookup(channel string) (perms Perms, ok bool) {
|
||||
p.mu.RLock()
|
||||
perms, ok = p.channels[ToRFC1459(channel)]
|
||||
p.mu.RUnlock()
|
||||
|
||||
return perms, ok
|
||||
}
|
||||
|
||||
func (p *UserPerms) set(channel string, perms Perms) {
|
||||
p.mu.Lock()
|
||||
p.channels[ToRFC1459(channel)] = perms
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
func (p *UserPerms) remove(channel string) {
|
||||
p.mu.Lock()
|
||||
delete(p.channels, ToRFC1459(channel))
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
// Perms contains all channel-based user permissions. The minimum op, and
|
||||
// voice should be supported on all networks. This also supports non-rfc
|
||||
// Owner, Admin, and HalfOp, if the network has support for it.
|
||||
type Perms struct {
|
||||
// Owner (non-rfc) indicates that the user has full permissions to the
|
||||
// channel. More than one user can have owner permission.
|
||||
Owner bool `json:"owner"`
|
||||
// Admin (non-rfc) is commonly given to users that are trusted enough
|
||||
// to manage channel permissions, as well as higher level service settings.
|
||||
Admin bool `json:"admin"`
|
||||
// Op is commonly given to trusted users who can manage a given channel
|
||||
// by kicking, and banning users.
|
||||
Op bool `json:"op"`
|
||||
// HalfOp (non-rfc) is commonly used to give users permissions like the
|
||||
// ability to kick, without giving them greater abilities to ban all users.
|
||||
HalfOp bool `json:"half_op"`
|
||||
// Voice indicates the user has voice permissions, commonly given to known
|
||||
// users, with very light trust, or to indicate a user is active.
|
||||
Voice bool `json:"voice"`
|
||||
}
|
||||
|
||||
// IsAdmin indicates that the user has banning abilities, and are likely a
|
||||
// very trustable user (e.g. op+).
|
||||
func (m Perms) IsAdmin() bool {
|
||||
if m.Owner || m.Admin || m.Op {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsTrusted indicates that the user at least has modes set upon them, higher
|
||||
// than a regular joining user.
|
||||
func (m Perms) IsTrusted() bool {
|
||||
if m.IsAdmin() || m.HalfOp || m.Voice {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// reset resets the modes of a user.
|
||||
func (m *Perms) reset() {
|
||||
m.Owner = false
|
||||
m.Admin = false
|
||||
m.Op = false
|
||||
m.HalfOp = false
|
||||
m.Voice = false
|
||||
}
|
||||
|
||||
// set translates raw prefix characters into proper permissions. Only
|
||||
// use this function when you have a session lock.
|
||||
func (m *Perms) set(prefix string, append bool) {
|
||||
if !append {
|
||||
m.reset()
|
||||
}
|
||||
|
||||
for i := 0; i < len(prefix); i++ {
|
||||
switch string(prefix[i]) {
|
||||
case OwnerPrefix:
|
||||
m.Owner = true
|
||||
case AdminPrefix:
|
||||
m.Admin = true
|
||||
case OperatorPrefix:
|
||||
m.Op = true
|
||||
case HalfOperatorPrefix:
|
||||
m.HalfOp = true
|
||||
case VoicePrefix:
|
||||
m.Voice = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setFromMode sets user-permissions based on channel user mode chars. E.g.
|
||||
// "o" being oper, "v" being voice, etc.
|
||||
func (m *Perms) setFromMode(mode CMode) {
|
||||
switch string(mode.name) {
|
||||
case ModeOwner:
|
||||
m.Owner = mode.add
|
||||
case ModeAdmin:
|
||||
m.Admin = mode.add
|
||||
case ModeOperator:
|
||||
m.Op = mode.add
|
||||
case ModeHalfOperator:
|
||||
m.HalfOp = mode.add
|
||||
case ModeVoice:
|
||||
m.Voice = mode.add
|
||||
}
|
||||
}
|
||||
|
||||
// parseUserPrefix parses a raw mode line, like "@user" or "@+user".
|
||||
func parseUserPrefix(raw string) (modes, nick string, success bool) {
|
||||
for i := 0; i < len(raw); i++ {
|
||||
char := string(raw[i])
|
||||
|
||||
if char == OwnerPrefix || char == AdminPrefix || char == HalfOperatorPrefix ||
|
||||
char == OperatorPrefix || char == VoicePrefix {
|
||||
modes += char
|
||||
continue
|
||||
}
|
||||
|
||||
// Assume we've gotten to the nickname part.
|
||||
return modes, raw[i:], true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
489
vendor/github.com/lrstanley/girc/state.go
generated
vendored
Normal file
489
vendor/github.com/lrstanley/girc/state.go
generated
vendored
Normal file
@ -0,0 +1,489 @@
|
||||
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||
// of this source code is governed by the MIT license that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
package girc
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// state represents the actively-changing variables within the client
|
||||
// runtime. Note that everything within the state should be guarded by the
|
||||
// embedded sync.RWMutex.
|
||||
type state struct {
|
||||
sync.RWMutex
|
||||
// nick, ident, and host are the internal trackers for our user.
|
||||
nick, ident, host string
|
||||
// channels represents all channels we're active in.
|
||||
channels map[string]*Channel
|
||||
// users represents all of users that we're tracking.
|
||||
users map[string]*User
|
||||
// enabledCap are the capabilities which are enabled for this connection.
|
||||
enabledCap []string
|
||||
// tmpCap are the capabilties which we share with the server during the
|
||||
// last capability check. These will get sent once we have received the
|
||||
// last capability list command from the server.
|
||||
tmpCap []string
|
||||
// serverOptions are the standard capabilities and configurations
|
||||
// supported by the server at connection time. This also includes
|
||||
// RPL_ISUPPORT entries.
|
||||
serverOptions map[string]string
|
||||
// motd is the servers message of the day.
|
||||
motd string
|
||||
}
|
||||
|
||||
// notify sends state change notifications so users can update their refs
|
||||
// when state changes.
|
||||
func (s *state) notify(c *Client, ntype string) {
|
||||
c.RunHandlers(&Event{Command: ntype})
|
||||
}
|
||||
|
||||
// reset resets the state back to it's original form.
|
||||
func (s *state) reset() {
|
||||
s.Lock()
|
||||
s.nick = ""
|
||||
s.ident = ""
|
||||
s.host = ""
|
||||
s.channels = make(map[string]*Channel)
|
||||
s.users = make(map[string]*User)
|
||||
s.serverOptions = make(map[string]string)
|
||||
s.enabledCap = []string{}
|
||||
s.motd = ""
|
||||
s.Unlock()
|
||||
}
|
||||
|
||||
// User represents an IRC user and the state attached to them.
|
||||
type User struct {
|
||||
// Nick is the users current nickname. rfc1459 compliant.
|
||||
Nick string `json:"nick"`
|
||||
// Ident is the users username/ident. Ident is commonly prefixed with a
|
||||
// "~", which indicates that they do not have a identd server setup for
|
||||
// authentication.
|
||||
Ident string `json:"ident"`
|
||||
// Host is the visible host of the users connection that the server has
|
||||
// provided to us for their connection. May not always be accurate due to
|
||||
// many networks spoofing/hiding parts of the hostname for privacy
|
||||
// reasons.
|
||||
Host string `json:"host"`
|
||||
|
||||
// ChannelList is a sorted list of all channels that we are currently
|
||||
// tracking the user in. Each channel name is rfc1459 compliant. See
|
||||
// User.Channels() for a shorthand if you're looking for the *Channel
|
||||
// version of the channel list.
|
||||
ChannelList []string `json:"channels"`
|
||||
|
||||
// FirstSeen represents the first time that the user was seen by the
|
||||
// client for the given channel. Only usable if from state, not in past.
|
||||
FirstSeen time.Time `json:"first_seen"`
|
||||
// LastActive represents the last time that we saw the user active,
|
||||
// which could be during nickname change, message, channel join, etc.
|
||||
// Only usable if from state, not in past.
|
||||
LastActive time.Time `json:"last_active"`
|
||||
|
||||
// Perms are the user permissions applied to this user that affect the given
|
||||
// channel. This supports non-rfc style modes like Admin, Owner, and HalfOp.
|
||||
Perms *UserPerms `json:"perms"`
|
||||
|
||||
// Extras are things added on by additional tracking methods, which may
|
||||
// or may not work on the IRC server in mention.
|
||||
Extras struct {
|
||||
// Name is the users "realname" or full name. Commonly contains links
|
||||
// to the IRC client being used, or something of non-importance. May
|
||||
// also be empty if unsupported by the server/tracking is disabled.
|
||||
Name string `json:"name"`
|
||||
// Account refers to the account which the user is authenticated as.
|
||||
// This differs between each network (e.g. usually Nickserv, but
|
||||
// could also be something like Undernet). May also be empty if
|
||||
// unsupported by the server/tracking is disabled.
|
||||
Account string `json:"account"`
|
||||
// Away refers to the away status of the user. An empty string
|
||||
// indicates that they are active, otherwise the string is what they
|
||||
// set as their away message. May also be empty if unsupported by the
|
||||
// server/tracking is disabled.
|
||||
Away string `json:"away"`
|
||||
} `json:"extras"`
|
||||
}
|
||||
|
||||
// Channels returns a reference of *Channels that the client knows the user
|
||||
// is in. If you're just looking for the namme of the channels, use
|
||||
// User.ChannelList.
|
||||
func (u User) Channels(c *Client) []*Channel {
|
||||
if c == nil {
|
||||
panic("nil Client provided")
|
||||
}
|
||||
|
||||
channels := []*Channel{}
|
||||
|
||||
c.state.RLock()
|
||||
for i := 0; i < len(u.ChannelList); i++ {
|
||||
ch := c.state.lookupChannel(u.ChannelList[i])
|
||||
if ch != nil {
|
||||
channels = append(channels, ch)
|
||||
}
|
||||
}
|
||||
c.state.RUnlock()
|
||||
|
||||
return channels
|
||||
}
|
||||
|
||||
// Copy returns a deep copy of the user which can be modified without making
|
||||
// changes to the actual state.
|
||||
func (u *User) Copy() *User {
|
||||
nu := &User{}
|
||||
*nu = *u
|
||||
|
||||
nu.Perms = u.Perms.Copy()
|
||||
_ = copy(nu.ChannelList, u.ChannelList)
|
||||
|
||||
return nu
|
||||
}
|
||||
|
||||
// addChannel adds the channel to the users channel list.
|
||||
func (u *User) addChannel(name string) {
|
||||
if u.InChannel(name) {
|
||||
return
|
||||
}
|
||||
|
||||
u.ChannelList = append(u.ChannelList, ToRFC1459(name))
|
||||
sort.StringsAreSorted(u.ChannelList)
|
||||
|
||||
u.Perms.set(name, Perms{})
|
||||
}
|
||||
|
||||
// deleteChannel removes an existing channel from the users channel list.
|
||||
func (u *User) deleteChannel(name string) {
|
||||
name = ToRFC1459(name)
|
||||
|
||||
j := -1
|
||||
for i := 0; i < len(u.ChannelList); i++ {
|
||||
if u.ChannelList[i] == name {
|
||||
j = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if j != -1 {
|
||||
u.ChannelList = append(u.ChannelList[:j], u.ChannelList[j+1:]...)
|
||||
}
|
||||
|
||||
u.Perms.remove(name)
|
||||
}
|
||||
|
||||
// InChannel checks to see if a user is in the given channel.
|
||||
func (u *User) InChannel(name string) bool {
|
||||
name = ToRFC1459(name)
|
||||
|
||||
for i := 0; i < len(u.ChannelList); i++ {
|
||||
if u.ChannelList[i] == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Lifetime represents the amount of time that has passed since we have first
|
||||
// seen the user.
|
||||
func (u *User) Lifetime() time.Duration {
|
||||
return time.Since(u.FirstSeen)
|
||||
}
|
||||
|
||||
// Active represents the the amount of time that has passed since we have
|
||||
// last seen the user.
|
||||
func (u *User) Active() time.Duration {
|
||||
return time.Since(u.LastActive)
|
||||
}
|
||||
|
||||
// IsActive returns true if they were active within the last 30 minutes.
|
||||
func (u *User) IsActive() bool {
|
||||
return u.Active() < (time.Minute * 30)
|
||||
}
|
||||
|
||||
// Channel represents an IRC channel and the state attached to it.
|
||||
type Channel struct {
|
||||
// Name of the channel. Must be rfc1459 compliant.
|
||||
Name string `json:"name"`
|
||||
// Topic of the channel.
|
||||
Topic string `json:"topic"`
|
||||
|
||||
// UserList is a sorted list of all users we are currently tracking within
|
||||
// the channel. Each is the nickname, and is rfc1459 compliant.
|
||||
UserList []string `json:"user_list"`
|
||||
// Joined represents the first time that the client joined the channel.
|
||||
Joined time.Time `json:"joined"`
|
||||
// Modes are the known channel modes that the bot has captured.
|
||||
Modes CModes `json:"modes"`
|
||||
}
|
||||
|
||||
// Users returns a reference of *Users that the client knows the channel has
|
||||
// If you're just looking for just the name of the users, use Channnel.UserList.
|
||||
func (ch Channel) Users(c *Client) []*User {
|
||||
if c == nil {
|
||||
panic("nil Client provided")
|
||||
}
|
||||
|
||||
users := []*User{}
|
||||
|
||||
c.state.RLock()
|
||||
for i := 0; i < len(ch.UserList); i++ {
|
||||
user := c.state.lookupUser(ch.UserList[i])
|
||||
if user != nil {
|
||||
users = append(users, user)
|
||||
}
|
||||
}
|
||||
c.state.RUnlock()
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
// Trusted returns a list of users which have voice or greater in the given
|
||||
// channel. See Perms.IsTrusted() for more information.
|
||||
func (ch Channel) Trusted(c *Client) []*User {
|
||||
if c == nil {
|
||||
panic("nil Client provided")
|
||||
}
|
||||
|
||||
users := []*User{}
|
||||
|
||||
c.state.RLock()
|
||||
for i := 0; i < len(ch.UserList); i++ {
|
||||
user := c.state.lookupUser(ch.UserList[i])
|
||||
if user == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
perms, ok := user.Perms.Lookup(ch.Name)
|
||||
if ok && perms.IsTrusted() {
|
||||
users = append(users, user)
|
||||
}
|
||||
}
|
||||
c.state.RUnlock()
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
// Admins returns a list of users which have half-op (if supported), or
|
||||
// greater permissions (op, admin, owner, etc) in the given channel. See
|
||||
// Perms.IsAdmin() for more information.
|
||||
func (ch Channel) Admins(c *Client) []*User {
|
||||
if c == nil {
|
||||
panic("nil Client provided")
|
||||
}
|
||||
|
||||
users := []*User{}
|
||||
|
||||
c.state.RLock()
|
||||
for i := 0; i < len(ch.UserList); i++ {
|
||||
user := c.state.lookupUser(ch.UserList[i])
|
||||
if user == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
perms, ok := user.Perms.Lookup(ch.Name)
|
||||
if ok && perms.IsAdmin() {
|
||||
users = append(users, user)
|
||||
}
|
||||
}
|
||||
c.state.RUnlock()
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
// addUser adds a user to the users list.
|
||||
func (ch *Channel) addUser(nick string) {
|
||||
if ch.UserIn(nick) {
|
||||
return
|
||||
}
|
||||
|
||||
ch.UserList = append(ch.UserList, ToRFC1459(nick))
|
||||
sort.Strings(ch.UserList)
|
||||
}
|
||||
|
||||
// deleteUser removes an existing user from the users list.
|
||||
func (ch *Channel) deleteUser(nick string) {
|
||||
nick = ToRFC1459(nick)
|
||||
|
||||
j := -1
|
||||
for i := 0; i < len(ch.UserList); i++ {
|
||||
if ch.UserList[i] == nick {
|
||||
j = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if j != -1 {
|
||||
ch.UserList = append(ch.UserList[:j], ch.UserList[j+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy returns a deep copy of a given channel.
|
||||
func (ch *Channel) Copy() *Channel {
|
||||
nc := &Channel{}
|
||||
*nc = *ch
|
||||
|
||||
_ = copy(nc.UserList, ch.UserList)
|
||||
|
||||
// And modes.
|
||||
nc.Modes = ch.Modes.Copy()
|
||||
|
||||
return nc
|
||||
}
|
||||
|
||||
// Len returns the count of users in a given channel.
|
||||
func (ch *Channel) Len() int {
|
||||
return len(ch.UserList)
|
||||
}
|
||||
|
||||
// UserIn checks to see if a given user is in a channel.
|
||||
func (ch *Channel) UserIn(name string) bool {
|
||||
name = ToRFC1459(name)
|
||||
|
||||
for i := 0; i < len(ch.UserList); i++ {
|
||||
if ch.UserList[i] == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Lifetime represents the amount of time that has passed since we have first
|
||||
// joined the channel.
|
||||
func (ch *Channel) Lifetime() time.Duration {
|
||||
return time.Since(ch.Joined)
|
||||
}
|
||||
|
||||
// createChannel creates the channel in state, if not already done.
|
||||
func (s *state) createChannel(name string) (ok bool) {
|
||||
supported := s.chanModes()
|
||||
prefixes, _ := parsePrefixes(s.userPrefixes())
|
||||
|
||||
if _, ok := s.channels[ToRFC1459(name)]; ok {
|
||||
return false
|
||||
}
|
||||
|
||||
s.channels[ToRFC1459(name)] = &Channel{
|
||||
Name: name,
|
||||
UserList: []string{},
|
||||
Joined: time.Now(),
|
||||
Modes: NewCModes(supported, prefixes),
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// deleteChannel removes the channel from state, if not already done.
|
||||
func (s *state) deleteChannel(name string) {
|
||||
name = ToRFC1459(name)
|
||||
|
||||
_, ok := s.channels[name]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for _, user := range s.channels[name].UserList {
|
||||
s.users[user].deleteChannel(name)
|
||||
|
||||
if len(s.users[user].ChannelList) == 0 {
|
||||
// Assume we were only tracking them in this channel, and they
|
||||
// should be removed from state.
|
||||
|
||||
delete(s.users, user)
|
||||
}
|
||||
}
|
||||
|
||||
delete(s.channels, name)
|
||||
}
|
||||
|
||||
// lookupChannel returns a reference to a channel, nil returned if no results
|
||||
// found.
|
||||
func (s *state) lookupChannel(name string) *Channel {
|
||||
return s.channels[ToRFC1459(name)]
|
||||
}
|
||||
|
||||
// lookupUser returns a reference to a user, nil returned if no results
|
||||
// found.
|
||||
func (s *state) lookupUser(name string) *User {
|
||||
return s.users[ToRFC1459(name)]
|
||||
}
|
||||
|
||||
// createUser creates the user in state, if not already done.
|
||||
func (s *state) createUser(nick string) (ok bool) {
|
||||
if _, ok := s.users[ToRFC1459(nick)]; ok {
|
||||
// User already exists.
|
||||
return false
|
||||
}
|
||||
|
||||
s.users[ToRFC1459(nick)] = &User{
|
||||
Nick: nick,
|
||||
FirstSeen: time.Now(),
|
||||
LastActive: time.Now(),
|
||||
Perms: &UserPerms{channels: make(map[string]Perms)},
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// deleteUser removes the user from channel state.
|
||||
func (s *state) deleteUser(channelName, nick string) {
|
||||
user := s.lookupUser(nick)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if channelName == "" {
|
||||
for i := 0; i < len(user.ChannelList); i++ {
|
||||
s.channels[user.ChannelList[i]].deleteUser(nick)
|
||||
}
|
||||
|
||||
delete(s.users, ToRFC1459(nick))
|
||||
return
|
||||
}
|
||||
|
||||
channel := s.lookupChannel(channelName)
|
||||
if channel == nil {
|
||||
return
|
||||
}
|
||||
|
||||
user.deleteChannel(channelName)
|
||||
channel.deleteUser(nick)
|
||||
|
||||
if len(user.ChannelList) == 0 {
|
||||
// This means they are no longer in any channels we track, delete
|
||||
// them from state.
|
||||
|
||||
delete(s.users, ToRFC1459(nick))
|
||||
}
|
||||
}
|
||||
|
||||
// renameUser renames the user in state, in all locations where relevant.
|
||||
func (s *state) renameUser(from, to string) {
|
||||
from = ToRFC1459(from)
|
||||
|
||||
// Update our nickname.
|
||||
if from == ToRFC1459(s.nick) {
|
||||
s.nick = to
|
||||
}
|
||||
|
||||
user := s.lookupUser(from)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
delete(s.users, from)
|
||||
|
||||
user.Nick = to
|
||||
user.LastActive = time.Now()
|
||||
s.users[ToRFC1459(to)] = user
|
||||
|
||||
for i := 0; i < len(user.ChannelList); i++ {
|
||||
for j := 0; j < len(s.channels[user.ChannelList[i]].UserList); j++ {
|
||||
if s.channels[user.ChannelList[i]].UserList[j] == from {
|
||||
s.channels[user.ChannelList[i]].UserList[j] = ToRFC1459(to)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
90
vendor/github.com/matrix-org/gomatrix/client.go
generated
vendored
90
vendor/github.com/matrix-org/gomatrix/client.go
generated
vendored
@ -79,7 +79,7 @@ func (cli *Client) BuildBaseURL(urlPath ...string) string {
|
||||
return hsURL.String()
|
||||
}
|
||||
|
||||
// BuildURLWithQuery builds a URL with query paramters in addition to the Client's homeserver/prefix/access_token set already.
|
||||
// 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()
|
||||
@ -387,6 +387,20 @@ func (cli *Client) JoinRoom(roomIDorAlias, serverName string, content interface{
|
||||
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")
|
||||
@ -450,6 +464,35 @@ func (cli *Client) SendText(roomID, text string) (*RespSendEvent, error) {
|
||||
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()
|
||||
@ -518,6 +561,14 @@ func (cli *Client) UnbanUser(roomID string, req *ReqUnbanUser) (resp *RespUnbanU
|
||||
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
|
||||
@ -556,8 +607,15 @@ func (cli *Client) UploadToContentRepo(content io.Reader, contentType string, co
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
contents, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, HTTPError{
|
||||
Message: "Upload request failed",
|
||||
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,
|
||||
}
|
||||
}
|
||||
@ -588,6 +646,34 @@ func (cli *Client) JoinedRooms() (resp *RespJoinedRooms, err error) {
|
||||
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)
|
||||
}
|
||||
|
33
vendor/github.com/matrix-org/gomatrix/events.go
generated
vendored
33
vendor/github.com/matrix-org/gomatrix/events.go
generated
vendored
@ -7,10 +7,10 @@ import (
|
||||
|
||||
// Event represents a single Matrix event.
|
||||
type Event struct {
|
||||
StateKey string `json:"state_key"` // The state key for the event. Only present on State Events.
|
||||
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 int `json:"origin_server_ts"` // The unix timestamp when this message was sent by the origin server
|
||||
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.
|
||||
@ -44,12 +44,31 @@ type TextMessage struct {
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
// ImageInfo contains info about an image
|
||||
// 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"`
|
||||
Width uint `json:"w"`
|
||||
Mimetype string `json:"mimetype"`
|
||||
Size uint `json:"size"`
|
||||
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
|
||||
|
43
vendor/github.com/matrix-org/gomatrix/filter.go
generated
vendored
Normal file
43
vendor/github.com/matrix-org/gomatrix/filter.go
generated
vendored
Normal 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"`
|
||||
}
|
6
vendor/github.com/matrix-org/gomatrix/requests.go
generated
vendored
6
vendor/github.com/matrix-org/gomatrix/requests.go
generated
vendored
@ -70,3 +70,9 @@ type ReqBanUser struct {
|
||||
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"`
|
||||
}
|
||||
|
32
vendor/github.com/matrix-org/gomatrix/responses.go
generated
vendored
32
vendor/github.com/matrix-org/gomatrix/responses.go
generated
vendored
@ -45,6 +45,9 @@ 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"`
|
||||
@ -58,6 +61,13 @@ type RespJoinedMembers struct {
|
||||
} `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"`
|
||||
@ -90,6 +100,11 @@ func (r RespUserInteractive) HasSingleStageFlow(stageName string) bool {
|
||||
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"`
|
||||
@ -125,6 +140,16 @@ type RespSync 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"`
|
||||
@ -142,3 +167,10 @@ type RespSync struct {
|
||||
} `json:"invite"`
|
||||
} `json:"rooms"`
|
||||
}
|
||||
|
||||
type RespTurnServer struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
TTL int `json:"ttl"`
|
||||
URIs []string `json:"uris"`
|
||||
}
|
||||
|
2
vendor/github.com/matrix-org/gomatrix/room.go
generated
vendored
2
vendor/github.com/matrix-org/gomatrix/room.go
generated
vendored
@ -13,7 +13,7 @@ func (room Room) UpdateState(event *Event) {
|
||||
if !exists {
|
||||
room.State[event.Type] = make(map[string]*Event)
|
||||
}
|
||||
room.State[event.Type][event.StateKey] = event
|
||||
room.State[event.Type][*event.StateKey] = event
|
||||
}
|
||||
|
||||
// GetStateEvent returns the state event for the given type/state_key combo, or nil.
|
||||
|
12
vendor/github.com/matrix-org/gomatrix/sync.go
generated
vendored
12
vendor/github.com/matrix-org/gomatrix/sync.go
generated
vendored
@ -73,6 +73,16 @@ func (s *DefaultSyncer) ProcessResponse(res *RespSync, since string) (err error)
|
||||
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
|
||||
}
|
||||
|
||||
@ -102,7 +112,7 @@ func (s *DefaultSyncer) shouldProcessResponse(resp *RespSync, since string) bool
|
||||
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 == s.UserID {
|
||||
if e.Type == "m.room.member" && e.StateKey != nil && *e.StateKey == s.UserID {
|
||||
m := e.Content["membership"]
|
||||
mship, ok := m.(string)
|
||||
if !ok {
|
||||
|
201
vendor/github.com/matterbridge/gomatrix/LICENSE
generated
vendored
Normal file
201
vendor/github.com/matterbridge/gomatrix/LICENSE
generated
vendored
Normal 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
703
vendor/github.com/matterbridge/gomatrix/client.go
generated
vendored
Normal 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
102
vendor/github.com/matterbridge/gomatrix/events.go
generated
vendored
Normal 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
43
vendor/github.com/matterbridge/gomatrix/filter.go
generated
vendored
Normal 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
78
vendor/github.com/matterbridge/gomatrix/requests.go
generated
vendored
Normal 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
176
vendor/github.com/matterbridge/gomatrix/responses.go
generated
vendored
Normal 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"`
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user