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

Compare commits

..

7 Commits

Author SHA1 Message Date
Wim
d9ff0b72fa Release 0.7.1 2016-11-20 17:28:56 +01:00
Wim
8c83eb03c7 Update documentation. Prepare release 2016-11-20 17:27:48 +01:00
Wim
e28649b592 Remove callbacks after being called. Fixes #88 (irc) 2016-11-20 17:20:52 +01:00
Wim
e4e822ef6a Fix !users command for irc. Closes #78. 2016-11-14 00:10:55 +01:00
Wim
69d6f4b2da Remove double username modify. Fixes #77 2016-11-13 23:50:12 +01:00
Wim
f7e22983a5 Release 0.7.0 2016-11-12 22:43:24 +01:00
Wim
cac9fb838c Update documentation 2016-11-12 22:41:56 +01:00
1252 changed files with 6061 additions and 711066 deletions

View File

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

View File

@ -1,49 +0,0 @@
language: go
go:
#- 1.7.x
- 1.9.x
# - tip
# we have everything vendored
install: true
env:
- GOOS=linux GOARCH=amd64
# - GOOS=windows GOARCH=amd64
#- GOOS=linux GOARCH=arm
matrix:
# It's ok if our code fails on unstable development versions of Go.
allow_failures:
- go: tip
# Don't wait for tip tests to finish. Mark the test run green if the
# tests pass on the stable versions of Go.
fast_finish: true
notifications:
email: false
before_script:
- MY_VERSION=$(git describe --tags)
- GO_FILES=$(find . -iname '*.go' | grep -v /vendor/) # All the .go files, excluding vendor/
- PKGS=$(go list ./... | grep -v /vendor/) # All the import paths, excluding vendor/
# - go get github.com/golang/lint/golint # Linter
- go get honnef.co/go/tools/cmd/megacheck # Badass static analyzer/linter
# Anything in before_script: that returns a nonzero exit code will
# 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
- 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
- /bin/bash ci/bintray.sh
#- golint -set_exit_status $PKGS # one last linter
deploy:
provider: bintray
file: ci/deploy.json
user: 42wim
key:
secure: "CeXXe6JOmt7HYR81MdWLua0ltQHhDdkIeRGBFbgd7hkb1wi8eF9DgpAcQrTso8NIlHNZmSAP46uhFgsRvkuezzX0ygalZ7DCJyAyn3sAMEh+UQSHV1WGThRehTtidqRGjetzsIGSwdrJOWil+XTfbO1Z8DGzfakhSuAZka8CM4BAoe3YeP9rYK8h+84x0GHfczvsLtXZ3mWLvQuwe4pK6+ItBCUg0ae7O7ZUpWHy0xQQkkWztY/6RAzXfaG7DuGjIw+20fhx3WOXRNpHCtZ6Bc3qERCpk0s1HhlQWlrN9wDaFTBWYwlvSnNgvxxMbNXJ6RrRJ0l0bA7FUswYwyroxhzrGLdzWDg8dHaQkypocngdalfhpsnoO9j3ApJhomUFJ3UoEq5nOGRUrKn8MPi+dP0zE4kNQ3e4VNa1ufNrvfpWolMg3xh8OXuhQdD5wIM5zFAbRJLqWSCVAjPq4DDPecmvXBOlIial7oa312lN5qnBnUjvAcxszZ+FUyDHT1Grxzna4tMwxY9obPzZUzm7359AOCCwIQFVB8GLqD2nwIstcXS0zGRz+fhviPipHuBa02q5bGUZwmkvrSNab0s8Jo7pCrel2Rz3nWPKaiCfq2WjbW1CLheSMkOQrjsdUd1hhbqNWFPUjJPInTc77NAKCfm5runv5uyowRLh4NNd0sI="

View File

@ -6,6 +6,6 @@ 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 \
&& go build -o /bin/matterbridge \
&& rm -rf /go \
&& apk del --purge git go gcc musl-dev

115
README-0.6.md Normal file
View File

@ -0,0 +1,115 @@
# matterbridge
Simple bridge between mattermost, IRC, XMPP, Gitter and Slack
* Relays public channel messages between mattermost, IRC, XMPP, Gitter and Slack. Pick and mix.
* Supports multiple channels.
* Matterbridge can also work with private groups on your mattermost.
Look at [matterbridge.conf.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.conf.sample) for documentation and an example.
## Changelog
Since v0.6.1 support for XMPP, Gitter and Slack is added. More details in [changelog.md] (https://github.com/42wim/matterbridge/blob/master/changelog.md)
## Requirements:
Accounts to one of the supported bridges
* [Mattermost] (https://github.com/mattermost/platform/)
* [IRC] (http://www.mirc.com/servers.html)
* [XMPP] (https://jabber.org)
* [Gitter] (https://gitter.im)
* [Slack] (https://www.slack.com)
## binaries
Binaries can be found [here] (https://github.com/42wim/matterbridge/releases/)
* For use with mattermost 3.3.0+ [v0.6.1](https://github.com/42wim/matterircd/releases/tag/v0.6.1)
* For use with mattermost 3.0.0-3.2.0 [v0.5.0](https://github.com/42wim/matterircd/releases/tag/v0.5.0)
## Docker
Create your matterbridge.conf file locally eg in ```/tmp/matterbridge.conf```
```
docker run -ti -v /tmp/matterbridge.conf:/matterbridge.conf 42wim/matterbridge:0.6.1
```
## Compatibility
### Mattermost
* Matterbridge v0.6.1 works with mattermost 3.3.0 and higher [3.3.0 release](https://github.com/mattermost/platform/releases/tag/v3.3.0)
* Matterbridge v0.5.0 works with mattermost 3.0.0 - 3.2.0 [3.2.0 release](https://github.com/mattermost/platform/releases/tag/v3.2.0)
#### Webhooks version
* Configured incoming/outgoing [webhooks](https://www.mattermost.org/webhooks/) on your mattermost instance.
#### Plus (API) version
* A dedicated user(bot) on your mattermost instance.
## building
Go 1.6+ 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
go get github.com/42wim/matterbridge
```
You should now have matterbridge binary in the bin directory:
```
$ ls bin/
matterbridge
```
## running
1) Copy the matterbridge.conf.sample to matterbridge.conf in the same directory as the matterbridge binary.
2) Edit matterbridge.conf with the settings for your environment. See below for more config information.
3) Now you can run matterbridge.
```
Usage of ./matterbridge:
-conf string
config file (default "matterbridge.conf")
-debug
enable debug
-plus
running using API instead of webhooks (deprecated, set Plus flag in [general] config)
-version
show version
```
## config
### matterbridge
matterbridge looks for matterbridge.conf in current directory. (use -conf to specify another file)
Look at [matterbridge.conf.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.conf.sample) for an example.
### mattermost
#### webhooks version
You'll have to configure the incoming and outgoing webhooks.
* incoming webhooks
Go to "account settings" - integrations - "incoming webhooks".
Choose a channel at "Add a new incoming webhook", this will create a webhook URL right below.
This URL should be set in the matterbridge.conf in the [mattermost] section (see above)
* outgoing webhooks
Go to "account settings" - integrations - "outgoing webhooks".
Choose a channel (the same as the one from incoming webhooks) and fill in the address and port of the server matterbridge will run on.
e.g. http://192.168.1.1:9999 (192.168.1.1:9999 is the BindAddress specified in [mattermost] section of matterbridge.conf)
#### plus version
You'll have to create a new dedicated user on your mattermost instance.
Specify the login and password in [mattermost] section of matterbridge.conf
## FAQ
Please look at [matterbridge.conf.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.conf.sample) for more information first.
### Mattermost doesn't show the IRC nicks
If you're running the webhooks version, this can be fixed by either:
* enabling "override usernames". See [mattermost documentation](http://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks)
* setting ```PrefixMessagesWithNick``` to ```true``` in ```mattermost``` section of your matterbridge.conf.
If you're running the plus version you'll need to:
* setting ```PrefixMessagesWithNick``` to ```true``` in ```mattermost``` section of your matterbridge.conf.
Also look at the ```RemoteNickFormat``` setting.

226
README.md
View File

@ -1,74 +1,55 @@
# matterbridge
Click on one of the badges below to join the chat
Simple bridge between mattermost, IRC, XMPP, Gitter, Slack and Discord
[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?colorB=42f4242)](https://gitter.im/42wim/matterbridge) [![Join the IRC chat at https://webchat.freenode.net/?channels=matterbridgechat](https://img.shields.io/badge/IRC-matterbridgechat-green.svg?colorB=42f4242)](https://webchat.freenode.net/?channels=matterbridgechat) [![Discord](https://img.shields.io/badge/discord-matterbridge-green.svg?colorB=42f4242)](https://discord.gg/AkKPtrQ) [![Matrix](https://img.shields.io/badge/matrix-matterbridge-green.svg?colorB=42f4242)](https://riot.im/app/#/room/#matterbridge:matrix.org) [![Slack](https://img.shields.io/badge/slack-matterbridgechat-green.svg?colorB=42f4242)](https://join.slack.com/matterbridgechat/shared_invite/MjEwODMxNjU1NDMwLTE0OTk2MTU3NTMtMzZkZmRiNDZhOA) [![Mattermost](https://img.shields.io/badge/mattermost-matterbridge-green.svg?colorB=42f4242)](https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e) [![Xmpp](https://img.shields.io/badge/xmpp-matterbridge@conference.jabber.de-green.svg?colorB=42f4242)](https://inverse.chat) [![Twitch](https://img.shields.io/badge/twitch-matterbridge-green.svg?colorB=42f4242)](https://www.twitch.tv/matterbridge)
[![Download stable](https://img.shields.io/github/release/42wim/matterbridge.svg?label=download%20stable)](https://github.com/42wim/matterbridge/releases/latest) [![Download dev](https://img.shields.io/bintray/v/42wim/nightly/Matterbridge.svg?label=download%20dev&colorB=007ec6)](https://bintray.com/42wim/nightly/Matterbridge/_latestVersion)
![matterbridge.gif](https://s15.postimg.org/qpjhp6y3f/matterbridge.gif)
Simple bridge between Mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram, Rocket.Chat, Hipchat(via xmpp), Matrix and Steam.
Has a REST API.
Minecraft server chat support via [MatterLink](https://github.com/elytra/MatterLink)
# Table of Contents
* [Features](#features)
* [Requirements](#requirements)
* [Screenshots](https://github.com/42wim/matterbridge/wiki/)
* [Installing](#installing)
* [Binaries](#binaries)
* [Building](#building)
* [Configuration](#configuration)
* [Howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config)
* [Examples](#examples)
* [Running](#running)
* [Docker](#docker)
* [Changelog](#changelog)
* [FAQ](#faq)
* [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.
* Relays public channel messages between multiple mattermost, IRC, XMPP, Gitter, Slack and Discord. Pick and mix.
* Supports multiple channels.
* Matterbridge can also work with private groups on your mattermost.
* Allow for bridging the same bridges, which means you can eg bridge between multiple mattermosts.
* 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).
## API
The API is very basic at the moment and rather undocumented.
Look at [matterbridge.toml.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for documentation and an example.
Look at [matterbridge.toml.simple] (https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.simple) for a simple example.
Used by at least 2 projects. Feel free to make a PR to add your project to this list.
## Changelog
Since v0.7.0 the configuration has changed. More details in [changelog.md] (https://github.com/42wim/matterbridge/blob/master/changelog.md)
* [MatterLink](https://github.com/elytra/MatterLink) (Matterbridge link for Minecraft Server chat)
* [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
# Requirements
## Requirements
Accounts to one of the supported bridges
* [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)
* [Slack](https://slack.com)
* [Discord](https://discordapp.com)
* [Telegram](https://telegram.org)
* [Hipchat](https://www.hipchat.com)
* [Rocket.chat](https://rocket.chat)
* [Matrix](https://matrix.org)
* [Steam](https://store.steampowered.com/)
* [Twitch](https://twitch.tv)
* [Mattermost] (https://github.com/mattermost/platform/)
* [IRC] (http://www.mirc.com/servers.html)
* [XMPP] (https://jabber.org)
* [Gitter] (https://gitter.im)
* [Slack] (https://slack.com)
* [Discord] (https://discordapp.com)
# Screenshots
See https://github.com/42wim/matterbridge/wiki
## Docker
Create your matterbridge.toml file locally eg in ```/tmp/matterbridge.toml```
```
docker run -ti -v /tmp/matterbridge.toml:/matterbridge.toml 42wim/matterbridge
```
# Installing
## Binaries
* Latest stable release [v1.7.1](https://github.com/42wim/matterbridge/releases/latest)
* Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/)
## binaries
Binaries can be found [here] (https://github.com/42wim/matterbridge/releases/)
* For use with mattermost 3.5.0+ [v0.8.1](https://github.com/42wim/matterircd/releases/tag/v0.8.1)
* For use with mattermost 3.3.0 - 3.4.0 [v0.7.1](https://github.com/42wim/matterircd/releases/tag/v0.7.1)
* For use with mattermost 3.0.0 - 3.2.0 [v0.5.0](https://github.com/42wim/matterircd/releases/tag/v0.5.0) (not maintained anymore)
## Building
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)
## Compatibility
### Mattermost
* Matterbridge v0.8.1 works with mattermost 3.5.0+ [3.5.0 release](https://github.com/mattermost/platform/releases/tag/v3.5.0)
* Matterbridge v0.7.1 works with mattermost 3.3.0 - 3.4.0 [3.4.0 release](https://github.com/mattermost/platform/releases/tag/v3.4.0)
* Matterbridge v0.5.0 works with mattermost 3.0.0 - 3.2.0 [3.2.0 release](https://github.com/mattermost/platform/releases/tag/v3.2.0)
#### Webhooks version
* Configured incoming/outgoing [webhooks](https://www.mattermost.org/webhooks/) on your mattermost instance.
#### API version
* A dedicated user(bot) on your mattermost instance.
## building
Go 1.6+ 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
@ -82,73 +63,10 @@ $ ls bin/
matterbridge
```
# Configuration
## Basic configuration
See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
## Advanced configuration
* [matterbridge.toml.sample](https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for documentation and an example.
## Examples
### Bridge mattermost (off-topic) - irc (#testing)
```
[irc]
[irc.freenode]
Server="irc.freenode.net:6667"
Nick="yourbotname"
[mattermost]
[mattermost.work]
Server="yourmattermostserver.tld"
Team="yourteam"
Login="yourlogin"
Password="yourpass"
PrefixMessagesWithNick=true
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
[[gateway]]
name="mygateway"
enable=true
[[gateway.inout]]
account="irc.freenode"
channel="#testing"
[[gateway.inout]]
account="mattermost.work"
channel="off-topic"
```
### Bridge slack (#general) - discord (general)
```
[slack]
[slack.test]
Token="yourslacktoken"
PrefixMessagesWithNick=true
[discord]
[discord.test]
Token="yourdiscordtoken"
Server="yourdiscordservername"
[general]
RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> "
[[gateway]]
name = "mygateway"
enable=true
[[gateway.inout]]
account = "discord.test"
channel="general"
[[gateway.inout]]
account ="slack.test"
channel = "general"
```
# Running
See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
## running
1) Copy the matterbridge.conf.sample to matterbridge.conf in the same directory as the matterbridge binary.
2) Edit matterbridge.conf with the settings for your environment. See below for more config information.
3) Now you can run matterbridge.
```
Usage of ./matterbridge:
@ -156,39 +74,39 @@ Usage of ./matterbridge:
config file (default "matterbridge.toml")
-debug
enable debug
-gops
enable gops agent
-version
show version
```
## Docker
Create your matterbridge.toml file locally eg in ```/tmp/matterbridge.toml```
```
docker run -ti -v /tmp/matterbridge.toml:/matterbridge.toml 42wim/matterbridge
```
## config
### matterbridge
matterbridge looks for matterbridge.toml in current directory. (use -conf to specify another file)
# Changelog
See [changelog.md](https://github.com/42wim/matterbridge/blob/master/changelog.md)
Look at [matterbridge.toml.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for an example.
# FAQ
### mattermost
#### webhooks version
You'll have to configure the incoming and outgoing webhooks.
See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
* incoming webhooks
Go to "account settings" - integrations - "incoming webhooks".
Choose a channel at "Add a new incoming webhook", this will create a webhook URL right below.
This URL should be set in the matterbridge.conf in the [mattermost] section (see above)
Want to tip ?
* eth: 0xb3f9b5387c66ad6be892bcb7bbc67862f3abc16f
* btc: 1N7cKHj5SfqBHBzDJ6kad4BzeqUBBS2zhs
* outgoing webhooks
Go to "account settings" - integrations - "outgoing webhooks".
Choose a channel (the same as the one from incoming webhooks) and fill in the address and port of the server matterbridge will run on.
# Thanks
Matterbridge wouldn't exist without these libraries:
* discord - https://github.com/bwmarrin/discordgo
* echo - https://github.com/labstack/echo
* gitter - https://github.com/sromku/go-gitter
* gops - https://github.com/google/gops
* 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
* steam - https://github.com/Philipp15b/go-steam
* telegram - https://github.com/go-telegram-bot-api/telegram-bot-api
* xmpp - https://github.com/mattn/go-xmpp
e.g. http://192.168.1.1:9999 (192.168.1.1:9999 is the BindAddress specified in [mattermost] section of matterbridge.conf)
## FAQ
Please look at [matterbridge.toml.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for more information first.
### Mattermost doesn't show the IRC nicks
If you're running the webhooks version, this can be fixed by either:
* enabling "override usernames". See [mattermost documentation](http://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks)
* setting ```PrefixMessagesWithNick``` to ```true``` in ```mattermost``` section of your matterbridge.toml.
If you're running the plus version you'll need to:
* setting ```PrefixMessagesWithNick``` to ```true``` in ```mattermost``` section of your matterbridge.toml.
Also look at the ```RemoteNickFormat``` setting.

View File

@ -1,124 +0,0 @@
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"
"github.com/zfjagann/golang-ring"
"net/http"
"sync"
"time"
)
type Api struct {
Messages ring.Ring
sync.RWMutex
*config.BridgeConfig
}
type ApiMessage struct {
Text string `json:"text"`
Username string `json:"username"`
UserID string `json:"userid"`
Avatar string `json:"avatar"`
Gateway string `json:"gateway"`
}
var flog *log.Entry
var protocol = "api"
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.BridgeConfig) *Api {
b := &Api{BridgeConfig: cfg}
e := echo.New()
b.Messages = ring.Ring{}
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(b.Config.BindAddress))
}()
return b
}
func (b *Api) Connect() error {
return nil
}
func (b *Api) Disconnect() error {
return nil
}
func (b *Api) JoinChannel(channel config.ChannelInfo) error {
return nil
}
func (b *Api) Send(msg config.Message) (string, error) {
b.Lock()
defer b.Unlock()
// ignore delete messages
if msg.Event == config.EVENT_MSG_DELETE {
return "", nil
}
b.Messages.Enqueue(&msg)
return "", nil
}
func (b *Api) handlePostMessage(c echo.Context) error {
message := &ApiMessage{}
if err := c.Bind(message); err != nil {
return err
}
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",
}
return c.JSON(http.StatusOK, message)
}
func (b *Api) handleMessages(c echo.Context) error {
b.Lock()
defer b.Unlock()
c.JSONPretty(http.StatusOK, b.Messages.Values(), " ")
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)
}
}
}

View File

@ -1,112 +1,45 @@
package bridge
import (
"github.com/42wim/matterbridge/bridge/api"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/discord"
"github.com/42wim/matterbridge/bridge/gitter"
"github.com/42wim/matterbridge/bridge/irc"
"github.com/42wim/matterbridge/bridge/matrix"
"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"
"strings"
)
type Bridger interface {
Send(msg config.Message) (string, error)
type Bridge interface {
Send(msg config.Message) error
Name() string
Connect() error
JoinChannel(channel config.ChannelInfo) error
Disconnect() error
FullOrigin() string
Origin() string
Protocol() string
JoinChannel(channel string) error
}
type Bridge struct {
Config config.Protocol
Bridger
Name string
Account string
Protocol string
Channels map[string]config.ChannelInfo
Joined map[string]bool
}
func New(cfg *config.Config, bridge *config.Bridge, c chan config.Message) *Bridge {
b := new(Bridge)
b.Channels = make(map[string]config.ChannelInfo)
func New(cfg *config.Config, bridge *config.Bridge, c chan config.Message) Bridge {
accInfo := strings.Split(bridge.Account, ".")
protocol := accInfo[0]
name := accInfo[1]
b.Name = name
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":
bridgeConfig.Config = cfg.Mattermost[name]
b.Bridger = bmattermost.New(bridgeConfig)
return bmattermost.New(cfg.Mattermost[name], name, c)
case "irc":
bridgeConfig.Config = cfg.IRC[name]
b.Bridger = birc.New(bridgeConfig)
return birc.New(cfg.IRC[name], name, c)
case "gitter":
bridgeConfig.Config = cfg.Gitter[name]
b.Bridger = bgitter.New(bridgeConfig)
return bgitter.New(cfg.Gitter[name], name, c)
case "slack":
bridgeConfig.Config = cfg.Slack[name]
b.Bridger = bslack.New(bridgeConfig)
return bslack.New(cfg.Slack[name], name, c)
case "xmpp":
bridgeConfig.Config = cfg.Xmpp[name]
b.Bridger = bxmpp.New(bridgeConfig)
return bxmpp.New(cfg.Xmpp[name], name, c)
case "discord":
bridgeConfig.Config = cfg.Discord[name]
b.Bridger = bdiscord.New(bridgeConfig)
case "telegram":
bridgeConfig.Config = cfg.Telegram[name]
b.Bridger = btelegram.New(bridgeConfig)
case "rocketchat":
bridgeConfig.Config = cfg.Rocketchat[name]
b.Bridger = brocketchat.New(bridgeConfig)
case "matrix":
bridgeConfig.Config = cfg.Matrix[name]
b.Bridger = bmatrix.New(bridgeConfig)
case "steam":
bridgeConfig.Config = cfg.Steam[name]
b.Bridger = bsteam.New(bridgeConfig)
case "sshchat":
bridgeConfig.Config = cfg.Sshchat[name]
b.Bridger = bsshchat.New(bridgeConfig)
case "api":
bridgeConfig.Config = cfg.Api[name]
b.Bridger = api.New(bridgeConfig)
}
b.Config = bridgeConfig.Config
return b
}
func (b *Bridge) JoinChannels() error {
err := b.joinChannels(b.Channels, b.Joined)
return err
}
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)
err := b.JoinChannel(channel)
if err != nil {
return err
}
exists[ID] = true
}
return bdiscord.New(cfg.Discord[name], name, c)
}
return nil
}

View File

@ -6,115 +6,52 @@ import (
"os"
"reflect"
"strings"
"time"
)
const (
EVENT_JOIN_LEAVE = "join_leave"
EVENT_FAILURE = "failure"
EVENT_REJOIN_CHANNELS = "rejoin_channels"
EVENT_USER_ACTION = "user_action"
EVENT_MSG_DELETE = "msg_delete"
)
type Message struct {
Text string `json:"text"`
Channel string `json:"channel"`
Username string `json:"username"`
UserID string `json:"userid"` // userid on the bridge
Avatar string `json:"avatar"`
Account string `json:"account"`
Event string `json:"event"`
Protocol string `json:"protocol"`
Gateway string `json:"gateway"`
Timestamp time.Time `json:"timestamp"`
ID string `json:"id"`
Extra map[string][]interface{}
}
type FileInfo struct {
Name string
Data *[]byte
Comment string
URL string
}
type ChannelInfo struct {
Name string
Account string
Direction string
ID string
SameChannel map[string]bool
Options ChannelOptions
Text string
Channel string
Username string
Origin string
FullOrigin string
Protocol string
Avatar string
}
type Protocol struct {
AuthCode string // steam
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
BindAddress string // mattermost, slack
IconURL string // mattermost, slack
IgnoreNicks string // all protocols
IgnoreMessages string // all protocols
Jid string // xmpp
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
NickFormatter string // mattermost, slack
NickServNick string // IRC
NickServUsername string // IRC
NickServPassword string // IRC
NicksPerRow int // mattermost, slack
NoHomeServerSuffix bool // matrix
NoTLS bool // mattermost
Password string // IRC,mattermost,XMPP,matrix
PrefixMessagesWithNick bool // mattemost, slack
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
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
UseAPI bool // mattermost, slack
UseSASL bool // IRC
UseTLS bool // IRC
UseFirstName bool // telegram
UseUserName bool // discord
UseInsecureURL bool // telegram
WebhookBindAddress string // mattermost, slack
WebhookURL string // mattermost, slack
WebhookUse string // mattermost, slack, discord
}
type ChannelOptions struct {
Key string // irc
WebhookURL string // discord
Login string // mattermost
Muc string // xmpp
Name string // all protocols
Nick string // all protocols
NickFormatter string // mattermost, slack
NickServNick string // IRC
NickServPassword string // IRC
NicksPerRow int // mattermost, slack
NoTLS bool // mattermost
Password string // IRC,mattermost,XMPP
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
RemoteNickFormat string // all protocols
Server string // IRC,mattermost,XMPP,discord
ShowJoinPart bool // all protocols
SkipTLSVerify bool // IRC, mattermost
Team string // mattermost
Token string // gitter, slack, discord
URL string // mattermost, slack
UseAPI bool // mattermost, slack
UseSASL bool // IRC
UseTLS bool // IRC
}
type Bridge struct {
Account string
Channel string
Options ChannelOptions
SameChannel bool
Account string
Channel string
}
type Gateway struct {
@ -122,7 +59,6 @@ type Gateway struct {
Enable bool
In []Bridge
Out []Bridge
InOut []Bridge
}
type SameChannelGateway struct {
@ -133,60 +69,21 @@ type SameChannelGateway struct {
}
type Config struct {
Api map[string]Protocol
IRC map[string]Protocol
Mattermost map[string]Protocol
Matrix map[string]Protocol
Slack map[string]Protocol
Steam map[string]Protocol
Gitter map[string]Protocol
Xmpp map[string]Protocol
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 {
log.Fatal(err)
}
fail := false
for k, v := range cfg.Mattermost {
res := Deprecated(v, "mattermost."+k)
if res {
fail = res
}
}
for k, v := range cfg.Slack {
res := Deprecated(v, "slack."+k)
if res {
fail = res
}
}
for k, v := range cfg.Rocketchat {
res := Deprecated(v, "rocketchat."+k)
if res {
fail = res
}
}
if fail {
log.Fatalf("Fix your config. Please see changelog for more information")
}
if cfg.General.MediaDownloadSize == 0 {
cfg.General.MediaDownloadSize = 1000000
}
return &cfg
}
@ -229,25 +126,16 @@ func OverrideCfgFromEnv(cfg *Config, protocol string, account string) {
func GetIconURL(msg *Message, cfg *Protocol) string {
iconURL := cfg.IconURL
info := strings.Split(msg.Account, ".")
protocol := info[0]
name := info[1]
iconURL = strings.Replace(iconURL, "{NICK}", msg.Username, -1)
iconURL = strings.Replace(iconURL, "{BRIDGE}", name, -1)
iconURL = strings.Replace(iconURL, "{PROTOCOL}", protocol, -1)
iconURL = strings.Replace(iconURL, "{BRIDGE}", msg.Origin, -1)
iconURL = strings.Replace(iconURL, "{PROTOCOL}", msg.Protocol, -1)
return iconURL
}
func Deprecated(cfg Protocol, account string) bool {
if cfg.BindAddress != "" {
log.Printf("ERROR: %s BindAddress is deprecated, you need to change it to WebhookBindAddress.", account)
} else if cfg.URL != "" {
log.Printf("ERROR: %s URL is deprecated, you need to change it to WebhookURL.", account)
} else if cfg.UseAPI {
log.Printf("ERROR: %s UseAPI is deprecated, it's enabled by default, please remove it from your config file.", account)
} else {
return false
}
return true
//log.Fatalf("ERROR: Fix your config: %s", account)
func GetNick(msg *Message, cfg *Protocol) string {
nick := cfg.RemoteNickFormat
nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
nick = strings.Replace(nick, "{BRIDGE}", msg.Origin, -1)
nick = strings.Replace(nick, "{PROTOCOL}", msg.Protocol, -1)
return nick
}

View File

@ -1,27 +1,21 @@
package bdiscord
import (
"bytes"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
"github.com/bwmarrin/discordgo"
"regexp"
"strings"
"sync"
)
type bdiscord struct {
c *discordgo.Session
Channels []*discordgo.Channel
Nick string
UseChannelID bool
userMemberMap map[string]*discordgo.Member
guildID string
webhookID string
webhookToken string
channelInfoMap map[string]*config.ChannelInfo
sync.RWMutex
*config.BridgeConfig
c *discordgo.Session
Config *config.Protocol
Remote chan config.Message
protocol string
origin string
Channels []*discordgo.Channel
Nick string
UseChannelID bool
}
var flog *log.Entry
@ -31,28 +25,18 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
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 != "" {
flog.Debug("Configuring Discord Incoming Webhook")
b.webhookID, b.webhookToken = b.splitURL(b.Config.WebhookURL)
}
func New(cfg config.Protocol, origin string, c chan config.Message) *bdiscord {
b := &bdiscord{}
b.Config = &cfg
b.Remote = c
b.protocol = protocol
b.origin = origin
return b
}
func (b *bdiscord) Connect() error {
var err error
flog.Info("Connecting")
if b.Config.WebhookURL == "" {
flog.Info("Connecting using token")
} else {
flog.Info("Connecting using webhookurl (for posting) and token")
}
if !strings.HasPrefix(b.Config.Token, "Bot ") {
b.Config.Token = "Bot " + b.Config.Token
}
b.c, err = discordgo.New(b.Config.Token)
if err != nil {
flog.Debugf("%#v", err)
@ -60,15 +44,12 @@ func (b *bdiscord) Connect() error {
}
flog.Info("Connection succeeded")
b.c.AddHandler(b.messageCreate)
b.c.AddHandler(b.memberUpdate)
b.c.AddHandler(b.messageUpdate)
b.c.AddHandler(b.messageDelete)
err = b.c.Open()
if err != nil {
flog.Debugf("%#v", err)
return err
}
guilds, err := b.c.UserGuilds(100, "", "")
guilds, err := b.c.UserGuilds()
if err != nil {
flog.Debugf("%#v", err)
return err
@ -82,7 +63,6 @@ func (b *bdiscord) Connect() error {
for _, guild := range guilds {
if guild.Name == b.Config.Server {
b.Channels, err = b.c.GuildChannels(guild.ID)
b.guildID = guild.ID
if err != nil {
flog.Debugf("%#v", err)
return err
@ -92,109 +72,40 @@ func (b *bdiscord) Connect() error {
return nil
}
func (b *bdiscord) Disconnect() error {
return nil
func (b *bdiscord) FullOrigin() string {
return b.protocol + "." + b.origin
}
func (b *bdiscord) JoinChannel(channel config.ChannelInfo) error {
b.channelInfoMap[channel.ID] = &channel
idcheck := strings.Split(channel.Name, "ID:")
func (b *bdiscord) JoinChannel(channel string) error {
idcheck := strings.Split(channel, "ID:")
if len(idcheck) > 1 {
b.UseChannelID = true
}
return nil
}
func (b *bdiscord) Send(msg config.Message) (string, error) {
func (b *bdiscord) Name() string {
return b.protocol + "." + b.origin
}
func (b *bdiscord) Protocol() string {
return b.protocol
}
func (b *bdiscord) Origin() string {
return b.origin
}
func (b *bdiscord) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg)
channelID := b.getChannelID(msg.Channel)
if channelID == "" {
flog.Errorf("Could not find channelID for %v", msg.Channel)
return "", nil
}
if msg.Event == config.EVENT_USER_ACTION {
msg.Text = "_" + msg.Text + "_"
}
wID := b.webhookID
wToken := b.webhookToken
if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok {
if ci.Options.WebhookURL != "" {
wID, wToken = b.splitURL(ci.Options.WebhookURL)
}
}
if wID == "" {
flog.Debugf("Broadcasting using token (API)")
if msg.Event == config.EVENT_MSG_DELETE {
if msg.ID == "" {
return "", nil
}
err := b.c.ChannelMessageDelete(channelID, msg.ID)
return "", err
}
if msg.ID != "" {
_, err := b.c.ChannelMessageEdit(channelID, msg.ID, msg.Username+msg.Text)
return msg.ID, err
}
if msg.Extra != nil {
// 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
}
return res.ID, err
}
flog.Debugf("Broadcasting using Webhook")
err := b.c.WebhookExecute(
wID,
wToken,
true,
&discordgo.WebhookParams{
Content: msg.Text,
Username: msg.Username,
AvatarURL: msg.Avatar,
})
return "", err
}
func (b *bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) {
rmsg := config.Message{Account: b.Account, ID: m.ID, Event: config.EVENT_MSG_DELETE, Text: config.EVENT_MSG_DELETE}
rmsg.Channel = b.getChannelName(m.ChannelID)
if b.UseChannelID {
rmsg.Channel = "ID:" + m.ChannelID
}
flog.Debugf("Sending message from %s to gateway", b.Account)
flog.Debugf("Message is %#v", rmsg)
b.Remote <- rmsg
}
func (b *bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) {
if b.Config.EditDisable {
return
}
// only when message is actually edited
if m.Message.EditedTimestamp != "" {
flog.Debugf("Sending edit message")
m.Content = m.Content + b.Config.EditSuffix
b.messageCreate(s, (*discordgo.MessageCreate)(m))
return nil
}
nick := config.GetNick(&msg, b.Config)
b.c.ChannelMessageSend(channelID, nick+msg.Text)
return nil
}
func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
@ -202,98 +113,21 @@ func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
if m.Author.Username == b.Nick {
return
}
// if using webhooks, do not relay if it's ours
if b.useWebhook() && m.Author.Bot && b.isWebhookID(m.Author.ID) {
return
}
if len(m.Attachments) > 0 {
for _, attach := range m.Attachments {
m.Content = m.Content + "\n" + attach.URL
}
}
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 = 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}
rmsg.Channel = b.getChannelName(m.ChannelID)
if b.UseChannelID {
rmsg.Channel = "ID:" + m.ChannelID
}
if !b.Config.UseUserName {
rmsg.Username = b.getNick(m.Author)
} else {
rmsg.Username = m.Author.Username
}
if b.Config.ShowEmbeds && m.Message.Embeds != nil {
for _, embed := range m.Message.Embeds {
text = text + "embed: " + embed.Title + " - " + embed.Description + " - " + embed.URL + "\n"
}
}
// no empty messages
if text == "" {
if m.Content == "" {
return
}
text, ok := b.replaceAction(text)
if ok {
rmsg.Event = config.EVENT_USER_ACTION
flog.Debugf("Sending message from %s on %s to gateway", m.Author.Username, b.FullOrigin())
channelName := b.getChannelName(m.ChannelID)
if b.UseChannelID {
channelName = "ID:" + m.ChannelID
}
rmsg.Text = text
flog.Debugf("Sending message from %s on %s to gateway", m.Author.Username, b.Account)
flog.Debugf("Message is %#v", rmsg)
b.Remote <- rmsg
}
func (b *bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) {
b.Lock()
if _, ok := b.userMemberMap[m.Member.User.ID]; ok {
flog.Debugf("%s: memberupdate: user %s (nick %s) changes nick to %s", b.Account, m.Member.User.Username, b.userMemberMap[m.Member.User.ID].Nick, m.Member.Nick)
}
b.userMemberMap[m.Member.User.ID] = m.Member
b.Unlock()
}
func (b *bdiscord) getNick(user *discordgo.User) string {
var err error
b.Lock()
defer b.Unlock()
if _, ok := b.userMemberMap[user.ID]; ok {
if b.userMemberMap[user.ID] != nil {
if b.userMemberMap[user.ID].Nick != "" {
// only return if nick is set
return b.userMemberMap[user.ID].Nick
}
// otherwise return username
return user.Username
}
}
// if we didn't find nick, search for it
member, err := b.c.GuildMember(b.guildID, user.ID)
if err != nil {
return user.Username
}
b.userMemberMap[user.ID] = member
// only return if nick is set
if b.userMemberMap[user.ID].Nick != "" {
return b.userMemberMap[user.ID].Nick
}
return user.Username
b.Remote <- config.Message{Username: m.Author.Username, Text: m.ContentWithMentionsReplaced(), Channel: channelName,
Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin(), Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg"}
}
func (b *bdiscord) getChannelID(name string) string {
@ -317,85 +151,3 @@ 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]+>")
text = re.ReplaceAllStringFunc(text, func(m string) string {
channel := b.getChannelName(m[2 : len(m)-1])
// if at first don't succeed, try again
if channel == "" {
b.Channels, err = b.c.GuildChannels(b.guildID)
if err != nil {
return "#unknownchannel"
}
channel = b.getChannelName(m[2 : len(m)-1])
return "#" + channel
}
return "#" + channel
})
return text
}
func (b *bdiscord) replaceAction(text string) (string, bool) {
if strings.HasPrefix(text, "_") && strings.HasSuffix(text, "_") {
return strings.Replace(text, "_", "", -1), true
}
return text, false
}
func (b *bdiscord) stripCustomoji(text string) string {
// <:doge:302803592035958784>
re := regexp.MustCompile("<(:.*?:)[0-9]+>")
return re.ReplaceAllString(text, `$1`)
}
// splitURL splits a webhookURL and returns the id and token
func (b *bdiscord) splitURL(url string) (string, string) {
webhookURLSplit := strings.Split(url, "/")
return webhookURLSplit[len(webhookURLSplit)-2], webhookURLSplit[len(webhookURLSplit)-1]
}
// useWebhook returns true if we have a webhook defined somewhere
func (b *bdiscord) useWebhook() bool {
if b.Config.WebhookURL != "" {
return true
}
for _, channel := range b.channelInfoMap {
if channel.Options.WebhookURL != "" {
return true
}
}
return false
}
// isWebhookID returns true if the specified id is used in a defined webhook
func (b *bdiscord) isWebhookID(id string) bool {
if b.Config.WebhookURL != "" {
wID, _ := b.splitURL(b.Config.WebhookURL)
if wID == id {
return true
}
}
for _, channel := range b.channelInfoMap {
if channel.Options.WebhookURL != "" {
wID, _ := b.splitURL(channel.Options.WebhookURL)
if wID == id {
return true
}
}
}
return false
}

View File

@ -1,7 +1,6 @@
package bgitter
import (
"fmt"
"github.com/42wim/go-gitter"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
@ -9,11 +8,13 @@ import (
)
type Bgitter struct {
c *gitter.Gitter
User *gitter.User
Users []gitter.User
Rooms []gitter.Room
*config.BridgeConfig
c *gitter.Gitter
Config *config.Protocol
Remote chan config.Message
protocol string
origin string
Users []gitter.User
Rooms []gitter.Room
}
var flog *log.Entry
@ -23,15 +24,20 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.BridgeConfig) *Bgitter {
return &Bgitter{BridgeConfig: cfg}
func New(cfg config.Protocol, origin string, c chan config.Message) *Bgitter {
b := &Bgitter{}
b.Config = &cfg
b.Remote = c
b.protocol = protocol
b.origin = origin
return b
}
func (b *Bgitter) Connect() error {
var err error
flog.Info("Connecting")
b.c = gitter.New(b.Config.Token)
b.User, err = b.c.GetUser()
_, err = b.c.GetUser()
if err != nil {
flog.Debugf("%#v", err)
return err
@ -41,21 +47,16 @@ func (b *Bgitter) Connect() error {
return nil
}
func (b *Bgitter) Disconnect() error {
return nil
func (b *Bgitter) FullOrigin() string {
return b.protocol + "." + b.origin
}
func (b *Bgitter) JoinChannel(channel config.ChannelInfo) error {
roomID, err := b.c.GetRoomId(channel.Name)
if err != nil {
return fmt.Errorf("Could not find roomID for %v. Please create the room on gitter.im", channel.Name)
func (b *Bgitter) JoinChannel(channel string) error {
room := channel
roomID := b.getRoomID(room)
if roomID == "" {
return nil
}
room, err := b.c.GetRoom(roomID)
if err != nil {
return err
}
b.Rooms = append(b.Rooms, *room)
user, err := b.c.GetUser()
if err != nil {
return err
@ -70,77 +71,46 @@ func (b *Bgitter) JoinChannel(channel config.ChannelInfo) error {
go b.c.Listen(stream)
go func(stream *gitter.Stream, room string) {
for event := range stream.Event {
for {
event := <-stream.Event
switch ev := event.Data.(type) {
case *gitter.MessageReceived:
if ev.Message.From.ID != b.User.ID {
flog.Debugf("Sending message from %s on %s to gateway", ev.Message.From.Username, b.Account)
rmsg := config.Message{Username: ev.Message.From.Username, Text: ev.Message.Text, Channel: room,
Account: b.Account, Avatar: b.getAvatar(ev.Message.From.Username), UserID: ev.Message.From.ID,
ID: ev.Message.ID}
if strings.HasPrefix(ev.Message.Text, "@"+ev.Message.From.Username) {
rmsg.Event = config.EVENT_USER_ACTION
rmsg.Text = strings.Replace(rmsg.Text, "@"+ev.Message.From.Username+" ", "", -1)
}
flog.Debugf("Message is %#v", rmsg)
b.Remote <- rmsg
// check for ZWSP to see if it's not an echo
if !strings.HasSuffix(ev.Message.Text, "") {
flog.Debugf("Sending message from %s on %s to gateway", ev.Message.From.Username, b.FullOrigin())
b.Remote <- config.Message{Username: ev.Message.From.Username, Text: ev.Message.Text, Channel: room,
Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin(), Avatar: b.getAvatar(ev.Message.From.Username)}
}
case *gitter.GitterConnectionClosed:
flog.Errorf("connection with gitter closed for room %s", room)
}
}
}(stream, room.URI)
}(stream, room)
return nil
}
func (b *Bgitter) Send(msg config.Message) (string, error) {
func (b *Bgitter) Name() string {
return b.protocol + "." + b.origin
}
func (b *Bgitter) Protocol() string {
return b.protocol
}
func (b *Bgitter) Origin() string {
return b.origin
}
func (b *Bgitter) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg)
roomID := b.getRoomID(msg.Channel)
if roomID == "" {
flog.Errorf("Could not find roomID for %v", msg.Channel)
return "", nil
return nil
}
if msg.Event == config.EVENT_MSG_DELETE {
if msg.ID == "" {
return "", nil
}
// gitter has no delete message api
_, err := b.c.UpdateMessage(roomID, msg.ID, "")
if err != nil {
return "", err
}
return "", nil
}
if msg.ID != "" {
flog.Debugf("updating message with id %s", msg.ID)
_, err := b.c.UpdateMessage(roomID, msg.ID, msg.Username+msg.Text)
if err != nil {
return "", err
}
return "", nil
}
if msg.Extra != nil {
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
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
}
return resp.ID, nil
nick := config.GetNick(&msg, b.Config)
// add ZWSP because gitter echoes our own messages
return b.c.SendMessage(roomID, nick+msg.Text+" ")
}
func (b *Bgitter) getRoomID(channel string) string {

View File

@ -1,40 +0,0 @@
package helper
import (
"bytes"
"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 {
resp.Body.Close()
return nil, err
}
io.Copy(&buf, resp.Body)
data := buf.Bytes()
resp.Body.Close()
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
}

View File

@ -4,7 +4,6 @@ import (
"strings"
)
/*
func tableformatter(nicks []string, nicksPerRow int, continued bool) string {
result := "|IRC users"
if continued {
@ -30,7 +29,6 @@ func tableformatter(nicks []string, nicksPerRow int, continued bool) string {
}
return result
}
*/
func plainformatter(nicks []string, nicksPerRow int) string {
return strings.Join(nicks, ", ") + " currently on IRC"

View File

@ -1,19 +1,12 @@
package birc
import (
"bytes"
"crypto/tls"
"fmt"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
log "github.com/Sirupsen/logrus"
"github.com/lrstanley/girc"
"github.com/paulrosania/go-charset/charset"
_ "github.com/paulrosania/go-charset/data"
"github.com/saintfish/chardet"
"io"
"io/ioutil"
"net"
ircm "github.com/sorcix/irc"
"github.com/thoj/go-ircevent"
"regexp"
"sort"
"strconv"
@ -22,14 +15,15 @@ import (
)
type Birc struct {
i *girc.Client
Nick string
names map[string][]string
connected chan struct{}
Local chan config.Message // local queue for flood control
FirstConnection bool
*config.BridgeConfig
i *irc.Connection
Nick string
names map[string][]string
Config *config.Protocol
origin string
protocol string
Remote chan config.Message
connected chan struct{}
Local chan config.Message // local queue for flood control
}
var flog *log.Entry
@ -39,11 +33,14 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.BridgeConfig) *Birc {
func New(cfg config.Protocol, origin string, c chan config.Message) *Birc {
b := &Birc{}
b.BridgeConfig = cfg
b.Config = &cfg
b.Nick = b.Config.Nick
b.Remote = c
b.names = make(map[string][]string)
b.origin = origin
b.protocol = protocol
b.connected = make(chan struct{})
if b.Config.MessageDelay == 0 {
b.Config.MessageDelay = 1300
@ -51,80 +48,39 @@ func New(cfg *config.BridgeConfig) *Birc {
if b.Config.MessageQueue == 0 {
b.Config.MessageQueue = 30
}
if b.Config.MessageLength == 0 {
b.Config.MessageLength = 400
}
b.FirstConnection = true
b.Local = make(chan config.Message, b.Config.MessageQueue+10)
return b
}
func (b *Birc) Command(msg *config.Message) string {
switch msg.Text {
case "!users":
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)
b.i.AddCallback(ircm.RPL_NAMREPLY, b.storeNames)
b.i.AddCallback(ircm.RPL_ENDOFNAMES, b.endNames)
b.i.SendRaw("NAMES " + msg.Channel)
}
return ""
}
func (b *Birc) Connect() error {
b.Local = make(chan config.Message, b.Config.MessageQueue+10)
flog.Infof("Connecting %s", b.Config.Server)
server, portstr, err := net.SplitHostPort(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}
if b.Config.Password != "" {
i.Password = b.Config.Password
}
i.AddCallback(ircm.RPL_WELCOME, b.handleNewConnection)
err := i.Connect(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:
@ -132,263 +88,176 @@ func (b *Birc) Connect() error {
case <-time.After(time.Second * 30):
return fmt.Errorf("connection timed out")
}
//i.Debug = false
i.Handlers.Clear("*")
i.Debug = false
go b.doSend()
return nil
}
func (b *Birc) Disconnect() error {
//b.i.Disconnect()
close(b.Local)
func (b *Birc) FullOrigin() string {
return b.protocol + "." + b.origin
}
func (b *Birc) JoinChannel(channel string) error {
b.i.Join(channel)
return nil
}
func (b *Birc) JoinChannel(channel config.ChannelInfo) error {
if channel.Options.Key != "" {
flog.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name)
b.i.Cmd.JoinKey(channel.Name, channel.Options.Key)
} else {
b.i.Cmd.Join(channel.Name)
}
return nil
func (b *Birc) Name() string {
return b.protocol + "." + b.origin
}
func (b *Birc) Send(msg config.Message) (string, error) {
// ignore delete messages
if msg.Event == config.EVENT_MSG_DELETE {
return "", nil
}
func (b *Birc) Protocol() string {
return b.protocol
}
func (b *Birc) Origin() string {
return b.origin
}
func (b *Birc) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg)
if msg.FullOrigin == b.FullOrigin() {
return nil
}
if strings.HasPrefix(msg.Text, "!") {
b.Command(&msg)
return nil
}
if b.Config.Charset != "" {
buf := new(bytes.Buffer)
w, err := charset.NewWriter(b.Config.Charset, buf)
if err != nil {
flog.Errorf("charset from utf-8 conversion failed: %s", err)
return "", err
}
fmt.Fprintf(w, msg.Text)
w.Close()
msg.Text = buf.String()
}
if msg.Extra != nil {
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
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)
}
nick := config.GetNick(&msg, b.Config)
for _, text := range strings.Split(msg.Text, "\n") {
input := []rune(text)
if len(text) > b.Config.MessageLength {
text = string(input[:b.Config.MessageLength]) + " <message clipped>"
}
if len(b.Local) < b.Config.MessageQueue {
if len(b.Local) == b.Config.MessageQueue-1 {
text = text + " <message clipped>"
}
b.Local <- config.Message{Text: text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event}
b.Local <- config.Message{Text: text, Username: nick, Channel: msg.Channel}
} else {
flog.Debugf("flooding, dropping message (queue at %d)", len(b.Local))
}
}
return "", nil
return nil
}
func (b *Birc) doSend() {
rate := time.Millisecond * time.Duration(b.Config.MessageDelay)
throttle := time.NewTicker(rate)
throttle := time.Tick(rate)
for msg := range b.Local {
<-throttle.C
if msg.Event == config.EVENT_USER_ACTION {
b.i.Cmd.Action(msg.Channel, msg.Username+msg.Text)
} else {
b.i.Cmd.Message(msg.Channel, msg.Username+msg.Text)
}
<-throttle
b.i.Privmsg(msg.Channel, msg.Username+msg.Text)
}
}
func (b *Birc) endNames(client *girc.Client, event girc.Event) {
channel := event.Params[1]
func (b *Birc) endNames(event *irc.Event) {
channel := event.Arguments[1]
sort.Strings(b.names[channel])
maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow()
continued := false
for len(b.names[channel]) > maxNamesPerPost {
b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost], continued),
Channel: channel, Account: b.Account}
Channel: channel, Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin()}
b.names[channel] = b.names[channel][maxNamesPerPost:]
continued = true
}
b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel], continued),
Channel: channel, Account: b.Account}
b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel], continued), Channel: channel,
Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin()}
b.names[channel] = nil
b.i.Handlers.Clear(girc.RPL_NAMREPLY)
b.i.Handlers.Clear(girc.RPL_ENDOFNAMES)
b.i.ClearCallback(ircm.RPL_NAMREPLY)
b.i.ClearCallback(ircm.RPL_ENDOFNAMES)
}
func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) {
func (b *Birc) handleNewConnection(event *irc.Event) {
flog.Debug("Registering callbacks")
i := b.i
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)
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("*", b.handleOther)
// we are now fully connected
b.connected <- struct{}{}
}
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.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.Source.Name != b.Nick {
flog.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
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(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)
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)
} else {
b.handlePrivMsg(client, event)
b.handlePrivMsg(event)
}
}
func (b *Birc) handleOther(client *girc.Client, event girc.Event) {
switch event.Command {
func (b *Birc) handleOther(event *irc.Event) {
switch event.Code {
case "372", "375", "376", "250", "251", "252", "253", "254", "255", "265", "266", "002", "003", "004", "005":
return
}
flog.Debugf("%#v", event.String())
flog.Debugf("%#v", event.Raw)
}
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.Cmd.Message(b.Config.NickServNick, "AUTH "+b.Config.NickServUsername+" "+b.Config.NickServPassword)
}
}
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.Command == "NOTICE" {
return
}
func (b *Birc) handlePrivMsg(event *irc.Event) {
// don't forward queries to the bot
if event.Params[0] == b.Nick {
if event.Arguments[0] == b.Nick {
return
}
// don't forward message from ourself
if event.Source.Name == b.Nick {
if event.Nick == b.Nick {
return
}
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)
flog.Debugf("handlePrivMsg() %s %s %#v", event.Nick, event.Message(), event)
msg := ""
if event.IsAction() {
rmsg.Event = config.EVENT_USER_ACTION
if event.Code == "CTCP_ACTION" {
msg = event.Nick + " "
}
msg += event.StripAction()
msg += event.Message()
// strip IRC colors
re := regexp.MustCompile(`[[:cntrl:]](?:\d{1,2}(?:,\d{1,2})?)?`)
re := regexp.MustCompile(`[[:cntrl:]](\d+,|)\d+`)
msg = re.ReplaceAllString(msg, "")
var r io.Reader
var err error
mycharset := b.Config.Charset
if mycharset == "" {
// detect what were sending so that we convert it to utf-8
detector := chardet.NewTextDetector()
result, err := detector.DetectBest([]byte(msg))
if err != nil {
flog.Infof("detection failed for msg: %#v", msg)
return
}
flog.Debugf("detected %s confidence %#v", result.Charset, result.Confidence)
mycharset = result.Charset
// if we're not sure, just pick ISO-8859-1
if result.Confidence < 80 {
mycharset = "ISO-8859-1"
}
}
r, err = charset.NewReader(mycharset, strings.NewReader(msg))
if err != nil {
flog.Errorf("charset to utf-8 conversion failed: %s", err)
return
}
output, _ := ioutil.ReadAll(r)
msg = string(output)
flog.Debugf("Sending message from %s on %s to gateway", event.Params[0], b.Account)
rmsg.Text = msg
b.Remote <- rmsg
flog.Debugf("Sending message from %s on %s to gateway", event.Arguments[0], b.FullOrigin())
b.Remote <- config.Message{Username: event.Nick, Text: msg, Channel: event.Arguments[0], Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin()}
}
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)
func (b *Birc) handleTopicWhoTime(event *irc.Event) {
parts := strings.Split(event.Arguments[2], "!")
t, err := strconv.ParseInt(event.Arguments[3], 10, 64)
if err != nil {
flog.Errorf("Invalid time stamp: %s", event.Params[3])
flog.Errorf("Invalid time stamp: %s", event.Arguments[3])
}
user := parts[0]
if len(parts) > 1 {
user += " [" + parts[1] + "]"
}
flog.Debugf("%s: Topic set by %s [%s]", event.Command, user, time.Unix(t, 0))
flog.Debugf("%s: Topic set by %s [%s]", event.Code, 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(client *girc.Client, event girc.Event) {
channel := event.Params[2]
func (b *Birc) storeNames(event *irc.Event) {
channel := event.Arguments[2]
b.names[channel] = append(
b.names[channel],
strings.Split(strings.TrimSpace(event.Trailing), " ")...)
strings.Split(strings.TrimSpace(event.Message()), " ")...)
}
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())
}
*/
}

View File

@ -1,237 +0,0 @@
package bmatrix
import (
"bytes"
"mime"
"regexp"
"strings"
"sync"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
log "github.com/Sirupsen/logrus"
matrix "github.com/matterbridge/gomatrix"
)
type Bmatrix struct {
mc *matrix.Client
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})
}
func New(cfg *config.BridgeConfig) *Bmatrix {
b := &Bmatrix{BridgeConfig: cfg}
b.RoomMap = make(map[string]string)
return b
}
func (b *Bmatrix) Connect() error {
var err error
flog.Infof("Connecting %s", b.Config.Server)
b.mc, err = matrix.NewClient(b.Config.Server, "", "")
if err != nil {
flog.Debugf("%#v", err)
return err
}
resp, err := b.mc.Login(&matrix.ReqLogin{
Type: "m.login.password",
User: b.Config.Login,
Password: b.Config.Password,
})
if err != nil {
flog.Debugf("%#v", err)
return err
}
b.mc.SetCredentials(resp.UserID, resp.AccessToken)
b.UserID = resp.UserID
flog.Info("Connection succeeded")
go b.handlematrix()
return nil
}
func (b *Bmatrix) Disconnect() error {
return nil
}
func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error {
resp, err := b.mc.JoinRoom(channel.Name, "", nil)
if err != nil {
return err
}
b.Lock()
b.RoomMap[resp.RoomID] = channel.Name
b.Unlock()
return err
}
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
}
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 {
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 {
// 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") {
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
}
}
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 {
b.RLock()
defer b.RUnlock()
for ID, name := range b.RoomMap {
if name == channel {
return ID
}
}
return ""
}
func (b *Bmatrix) handlematrix() error {
syncer := b.mc.Syncer.(*matrix.DefaultSyncer)
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()
if !ok {
flog.Debugf("Unknown room %s", ev.RoomID)
return
}
username := ev.Sender[1:]
if b.Config.NoHomeServerSuffix {
re := regexp.MustCompile("(.*?):.*")
username = re.ReplaceAllString(username, `$1`)
}
var text string
text, _ = ev.Content["body"].(string)
rmsg := config.Message{Username: username, Text: text, Channel: channel, Account: b.Account, UserID: ev.Sender}
rmsg.ID = ev.ID
if ev.Type == "m.room.redaction" {
rmsg.Event = config.EVENT_MSG_DELETE
rmsg.ID = ev.Redacts
rmsg.Text = config.EVENT_MSG_DELETE
b.Remote <- rmsg
return
}
if ev.Content["msgtype"].(string) == "m.emote" {
rmsg.Event = config.EVENT_USER_ACTION
}
if ev.Content["msgtype"] != nil && ev.Content["msgtype"].(string) == "m.image" ||
ev.Content["msgtype"].(string) == "m.video" ||
ev.Content["msgtype"].(string) == "m.file" {
flog.Debugf("ev: %#v", ev)
rmsg.Extra = make(map[string][]interface{})
url := ev.Content["url"].(string)
url = strings.Replace(url, "mxc://", b.Config.Server+"/_matrix/media/v1/download/", -1)
info := ev.Content["info"].(map[string]interface{})
size := info["size"].(float64)
name := ev.Content["body"].(string)
// check if we have an image uploaded without extension
if !strings.Contains(name, ".") {
if ev.Content["msgtype"].(string) == "m.image" {
if mtype, ok := ev.Content["mimetype"].(string); ok {
mext, _ := mime.ExtensionsByType(mtype)
if len(mext) > 0 {
name = name + mext[0]
}
} else {
// just a default .png extension if we don't have mime info
name = name + ".png"
}
}
}
flog.Debugf("trying to download %#v with size %#v", name, size)
if size <= float64(b.General.MediaDownloadSize) {
data, err := helper.DownloadFile(url)
if err != nil {
flog.Errorf("download %s failed %#v", url, err)
} else {
flog.Debugf("download OK %#v %#v %#v", name, len(*data), len(url))
rmsg.Extra["file"] = append(rmsg.Extra["file"], config.FileInfo{Name: name, Data: data})
}
}
rmsg.Text = ""
}
flog.Debugf("Sending message from %s on %s to gateway", ev.Sender, b.Account)
b.Remote <- rmsg
}
}

View File

@ -1,13 +1,10 @@
package bmattermost
import (
"errors"
"fmt"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/matterclient"
"github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus"
"strings"
)
type MMhook struct {
@ -15,25 +12,26 @@ type MMhook struct {
}
type MMapi struct {
mc *matterclient.MMClient
mmMap map[string]string
mc *matterclient.MMClient
mmMap map[string]string
mmIgnoreNicks []string
}
type MMMessage struct {
Text string
Channel string
Username string
UserID string
ID string
Event string
Extra map[string][]interface{}
}
type Bmattermost struct {
MMhook
MMapi
TeamId string
*config.BridgeConfig
Config *config.Protocol
Remote chan config.Message
name string
origin string
protocol string
TeamId string
}
var flog *log.Entry
@ -43,8 +41,13 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.BridgeConfig) *Bmattermost {
b := &Bmattermost{BridgeConfig: cfg}
func New(cfg config.Protocol, origin string, c chan config.Message) *Bmattermost {
b := &Bmattermost{}
b.Config = &cfg
b.origin = origin
b.Remote = c
b.protocol = "mattermost"
b.name = cfg.Name
b.mmMap = make(map[string]string)
return b
}
@ -54,229 +57,110 @@ func (b *Bmattermost) Command(cmd string) string {
}
func (b *Bmattermost) Connect() error {
if b.Config.WebhookBindAddress != "" {
if b.Config.WebhookURL != "" {
flog.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
b.mh = matterhook.New(b.Config.WebhookURL,
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
BindAddress: b.Config.WebhookBindAddress})
} else if b.Config.Token != "" {
flog.Info("Connecting using token (sending)")
err := b.apiLogin()
if err != nil {
return err
}
} else if b.Config.Login != "" {
flog.Info("Connecting using login/password (sending)")
err := b.apiLogin()
if err != nil {
return err
}
} else {
flog.Info("Connecting using webhookbindaddress (receiving)")
b.mh = matterhook.New(b.Config.WebhookURL,
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
BindAddress: b.Config.WebhookBindAddress})
}
go b.handleMatter()
return nil
}
if b.Config.WebhookURL != "" {
flog.Info("Connecting using webhookurl (sending)")
b.mh = matterhook.New(b.Config.WebhookURL,
if !b.Config.UseAPI {
flog.Info("Connecting webhooks")
b.mh = matterhook.New(b.Config.URL,
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
DisableServer: true})
if b.Config.Token != "" {
flog.Info("Connecting using token (receiving)")
err := b.apiLogin()
if err != nil {
return err
}
go b.handleMatter()
} else if b.Config.Login != "" {
flog.Info("Connecting using login/password (receiving)")
err := b.apiLogin()
if err != nil {
return err
}
go b.handleMatter()
}
return nil
} else if b.Config.Token != "" {
flog.Info("Connecting using token (sending and receiving)")
err := b.apiLogin()
BindAddress: b.Config.BindAddress})
} else {
b.mc = matterclient.New(b.Config.Login, b.Config.Password,
b.Config.Team, b.Config.Server)
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)
err := b.mc.Login()
if err != nil {
return err
}
go b.handleMatter()
} else if b.Config.Login != "" {
flog.Info("Connecting using login/password (sending and receiving)")
err := b.apiLogin()
if err != nil {
return err
}
go b.handleMatter()
}
if b.Config.WebhookBindAddress == "" && b.Config.WebhookURL == "" && b.Config.Login == "" && b.Config.Token == "" {
return errors.New("No connection method found. See that you have WebhookBindAddress, WebhookURL or Token/Login/Password/Server/Team configured.")
flog.Info("Connection succeeded")
b.TeamId = b.mc.GetTeamId()
go b.mc.WsReceiver()
}
go b.handleMatter()
return nil
}
func (b *Bmattermost) Disconnect() error {
return nil
func (b *Bmattermost) FullOrigin() string {
return b.protocol + "." + b.origin
}
func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error {
func (b *Bmattermost) JoinChannel(channel string) error {
// we can only join channels using the API
if b.Config.WebhookURL == "" && b.Config.WebhookBindAddress == "" {
id := b.mc.GetChannelId(channel.Name, "")
if id == "" {
return fmt.Errorf("Could not find channel ID for channel %s", channel.Name)
}
return b.mc.JoinChannel(id)
if b.Config.UseAPI {
return b.mc.JoinChannel(b.mc.GetChannelId(channel, ""))
}
return nil
}
func (b *Bmattermost) Send(msg config.Message) (string, error) {
func (b *Bmattermost) Name() string {
return b.protocol + "." + b.origin
}
func (b *Bmattermost) Origin() string {
return b.origin
}
func (b *Bmattermost) Protocol() string {
return b.protocol
}
func (b *Bmattermost) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg)
if msg.Event == config.EVENT_USER_ACTION {
msg.Text = "*" + msg.Text + "*"
}
nick := msg.Username
nick := config.GetNick(&msg, b.Config)
message := msg.Text
channel := msg.Channel
if b.Config.PrefixMessagesWithNick {
message = nick + message
/*if IsMarkup(message) {
message = nick + "\n\n" + message
} else {
*/
message = nick + " " + message
//}
}
if b.Config.WebhookURL != "" {
if !b.Config.UseAPI {
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)
if err != nil {
flog.Info(err)
return "", err
return err
}
return "", nil
return nil
}
if msg.Event == config.EVENT_MSG_DELETE {
if msg.ID == "" {
return "", nil
}
return msg.ID, b.mc.DeleteMessage(msg.ID)
}
if msg.Extra != nil {
if len(msg.Extra["file"]) > 0 {
var err error
var res, id string
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
id, err = b.mc.UploadFile(*fi.Data, b.mc.GetChannelId(channel, ""), fi.Name)
if err != nil {
flog.Debugf("ERROR %#v", err)
return "", err
}
message = fi.Comment
if b.Config.PrefixMessagesWithNick {
message = nick + fi.Comment
}
res, err = b.mc.PostMessageWithFiles(b.mc.GetChannelId(channel, ""), message, []string{id})
}
return res, err
}
}
if msg.ID != "" {
return b.mc.EditMessage(msg.ID, message)
}
return b.mc.PostMessage(b.mc.GetChannelId(channel, ""), message)
b.mc.PostMessage(b.mc.GetChannelId(channel, ""), message)
return nil
}
func (b *Bmattermost) handleMatter() {
flog.Debugf("Choosing API based Mattermost connection: %t", b.Config.UseAPI)
mchan := make(chan *MMMessage)
if b.Config.WebhookBindAddress != "" {
flog.Debugf("Choosing webhooks based receiving")
go b.handleMatterHook(mchan)
} else {
if b.Config.Token != "" {
flog.Debugf("Choosing token based receiving")
} else {
flog.Debugf("Choosing login/password based receiving")
}
if b.Config.UseAPI {
go b.handleMatterClient(mchan)
} else {
go b.handleMatterHook(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}
text, ok := b.replaceAction(message.Text)
if ok {
rmsg.Event = config.EVENT_USER_ACTION
}
rmsg.Text = text
flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account)
flog.Debugf("Message is %#v", rmsg)
b.Remote <- rmsg
flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.FullOrigin())
b.Remote <- config.Message{Text: message.Text, Username: message.Username, Channel: message.Channel, Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin()}
}
}
func (b *Bmattermost) handleMatterClient(mchan chan *MMMessage) {
for message := range b.mc.MessageChan {
flog.Debugf("%#v", message.Raw.Data)
if message.Type == "system_join_leave" ||
message.Type == "system_join_channel" ||
message.Type == "system_leave_channel" {
flog.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
b.Remote <- config.Message{Username: "system", Text: message.Text, Channel: message.Channel, Account: b.Account, Event: config.EVENT_JOIN_LEAVE}
continue
}
if (message.Raw.Event == "post_edited") && b.Config.EditDisable {
continue
}
m := &MMMessage{Extra: make(map[string][]interface{})}
props := message.Post.Props
if props != nil {
if _, ok := props["matterbridge"].(bool); ok {
flog.Debugf("sent by matterbridge, ignoring")
continue
}
if _, ok := props["override_username"].(string); ok {
message.Username = props["override_username"].(string)
}
if _, ok := props["attachments"].([]interface{}); ok {
m.Extra["attachments"] = props["attachments"].([]interface{})
}
}
// do not post our own messages back to irc
// only listen to message from our team
if (message.Raw.Event == "posted" || message.Raw.Event == "post_edited" || message.Raw.Event == "post_deleted") &&
b.mc.User.Username != message.Username && message.Raw.Data["team_id"].(string) == b.TeamId {
// if the message has reactions don't repost it (for now, until we can correlate reaction with message)
if message.Post.HasReactions {
continue
}
if message.Raw.Event == "posted" && b.mc.User.Username != message.Username && message.Raw.TeamId == b.TeamId {
flog.Debugf("Receiving from matterclient %#v", message)
m.UserID = message.UserID
m := &MMMessage{}
m.Username = message.Username
m.Channel = message.Channel
m.Text = message.Text
m.ID = message.Post.Id
if message.Raw.Event == "post_edited" && !b.Config.EditDisable {
m.Text = message.Text + b.Config.EditSuffix
}
if message.Raw.Event == "post_deleted" {
m.Event = config.EVENT_MSG_DELETE
}
if len(message.Post.FileIds) > 0 {
for _, link := range b.mc.GetFileLinks(message.Post.FileIds) {
if len(message.Post.Filenames) > 0 {
for _, link := range b.mc.GetPublicLinks(message.Post.Filenames) {
m.Text = m.Text + "\n" + link
}
}
@ -290,39 +174,9 @@ func (b *Bmattermost) handleMatterHook(mchan chan *MMMessage) {
message := b.mh.Receive()
flog.Debugf("Receiving from matterhook %#v", message)
m := &MMMessage{}
m.UserID = message.UserID
m.Username = message.UserName
m.Text = message.Text
m.Channel = message.ChannelName
mchan <- m
}
}
func (b *Bmattermost) apiLogin() error {
password := b.Config.Password
if b.Config.Token != "" {
password = "MMAUTHTOKEN=" + b.Config.Token
}
b.mc = matterclient.New(b.Config.Login, password,
b.Config.Team, b.Config.Server)
b.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)
err := b.mc.Login()
if err != nil {
return err
}
flog.Info("Connection succeeded")
b.TeamId = b.mc.GetTeamId()
go b.mc.WsReceiver()
go b.mc.StatusLoop()
return nil
}
func (b *Bmattermost) replaceAction(text string) (string, bool) {
if strings.HasPrefix(text, "*") && strings.HasSuffix(text, "*") {
return strings.Replace(text, "*", "", -1), true
}
return text, false
}

View File

@ -1,84 +0,0 @@
package brocketchat
import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/hook/rockethook"
"github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus"
)
type MMhook struct {
mh *matterhook.Client
rh *rockethook.Client
}
type Brocketchat struct {
MMhook
*config.BridgeConfig
}
var flog *log.Entry
var protocol = "rocketchat"
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.BridgeConfig) *Brocketchat {
return &Brocketchat{BridgeConfig: cfg}
}
func (b *Brocketchat) Command(cmd string) string {
return ""
}
func (b *Brocketchat) Connect() error {
flog.Info("Connecting webhooks")
b.mh = matterhook.New(b.Config.WebhookURL,
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
DisableServer: true})
b.rh = rockethook.New(b.Config.WebhookURL, rockethook.Config{BindAddress: b.Config.WebhookBindAddress})
go b.handleRocketHook()
return nil
}
func (b *Brocketchat) Disconnect() error {
return nil
}
func (b *Brocketchat) JoinChannel(channel config.ChannelInfo) error {
return nil
}
func (b *Brocketchat) Send(msg config.Message) (string, error) {
// ignore delete messages
if msg.Event == config.EVENT_MSG_DELETE {
return "", nil
}
flog.Debugf("Receiving %#v", msg)
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
matterMessage.Channel = msg.Channel
matterMessage.UserName = msg.Username
matterMessage.Type = ""
matterMessage.Text = msg.Text
err := b.mh.Send(matterMessage)
if err != nil {
flog.Info(err)
return "", err
}
return "", nil
}
func (b *Brocketchat) handleRocketHook() {
for {
message := b.rh.Receive()
flog.Debugf("Receiving from rockethook %#v", message)
// do not loop
if message.UserName == b.Config.Nick {
continue
}
flog.Debugf("Sending message from %s on %s to gateway", message.UserName, b.Account)
b.Remote <- config.Message{Text: message.Text, Username: message.UserName, Channel: message.ChannelName, Account: b.Account, UserID: message.UserID}
}
}

View File

@ -1,17 +1,11 @@
package bslack
import (
"bytes"
"errors"
"fmt"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus"
"github.com/nlopes/slack"
"html"
"io"
"net/http"
"regexp"
"strings"
"time"
)
@ -20,19 +14,21 @@ type MMMessage struct {
Text string
Channel string
Username string
UserID string
Raw *slack.MessageEvent
}
type Bslack struct {
mh *matterhook.Client
sc *slack.Client
Config *config.Protocol
rtm *slack.RTM
Plus bool
Remote chan config.Message
Users []slack.User
protocol string
origin string
si *slack.Info
channels []slack.Channel
*config.BridgeConfig
}
var flog *log.Entry
@ -42,8 +38,13 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.BridgeConfig) *Bslack {
return &Bslack{BridgeConfig: cfg}
func New(cfg config.Protocol, origin string, c chan config.Message) *Bslack {
b := &Bslack{}
b.Config = &cfg
b.Remote = c
b.protocol = protocol
b.origin = origin
return b
}
func (b *Bslack) Command(cmd string) string {
@ -51,89 +52,59 @@ func (b *Bslack) Command(cmd string) string {
}
func (b *Bslack) Connect() error {
if b.Config.WebhookBindAddress != "" {
if b.Config.WebhookURL != "" {
flog.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
b.mh = matterhook.New(b.Config.WebhookURL,
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
BindAddress: b.Config.WebhookBindAddress})
} else if b.Config.Token != "" {
flog.Info("Connecting using token (sending)")
b.sc = slack.New(b.Config.Token)
b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection()
flog.Info("Connecting using webhookbindaddress (receiving)")
b.mh = matterhook.New(b.Config.WebhookURL,
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
BindAddress: b.Config.WebhookBindAddress})
} else {
flog.Info("Connecting using webhookbindaddress (receiving)")
b.mh = matterhook.New(b.Config.WebhookURL,
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
BindAddress: b.Config.WebhookBindAddress})
}
go b.handleSlack()
return nil
}
if b.Config.WebhookURL != "" {
flog.Info("Connecting using webhookurl (sending)")
b.mh = matterhook.New(b.Config.WebhookURL,
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
DisableServer: true})
if b.Config.Token != "" {
flog.Info("Connecting using token (receiving)")
b.sc = slack.New(b.Config.Token)
b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection()
go b.handleSlack()
}
} else if b.Config.Token != "" {
flog.Info("Connecting using token (sending and receiving)")
flog.Info("Connecting")
if !b.Config.UseAPI {
b.mh = matterhook.New(b.Config.URL,
matterhook.Config{BindAddress: b.Config.BindAddress})
} else {
b.sc = slack.New(b.Config.Token)
b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection()
go b.handleSlack()
}
if b.Config.WebhookBindAddress == "" && b.Config.WebhookURL == "" && b.Config.Token == "" {
return errors.New("No connection method found. See that you have WebhookBindAddress, WebhookURL or Token configured.")
}
flog.Info("Connection succeeded")
go b.handleSlack()
return nil
}
func (b *Bslack) Disconnect() error {
return nil
func (b *Bslack) FullOrigin() string {
return b.protocol + "." + b.origin
}
func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
func (b *Bslack) JoinChannel(channel string) error {
// we can only join channels using the API
if b.sc != nil {
if strings.HasPrefix(b.Config.Token, "xoxb") {
// TODO check if bot has already joined channel
return nil
}
_, err := b.sc.JoinChannel(channel.Name)
if b.Config.UseAPI {
_, err := b.sc.JoinChannel(channel)
if err != nil {
if err.Error() != "name_taken" {
return err
}
return err
}
}
return nil
}
func (b *Bslack) Send(msg config.Message) (string, error) {
func (b *Bslack) Name() string {
return b.protocol + "." + b.origin
}
func (b *Bslack) Protocol() string {
return b.protocol
}
func (b *Bslack) Origin() string {
return b.origin
}
func (b *Bslack) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg)
if msg.Event == config.EVENT_USER_ACTION {
msg.Text = "_" + msg.Text + "_"
if msg.FullOrigin == b.FullOrigin() {
return nil
}
nick := msg.Username
nick := config.GetNick(&msg, b.Config)
message := msg.Text
channel := msg.Channel
if b.Config.PrefixMessagesWithNick {
message = nick + " " + message
}
if b.Config.WebhookURL != "" {
if !b.Config.UseAPI {
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
matterMessage.Channel = channel
matterMessage.UserName = nick
@ -142,70 +113,31 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
err := b.mh.Send(matterMessage)
if err != nil {
flog.Info(err)
return "", err
return err
}
return "", nil
return nil
}
schannel, err := b.getChannelByName(channel)
if err != nil {
return "", err
return err
}
np := slack.NewPostMessageParameters()
if b.Config.PrefixMessagesWithNick {
if b.Config.PrefixMessagesWithNick == true {
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
}
np.Attachments = append(np.Attachments, slack.Attachment{CallbackID: "matterbridge"})
np.Attachments = append(np.Attachments, b.createAttach(msg.Extra)...)
b.sc.PostMessage(schannel.ID, message, np)
// replace mentions
np.LinkNames = 1
/*
newmsg := b.rtm.NewOutgoingMessage(message, schannel.ID)
b.rtm.SendMessage(newmsg)
*/
if msg.Event == config.EVENT_MSG_DELETE {
// some protocols echo deletes, but with empty ID
if msg.ID == "" {
return "", nil
}
// we get a "slack <ID>", split it
ts := strings.Fields(msg.ID)
b.sc.DeleteMessage(schannel.ID, ts[1])
return "", nil
}
// if we have no ID it means we're creating a new message, not updating an existing one
if msg.ID != "" {
ts := strings.Fields(msg.ID)
b.sc.UpdateMessage(schannel.ID, ts[1], message)
return "", nil
}
if msg.Extra != nil {
// 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
}
return "slack " + id, nil
return nil
}
func (b *Bslack) getAvatar(user string) string {
@ -222,182 +154,72 @@ func (b *Bslack) getAvatar(user string) string {
func (b *Bslack) getChannelByName(name string) (*slack.Channel, error) {
if b.channels == nil {
return nil, fmt.Errorf("%s: channel %s not found (no channels found)", b.Account, name)
return nil, fmt.Errorf("%s: channel %s not found (no channels found)", b.FullOrigin(), name)
}
for _, channel := range b.channels {
if channel.Name == name {
return &channel, nil
}
}
return nil, fmt.Errorf("%s: channel %s not found", b.Account, name)
}
func (b *Bslack) getChannelByID(ID string) (*slack.Channel, error) {
if b.channels == nil {
return nil, fmt.Errorf("%s: channel %s not found (no channels found)", b.Account, ID)
}
for _, channel := range b.channels {
if channel.ID == ID {
return &channel, nil
}
}
return nil, fmt.Errorf("%s: channel %s not found", b.Account, ID)
return nil, fmt.Errorf("%s: channel %s not found", b.FullOrigin(), name)
}
func (b *Bslack) handleSlack() {
flog.Debugf("Choosing API based slack connection: %t", b.Config.UseAPI)
mchan := make(chan *MMMessage)
if b.Config.WebhookBindAddress != "" {
flog.Debugf("Choosing webhooks based receiving")
go b.handleMatterHook(mchan)
} else {
flog.Debugf("Choosing token based receiving")
if b.Config.UseAPI {
go b.handleSlackClient(mchan)
} else {
go b.handleMatterHook(mchan)
}
time.Sleep(time.Second)
flog.Debug("Start listening for Slack messages")
for message := range mchan {
// do not send messages from ourself
if b.Config.WebhookURL == "" && b.Config.WebhookBindAddress == "" && message.Username == b.si.User.Name {
if message.Username == b.si.User.Name {
continue
}
if (message.Text == "" || message.Username == "") && message.Raw.SubType != "message_deleted" {
continue
texts := strings.Split(message.Text, "\n")
for _, text := range texts {
flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.FullOrigin())
b.Remote <- config.Message{Text: text, Username: message.Username, Channel: message.Channel, Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin(), Avatar: b.getAvatar(message.Username)}
}
text := message.Text
text = b.replaceURL(text)
text = html.UnescapeString(text)
flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account)
msg := config.Message{Text: text, Username: message.Username, Channel: message.Channel, Account: b.Account, Avatar: b.getAvatar(message.Username), UserID: message.UserID, ID: "slack " + message.Raw.Timestamp, Extra: make(map[string][]interface{})}
if message.Raw.SubType == "me_message" {
msg.Event = config.EVENT_USER_ACTION
}
if message.Raw.SubType == "channel_leave" || message.Raw.SubType == "channel_join" {
msg.Username = "system"
msg.Event = config.EVENT_JOIN_LEAVE
}
// edited messages have a submessage, use this timestamp
if message.Raw.SubMessage != nil {
msg.ID = "slack " + message.Raw.SubMessage.Timestamp
}
if message.Raw.SubType == "message_deleted" {
msg.Text = config.EVENT_MSG_DELETE
msg.Event = config.EVENT_MSG_DELETE
msg.ID = "slack " + message.Raw.DeletedTimestamp
}
// 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 <= b.General.MediaDownloadSize {
comment := ""
data, err := b.downloadFile(message.Raw.File.URLPrivateDownload)
if err != nil {
flog.Errorf("download %s failed %#v", message.Raw.File.URLPrivateDownload, err)
} else {
results := regexp.MustCompile(`.*?commented: (.*)`).FindAllStringSubmatch(msg.Text, -1)
if len(results) > 0 {
comment = results[0][1]
}
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: message.Raw.File.Name, Data: data, Comment: comment})
}
}
}
flog.Debugf("Message is %#v", msg)
b.Remote <- msg
}
}
func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
count := 0
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:
if len(ev.Attachments) > 0 {
// skip messages we made ourselves
if ev.Attachments[0].CallbackID == "matterbridge" {
// ignore first message
if count > 0 {
flog.Debugf("Receiving from slackclient %#v", ev)
//ev.ReplyTo
channel, err := b.rtm.GetChannelInfo(ev.Channel)
if err != nil {
continue
}
}
if !b.Config.EditDisable && ev.SubMessage != nil && ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp {
flog.Debugf("SubMessage %#v", ev.SubMessage)
ev.User = ev.SubMessage.User
ev.Text = ev.SubMessage.Text + b.Config.EditSuffix
// it seems ev.SubMessage.Edited == nil when slack unfurls
// do not forward these messages #266
if ev.SubMessage.Edited == nil {
continue
}
}
// use our own func because rtm.GetChannelInfo doesn't work for private channels
channel, err := b.getChannelByID(ev.Channel)
if err != nil {
continue
}
m := &MMMessage{}
if ev.BotID == "" && ev.SubType != "message_deleted" {
user, err := b.rtm.GetUserInfo(ev.User)
if err != nil {
continue
}
m.UserID = user.ID
m := &MMMessage{}
m.Username = user.Name
if user.Profile.DisplayName != "" {
m.Username = user.Profile.DisplayName
}
m.Channel = channel.Name
m.Text = ev.Text
m.Raw = ev
mchan <- m
}
m.Channel = channel.Name
m.Text = ev.Text
if m.Text == "" {
for _, attach := range ev.Attachments {
if attach.Text != "" {
m.Text = attach.Text
} else {
m.Text = attach.Fallback
}
}
}
m.Raw = ev
m.Text = b.replaceMention(m.Text)
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)
if err != nil {
continue
}
if bot.Name != "" {
m.Username = bot.Name
if ev.Username != "" {
m.Username = ev.Username
}
m.UserID = bot.ID
}
}
mchan <- m
count++
case *slack.OutgoingErrorEvent:
flog.Debugf("%#v", ev.Error())
case *slack.ChannelJoinedEvent:
b.Users, _ = b.sc.GetUsers()
case *slack.ConnectedEvent:
b.channels = ev.Info.Channels
b.si = ev.Info
b.Users, _ = b.sc.GetUsers()
// add private channels
groups, _ := b.sc.GetGroups(true)
for _, g := range groups {
channel := new(slack.Channel)
channel.ID = g.ID
channel.Name = g.Name
b.channels = append(b.channels, *channel)
}
case *slack.InvalidAuthEvent:
flog.Fatalf("Invalid Token %#v", ev)
case *slack.ConnectionErrorEvent:
flog.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj)
default:
}
}
@ -410,105 +232,7 @@ func (b *Bslack) handleMatterHook(mchan chan *MMMessage) {
m := &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
}
mchan <- m
}
}
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)
for _, r := range results {
text = strings.Replace(text, r[0], r[1], -1)
}
return text
}
func (b *Bslack) createAttach(extra map[string][]interface{}) []slack.Attachment {
var attachs []slack.Attachment
for _, v := range extra["attachments"] {
entry := v.(map[string]interface{})
s := slack.Attachment{}
s.Fallback = entry["fallback"].(string)
s.Color = entry["color"].(string)
s.Pretext = entry["pretext"].(string)
s.AuthorName = entry["author_name"].(string)
s.AuthorLink = entry["author_link"].(string)
s.AuthorIcon = entry["author_icon"].(string)
s.Title = entry["title"].(string)
s.TitleLink = entry["title_link"].(string)
s.Text = entry["text"].(string)
s.ImageURL = entry["image_url"].(string)
s.ThumbURL = entry["thumb_url"].(string)
s.Footer = entry["footer"].(string)
s.FooterIcon = entry["footer_icon"].(string)
attachs = append(attachs, s)
}
return attachs
}
func (b *Bslack) 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
}
req.Header.Add("Authorization", "Bearer "+b.Config.Token)
resp, err := client.Do(req)
if err != nil {
resp.Body.Close()
return nil, err
}
io.Copy(&buf, resp.Body)
data := buf.Bytes()
resp.Body.Close()
return &data, nil
}

View File

@ -1,132 +0,0 @@
package bsshchat
import (
"bufio"
"github.com/42wim/matterbridge/bridge/config"
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{"module": 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 {
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
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
}
}
}
}

View File

@ -1,164 +0,0 @@
package bsteam
import (
"fmt"
"github.com/42wim/matterbridge/bridge/config"
"github.com/Philipp15b/go-steam"
"github.com/Philipp15b/go-steam/protocol/steamlang"
"github.com/Philipp15b/go-steam/steamid"
log "github.com/Sirupsen/logrus"
//"io/ioutil"
"strconv"
"sync"
"time"
)
type Bsteam struct {
c *steam.Client
connected chan struct{}
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})
}
func New(cfg *config.BridgeConfig) *Bsteam {
b := &Bsteam{BridgeConfig: cfg}
b.userMap = make(map[steamid.SteamId]string)
b.connected = make(chan struct{})
return b
}
func (b *Bsteam) Connect() error {
flog.Info("Connecting")
b.c = steam.NewClient()
go b.handleEvents()
go b.c.Connect()
select {
case <-b.connected:
flog.Info("Connection succeeded")
case <-time.After(time.Second * 30):
return fmt.Errorf("connection timed out")
}
return nil
}
func (b *Bsteam) Disconnect() error {
b.c.Disconnect()
return nil
}
func (b *Bsteam) JoinChannel(channel config.ChannelInfo) error {
id, err := steamid.NewId(channel.Name)
if err != nil {
return err
}
b.c.Social.JoinChat(id)
return nil
}
func (b *Bsteam) Send(msg config.Message) (string, error) {
// ignore delete messages
if msg.Event == config.EVENT_MSG_DELETE {
return "", nil
}
id, err := steamid.NewId(msg.Channel)
if err != nil {
return "", err
}
b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text)
return "", nil
}
func (b *Bsteam) getNick(id steamid.SteamId) string {
b.RLock()
defer b.RUnlock()
if name, ok := b.userMap[id]; ok {
return name
}
return "unknown"
}
func (b *Bsteam) handleEvents() {
myLoginInfo := new(steam.LogOnDetails)
myLoginInfo.Username = b.Config.Login
myLoginInfo.Password = b.Config.Password
myLoginInfo.AuthCode = b.Config.AuthCode
// Attempt to read existing auth hash to avoid steam guard.
// Maybe works
//myLoginInfo.SentryFileHash, _ = ioutil.ReadFile("sentry")
for event := range b.c.Events() {
//flog.Info(event)
switch e := event.(type) {
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
}
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:
flog.Debugf("PersonaStateEvent: %#v\n", e)
b.Lock()
b.userMap[e.FriendId] = e.Name
b.Unlock()
case *steam.ConnectedEvent:
b.c.Auth.LogOn(myLoginInfo)
case *steam.MachineAuthUpdateEvent:
/*
flog.Info("authupdate", e)
flog.Info("hash", e.Hash)
ioutil.WriteFile("sentry", e.Hash, 0666)
*/
case *steam.LogOnFailedEvent:
flog.Info("Logon failed", e)
switch e.Result {
case steamlang.EResult_AccountLogonDeniedNeedTwoFactorCode:
{
flog.Info("Steam guard isn't letting me in! Enter 2FA code:")
var code string
fmt.Scanf("%s", &code)
myLoginInfo.TwoFactorCode = code
}
case steamlang.EResult_AccountLogonDenied:
{
flog.Info("Steam guard isn't letting me in! Enter auth code:")
var code string
fmt.Scanf("%s", &code)
myLoginInfo.AuthCode = code
}
default:
log.Errorf("LogOnFailedEvent: %#v ", e.Result)
// TODO: Handle EResult_InvalidLoginAuthCode
return
}
case *steam.LoggedOnEvent:
flog.Debugf("LoggedOnEvent: %#v", e)
b.connected <- struct{}{}
flog.Debugf("setting online")
b.c.Social.SetPersonaState(steamlang.EPersonaState_Online)
case *steam.DisconnectedEvent:
flog.Info("Disconnected")
flog.Info("Attempting to reconnect...")
b.c.Connect()
case steam.FatalErrorEvent:
flog.Error(e)
case error:
flog.Error(e)
default:
flog.Debugf("unknown event %#v", e)
}
}
}

View File

@ -1,64 +0,0 @@
package btelegram
import (
"bytes"
"github.com/russross/blackfriday"
"html"
)
type customHtml struct {
blackfriday.Renderer
}
func (options *customHtml) Paragraph(out *bytes.Buffer, text func() bool) {
marker := out.Len()
if !text() {
out.Truncate(marker)
return
}
out.WriteString("\n")
}
func (options *customHtml) BlockCode(out *bytes.Buffer, text []byte, lang string) {
out.WriteString("<pre>")
out.WriteString(html.EscapeString(string(text)))
out.WriteString("</pre>\n")
}
func (options *customHtml) Header(out *bytes.Buffer, text func() bool, level int, id string) {
options.Paragraph(out, text)
}
func (options *customHtml) HRule(out *bytes.Buffer) {
out.WriteByte('\n')
}
func (options *customHtml) BlockQuote(out *bytes.Buffer, text []byte) {
out.WriteString("> ")
out.Write(text)
out.WriteByte('\n')
}
func (options *customHtml) List(out *bytes.Buffer, text func() bool, flags int) {
options.Paragraph(out, text)
}
func (options *customHtml) ListItem(out *bytes.Buffer, text []byte, flags int) {
out.WriteString("- ")
out.Write(text)
out.WriteByte('\n')
}
func makeHTML(input string) string {
return string(blackfriday.Markdown([]byte(input),
&customHtml{blackfriday.HtmlRenderer(blackfriday.HTML_USE_XHTML|blackfriday.HTML_SKIP_IMAGES, "", "")},
blackfriday.EXTENSION_NO_INTRA_EMPHASIS|
blackfriday.EXTENSION_FENCED_CODE|
blackfriday.EXTENSION_AUTOLINK|
blackfriday.EXTENSION_SPACE_HEADERS|
blackfriday.EXTENSION_HEADER_IDS|
blackfriday.EXTENSION_BACKSLASH_LINE_BREAK|
blackfriday.EXTENSION_DEFINITION_LISTS))
}

View File

@ -1,325 +0,0 @@
package btelegram
import (
"regexp"
"strconv"
"strings"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
log "github.com/Sirupsen/logrus"
"github.com/go-telegram-bot-api/telegram-bot-api"
)
type Btelegram struct {
c *tgbotapi.BotAPI
*config.BridgeConfig
}
var flog *log.Entry
var protocol = "telegram"
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.BridgeConfig) *Btelegram {
return &Btelegram{BridgeConfig: cfg}
}
func (b *Btelegram) Connect() error {
var err error
flog.Info("Connecting")
b.c, err = tgbotapi.NewBotAPI(b.Config.Token)
if err != nil {
flog.Debugf("%#v", err)
return err
}
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates, err := b.c.GetUpdatesChan(u)
if err != nil {
flog.Debugf("%#v", err)
return err
}
flog.Info("Connection succeeded")
go b.handleRecv(updates)
return nil
}
func (b *Btelegram) Disconnect() error {
return nil
}
func (b *Btelegram) JoinChannel(channel config.ChannelInfo) error {
return nil
}
func (b *Btelegram) Send(msg config.Message) (string, error) {
flog.Debugf("Receiving %#v", msg)
chatid, err := strconv.ParseInt(msg.Channel, 10, 64)
if err != nil {
return "", err
}
if b.Config.MessageFormat == "HTML" {
msg.Text = makeHTML(msg.Text)
}
if msg.Event == config.EVENT_MSG_DELETE {
if msg.ID == "" {
return "", nil
}
msgid, err := strconv.Atoi(msg.ID)
if err != nil {
return "", err
}
_, err = b.c.DeleteMessage(tgbotapi.DeleteMessageConfig{ChatID: chatid, MessageID: msgid})
return "", err
}
// edit the message if we have a msg ID
if msg.ID != "" {
msgid, err := strconv.Atoi(msg.ID)
if err != nil {
return "", err
}
m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text)
if b.Config.MessageFormat == "HTML" {
m.ParseMode = tgbotapi.ModeHTML
}
_, err = b.c.Send(m)
if err != nil {
return "", err
}
return "", nil
}
if msg.Extra != nil {
// 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 {
log.Errorf("file upload failed: %#v", err)
}
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)
var message *tgbotapi.Message
username := ""
channel := ""
text := ""
fmsg := config.Message{Extra: make(map[string][]interface{})}
// handle channels
if update.ChannelPost != nil {
message = update.ChannelPost
}
if update.EditedChannelPost != nil && !b.Config.EditDisable {
message = update.EditedChannelPost
message.Text = message.Text + b.Config.EditSuffix
}
// handle groups
if update.Message != nil {
message = update.Message
}
if update.EditedMessage != nil && !b.Config.EditDisable {
message = update.EditedMessage
message.Text = message.Text + b.Config.EditSuffix
}
if message.From != nil {
if b.Config.UseFirstName {
username = message.From.FirstName
}
if username == "" {
username = message.From.UserName
if username == "" {
username = message.From.FirstName
}
}
text = message.Text
channel = strconv.FormatInt(message.Chat.ID, 10)
}
if username == "" {
username = "unknown"
}
if message.Sticker != nil {
b.handleDownload(message.Sticker, &fmsg)
}
if message.Video != nil {
b.handleDownload(message.Video, &fmsg)
}
if message.Photo != nil {
b.handleDownload(message.Photo, &fmsg)
}
if message.Document != nil {
b.handleDownload(message.Document, &fmsg)
}
if message.Voice != nil {
b.handleDownload(message.Voice, &fmsg)
}
if message.Audio != nil {
b.handleDownload(message.Audio, &fmsg)
}
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
if message.ReplyToMessage != nil {
usernameReply := ""
if message.ReplyToMessage.From != nil {
if b.Config.UseFirstName {
usernameReply = message.ReplyToMessage.From.FirstName
}
if usernameReply == "" {
usernameReply = message.ReplyToMessage.From.UserName
if usernameReply == "" {
usernameReply = message.ReplyToMessage.From.FirstName
}
}
}
if usernameReply == "" {
usernameReply = "unknown"
}
text = text + " (re @" + usernameReply + ":" + message.ReplyToMessage.Text + ")"
}
if text != "" || len(fmsg.Extra) > 0 {
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), Extra: fmsg.Extra}
flog.Debugf("Message is %#v", msg)
b.Remote <- msg
}
}
}
func (b *Btelegram) getFileDirectURL(id string) string {
res, err := b.c.GetFileDirectURL(id)
if err != nil {
return ""
}
return res
}
func (b *Btelegram) handleDownload(file interface{}, 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 {
msg.Text = text
return
}
// if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra
// limit to 1MB for now
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})
}
}
}
func (b *Btelegram) sendMessage(chatid int64, text string) (string, error) {
m := tgbotapi.NewMessage(chatid, text)
if b.Config.MessageFormat == "HTML" {
m.ParseMode = tgbotapi.ModeHTML
}
res, err := b.c.Send(m)
if err != nil {
return "", err
}
return strconv.Itoa(res.MessageID), nil
}

View File

@ -1,10 +1,8 @@
package bxmpp
import (
"crypto/tls"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
"github.com/jpillora/backoff"
"github.com/mattn/go-xmpp"
"strings"
@ -12,9 +10,12 @@ import (
)
type Bxmpp struct {
xc *xmpp.Client
xmppMap map[string]string
*config.BridgeConfig
xc *xmpp.Client
xmppMap map[string]string
Config *config.Protocol
origin string
protocol string
Remote chan config.Message
}
var flog *log.Entry
@ -24,9 +25,13 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.BridgeConfig) *Bxmpp {
b := &Bxmpp{BridgeConfig: cfg}
func New(cfg config.Protocol, origin string, c chan config.Message) *Bxmpp {
b := &Bxmpp{}
b.xmppMap = make(map[string]string)
b.Config = &cfg
b.protocol = protocol
b.origin = origin
b.Remote = c
return b
}
@ -39,78 +44,47 @@ func (b *Bxmpp) Connect() error {
return err
}
flog.Info("Connection succeeded")
go func() {
initial := true
bf := &backoff.Backoff{
Min: time.Second,
Max: 5 * time.Minute,
Jitter: true,
}
for {
if initial {
b.handleXmpp()
initial = false
}
d := bf.Duration()
flog.Infof("Disconnected. Reconnecting in %s", d)
time.Sleep(d)
b.xc, err = b.createXMPP()
if err == nil {
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS}
b.handleXmpp()
bf.Reset()
}
}
}()
go b.handleXmpp()
return nil
}
func (b *Bxmpp) Disconnect() error {
func (b *Bxmpp) FullOrigin() string {
return b.protocol + "." + b.origin
}
func (b *Bxmpp) JoinChannel(channel string) error {
b.xc.JoinMUCNoHistory(channel+"@"+b.Config.Muc, b.Config.Nick)
return nil
}
func (b *Bxmpp) JoinChannel(channel config.ChannelInfo) error {
b.xc.JoinMUCNoHistory(channel.Name+"@"+b.Config.Muc, b.Config.Nick)
return nil
func (b *Bxmpp) Name() string {
return b.protocol + "." + b.origin
}
func (b *Bxmpp) Send(msg config.Message) (string, error) {
// ignore delete messages
if msg.Event == config.EVENT_MSG_DELETE {
return "", nil
}
func (b *Bxmpp) Protocol() string {
return b.protocol
}
func (b *Bxmpp) Origin() string {
return b.origin
}
func (b *Bxmpp) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg)
if msg.Extra != nil {
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
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
nick := config.GetNick(&msg, b.Config)
b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.Config.Muc, Text: nick + msg.Text})
return nil
}
func (b *Bxmpp) createXMPP() (*xmpp.Client, error) {
tc := new(tls.Config)
tc.InsecureSkipVerify = b.Config.SkipTLSVerify
tc.ServerName = strings.Split(b.Config.Server, ":")[0]
options := xmpp.Options{
Host: b.Config.Server,
User: b.Config.Jid,
Password: b.Config.Password,
NoTLS: true,
StartTLS: true,
TLSConfig: tc,
Host: b.Config.Server,
User: b.Config.Jid,
Password: b.Config.Password,
NoTLS: true,
StartTLS: true,
//StartTLS: false,
Debug: b.General.Debug,
Debug: true,
Session: true,
Status: "",
StatusMessage: "",
@ -123,32 +97,19 @@ func (b *Bxmpp) createXMPP() (*xmpp.Client, error) {
return b.xc, err
}
func (b *Bxmpp) xmppKeepAlive() chan bool {
done := make(chan bool)
func (b *Bxmpp) xmppKeepAlive() {
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
b.xc.Send(xmpp.Chat{})
}
}
}()
return done
}
func (b *Bxmpp) handleXmpp() error {
var ok bool
done := b.xmppKeepAlive()
defer close(done)
nodelay := time.Time{}
for {
m, err := b.xc.Recv()
if err != nil {
@ -159,21 +120,16 @@ func (b *Bxmpp) handleXmpp() error {
var channel, nick string
if v.Type == "groupchat" {
s := strings.Split(v.Remote, "@")
if len(s) >= 2 {
if len(s) == 2 {
channel = s[0]
}
s = strings.Split(s[1], "/")
if len(s) == 2 {
nick = s[1]
}
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 {
rmsg.Event = config.EVENT_USER_ACTION
}
flog.Debugf("Sending message from %s on %s to gateway", nick, b.Account)
b.Remote <- rmsg
if nick != b.Config.Nick {
flog.Debugf("Sending message from %s on %s to gateway", nick, b.FullOrigin())
b.Remote <- config.Message{Username: nick, Text: v.Text, Channel: channel, Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin()}
}
}
case xmpp.Presence:
@ -181,10 +137,3 @@ func (b *Bxmpp) handleXmpp() error {
}
}
}
func (b *Bxmpp) replaceAction(text string) (string, bool) {
if strings.HasPrefix(text, "/me ") {
return strings.Replace(text, "/me ", "", -1), true
}
return text, false
}

View File

@ -1,460 +1,3 @@
# 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
* Add support for quoting previous message when replying (telegram). #237
* Add support for Quakenet auth (irc). Closes #263
* Download files (max size 1MB) from slack and reupload to mattermost (slack/mattermost). Closes #255
## Enhancements
* Backoff for 60 seconds when reconnecting too fast (irc) #267
* Use override username if specified (mattermost). #260
## Bugfix
* Try to not forward slack unfurls. Closes #266
# v1.2.0
## Breaking changes
* If you're running a discord bridge, update to this release before 16 october otherwise
it will stop working. (see https://discordapp.com/developers/docs/reference)
## New features
* general: Add delete support. (actually delete the messages on bridges that support it)
(mattermost,discord,gitter,slack,telegram)
## Bugfix
* Do not break messages on newline (slack). Closes #258
* Update telegram library
* Update discord library (supports v6 API now). Old API is deprecated on 16 October
# v1.1.2
## New features
* general: also build darwin binaries
* mattermost: add support for mattermost 4.2.x
## Bugfix
* mattermost: Send images when text is empty regression. (mattermost). Closes #254
* slack: also send the first messsage after connect. #252
# v1.1.1
## Bugfix
* mattermost: fix public links
# v1.1.0
## New features
* general: Add better editing support. (actually edit the messages on bridges that support it)
(mattermost,discord,gitter,slack,telegram)
* mattermost: use API v4 (removes support for mattermost < 3.8)
* mattermost: add support for personal access tokens (since mattermost 4.1)
Use ```Token="yourtoken"``` in mattermost config
See https://docs.mattermost.com/developer/personal-access-tokens.html for more info
* matrix: Relay notices (matrix). Closes #243
* irc: Add a charset option. Closes #247
## Bugfix
* slack: Handle leave/join events (slack). Closes #246
* slack: Replace mentions from other bridges. (slack). Closes #233
* gitter: remove ZWSP after messages
# v1.0.1
## New features
* mattermost: add support for mattermost 4.1.x
* discord: allow a webhookURL per channel #239
# v1.0.0
## New features
* general: Add action support for slack,mattermost,irc,gitter,matrix,xmpp,discord. #199
* discord: Shows the username instead of the server nickname #234
# v1.0.0-rc1
## New features
* general: Add action support for slack,mattermost,irc,gitter,matrix,xmpp,discord. #199
## Bugfix
* general: Handle same account in multiple gateways better
* mattermost: ignore edited messages with reactions
* mattermost: Fix double posting of edited messages by using lru cache
* irc: update vendor
# v0.16.3
## Bugfix
* general: Fix in/out logic. Closes #224
* general: Fix message modification
* slack: Disable message from other bots when using webhooks (slack)
* mattermost: Return better error messages on mattermost connect
# v0.16.2
## New features
* general: binary builds against latest commit are now available on https://bintray.com/42wim/nightly/Matterbridge/_latestVersion
## Bugfix
* slack: fix loop introduced by relaying message of other bots #219
* slack: Suppress parent message when child message is received #218
* mattermost: fix regression when using webhookurl and webhookbindaddress #221
# v0.16.1
## New features
* slack: also relay messages of other bots #213
* mattermost: show also links if public links have not been enabled.
## Bugfix
* mattermost, slack: fix connecting logic #216
# v0.16.0
## Breaking Changes
* URL,UseAPI,BindAddress is deprecated. Your config has to be updated.
* URL => WebhookURL
* BindAddress => WebhookBindAddress
* UseAPI => removed
This change allows you to specify a WebhookURL and a token (slack,discord), so that
messages will be sent with the webhook, but received via the token (API)
If you have not specified WebhookURL and WebhookBindAddress the API (login or token)
will be used automatically. (no need for UseAPI)
## New features
* mattermost: add support for mattermost 4.0
* steam: New protocol support added (http://store.steampowered.com/)
* discord: Support for embedded messages (sent by other bots)
Shows title, description and URL of embedded messages (sent by other bots)
To enable add ```ShowEmbeds=true``` to your discord config
* discord: ```WebhookURL``` posting support added (thanks @saury07) #204
Discord API does not allow to change the name of the user posting, but webhooks does.
## Changes
* general: all :emoji: will be converted to unicode, providing consistent emojis across all bridges
* telegram: Add ```UseInsecureURL``` option for telegram (default false)
WARNING! If enabled this will relay GIF/stickers/documents and other attachments as URLs
Those URLs will contain your bot-token. This may not be what you want.
For now there is no secure way to relay GIF/stickers/documents without seeing your token.
## Bugfix
* irc: detect charset and try to convert it to utf-8 before sending it to other bridges. #209 #210
* slack: Remove label from URLs (slack). #205
* slack: Relay <>& correctly to other bridges #215
* steam: Fix channel id bug in steam (channels are off by 0x18000000000000)
* general: various improvements
* general: samechannelgateway now relays messages correct again #207
# v0.16.0-rc2
## Breaking Changes
* URL,UseAPI,BindAddress is deprecated. Your config has to be updated.
* URL => WebhookURL
* BindAddress => WebhookBindAddress
* UseAPI => removed
This change allows you to specify a WebhookURL and a token (slack,discord), so that
messages will be sent with the webhook, but received via the token (API)
If you have not specified WebhookURL and WebhookBindAddress the API (login or token)
will be used automatically. (no need for UseAPI)
## Bugfix since rc1
* steam: Fix channel id bug in steam (channels are off by 0x18000000000000)
* telegram: Add UseInsecureURL option for telegram (default false)
WARNING! If enabled this will relay GIF/stickers/documents and other attachments as URLs
Those URLs will contain your bot-token. This may not be what you want.
For now there is no secure way to relay GIF/stickers/documents without seeing your token.
* irc: detect charset and try to convert it to utf-8 before sending it to other bridges. #209 #210
* general: various improvements
# v0.16.0-rc1
## Breaking Changes
* URL,UseAPI,BindAddress is deprecated. Your config has to be updated.
* URL => WebhookURL
* BindAddress => WebhookBindAddress
* UseAPI => removed
This change allows you to specify a WebhookURL and a token (slack,discord), so that
messages will be sent with the webhook, but received via the token (API)
If you have not specified WebhookURL and WebhookBindAddress the API (login or token)
will be used automatically. (no need for UseAPI)
## New features
* steam: New protocol support added (http://store.steampowered.com/)
* discord: WebhookURL posting support added (thanks @saury07) #204
Discord API does not allow to change the name of the user posting, but webhooks does.
## Bugfix
* general: samechannelgateway now relays messages correct again #207
* slack: Remove label from URLs (slack). #205
# v0.15.0
## New features
* general: add option IgnoreMessages for all protocols (see mattebridge.toml.sample)
Messages matching these regexp will be ignored and not sent to other bridges
e.g. IgnoreMessages="^~~ badword"
* telegram: add support for sticker/video/photo/document #184
## Changes
* api: add userid to each message #200
## Bugfix
* discord: fix crash in memberupdate #198
* mattermost: Fix incorrect behaviour of EditDisable (mattermost). Fixes #197
* irc: Do not relay join/part of ourselves (irc). Closes #190
* irc: make reconnections more robust. #153
* gitter: update library, fixes possible crash
# v0.14.0
## New features
* api: add token authentication
* mattermost: add support for mattermost 3.10.0
## Changes
* api: gateway name is added in JSON messages
* api: lowercase JSON keys
* api: channel name isn't needed in config #195
## Bugfix
* discord: Add hashtag to channelname (when translating from id) (discord)
* mattermost: Fix a panic. #186
* mattermost: use teamid cache if possible. Fixes a panic
* api: post valid json. #185
* api: allow reuse of api in different gateways. #189
* general: Fix utf-8 issues for {NOPINGNICK}. #193
# v0.13.0
## New features
* irc: Limit message length. ```MessageLength=400```
Maximum length of message sent to irc server. If it exceeds <message clipped> will be add to the message.
* irc: Add NOPINGNICK option.
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
## Bugfix
* slack: Fix sending to different channels on same account (slack). Closes #177
* telegram: Fix incorrect usernames being sent. Closes #181
# v0.12.1
## New features
* telegram: Add UseFirstName option (telegram). Closes #144
* matrix: Add NoHomeServerSuffix. Option to disable homeserver on username (matrix). Closes #160.
## Bugfix
* xmpp: Add Compatibility for Cisco Jabber (xmpp) (#166)
* irc: Fix JoinChannel argument to use IRC channel key (#172)
* discord: Fix possible crash on nil (discord)
* discord: Replace long ids in channel metions (discord). Fixes #174
# v0.12.0
## Changes
* general: edited messages are now being sent by default on discord/mattermost/telegram/slack. See "New Features"
## New features
* general: add support for edited messages.
Add new keyword EditDisable (false/true), default false. Which means by default edited messages will be sent to other bridges.
Add new keyword EditSuffix , default "". You can change this eg to "(edited)", this will be appended to every edit message.
* mattermost: support mattermost v3.9.x
* general: Add support for HTTP{S}_PROXY env variables (#162)
* discord: Strip custom emoji metadata (discord). Closes #148
## Bugfix
* slack: Ignore error on private channel join (slack) Fixes #150
* mattermost: fix crash on reconnects when server is down. Closes #163
* irc: Relay messages starting with ! (irc). Closes #164
# v0.11.0
## New features
* general: reusing the same account on multiple gateways now also reuses the connection.
This is particuarly useful for irc. See #87
* general: the Name is now REQUIRED and needs to be UNIQUE for each gateway configuration
* telegram: Support edited messages (telegram). See #141
* mattermost: Add support for showing/hiding join/leave messages from mattermost. Closes #147
* mattermost: Reconnect on session removal/timeout (mattermost)
* mattermost: Support mattermost v3.8.x
* irc: Rejoin channel when kicked (irc).
## Bugfix
* mattermost: Remove space after nick (mattermost). Closes #142
* mattermost: Modify iconurl correctly (mattermost).
* irc: Fix join/leave regression (irc)
# v0.10.3
## Bugfix
* slack: Allow bot tokens for now without warning (slack). Closes #140 (fixes user_is_bot message on channel join)
# v0.10.2
## New features
* general: gops agent added. Allows for more debugging. See #134
* general: toml inline table support added for config file
## Bugfix
* all: vendored libs updated
## Changes
* general: add more informative messages on startup
# v0.10.1
## Bugfix
* gitter: Fix sending messages on new channel join.
# v0.10.0
## New features
* matrix: New protocol support added (https://matrix.org)
* mattermost: works with mattermost release v3.7.0
* discord: Replace role ids in mentions to role names (discord). Closes #133
## Bugfix
* mattermost: Add ReadTimeout to close lingering connections (mattermost). See #125
* gitter: Join rooms not already joined by the bot (gitter). See #135
* general: Fail when bridge is unable to join a channel (general)
## Changes
* telegram: Do not use HTML parsemode by default. Set ```MessageFormat="HTML"``` to use it. Closes #126
# v0.9.3
## New features
* API: rest interface to read / post messages (see API section in matterbridge.toml.sample)
## Bugfix
* slack: fix receiving messages from private channels #118
* slack: fix echo when using webhooks #119
* mattermost: reconnecting should work better now
* irc: keeps reconnecting (every 60 seconds) now after ping timeout/disconnects.
# v0.9.2
## New features
* slack: support private channels #118
## Bugfix
* general: make ignorenicks work again #115
* telegram: fix receiving from channels and groups #112
* telegram: use html for username
* telegram: use ```unknown``` as username when username is not visible.
* irc: update vendor (fixes some crashes) #117
* xmpp: fix tls by setting ServerName #114
# v0.9.1
## New features
* Rocket.Chat: New protocol support added (https://rocket.chat)
* irc: add channel key support #27 (see matterbrige.toml.sample for example)
* xmpp: add SkipTLSVerify #106
## Bugfix
* general: Exit when a bridge fails to start
* mattermost: Check errors only on first connect. Keep retrying after first connection succeeds. #95
* telegram: fix missing username #102
* slack: do not use API functions in webhook (slack) #110
# v0.9.0
## New features
* Telegram: New protocol support added (https://telegram.org)
* Hipchat: Add sample config to connect to hipchat via xmpp
* discord: add "Bot " tag to discord tokens automatically
* slack: Add support for dynamic Iconurl #43
* general: Add ```gateway.inout``` config option for bidirectional bridges #85
* general: Add ```[general]``` section so that ```RemoteNickFormat``` can be set globally
## Bugfix
* general: when using samechannelgateway NickFormat get doubled by the NICK #77
* general: fix ShowJoinPart for messages from irc bridge #72
* gitter: fix high cpu usage #89
* irc: fix !users command #78
* xmpp: fix keepalive
* xmpp: do not relay delayed/empty messages
* slack: Replace id-mentions to usernames #86
* mattermost: fix public links not working (API changes)
# v0.8.1
## Bugfix
* general: when using samechannelgateway NickFormat get doubled by the NICK #77
@ -504,7 +47,6 @@ See matterbridge.toml.sample for an example
# v0.6.1
## New features
* Slack support added. See matterbridge.conf.sample for more information
## Bugfix
* Fix 100% CPU bug on incorrect closed connections

View File

@ -1,27 +0,0 @@
#!/bin/bash
go version |grep go1.9 || exit
VERSION=$(git describe --tags)
mkdir ci/binaries
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-windows-amd64.exe
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-linux-amd64
GOOS=linux GOARCH=arm go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-linux-arm
GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-darwin-amd64
cd ci
cat > deploy.json <<EOF
{
"package": {
"name": "Matterbridge",
"repo": "nightly",
"subject": "42wim"
},
"version": {
"name": "$VERSION"
},
"files":
[
{"includePattern": "ci/binaries/(.*)", "uploadPattern":"\$1"}
],
"publish": true
}
EOF

View File

@ -1,11 +0,0 @@
[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

View File

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

View File

@ -1,380 +1,151 @@
package gateway
import (
"bytes"
"fmt"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
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"
"reflect"
"strings"
"time"
)
type Gateway struct {
*config.Config
Router *Router
MyConfig *config.Gateway
Bridges map[string]*bridge.Bridge
Channels map[string]*config.ChannelInfo
ChannelOptions map[string]config.ChannelOptions
Message chan config.Message
Name string
Messages *lru.Cache
MyConfig *config.Gateway
Bridges []bridge.Bridge
ChannelsOut map[string][]string
ChannelsIn map[string][]string
ignoreNicks map[string][]string
Name string
}
type BrMsgID struct {
br *bridge.Bridge
ID string
ChannelID string
}
func New(cfg config.Gateway, r *Router) *Gateway {
gw := &Gateway{Channels: make(map[string]*config.ChannelInfo), Message: r.Message,
Router: r, Bridges: make(map[string]*bridge.Bridge), Config: r.Config}
cache, _ := lru.New(5000)
gw.Messages = cache
gw.AddConfig(&cfg)
return gw
}
func (gw *Gateway) AddBridge(cfg *config.Bridge) error {
br := gw.Router.getBridge(cfg.Account)
if br == nil {
br = bridge.New(gw.Config, cfg, gw.Message)
func New(cfg *config.Config, gateway *config.Gateway) error {
c := make(chan config.Message)
gw := &Gateway{}
gw.Name = gateway.Name
gw.Config = cfg
gw.MyConfig = gateway
exists := make(map[string]bool)
for _, br := range append(gateway.In, gateway.Out...) {
if exists[br.Account] {
continue
}
log.Infof("Starting bridge: %s channel: %s", br.Account, br.Channel)
gw.Bridges = append(gw.Bridges, bridge.New(cfg, &br, c))
exists[br.Account] = true
}
gw.mapChannelsToBridge(br)
gw.Bridges[cfg.Account] = br
return nil
}
func (gw *Gateway) AddConfig(cfg *config.Gateway) error {
gw.Name = cfg.Name
gw.MyConfig = cfg
gw.mapChannels()
for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) {
err := gw.AddBridge(&br)
//TODO fix mapIgnores
//gw.mapIgnores()
exists = make(map[string]bool)
for _, br := range gw.Bridges {
err := br.Connect()
if err != nil {
return err
log.Fatalf("Bridge %s failed to start: %v", br.FullOrigin(), err)
}
for _, channel := range append(gw.ChannelsOut[br.FullOrigin()], gw.ChannelsIn[br.FullOrigin()]...) {
if exists[br.FullOrigin()+channel] {
continue
}
log.Infof("%s: joining %s", br.FullOrigin(), channel)
br.JoinChannel(channel)
exists[br.FullOrigin()+channel] = true
}
}
gw.handleReceive(c)
return nil
}
func (gw *Gateway) mapChannelsToBridge(br *bridge.Bridge) {
for ID, channel := range gw.Channels {
if br.Account == channel.Account {
br.Channels[ID] = *channel
}
}
}
func (gw *Gateway) reconnectBridge(br *bridge.Bridge) {
br.Disconnect()
time.Sleep(time.Second * 5)
RECONNECT:
log.Infof("Reconnecting %s", br.Account)
err := br.Connect()
if err != nil {
log.Errorf("Reconnection failed: %s. Trying again in 60 seconds", err)
time.Sleep(time.Second * 60)
goto RECONNECT
}
br.Joined = make(map[string]bool)
br.JoinChannels()
}
func (gw *Gateway) mapChannelConfig(cfg []config.Bridge, direction string) {
for _, br := range cfg {
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,
SameChannel: make(map[string]bool)}
channel.SameChannel[gw.Name] = br.SameChannel
gw.Channels[channel.ID] = channel
} else {
// if we already have a key and it's not our current direction it means we have a bidirectional inout
if gw.Channels[ID].Direction != direction {
gw.Channels[ID].Direction = "inout"
func (gw *Gateway) handleReceive(c chan config.Message) {
for {
select {
case msg := <-c:
for _, br := range gw.Bridges {
gw.handleMessage(msg, br)
}
}
gw.Channels[ID].SameChannel[gw.Name] = br.SameChannel
}
}
func (gw *Gateway) mapChannels() error {
gw.mapChannelConfig(gw.MyConfig.In, "in")
gw.mapChannelConfig(gw.MyConfig.Out, "out")
gw.mapChannelConfig(gw.MyConfig.InOut, "inout")
m := make(map[string][]string)
for _, br := range gw.MyConfig.Out {
m[br.Account] = append(m[br.Account], br.Channel)
}
gw.ChannelsOut = m
m = nil
m = make(map[string][]string)
for _, br := range gw.MyConfig.In {
m[br.Account] = append(m[br.Account], br.Channel)
}
gw.ChannelsIn = m
return nil
}
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
func (gw *Gateway) mapIgnores() {
m := make(map[string][]string)
for _, br := range gw.MyConfig.In {
accInfo := strings.Split(br.Account, ".")
m[br.Account] = strings.Fields(gw.Config.IRC[accInfo[1]].IgnoreNicks)
}
// if source channel is in only, do nothing
for _, channel := range gw.Channels {
// lookup the channel from the message
if channel.ID == getChannelID(*msg) {
// we only have destinations if the original message is from an "in" (sending) channel
if !strings.Contains(channel.Direction, "in") {
return channels
}
continue
}
}
for _, channel := range gw.Channels {
if _, ok := gw.Channels[getChannelID(*msg)]; !ok {
continue
}
// do samechannelgateway logic
if channel.SameChannel[msg.Gateway] {
if msg.Channel == channel.Name && msg.Account != dest.Account {
channels = append(channels, *channel)
}
continue
}
if strings.Contains(channel.Direction, "out") && channel.Account == dest.Account && gw.validGatewayDest(msg, channel) {
channels = append(channels, *channel)
}
}
return channels
gw.ignoreNicks = m
}
func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrMsgID {
var brMsgIDs []*BrMsgID
// TODO refactor
// 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 != "discord" &&
dest.Protocol != "slack" &&
dest.Protocol != "mattermost" &&
dest.Protocol != "telegram" &&
dest.Protocol != "matrix" &&
dest.Protocol != "xmpp" {
if msg.Text == "" {
return brMsgIDs
}
func (gw *Gateway) getDestChannel(msg *config.Message, dest string) []string {
channels := gw.ChannelsIn[msg.FullOrigin]
for _, channel := range channels {
if channel == msg.Channel {
return gw.ChannelsOut[dest]
}
}
// only relay join/part when configged
if msg.Event == config.EVENT_JOIN_LEAVE && !gw.Bridges[dest.Account].Config.ShowJoinPart {
return brMsgIDs
}
// broadcast to every out channel (irc QUIT)
if msg.Channel == "" && msg.Event != config.EVENT_JOIN_LEAVE {
log.Debug("empty channel")
return brMsgIDs
return []string{}
}
func (gw *Gateway) handleMessage(msg config.Message, dest bridge.Bridge) {
if gw.ignoreMessage(&msg) {
return
}
originchannel := msg.Channel
origmsg := msg
channels := gw.getDestChannel(&msg, *dest)
channels := gw.getDestChannel(&msg, dest.FullOrigin())
for _, channel := range channels {
// do not send to ourself
if channel.ID == getChannelID(origmsg) {
// do not send the message to the bridge we come from if also the channel is the same
if msg.FullOrigin == dest.FullOrigin() && channel == originchannel {
continue
}
log.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)
msg.ID = ""
if res, ok := gw.Messages.Get(origmsg.ID); ok {
IDs := res.([]*BrMsgID)
for _, id := range IDs {
// 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
}
}
msg.Channel = channel
if msg.Channel == "" {
log.Debug("empty channel")
return
}
// for api we need originchannel as channel
if dest.Protocol == "api" {
msg.Channel = originchannel
}
mID, err := dest.Send(msg)
log.Debugf("Sending %#v from %s (%s) to %s (%s)", msg, msg.FullOrigin, originchannel, dest.FullOrigin(), channel)
err := dest.Send(msg)
if err != nil {
fmt.Println(err)
}
// append the message ID (mID) from this bridge (dest) to our brMsgIDs slice
if mID != "" {
brMsgIDs = append(brMsgIDs, &BrMsgID{dest, mID, channel.ID})
}
}
return brMsgIDs
}
func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
// if we don't have the bridge, ignore it
if _, ok := gw.Bridges[msg.Account]; !ok {
return true
}
if msg.Text == "" {
// we have an attachment or actual bytes
if msg.Extra != nil && (msg.Extra["attachments"] != nil || len(msg.Extra["file"]) > 0) {
return false
}
log.Debugf("ignoring empty message %#v from %s", msg, msg.Account)
return true
}
for _, entry := range strings.Fields(gw.Bridges[msg.Account].Config.IgnoreNicks) {
// should we discard messages ?
for _, entry := range gw.ignoreNicks[msg.FullOrigin] {
if msg.Username == entry {
log.Debugf("ignoring %s from %s", msg.Username, msg.Account)
return true
}
}
// TODO do not compile regexps everytime
for _, entry := range strings.Fields(gw.Bridges[msg.Account].Config.IgnoreMessages) {
if entry != "" {
re, err := regexp.Compile(entry)
if err != nil {
log.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)
return true
}
}
}
return false
}
func (gw *Gateway) modifyUsername(msg config.Message, dest *bridge.Bridge) string {
br := gw.Bridges[msg.Account]
msg.Protocol = br.Protocol
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 {
log.Errorf("regexp in %s failed: %s", msg.Account, err)
func (gw *Gateway) modifyMessage(msg *config.Message, dest bridge.Bridge) {
val := reflect.ValueOf(gw.Config).Elem()
for i := 0; i < val.NumField(); i++ {
typeField := val.Type().Field(i)
// look for the protocol map (both lowercase)
if strings.ToLower(typeField.Name) == dest.Protocol() {
// get the Protocol struct from the map
protoCfg := val.Field(i).MapIndex(reflect.ValueOf(dest.Origin()))
//config.SetNickFormat(msg, protoCfg.Interface().(config.Protocol))
val.Field(i).SetMapIndex(reflect.ValueOf(dest.Origin()), protoCfg)
break
}
msg.Username = re.ReplaceAllString(msg.Username, replace)
}
if len(msg.Username) > 0 {
// fix utf-8 issue #193
i := 0
for index := range msg.Username {
if i == 1 {
i = index
break
}
i++
}
nick = strings.Replace(nick, "{NOPINGNICK}", msg.Username[:i]+""+msg.Username[i:], -1)
}
nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1)
nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1)
nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
return nick
}
func (gw *Gateway) modifyAvatar(msg config.Message, dest *bridge.Bridge) string {
iconurl := gw.Config.General.IconURL
if iconurl == "" {
iconurl = dest.Config.IconURL
}
iconurl = strings.Replace(iconurl, "{NICK}", msg.Username, -1)
if msg.Avatar == "" {
msg.Avatar = iconurl
}
return msg.Avatar
}
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 {
log.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
msg.Extra["file"][i] = extra
req, _ := http.NewRequest("PUT", url, reader)
req.Header.Set("Content-Type", "binary/octet-stream")
_, err := client.Do(req)
if err != nil {
log.Errorf("mediaserver upload failed: %#v", err)
}
log.Debugf("mediaserver download URL = %s", durl)
}
}
}
func getChannelID(msg config.Message) string {
return msg.Channel + msg.Account
}
func (gw *Gateway) validGatewayDest(msg *config.Message, channel *config.ChannelInfo) bool {
return msg.Gateway == gw.Name
}
func isApi(account string) bool {
return strings.HasPrefix(account, "api.")
}

View File

@ -1,288 +0,0 @@
package gateway
import (
"fmt"
"github.com/42wim/matterbridge/bridge/config"
"github.com/BurntSushi/toml"
"github.com/stretchr/testify/assert"
"strconv"
"testing"
)
var testconfig = `
[irc.freenode]
[mattermost.test]
[gitter.42wim]
[discord.test]
[slack.test]
[[gateway]]
name = "bridge1"
enable=true
[[gateway.inout]]
account = "irc.freenode"
channel = "#wimtesting"
[[gateway.inout]]
account="gitter.42wim"
channel="42wim/testroom"
#channel="matterbridge/Lobby"
[[gateway.inout]]
account = "discord.test"
channel = "general"
[[gateway.inout]]
account="slack.test"
channel="testing"
`
var testconfig2 = `
[irc.freenode]
[mattermost.test]
[gitter.42wim]
[discord.test]
[slack.test]
[[gateway]]
name = "bridge1"
enable=true
[[gateway.in]]
account = "irc.freenode"
channel = "#wimtesting"
[[gateway.in]]
account="gitter.42wim"
channel="42wim/testroom"
[[gateway.inout]]
account = "discord.test"
channel = "general"
[[gateway.out]]
account="slack.test"
channel="testing"
[[gateway]]
name = "bridge2"
enable=true
[[gateway.in]]
account = "irc.freenode"
channel = "#wimtesting2"
[[gateway.out]]
account="gitter.42wim"
channel="42wim/testroom"
[[gateway.out]]
account = "discord.test"
channel = "general2"
`
var testconfig3 = `
[irc.zzz]
[telegram.zzz]
[slack.zzz]
[[gateway]]
name="bridge"
enable=true
[[gateway.inout]]
account="irc.zzz"
channel="#main"
[[gateway.inout]]
account="telegram.zzz"
channel="-1111111111111"
[[gateway.inout]]
account="slack.zzz"
channel="irc"
[[gateway]]
name="announcements"
enable=true
[[gateway.in]]
account="telegram.zzz"
channel="-2222222222222"
[[gateway.out]]
account="irc.zzz"
channel="#main"
[[gateway.out]]
account="irc.zzz"
channel="#main-help"
[[gateway.out]]
account="telegram.zzz"
channel="--333333333333"
[[gateway.out]]
account="slack.zzz"
channel="general"
[[gateway]]
name="bridge2"
enable=true
[[gateway.inout]]
account="irc.zzz"
channel="#main-help"
[[gateway.inout]]
account="telegram.zzz"
channel="--444444444444"
[[gateway]]
name="bridge3"
enable=true
[[gateway.inout]]
account="irc.zzz"
channel="#main-telegram"
[[gateway.inout]]
account="telegram.zzz"
channel="--333333333333"
`
func maketestRouter(input string) *Router {
var cfg *config.Config
if _, err := toml.Decode(input, &cfg); err != nil {
fmt.Println(err)
}
r, err := NewRouter(cfg)
if err != nil {
fmt.Println(err)
}
return r
}
func TestNewRouter(t *testing.T) {
var cfg *config.Config
if _, err := toml.Decode(testconfig, &cfg); err != nil {
fmt.Println(err)
}
r, err := NewRouter(cfg)
if err != nil {
fmt.Println(err)
}
assert.Equal(t, 1, len(r.Gateways))
assert.Equal(t, 4, len(r.Gateways["bridge1"].Bridges))
assert.Equal(t, 4, len(r.Gateways["bridge1"].Channels))
r = maketestRouter(testconfig2)
assert.Equal(t, 2, len(r.Gateways))
assert.Equal(t, 4, len(r.Gateways["bridge1"].Bridges))
assert.Equal(t, 3, len(r.Gateways["bridge2"].Bridges))
assert.Equal(t, 4, len(r.Gateways["bridge1"].Channels))
assert.Equal(t, 3, len(r.Gateways["bridge2"].Channels))
assert.Equal(t, &config.ChannelInfo{Name: "42wim/testroom", Direction: "out",
ID: "42wim/testroomgitter.42wim", Account: "gitter.42wim",
SameChannel: map[string]bool{"bridge2": false}},
r.Gateways["bridge2"].Channels["42wim/testroomgitter.42wim"])
assert.Equal(t, &config.ChannelInfo{Name: "42wim/testroom", Direction: "in",
ID: "42wim/testroomgitter.42wim", Account: "gitter.42wim",
SameChannel: map[string]bool{"bridge1": false}},
r.Gateways["bridge1"].Channels["42wim/testroomgitter.42wim"])
assert.Equal(t, &config.ChannelInfo{Name: "general", Direction: "inout",
ID: "generaldiscord.test", Account: "discord.test",
SameChannel: map[string]bool{"bridge1": false}},
r.Gateways["bridge1"].Channels["generaldiscord.test"])
}
func TestGetDestChannel(t *testing.T) {
r := maketestRouter(testconfig2)
msg := &config.Message{Text: "test", Channel: "general", Account: "discord.test", Gateway: "bridge1", Protocol: "discord", Username: "test"}
for _, br := range r.Gateways["bridge1"].Bridges {
switch br.Account {
case "discord.test":
assert.Equal(t, []config.ChannelInfo{{Name: "general", Account: "discord.test", Direction: "inout", ID: "generaldiscord.test", SameChannel: map[string]bool{"bridge1": false}, Options: config.ChannelOptions{Key: ""}}},
r.Gateways["bridge1"].getDestChannel(msg, *br))
case "slack.test":
assert.Equal(t, []config.ChannelInfo{{Name: "testing", Account: "slack.test", Direction: "out", ID: "testingslack.test", SameChannel: map[string]bool{"bridge1": false}, Options: config.ChannelOptions{Key: ""}}},
r.Gateways["bridge1"].getDestChannel(msg, *br))
case "gitter.42wim":
assert.Equal(t, []config.ChannelInfo(nil), r.Gateways["bridge1"].getDestChannel(msg, *br))
case "irc.freenode":
assert.Equal(t, []config.ChannelInfo(nil), r.Gateways["bridge1"].getDestChannel(msg, *br))
}
}
}
func TestGetDestChannelAdvanced(t *testing.T) {
r := maketestRouter(testconfig3)
var msgs []*config.Message
i := 0
for _, gw := range r.Gateways {
for _, channel := range gw.Channels {
msgs = append(msgs, &config.Message{Text: "text" + strconv.Itoa(i), Channel: channel.Name, Account: channel.Account, Gateway: gw.Name, Username: "user" + strconv.Itoa(i)})
i++
}
}
hits := make(map[string]int)
for _, gw := range r.Gateways {
for _, br := range gw.Bridges {
for _, msg := range msgs {
channels := gw.getDestChannel(msg, *br)
if gw.Name != msg.Gateway {
assert.Equal(t, []config.ChannelInfo(nil), channels)
continue
}
switch gw.Name {
case "bridge":
if (msg.Channel == "#main" || msg.Channel == "-1111111111111" || msg.Channel == "irc") && (msg.Account == "irc.zzz" || msg.Account == "telegram.zzz" || msg.Account == "slack.zzz") {
hits[gw.Name]++
switch br.Account {
case "irc.zzz":
assert.Equal(t, []config.ChannelInfo{{Name: "#main", Account: "irc.zzz", Direction: "inout", ID: "#mainirc.zzz", SameChannel: map[string]bool{"bridge": false}, Options: config.ChannelOptions{Key: ""}}}, channels)
case "telegram.zzz":
assert.Equal(t, []config.ChannelInfo{{Name: "-1111111111111", Account: "telegram.zzz", Direction: "inout", ID: "-1111111111111telegram.zzz", SameChannel: map[string]bool{"bridge": false}, Options: config.ChannelOptions{Key: ""}}}, channels)
case "slack.zzz":
assert.Equal(t, []config.ChannelInfo{{Name: "irc", Account: "slack.zzz", Direction: "inout", ID: "ircslack.zzz", SameChannel: map[string]bool{"bridge": false}, Options: config.ChannelOptions{Key: ""}}}, channels)
}
}
case "bridge2":
if (msg.Channel == "#main-help" || msg.Channel == "--444444444444") && (msg.Account == "irc.zzz" || msg.Account == "telegram.zzz") {
hits[gw.Name]++
switch br.Account {
case "irc.zzz":
assert.Equal(t, []config.ChannelInfo{{Name: "#main-help", Account: "irc.zzz", Direction: "inout", ID: "#main-helpirc.zzz", SameChannel: map[string]bool{"bridge2": false}, Options: config.ChannelOptions{Key: ""}}}, channels)
case "telegram.zzz":
assert.Equal(t, []config.ChannelInfo{{Name: "--444444444444", Account: "telegram.zzz", Direction: "inout", ID: "--444444444444telegram.zzz", SameChannel: map[string]bool{"bridge2": false}, Options: config.ChannelOptions{Key: ""}}}, channels)
}
}
case "bridge3":
if (msg.Channel == "#main-telegram" || msg.Channel == "--333333333333") && (msg.Account == "irc.zzz" || msg.Account == "telegram.zzz") {
hits[gw.Name]++
switch br.Account {
case "irc.zzz":
assert.Equal(t, []config.ChannelInfo{{Name: "#main-telegram", Account: "irc.zzz", Direction: "inout", ID: "#main-telegramirc.zzz", SameChannel: map[string]bool{"bridge3": false}, Options: config.ChannelOptions{Key: ""}}}, channels)
case "telegram.zzz":
assert.Equal(t, []config.ChannelInfo{{Name: "--333333333333", Account: "telegram.zzz", Direction: "inout", ID: "--333333333333telegram.zzz", SameChannel: map[string]bool{"bridge3": false}, Options: config.ChannelOptions{Key: ""}}}, channels)
}
}
case "announcements":
if msg.Channel != "-2222222222222" && msg.Account != "telegram" {
assert.Equal(t, []config.ChannelInfo(nil), channels)
continue
}
hits[gw.Name]++
switch br.Account {
case "irc.zzz":
assert.Equal(t, []config.ChannelInfo{{Name: "#main", Account: "irc.zzz", Direction: "out", ID: "#mainirc.zzz", SameChannel: map[string]bool{"announcements": false}, Options: config.ChannelOptions{Key: ""}}, {Name: "#main-help", Account: "irc.zzz", Direction: "out", ID: "#main-helpirc.zzz", SameChannel: map[string]bool{"announcements": false}, Options: config.ChannelOptions{Key: ""}}}, channels)
case "slack.zzz":
assert.Equal(t, []config.ChannelInfo{{Name: "general", Account: "slack.zzz", Direction: "out", ID: "generalslack.zzz", SameChannel: map[string]bool{"announcements": false}, Options: config.ChannelOptions{Key: ""}}}, channels)
case "telegram.zzz":
assert.Equal(t, []config.ChannelInfo{{Name: "--333333333333", Account: "telegram.zzz", Direction: "out", ID: "--333333333333telegram.zzz", SameChannel: map[string]bool{"announcements": false}, Options: config.ChannelOptions{Key: ""}}}, channels)
}
}
}
}
}
assert.Equal(t, map[string]int{"bridge3": 4, "bridge": 9, "announcements": 3, "bridge2": 4}, hits)
}

View File

@ -1,113 +0,0 @@
package gateway
import (
"fmt"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/gateway/samechannel"
log "github.com/Sirupsen/logrus"
// "github.com/davecgh/go-spew/spew"
"time"
)
type Router struct {
Gateways map[string]*Gateway
Message chan config.Message
*config.Config
}
func NewRouter(cfg *config.Config) (*Router, error) {
r := &Router{}
r.Config = cfg
r.Message = make(chan config.Message)
r.Gateways = make(map[string]*Gateway)
sgw := samechannelgateway.New(cfg)
gwconfigs := sgw.GetConfig()
for _, entry := range append(gwconfigs, cfg.Gateway...) {
if !entry.Enable {
continue
}
if entry.Name == "" {
return nil, fmt.Errorf("%s", "Gateway without name found")
}
if _, ok := r.Gateways[entry.Name]; ok {
return nil, fmt.Errorf("Gateway with name %s already exists", entry.Name)
}
r.Gateways[entry.Name] = New(entry, r)
}
return r, nil
}
func (r *Router) Start() error {
m := make(map[string]*bridge.Bridge)
for _, gw := range r.Gateways {
for _, br := range gw.Bridges {
m[br.Account] = br
}
}
for _, br := range m {
log.Infof("Starting bridge: %s ", br.Account)
err := br.Connect()
if err != nil {
return fmt.Errorf("Bridge %s failed to start: %v", br.Account, err)
}
err = br.JoinChannels()
if err != nil {
return fmt.Errorf("Bridge %s failed to join channel: %v", br.Account, err)
}
}
go r.handleReceive()
return nil
}
func (r *Router) getBridge(account string) *bridge.Bridge {
for _, gw := range r.Gateways {
if br, ok := gw.Bridges[account]; ok {
return br
}
}
return nil
}
func (r *Router) handleReceive() {
for msg := range r.Message {
if msg.Event == config.EVENT_FAILURE {
Loop:
for _, gw := range r.Gateways {
for _, br := range gw.Bridges {
if msg.Account == br.Account {
go gw.reconnectBridge(br)
break Loop
}
}
}
}
if msg.Event == config.EVENT_REJOIN_CHANNELS {
for _, gw := range r.Gateways {
for _, br := range gw.Bridges {
if msg.Account == br.Account {
br.Joined = make(map[string]bool)
br.JoinChannels()
}
}
}
}
for _, gw := range r.Gateways {
// record all the message ID's of the different bridges
var msgIDs []*BrMsgID
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)...)
}
// only add the message ID if it doesn't already exists
if _, ok := gw.Messages.Get(msg.ID); !ok && msg.ID != "" {
gw.Messages.Add(msg.ID, msgIDs)
}
}
}
}
}

View File

@ -1,28 +1,78 @@
package samechannelgateway
import (
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
)
type SameChannelGateway struct {
*config.Config
MyConfig *config.SameChannelGateway
Bridges []bridge.Bridge
Channels []string
ignoreNicks map[string][]string
Name string
}
func New(cfg *config.Config) *SameChannelGateway {
return &SameChannelGateway{Config: cfg}
func New(cfg *config.Config, gateway *config.SameChannelGateway) error {
c := make(chan config.Message)
gw := &SameChannelGateway{}
gw.Name = gateway.Name
gw.Config = cfg
gw.MyConfig = gateway
gw.Channels = gateway.Channels
for _, account := range gateway.Accounts {
br := config.Bridge{Account: account}
log.Infof("Starting bridge: %s", account)
gw.Bridges = append(gw.Bridges, bridge.New(cfg, &br, c))
}
for _, br := range gw.Bridges {
err := br.Connect()
if err != nil {
log.Fatalf("Bridge %s failed to start: %v", br.FullOrigin(), err)
}
for _, channel := range gw.Channels {
log.Infof("%s: joining %s", br.FullOrigin(), channel)
br.JoinChannel(channel)
}
}
gw.handleReceive(c)
return nil
}
func (sgw *SameChannelGateway) GetConfig() []config.Gateway {
var gwconfigs []config.Gateway
cfg := sgw.Config
for _, gw := range cfg.SameChannelGateway {
gwconfig := config.Gateway{Name: gw.Name, Enable: gw.Enable}
for _, account := range gw.Accounts {
for _, channel := range gw.Channels {
gwconfig.InOut = append(gwconfig.InOut, config.Bridge{Account: account, Channel: channel, SameChannel: true})
func (gw *SameChannelGateway) handleReceive(c chan config.Message) {
for {
select {
case msg := <-c:
for _, br := range gw.Bridges {
gw.handleMessage(msg, br)
}
}
gwconfigs = append(gwconfigs, gwconfig)
}
return gwconfigs
}
func (gw *SameChannelGateway) handleMessage(msg config.Message, dest bridge.Bridge) {
// is this a configured channel
if !gw.validChannel(msg.Channel) {
return
}
// do not send the message to the bridge we come from if also the channel is the same
if msg.FullOrigin == dest.FullOrigin() {
return
}
log.Debugf("Sending %#v from %s (%s) to %s (%s)", msg, msg.FullOrigin, msg.Channel, dest.FullOrigin(), msg.Channel)
err := dest.Send(msg)
if err != nil {
log.Error(err)
}
}
func (gw *SameChannelGateway) validChannel(channel string) bool {
for _, c := range gw.Channels {
if c == channel {
return true
}
}
return false
}

View File

@ -1,31 +0,0 @@
package samechannelgateway
import (
"fmt"
"github.com/42wim/matterbridge/bridge/config"
"github.com/BurntSushi/toml"
"github.com/stretchr/testify/assert"
"testing"
)
var testconfig = `
[mattermost.test]
[slack.test]
[[samechannelgateway]]
enable = true
name = "blah"
accounts = [ "mattermost.test","slack.test" ]
channels = [ "testing","testing2","testing10"]
`
func TestGetConfig(t *testing.T) {
var cfg *config.Config
if _, err := toml.Decode(testconfig, &cfg); err != nil {
fmt.Println(err)
}
sgw := New(cfg)
configs := sgw.GetConfig()
assert.Equal(t, []config.Gateway{{Name: "blah", Enable: true, In: []config.Bridge(nil), Out: []config.Bridge(nil), InOut: []config.Bridge{{Account: "mattermost.test", Channel: "testing", Options: config.ChannelOptions{Key: ""}, SameChannel: true}, {Account: "mattermost.test", Channel: "testing2", Options: config.ChannelOptions{Key: ""}, SameChannel: true}, {Account: "mattermost.test", Channel: "testing10", Options: config.ChannelOptions{Key: ""}, SameChannel: true}, {Account: "slack.test", Channel: "testing", Options: config.ChannelOptions{Key: ""}, SameChannel: true}, {Account: "slack.test", Channel: "testing2", Options: config.ChannelOptions{Key: ""}, SameChannel: true}, {Account: "slack.test", Channel: "testing10", Options: config.ChannelOptions{Key: ""}, SameChannel: true}}}}, configs)
}

View File

@ -1,107 +0,0 @@
package rockethook
import (
"crypto/tls"
"encoding/json"
"io/ioutil"
"log"
"net"
"net/http"
)
// Message for rocketchat outgoing webhook.
type Message struct {
Token string `json:"token"`
ChannelID string `json:"channel_id"`
ChannelName string `json:"channel_name"`
Timestamp string `json:"timestamp"`
UserID string `json:"user_id"`
UserName string `json:"user_name"`
Text string `json:"text"`
}
// Client for Rocketchat.
type Client struct {
In chan Message
httpclient *http.Client
Config
}
// Config for client.
type Config struct {
BindAddress string // Address to listen on
Token string // Only allow this token from Rocketchat. (Allow everything when empty)
InsecureSkipVerify bool // disable certificate checking
}
// New Rocketchat client.
func New(url string, config Config) *Client {
c := &Client{In: make(chan Message), Config: config}
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify},
}
c.httpclient = &http.Client{Transport: tr}
_, _, err := net.SplitHostPort(c.BindAddress)
if err != nil {
log.Fatalf("incorrect bindaddress %s", c.BindAddress)
}
go c.StartServer()
return c
}
// StartServer starts a webserver listening for incoming mattermost POSTS.
func (c *Client) StartServer() {
mux := http.NewServeMux()
mux.Handle("/", c)
log.Printf("Listening on http://%v...\n", c.BindAddress)
if err := http.ListenAndServe(c.BindAddress, mux); err != nil {
log.Fatal(err)
}
}
// ServeHTTP implementation.
func (c *Client) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
log.Println("invalid " + r.Method + " connection from " + r.RemoteAddr)
http.NotFound(w, r)
return
}
msg := Message{}
body, err := ioutil.ReadAll(r.Body)
log.Println(string(body))
if err != nil {
log.Println(err)
http.NotFound(w, r)
return
}
defer r.Body.Close()
err = json.Unmarshal(body, &msg)
if err != nil {
log.Println(err)
http.NotFound(w, r)
return
}
if msg.Token == "" {
log.Println("no token from " + r.RemoteAddr)
http.NotFound(w, r)
return
}
msg.ChannelName = "#" + msg.ChannelName
if c.Token != "" {
if msg.Token != c.Token {
log.Println("invalid token " + msg.Token + " from " + r.RemoteAddr)
http.NotFound(w, r)
return
}
}
c.In <- msg
}
// Receive returns an incoming message from mattermost outgoing webhooks URL.
func (c *Client) Receive() Message {
var msg Message
for msg = range c.In {
return msg
}
return msg
}

287
matterbridge.conf.sample Normal file
View File

@ -0,0 +1,287 @@
#This is configuration for matterbridge.
###################################################################
#IRC section
###################################################################
[IRC]
#Enable enables this bridge
#OPTIONAL (default false)
Enable=true
#irc server to connect to.
#REQUIRED
Server="irc.freenode.net:6667"
#Enable to use TLS connection to your irc server.
#OPTIONAL (default false)
UseTLS=false
#Enable SASL (PLAIN) authentication. (freenode requires this from eg AWS hosts)
#It uses NickServNick and NickServPassword as login and password
#OPTIONAL (default false)
UseSASL=false
#Enable to not verify the certificate on your irc server. i
#e.g. when using selfsigned certificates
#OPTIONAL (default false)
SkipTLSVerify=true
#Your nick on irc.
#REQUIRED
Nick="matterbot"
#If you registered your bot with a service like Nickserv on freenode.
#Also being used when UseSASL=true
#OPTIONAL
NickServNick="nickserv"
NickServPassword="secret"
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#OPTIONAL (default {BRIDGE}-{NICK})
RemoteNickFormat="[{BRIDGE}] <{NICK}> "
#Nicks you want to ignore.
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
###################################################################
#XMPP section
###################################################################
[XMPP]
#Enable enables this bridge
#OPTIONAL (default false)
Enable=true
#xmpp server to connect to.
#REQUIRED
Server="jabber.example.com:5222"
#Jid
#REQUIRED
Jid="user@example.com"
#Password
#REQUIRED
Password="yourpass"
#MUC
#REQUIRED
Muc="conference.jabber.example.com"
#Your nick in the rooms
#REQUIRED
Nick="xmppbot"
###################################################################
#mattermost section
###################################################################
[mattermost]
#Enable enables this bridge
#OPTIONAL (default false)
Enable=true
#### Settings for webhook matterbridge.
#### These settings will not be used when using -plus switch which doesn't use
#### webhooks.
#Url is your incoming webhook url as specified in mattermost.
#See account settings - integrations - incoming webhooks on mattermost.
#REQUIRED
URL="https://yourdomain/hooks/yourhookkey"
#Address to listen on for outgoing webhook requests from mattermost.
#See account settings - integrations - outgoing webhooks on mattermost.
#This setting will not be used when using -plus switch which doesn't use
#webhooks
#REQUIRED
BindAddress="0.0.0.0:9999"
#Icon that will be showed in mattermost.
#OPTIONAL
IconURL="http://youricon.png"
#### Settings for matterbridge -plus
#### Thse settings will only be used when using the -plus switch.
#The mattermost hostname.
#REQUIRED
Server="yourmattermostserver.domain"
#Your team on mattermost.
#REQUIRED
Team="yourteam"
#login/pass of your bot.
#Use a dedicated user for this and not your own!
#REQUIRED
Login="yourlogin"
Password="yourpass"
#Enable this to make a http connection (instead of https) to your mattermost.
#OPTIONAL (default false)
NoTLS=false
#### Shared settings for matterbridge and -plus
#Enable to not verify the certificate on your mattermost server.
#e.g. when using selfsigned certificates
#OPTIONAL (default false)
SkipTLSVerify=true
#Enable to show IRC joins/parts in mattermost.
#OPTIONAL (default false)
ShowJoinPart=false
#Whether to prefix messages from other bridges to mattermost with the sender's nick.
#Useful if username overrides for incoming webhooks isn't enabled on the
#mattermost server. If you set PrefixMessagesWithNick to true, each message
#from bridge to Mattermost will by default be prefixed by "bridge-" + nick. You can,
#however, modify how the messages appear, by setting (and modifying) RemoteNickFormat
#OPTIONAL (default false)
PrefixMessagesWithNick=false
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#OPTIONAL (default {BRIDGE}-{NICK})
RemoteNickFormat="[{BRIDGE}] <{NICK}> "
#how to format the list of IRC nicks when displayed in mattermost.
#Possible options are "table" and "plain"
#OPTIONAL (default plain)
NickFormatter=plain
#How many nicks to list per row for formatters that support this.
#OPTIONAL (default 4)
NicksPerRow=4
#Nicks you want to ignore. Messages from those users will not be bridged.
#OPTIONAL
IgnoreNicks="mmbot spammer2"
###################################################################
#Gitter section
#Best to make a dedicated gitter account for the bot.
###################################################################
[Gitter]
#Enable enables this bridge
#OPTIONAL (default false)
Enable=true
#Token to connect with Gitter API
#You can get your token by going to https://developer.gitter.im/docs/welcome and SIGN IN
#REQUIRED
Token="Yourtokenhere"
#Nicks you want to ignore. Messages of those users will not be bridged.
#OPTIONAL
IgnoreNicks="spammer1 spammer2"
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#OPTIONAL (default {BRIDGE}-{NICK})
RemoteNickFormat="[{BRIDGE}] <{NICK}> "
###################################################################
#slack section
###################################################################
[slack]
#Enable enables this bridge
#OPTIONAL (default false)
Enable=true
#### Settings for webhook matterbridge.
#### These settings will not be used when useAPI is enabled
#Url is your incoming webhook url as specified in slack
#See account settings - integrations - incoming webhooks on slack
#REQUIRED (unless useAPI=true)
URL="https://hooks.slack.com/services/yourhook"
#Address to listen on for outgoing webhook requests from slack
#See account settings - integrations - outgoing webhooks on slack
#This setting will not be used when useAPI is eanbled
#webhooks
#REQUIRED (unless useAPI=true)
BindAddress="0.0.0.0:9999"
#Icon that will be showed in slack
#OPTIONAL
IconURL="http://youricon.png"
#### Settings for using slack API
#OPTIONAL
useAPI=false
#Token to connect with the Slack API
#REQUIRED (when useAPI=true)
Token="yourslacktoken"
#### Shared settings for webhooks and API
#Whether to prefix messages from other bridges to mattermost with the sender's nick.
#Useful if username overrides for incoming webhooks isn't enabled on the
#slack server. If you set PrefixMessagesWithNick to true, each message
#from bridge to Slack will by default be prefixed by "bridge-" + nick. You can,
#however, modify how the messages appear, by setting (and modifying) RemoteNickFormat
#OPTIONAL (default false)
PrefixMessagesWithNick=false
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#OPTIONAL (default {BRIDGE}-{NICK})
RemoteNickFormat="[{BRIDGE}] <{NICK}>
#how to format the list of IRC nicks when displayed in slack
#Possible options are "table" and "plain"
#OPTIONAL (default plain)
NickFormatter=plain
#How many nicks to list per row for formatters that support this.
#OPTIONAL (default 4)
NicksPerRow=4
#Nicks you want to ignore. Messages from those users will not be bridged.
#OPTIONAL
IgnoreNicks="mmbot spammer2"
###################################################################
#multiple channel config
###################################################################
#You can specify multiple channels.
#The name is just an identifier for you.
#REQUIRED (at least 1 channel)
[Channel "channel1"]
#Choose the IRC channel to send messages to.
IRC="#off-topic"
#Choose the mattermost channel to messages to.
mattermost="off-topic"
#Choose the xmpp channel to send messages to.
xmpp="off-topic"
#Choose the Gitter channel to send messages to.
#Gitter channels are named "user/repo"
gitter="42wim/matterbridge"
#Choose the slack channel to send messages to.
slack="general"
[Channel "testchannel"]
IRC="#testing"
mattermost="testing"
xmpp="testing"
gitter="user/repo"
slack="testing"
###################################################################
#general
###################################################################
[general]
#request your API key on https://github.com/giphy/GiphyAPI. This is a public beta key.
#OPTIONAL
GiphyApiKey="dc6zaTOxFJmzC"
#Enabling plus means you'll use the API version instead of the webhooks one
Plus=false

View File

@ -5,16 +5,11 @@ import (
"fmt"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/gateway"
"github.com/42wim/matterbridge/gateway/samechannel"
log "github.com/Sirupsen/logrus"
"github.com/google/gops/agent"
"os"
"strings"
)
var (
version = "1.7.1"
githash string
)
var version = "0.7.1"
func init() {
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
@ -24,34 +19,42 @@ func main() {
flagConfig := flag.String("conf", "matterbridge.toml", "config file")
flagDebug := flag.Bool("debug", false, "enable debug")
flagVersion := flag.Bool("version", false, "show version")
flagGops := flag.Bool("gops", false, "enable gops agent")
flag.Parse()
if *flagGops {
agent.Listen(&agent.Options{})
defer agent.Close()
}
if *flagVersion {
fmt.Printf("version: %s %s\n", version, githash)
fmt.Println("version:", version)
return
}
if *flagDebug || os.Getenv("DEBUG") == "1" {
log.Info("Enabling debug")
flag.Parse()
if *flagDebug {
log.Info("enabling debug")
log.SetLevel(log.DebugLevel)
}
log.Printf("Running version %s %s", version, githash)
if strings.Contains(version, "-dev") {
log.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.")
}
fmt.Println("running version", version)
cfg := config.NewConfig(*flagConfig)
cfg.General.Debug = *flagDebug
r, err := gateway.NewRouter(cfg)
if err != nil {
log.Fatalf("Starting gateway failed: %s", err)
for _, gw := range cfg.SameChannelGateway {
if !gw.Enable {
continue
}
fmt.Printf("starting samechannel gateway %#v\n", gw.Name)
go func(gw config.SameChannelGateway) {
err := samechannelgateway.New(cfg, &gw)
if err != nil {
log.Debugf("starting gateway failed %#v", err)
}
}(gw)
}
err = r.Start()
if err != nil {
log.Fatalf("Starting gateway failed: %s", err)
for _, gw := range cfg.Gateway {
if !gw.Enable {
continue
}
fmt.Printf("starting gateway %#v\n", gw.Name)
go func(gw config.Gateway) {
err := gateway.New(cfg, &gw)
if err != nil {
log.Debugf("starting gateway failed %#v", err)
}
}(gw)
}
log.Printf("Gateway(s) started succesfully. Now relaying messages")
select {}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,3 @@
#WARNING: as this file contains credentials, be sure to set correct file permissions
[irc]
[irc.freenode]
Server="irc.freenode.net:6667"
@ -6,8 +5,8 @@
[mattermost]
[mattermost.work]
#do not prefix it wit http:// or https://
Server="yourmattermostserver.domain"
useAPI=true
Server="yourmattermostserver.domain"
Team="yourteam"
Login="yourlogin"
Password="yourpass"
@ -16,19 +15,18 @@
[[gateway]]
name="gateway1"
enable=true
[[gateway.inout]]
[[gateway.in]]
account="irc.freenode"
channel="#testing"
[[gateway.inout]]
[[gateway.in]]
account="mattermost.work"
channel="off-topic"
[[gateway.out]]
account="irc.freenode"
channel="#testing"
[[gateway.out]]
account="mattermost.work"
channel="off-topic"
#simpler config possible since v0.10.2
#[[gateway]]
#name="gateway2"
#enable=true
#inout = [
# { account="irc.freenode", channel="#testing", options={key="channelkey"}},
# { account="mattermost.work", channel="off-topic" },
#]

View File

@ -1,11 +1,9 @@
package matterclient
import (
"crypto/md5"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/cookiejar"
"net/url"
@ -16,7 +14,6 @@ import (
log "github.com/Sirupsen/logrus"
"github.com/gorilla/websocket"
"github.com/hashicorp/golang-lru"
"github.com/jpillora/backoff"
"github.com/mattermost/platform/model"
)
@ -37,37 +34,32 @@ type Message struct {
Channel string
Username string
Text string
Type string
UserID string
}
type Team struct {
Team *model.Team
Id string
Channels []*model.Channel
MoreChannels []*model.Channel
Channels *model.ChannelList
MoreChannels *model.ChannelList
Users map[string]*model.User
}
type MMClient struct {
sync.RWMutex
*Credentials
Team *Team
OtherTeams []*Team
Client *model.Client4
User *model.User
Users map[string]*model.User
MessageChan chan *Message
log *log.Entry
WsClient *websocket.Conn
WsQuit bool
WsAway bool
WsConnected bool
WsSequence int64
WsPingChan chan *model.WebSocketResponse
ServerVersion string
OnWsConnect func()
lruCache *lru.Cache
Team *Team
OtherTeams []*Team
Client *model.Client
User *model.User
Users map[string]*model.User
MessageChan chan *Message
log *log.Entry
WsClient *websocket.Conn
WsQuit bool
WsAway bool
WsConnected bool
WsSequence int64
WsPingChan chan *model.WebSocketResponse
}
func New(login, pass, team, server string) *MMClient {
@ -75,7 +67,6 @@ func New(login, pass, team, server string) *MMClient {
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})
mmclient.lruCache, _ = lru.New(500)
return mmclient
}
@ -89,11 +80,6 @@ func (m *MMClient) SetLogLevel(level string) {
}
func (m *MMClient) Login() error {
// check if this is a first connect or a reconnection
firstConnection := true
if m.WsConnected {
firstConnection = false
}
m.WsConnected = false
if m.WsQuit {
return nil
@ -104,66 +90,46 @@ func (m *MMClient) Login() error {
Jitter: true,
}
uriScheme := "https://"
wsScheme := "wss://"
if m.NoTLS {
uriScheme = "http://"
wsScheme = "ws://"
}
// login to mattermost
m.Client = model.NewAPIv4Client(uriScheme + m.Credentials.Server)
m.Client.HttpClient.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, Proxy: http.ProxyFromEnvironment}
m.Client.HttpClient.Timeout = time.Second * 10
for {
d := b.Duration()
// bogus call to get the serverversion
_, resp := m.Client.Logout()
if resp.Error != nil {
return fmt.Errorf("%#v", resp.Error.Error())
}
if firstConnection && !supportedVersion(resp.ServerVersion) {
return fmt.Errorf("unsupported mattermost version: %s", resp.ServerVersion)
}
m.ServerVersion = resp.ServerVersion
if m.ServerVersion == "" {
m.log.Debugf("Server not up yet, reconnecting in %s", d)
time.Sleep(d)
} else {
m.log.Infof("Found version %s", m.ServerVersion)
break
}
}
b.Reset()
var resp *model.Response
//var myinfo *model.Result
m.Client = model.NewClient(uriScheme + m.Credentials.Server)
m.Client.HttpClient.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}}
var myinfo *model.Result
var appErr *model.AppError
var logmsg = "trying login"
for {
m.log.Debugf("%s %s %s %s", logmsg, m.Credentials.Team, m.Credentials.Login, m.Credentials.Server)
if strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN) {
m.log.Debugf(logmsg + " with token")
m.log.Debugf(logmsg+" with %s", model.SESSION_COOKIE_TOKEN)
token := strings.Split(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN+"=")
if len(token) != 2 {
return errors.New("incorrect MMAUTHTOKEN. valid input is MMAUTHTOKEN=yourtoken")
}
m.Client.HttpClient.Jar = m.createCookieJar(token[1])
m.Client.AuthToken = token[1]
m.Client.AuthType = model.HEADER_BEARER
m.User, resp = m.Client.GetMe("")
if resp.Error != nil {
return resp.Error
m.Client.MockSession(token[1])
myinfo, appErr = m.Client.GetMe("")
if appErr != nil {
return errors.New(appErr.DetailedError)
}
if m.User == nil {
if myinfo.Data.(*model.User) == nil {
m.log.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass)
return errors.New("invalid " + model.SESSION_COOKIE_TOKEN)
}
} else {
m.User, resp = m.Client.Login(m.Credentials.Login, m.Credentials.Pass)
myinfo, appErr = m.Client.Login(m.Credentials.Login, m.Credentials.Pass)
}
appErr = resp.Error
if appErr != nil {
d := b.Duration()
m.log.Debug(appErr.DetailedError)
if firstConnection {
//TODO more generic fix needed
if !strings.Contains(appErr.DetailedError, "connection refused") &&
!strings.Contains(appErr.DetailedError, "invalid character") &&
!strings.Contains(appErr.DetailedError, "connection reset by peer") &&
!strings.Contains(appErr.DetailedError, "connection timed out") {
if appErr.Message == "" {
return errors.New(appErr.DetailedError)
}
@ -187,34 +153,17 @@ func (m *MMClient) Login() error {
if m.Team == nil {
return errors.New("team not found")
}
m.wsConnect()
return nil
}
func (m *MMClient) wsConnect() {
b := &backoff.Backoff{
Min: time.Second,
Max: 5 * time.Minute,
Jitter: true,
}
m.WsConnected = false
wsScheme := "wss://"
if m.NoTLS {
wsScheme = "ws://"
}
// set our team id as default route
m.Client.SetTeamId(m.Team.Id)
// setup websocket connection
wsurl := wsScheme + m.Credentials.Server + model.API_URL_SUFFIX_V4 + "/websocket"
wsurl := wsScheme + m.Credentials.Server + model.API_URL_SUFFIX + "/users/websocket"
header := http.Header{}
header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken)
m.log.Debugf("WsClient: making connection: %s", wsurl)
m.log.Debug("WsClient: making connection")
for {
wsDialer := &websocket.Dialer{Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}}
var err error
m.WsClient, _, err = wsDialer.Dial(wsurl, header)
if err != nil {
d := b.Duration()
@ -224,12 +173,14 @@ func (m *MMClient) wsConnect() {
}
break
}
b.Reset()
m.log.Debug("WsClient: connected")
m.WsSequence = 1
m.WsPingChan = make(chan *model.WebSocketResponse)
// only start to parse WS messages when login is completely done
m.WsConnected = true
return nil
}
func (m *MMClient) Logout() error {
@ -237,13 +188,9 @@ func (m *MMClient) Logout() error {
m.WsQuit = true
m.WsClient.Close()
m.WsClient.UnderlyingConn().Close()
if strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN) {
m.log.Debug("Not invalidating session in logout, credential is a token")
return nil
}
_, resp := m.Client.Logout()
if resp.Error != nil {
return resp.Error
_, err := m.Client.Logout()
if err != nil {
return err
}
return nil
}
@ -266,31 +213,21 @@ func (m *MMClient) WsReceiver() {
if _, rawMsg, err = m.WsClient.ReadMessage(); err != nil {
m.log.Error("error:", err)
// reconnect
m.wsConnect()
m.Login()
}
var event model.WebSocketEvent
if err := json.Unmarshal(rawMsg, &event); err == nil && event.IsValid() {
m.log.Debugf("WsReceiver event: %#v", event)
m.log.Debugf("WsReceiver: %#v", event)
msg := &Message{Raw: &event, Team: m.Credentials.Team}
m.parseMessage(msg)
// check if we didn't empty the message
if msg.Text != "" {
m.MessageChan <- msg
continue
}
// if we have file attached but the message is empty, also send it
if msg.Post != nil {
if msg.Text != "" || len(msg.Post.FileIds) > 0 || msg.Post.Type == "slack_attachment" {
m.MessageChan <- msg
}
}
m.MessageChan <- msg
continue
}
var response model.WebSocketResponse
if err := json.Unmarshal(rawMsg, &response); err == nil && response.IsValid() {
m.log.Debugf("WsReceiver response: %#v", response)
m.log.Debugf("WsReceiver: %#v", response)
m.parseResponse(response)
continue
}
@ -299,7 +236,7 @@ func (m *MMClient) WsReceiver() {
func (m *MMClient) parseMessage(rmsg *Message) {
switch rmsg.Raw.Event {
case model.WEBSOCKET_EVENT_POSTED, model.WEBSOCKET_EVENT_POST_EDITED, model.WEBSOCKET_EVENT_POST_DELETED:
case model.WEBSOCKET_EVENT_POSTED:
m.parseActionPost(rmsg)
/*
case model.ACTION_USER_REMOVED:
@ -320,70 +257,46 @@ func (m *MMClient) parseResponse(rmsg model.WebSocketResponse) {
}
func (m *MMClient) parseActionPost(rmsg *Message) {
// add post to cache, if it already exists don't relay this again.
// this should fix reposts
if ok, _ := m.lruCache.ContainsOrAdd(digestString(rmsg.Raw.Data["post"].(string)), true); ok {
m.log.Debugf("message %#v in cache, not processing again", rmsg.Raw.Data["post"].(string))
rmsg.Text = ""
return
}
data := model.PostFromJson(strings.NewReader(rmsg.Raw.Data["post"].(string)))
// we don't have the user, refresh the userlist
if m.GetUser(data.UserId) == nil {
m.log.Infof("User %s is not known, ignoring message %s", data)
return
m.UpdateUsers()
}
rmsg.Username = m.GetUserName(data.UserId)
rmsg.Username = m.GetUser(data.UserId).Username
rmsg.Channel = m.GetChannelName(data.ChannelId)
rmsg.UserID = data.UserId
rmsg.Type = data.Type
teamid, _ := rmsg.Raw.Data["team_id"].(string)
// edit messsages have no team_id for some reason
if teamid == "" {
// we can find the team_id from the channelid
teamid = m.GetChannelTeamId(data.ChannelId)
rmsg.Raw.Data["team_id"] = teamid
}
if teamid != "" {
rmsg.Team = m.GetTeamName(teamid)
}
rmsg.Team = m.GetTeamName(rmsg.Raw.TeamId)
// direct message
if rmsg.Raw.Data["channel_type"] == "D" {
rmsg.Channel = m.GetUser(data.UserId).Username
}
rmsg.Text = data.Message
rmsg.Post = data
return
}
func (m *MMClient) UpdateUsers() error {
mmusers, resp := m.Client.GetUsers(0, 50000, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
mmusers, err := m.Client.GetProfilesForDirectMessageList(m.Team.Id)
if err != nil {
return errors.New(err.DetailedError)
}
m.Lock()
for _, user := range mmusers {
m.Users[user.Id] = user
}
m.Users = mmusers.Data.(map[string]*model.User)
m.Unlock()
return nil
}
func (m *MMClient) UpdateChannels() error {
mmchannels, resp := m.Client.GetChannelsForTeamForUser(m.Team.Id, m.User.Id, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
mmchannels, err := m.Client.GetChannels("")
if err != nil {
return errors.New(err.DetailedError)
}
mmchannels2, err := m.Client.GetMoreChannels("")
if err != nil {
return errors.New(err.DetailedError)
}
m.Lock()
m.Team.Channels = mmchannels
m.Unlock()
mmchannels, resp = m.Client.GetPublicChannelsForTeam(m.Team.Id, 0, 5000, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
m.Lock()
m.Team.MoreChannels = mmchannels
m.Team.Channels = mmchannels.Data.(*model.ChannelList)
m.Team.MoreChannels = mmchannels2.Data.(*model.ChannelList)
m.Unlock()
return nil
}
@ -392,21 +305,9 @@ func (m *MMClient) GetChannelName(channelId string) string {
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
if t == nil {
continue
}
if t.Channels != nil {
for _, channel := range t.Channels {
if channel.Id == channelId {
return channel.Name
}
}
}
if t.MoreChannels != nil {
for _, channel := range t.MoreChannels {
if channel.Id == channelId {
return channel.Name
}
for _, channel := range append(t.Channels.Channels, t.MoreChannels.Channels...) {
if channel.Id == channelId {
return channel.Name
}
}
}
@ -421,7 +322,7 @@ func (m *MMClient) GetChannelId(name string, teamId string) string {
}
for _, t := range m.OtherTeams {
if t.Id == teamId {
for _, channel := range append(t.Channels, t.MoreChannels...) {
for _, channel := range append(t.Channels.Channels, t.MoreChannels.Channels...) {
if channel.Name == name {
return channel.Id
}
@ -431,24 +332,11 @@ func (m *MMClient) GetChannelId(name string, teamId string) string {
return ""
}
func (m *MMClient) GetChannelTeamId(id string) string {
m.RLock()
defer m.RUnlock()
for _, t := range append(m.OtherTeams, m.Team) {
for _, channel := range append(t.Channels, t.MoreChannels...) {
if channel.Id == id {
return channel.TeamId
}
}
}
return ""
}
func (m *MMClient) GetChannelHeader(channelId string) string {
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
for _, channel := range append(t.Channels, t.MoreChannels...) {
for _, channel := range append(t.Channels.Channels, t.MoreChannels.Channels...) {
if channel.Id == channelId {
return channel.Header
}
@ -458,159 +346,101 @@ func (m *MMClient) GetChannelHeader(channelId string) string {
return ""
}
func (m *MMClient) PostMessage(channelId string, text string) (string, error) {
func (m *MMClient) PostMessage(channelId string, text string) {
post := &model.Post{ChannelId: channelId, Message: text}
res, resp := m.Client.CreatePost(post)
if resp.Error != nil {
return "", resp.Error
}
return res.Id, nil
}
func (m *MMClient) PostMessageWithFiles(channelId string, text string, fileIds []string) (string, error) {
post := &model.Post{ChannelId: channelId, Message: text, FileIds: fileIds}
res, resp := m.Client.CreatePost(post)
if resp.Error != nil {
return "", resp.Error
}
return res.Id, nil
}
func (m *MMClient) EditMessage(postId string, text string) (string, error) {
post := &model.Post{Message: text}
res, resp := m.Client.UpdatePost(postId, post)
if resp.Error != nil {
return "", resp.Error
}
return res.Id, nil
}
func (m *MMClient) DeleteMessage(postId string) error {
_, resp := m.Client.DeletePost(postId)
if resp.Error != nil {
return resp.Error
}
return nil
m.Client.CreatePost(post)
}
func (m *MMClient) JoinChannel(channelId string) error {
m.RLock()
defer m.RUnlock()
for _, c := range m.Team.Channels {
for _, c := range m.Team.Channels.Channels {
if c.Id == channelId {
m.log.Debug("Not joining ", channelId, " already joined.")
return nil
}
}
m.log.Debug("Joining ", channelId)
_, resp := m.Client.AddChannelMember(channelId, m.User.Id)
if resp.Error != nil {
return resp.Error
_, err := m.Client.JoinChannel(channelId)
if err != nil {
return errors.New("failed to join")
}
return nil
}
func (m *MMClient) GetPostsSince(channelId string, time int64) *model.PostList {
res, resp := m.Client.GetPostsSince(channelId, time)
if resp.Error != nil {
res, err := m.Client.GetPostsSince(channelId, time)
if err != nil {
return nil
}
return res
return res.Data.(*model.PostList)
}
func (m *MMClient) SearchPosts(query string) *model.PostList {
res, resp := m.Client.SearchPosts(m.Team.Id, query, false)
if resp.Error != nil {
res, err := m.Client.SearchPosts(query, false)
if err != nil {
return nil
}
return res
return res.Data.(*model.PostList)
}
func (m *MMClient) GetPosts(channelId string, limit int) *model.PostList {
res, resp := m.Client.GetPostsForChannel(channelId, 0, limit, "")
if resp.Error != nil {
res, err := m.Client.GetPosts(channelId, 0, limit, "")
if err != nil {
return nil
}
return res
return res.Data.(*model.PostList)
}
func (m *MMClient) GetPublicLink(filename string) string {
res, resp := m.Client.GetFileLink(filename)
if resp.Error != nil {
res, err := m.Client.GetPublicLink(filename)
if err != nil {
return ""
}
return res
return res.Data.(string)
}
func (m *MMClient) GetPublicLinks(filenames []string) []string {
var output []string
for _, f := range filenames {
res, resp := m.Client.GetFileLink(f)
if resp.Error != nil {
res, err := m.Client.GetPublicLink(f)
if err != nil {
continue
}
output = append(output, res)
}
return output
}
func (m *MMClient) GetFileLinks(filenames []string) []string {
uriScheme := "https://"
if m.NoTLS {
uriScheme = "http://"
}
var output []string
for _, f := range filenames {
res, resp := m.Client.GetFileLink(f)
if resp.Error != nil {
// public links is probably disabled, create the link ourselves
output = append(output, uriScheme+m.Credentials.Server+model.API_URL_SUFFIX_V3+"/files/"+f+"/get")
continue
}
output = append(output, res)
output = append(output, res.Data.(string))
}
return output
}
func (m *MMClient) UpdateChannelHeader(channelId string, header string) {
channel := &model.Channel{Id: channelId, Header: header}
data := make(map[string]string)
data["channel_id"] = channelId
data["channel_header"] = header
m.log.Debugf("updating channelheader %#v, %#v", channelId, header)
_, resp := m.Client.UpdateChannel(channel)
if resp.Error != nil {
log.Error(resp.Error)
_, err := m.Client.UpdateChannelHeader(data)
if err != nil {
log.Error(err)
}
}
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)
_, err := m.Client.UpdateLastViewedAt(channelId, true)
if err != nil {
m.log.Error(err)
}
}
func (m *MMClient) UpdateUserNick(nick string) error {
user := m.User
user.Nickname = nick
_, resp := m.Client.UpdateUser(user)
if resp.Error != nil {
return resp.Error
}
return nil
}
func (m *MMClient) UsernamesInChannel(channelId string) []string {
res, resp := m.Client.GetChannelMembers(channelId, 0, 50000, "")
if resp.Error != nil {
m.log.Errorf("UsernamesInChannel(%s) failed: %s", channelId, resp.Error)
ceiRes, err := m.Client.GetChannelExtraInfo(channelId, 5000, "")
if err != nil {
m.log.Errorf("UsernamesInChannel(%s) failed: %s", channelId, err)
return []string{}
}
allusers := m.GetUsers()
extra := ceiRes.Data.(*model.ChannelExtra)
result := []string{}
for _, member := range *res {
result = append(result, allusers[member.UserId].Nickname)
for _, member := range extra.Members {
result = append(result, member.Username)
}
return result
}
@ -634,15 +464,17 @@ func (m *MMClient) createCookieJar(token string) *cookiejar.Jar {
func (m *MMClient) SendDirectMessage(toUserId string, msg string) {
m.log.Debugf("SendDirectMessage to %s, msg %s", toUserId, msg)
// create DM channel (only happens on first message)
_, resp := m.Client.CreateDirectChannel(m.User.Id, toUserId)
if resp.Error != nil {
m.log.Debugf("SendDirectMessage to %#v failed: %s", toUserId, resp.Error)
return
_, err := m.Client.CreateDirectChannel(toUserId)
if err != nil {
m.log.Debugf("SendDirectMessage to %#v failed: %s", toUserId, err)
}
channelName := model.GetDMNameFromIds(toUserId, m.User.Id)
// update our channels
m.UpdateChannels()
mmchannels, _ := m.Client.GetChannels("")
m.Lock()
m.Team.Channels = mmchannels.Data.(*model.ChannelList)
m.Unlock()
// build & send the message
msg = strings.Replace(msg, "\r", "", -1)
@ -668,10 +500,10 @@ func (m *MMClient) GetChannels() []*model.Channel {
defer m.RUnlock()
var channels []*model.Channel
// our primary team channels first
channels = append(channels, m.Team.Channels...)
channels = append(channels, m.Team.Channels.Channels...)
for _, t := range m.OtherTeams {
if t.Id != m.Team.Id {
channels = append(channels, t.Channels...)
channels = append(channels, t.Channels.Channels...)
}
}
return channels
@ -683,7 +515,7 @@ func (m *MMClient) GetMoreChannels() []*model.Channel {
defer m.RUnlock()
var channels []*model.Channel
for _, t := range m.OtherTeams {
channels = append(channels, t.MoreChannels...)
channels = append(channels, t.MoreChannels.Channels...)
}
return channels
}
@ -694,10 +526,8 @@ func (m *MMClient) GetTeamFromChannel(channelId string) string {
defer m.RUnlock()
var channels []*model.Channel
for _, t := range m.OtherTeams {
channels = append(channels, t.Channels...)
if t.MoreChannels != nil {
channels = append(channels, t.MoreChannels...)
}
channels = append(channels, t.Channels.Channels...)
channels = append(channels, t.MoreChannels.Channels...)
for _, c := range channels {
if c.Id == channelId {
return t.Id
@ -710,11 +540,12 @@ func (m *MMClient) GetTeamFromChannel(channelId string) string {
func (m *MMClient) GetLastViewedAt(channelId string) int64 {
m.RLock()
defer m.RUnlock()
res, resp := m.Client.GetChannelMember(channelId, m.User.Id, "")
if resp.Error != nil {
return model.GetMillis()
for _, t := range m.OtherTeams {
if _, ok := t.Channels.Members[channelId]; ok {
return t.Channels.Members[channelId].LastViewedAt
}
}
return res.LastViewedAt
return 0
}
func (m *MMClient) GetUsers() map[string]*model.User {
@ -728,82 +559,31 @@ func (m *MMClient) GetUsers() map[string]*model.User {
}
func (m *MMClient) GetUser(userId string) *model.User {
m.Lock()
defer m.Unlock()
_, ok := m.Users[userId]
if !ok {
res, resp := m.Client.GetUser(userId, "")
if resp.Error != nil {
return nil
}
m.Users[userId] = res
}
m.RLock()
defer m.RUnlock()
return m.Users[userId]
}
func (m *MMClient) GetUserName(userId string) string {
user := m.GetUser(userId)
if user != nil {
return user.Username
}
return ""
}
func (m *MMClient) GetStatus(userId string) string {
res, resp := m.Client.GetUserStatus(userId, "")
if resp.Error != nil {
res, err := m.Client.GetStatuses()
if err != nil {
return ""
}
if res.Status == model.STATUS_AWAY {
status := res.Data.(map[string]string)
if status[userId] == model.STATUS_AWAY {
return "away"
}
if res.Status == model.STATUS_ONLINE {
if status[userId] == model.STATUS_ONLINE {
return "online"
}
return "offline"
}
func (m *MMClient) GetStatuses() map[string]string {
var ids []string
statuses := make(map[string]string)
for id := range m.Users {
ids = append(ids, id)
}
res, resp := m.Client.GetUsersStatusesByIds(ids)
if resp.Error != nil {
return statuses
}
for _, status := range res {
statuses[status.UserId] = "offline"
if status.Status == model.STATUS_AWAY {
statuses[status.UserId] = "away"
}
if status.Status == model.STATUS_ONLINE {
statuses[status.UserId] = "online"
}
}
return statuses
}
func (m *MMClient) GetTeamId() string {
return m.Team.Id
}
func (m *MMClient) UploadFile(data []byte, channelId string, filename string) (string, error) {
f, resp := m.Client.UploadFile(data, channelId, filename)
if resp.Error != nil {
return "", resp.Error
}
return f.FileInfos[0].Id, nil
}
func (m *MMClient) StatusLoop() {
retries := 0
backoff := time.Second * 60
if m.OnWsConnect != nil {
m.OnWsConnect()
}
m.log.Debug("StatusLoop:", m.OnWsConnect)
for {
if m.WsQuit {
return
@ -814,28 +594,13 @@ func (m *MMClient) StatusLoop() {
select {
case <-m.WsPingChan:
m.log.Debug("WS PONG received")
backoff = time.Second * 60
case <-time.After(time.Second * 5):
if retries > 3 {
m.log.Debug("StatusLoop() timeout")
m.Logout()
m.WsQuit = false
err := m.Login()
if err != nil {
log.Errorf("Login failed: %#v", err)
break
}
if m.OnWsConnect != nil {
m.OnWsConnect()
}
go m.WsReceiver()
} else {
retries++
backoff = time.Second * 5
}
m.Logout()
m.WsQuit = false
m.Login()
}
}
time.Sleep(backoff)
time.Sleep(time.Second * 60)
}
}
@ -843,39 +608,27 @@ func (m *MMClient) StatusLoop() {
func (m *MMClient) initUser() error {
m.Lock()
defer m.Unlock()
initLoad, err := m.Client.GetInitialLoad()
if err != nil {
return err
}
initData := initLoad.Data.(*model.InitialLoad)
m.User = initData.User
// we only load all team data on initial login.
// all other updates are for channels from our (primary) team only.
//m.log.Debug("initUser(): loading all team data")
teams, resp := m.Client.GetTeamsForUser(m.User.Id, "")
if resp.Error != nil {
return resp.Error
}
for _, team := range teams {
mmusers, resp := m.Client.GetUsersInTeam(team.Id, 0, 50000, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
usermap := make(map[string]*model.User)
for _, user := range mmusers {
usermap[user.Id] = user
}
t := &Team{Team: team, Users: usermap, Id: team.Id}
mmchannels, resp := m.Client.GetChannelsForTeamForUser(team.Id, m.User.Id, "")
if resp.Error != nil {
return resp.Error
}
t.Channels = mmchannels
mmchannels, resp = m.Client.GetPublicChannelsForTeam(team.Id, 0, 5000, "")
if resp.Error != nil {
return resp.Error
}
t.MoreChannels = mmchannels
for _, v := range initData.Teams {
m.Client.SetTeamId(v.Id)
mmusers, _ := m.Client.GetProfiles(v.Id, "")
t := &Team{Team: v, Users: mmusers.Data.(map[string]*model.User), Id: v.Id}
mmchannels, _ := m.Client.GetChannels("")
t.Channels = mmchannels.Data.(*model.ChannelList)
mmchannels, _ = m.Client.GetMoreChannels("")
t.MoreChannels = mmchannels.Data.(*model.ChannelList)
m.OtherTeams = append(m.OtherTeams, t)
if team.Name == m.Credentials.Team {
if v.Name == m.Credentials.Team {
m.Team = t
m.log.Debugf("initUser(): found our team %s (id: %s)", team.Name, team.Id)
m.log.Debugf("initUser(): found our team %s (id: %s)", v.Name, v.Id)
}
// add all users
for k, v := range t.Users {
@ -895,17 +648,3 @@ func (m *MMClient) sendWSRequest(action string, data map[string]interface{}) err
m.WsClient.WriteJSON(req)
return nil
}
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.") {
return true
}
return false
}
func digestString(s string) string {
return fmt.Sprintf("%x", md5.Sum([]byte(s)))
}

View File

@ -12,19 +12,17 @@ import (
"log"
"net"
"net/http"
"time"
)
// OMessage for mattermost incoming webhook. (send to mattermost)
type OMessage struct {
Channel string `json:"channel,omitempty"`
IconURL string `json:"icon_url,omitempty"`
IconEmoji string `json:"icon_emoji,omitempty"`
UserName string `json:"username,omitempty"`
Text string `json:"text"`
Attachments interface{} `json:"attachments,omitempty"`
Type string `json:"type,omitempty"`
Props map[string]interface{} `json:"props"`
Channel string `json:"channel,omitempty"`
IconURL string `json:"icon_url,omitempty"`
IconEmoji string `json:"icon_emoji,omitempty"`
UserName string `json:"username,omitempty"`
Text string `json:"text"`
Attachments interface{} `json:"attachments,omitempty"`
Type string `json:"type,omitempty"`
}
// IMessage for mattermost outgoing webhook. (received from mattermost)
@ -44,7 +42,6 @@ type IMessage struct {
ServiceId string `schema:"service_id"`
Text string `schema:"text"`
TriggerWord string `schema:"trigger_word"`
FileIDs string `schema:"file_ids"`
}
// Client for Mattermost.
@ -85,14 +82,8 @@ func New(url string, config Config) *Client {
func (c *Client) StartServer() {
mux := http.NewServeMux()
mux.Handle("/", c)
srv := &http.Server{
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
Handler: mux,
Addr: c.BindAddress,
}
log.Printf("Listening on http://%v...\n", c.BindAddress)
if err := srv.ListenAndServe(); err != nil {
if err := http.ListenAndServe(c.BindAddress, mux); err != nil {
log.Fatal(err)
}
}
@ -136,11 +127,12 @@ func (c *Client) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Receive returns an incoming message from mattermost outgoing webhooks URL.
func (c *Client) Receive() IMessage {
var msg IMessage
for msg := range c.In {
return msg
for {
select {
case msg := <-c.In:
return msg
}
}
return msg
}
// Send sends a msg to mattermost incoming webhooks URL.

50
migration.md Normal file
View File

@ -0,0 +1,50 @@
# Breaking changes from 0.4 to 0.5 for matterbridge (webhooks version)
## IRC section
### Server
Port removed, added to server
```
server="irc.freenode.net"
port=6667
```
changed to
```
server="irc.freenode.net:6667"
```
### Channel
Removed see Channels section below
### UseSlackCircumfix=true
Removed, can be done by using ```RemoteNickFormat="<{NICK}> "```
## Mattermost section
### BindAddress
Port removed, added to BindAddress
```
BindAddress="0.0.0.0"
port=9999
```
changed to
```
BindAddress="0.0.0.0:9999"
```
### Token
Removed
## Channels section
```
[Token "outgoingwebhooktoken1"]
IRCChannel="#off-topic"
MMChannel="off-topic"
```
changed to
```
[Channel "channelnameofchoice"]
IRC="#off-topic"
Mattermost="off-topic"
```

View File

@ -127,6 +127,7 @@ func (gitter *Gitter) GetRooms() ([]Room, error) {
// GetUsersInRoom returns the users in the room with the passed id
func (gitter *Gitter) GetUsersInRoom(roomID string) ([]User, error) {
var users []User
response, err := gitter.get(gitter.config.apiBaseURL + "rooms/" + roomID + "/users")
if err != nil {
@ -205,43 +206,17 @@ func (gitter *Gitter) GetMessage(roomID, messageID string) (*Message, error) {
}
// SendMessage sends a message to a room
func (gitter *Gitter) SendMessage(roomID, text string) (*Message, error) {
func (gitter *Gitter) SendMessage(roomID, text string) error {
message := Message{Text: text}
body, _ := json.Marshal(message)
response, err := gitter.post(gitter.config.apiBaseURL+"rooms/"+roomID+"/chatMessages", body)
_, err := gitter.post(gitter.config.apiBaseURL+"rooms/"+roomID+"/chatMessages", body)
if err != nil {
gitter.log(err)
return nil, err
return err
}
err = json.Unmarshal(response, &message)
if err != nil {
gitter.log(err)
return nil, err
}
return &message, nil
}
// UpdateMessage updates a message in a room
func (gitter *Gitter) UpdateMessage(roomID, msgID, text string) (*Message, error) {
message := Message{Text: text}
body, _ := json.Marshal(message)
response, err := gitter.put(gitter.config.apiBaseURL+"rooms/"+roomID+"/chatMessages/"+msgID, body)
if err != nil {
gitter.log(err)
return nil, err
}
err = json.Unmarshal(response, &message)
if err != nil {
gitter.log(err)
return nil, err
}
return &message, nil
return nil
}
// JoinRoom joins a room
@ -284,45 +259,6 @@ func (gitter *Gitter) SetDebug(debug bool, logWriter io.Writer) {
gitter.logWriter = logWriter
}
// SearchRooms queries the Rooms resources of gitter API
func (gitter *Gitter) SearchRooms(room string) ([]Room, error) {
var rooms struct {
Results []Room `json:"results"`
}
response, err := gitter.get(gitter.config.apiBaseURL + "rooms?q=" + room)
if err != nil {
gitter.log(err)
return nil, err
}
err = json.Unmarshal(response, &rooms)
if err != nil {
gitter.log(err)
return nil, err
}
return rooms.Results, nil
}
// GetRoomId returns the room ID of a given URI
func (gitter *Gitter) GetRoomId(uri string) (string, error) {
rooms, err := gitter.SearchRooms(uri)
if err != nil {
gitter.log(err)
return "", err
}
for _, element := range rooms {
if element.URI == uri {
return element.ID, nil
}
}
return "", APIError{What: "Room not found."}
}
// Pagination params
type Pagination struct {
@ -440,39 +376,6 @@ func (gitter *Gitter) post(url string, body []byte) ([]byte, error) {
return result, nil
}
func (gitter *Gitter) put(url string, body []byte) ([]byte, error) {
r, err := http.NewRequest("PUT", url, bytes.NewBuffer(body))
if err != nil {
gitter.log(err)
return nil, err
}
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Accept", "application/json")
r.Header.Set("Authorization", "Bearer "+gitter.config.token)
resp, err := gitter.config.client.Do(r)
if err != nil {
gitter.log(err)
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err = APIError{What: fmt.Sprintf("Status code: %v", resp.StatusCode)}
gitter.log(err)
return nil, err
}
result, err := ioutil.ReadAll(resp.Body)
if err != nil {
gitter.log(err)
return nil, err
}
return result, nil
}
func (gitter *Gitter) delete(url string) ([]byte, error) {
r, err := http.NewRequest("delete", url, nil)
if err != nil {

View File

@ -47,13 +47,13 @@ Loop:
}
break Loop
}
resp := stream.getResponse()
if resp.StatusCode != 200 {
gitter.log(fmt.Sprintf("Unexpected response code %v", resp.StatusCode))
continue
}
//"The JSON stream returns messages as JSON objects that are delimited by carriage return (\r)" <- Not true crap it's (\n) only
reader = bufio.NewReader(resp.Body)
line, err := reader.ReadBytes('\n')
@ -112,7 +112,6 @@ type Stream struct {
func (stream *Stream) destroy() {
close(stream.Event)
stream.streamConnection.currentRetries = 0
}
type Event struct {
@ -136,11 +135,10 @@ func (stream *Stream) connect() {
}
res, err := stream.gitter.getResponse(stream.url, stream)
if err != nil || res.StatusCode != 200 {
stream.gitter.log("Failed to get response, trying reconnect")
if res != nil {
stream.gitter.log(fmt.Sprintf("Status code: %v", res.StatusCode))
}
if stream.streamConnection.canceled {
// do nothing
} else if err != nil || res.StatusCode != 200 {
stream.gitter.log("Failed to get response, trying reconnect ")
stream.gitter.log(err)
// sleep and wait
@ -163,6 +161,9 @@ type streamConnection struct {
// connection was closed
closed bool
// canceled
canceled bool
// wait time till next try
wait time.Duration
@ -191,10 +192,13 @@ func (stream *Stream) Close() {
stream.gitter.log("Stream connection close request")
switch transport := stream.gitter.config.client.Transport.(type) {
case *httpclient.Transport:
stream.streamConnection.canceled = true
transport.CancelRequest(conn.request)
default:
}
}
conn.currentRetries = 0
}
func (stream *Stream) isClosed() bool {

View File

@ -199,3 +199,4 @@
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.

View File

@ -0,0 +1,434 @@
package bridge
import (
"crypto/tls"
"github.com/42wim/matterbridge-plus/matterclient"
"github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus"
"github.com/peterhellberg/giphy"
ircm "github.com/sorcix/irc"
"github.com/thoj/go-ircevent"
"regexp"
"sort"
"strconv"
"strings"
"time"
)
//type Bridge struct {
type MMhook struct {
mh *matterhook.Client
}
type MMapi struct {
mc *matterclient.MMClient
mmMap map[string]string
mmIgnoreNicks []string
}
type MMirc struct {
i *irc.Connection
ircNick string
ircMap map[string]string
names map[string][]string
ircIgnoreNicks []string
}
type MMMessage struct {
Text string
Channel string
Username string
}
type Bridge struct {
MMhook
MMapi
MMirc
*Config
kind string
}
type FancyLog struct {
irc *log.Entry
mm *log.Entry
}
var flog FancyLog
const Legacy = "legacy"
func initFLog() {
flog.irc = log.WithFields(log.Fields{"module": "irc"})
flog.mm = log.WithFields(log.Fields{"module": "mattermost"})
}
func NewBridge(name string, config *Config, kind string) *Bridge {
initFLog()
b := &Bridge{}
b.Config = config
b.kind = kind
b.ircNick = b.Config.IRC.Nick
b.ircMap = make(map[string]string)
b.MMirc.names = make(map[string][]string)
b.ircIgnoreNicks = strings.Fields(b.Config.IRC.IgnoreNicks)
b.mmIgnoreNicks = strings.Fields(b.Config.Mattermost.IgnoreNicks)
if kind == Legacy {
if len(b.Config.Token) > 0 {
for _, val := range b.Config.Token {
b.ircMap[val.IRCChannel] = val.MMChannel
}
}
b.mh = matterhook.New(b.Config.Mattermost.URL,
matterhook.Config{Port: b.Config.Mattermost.Port, Token: b.Config.Mattermost.Token,
InsecureSkipVerify: b.Config.Mattermost.SkipTLSVerify,
BindAddress: b.Config.Mattermost.BindAddress})
} else {
b.mmMap = make(map[string]string)
if len(b.Config.Channel) > 0 {
for _, val := range b.Config.Channel {
b.ircMap[val.IRC] = val.Mattermost
b.mmMap[val.Mattermost] = val.IRC
}
}
b.mc = matterclient.New(b.Config.Mattermost.Login, b.Config.Mattermost.Password,
b.Config.Mattermost.Team, b.Config.Mattermost.Server)
b.mc.SkipTLSVerify = b.Config.Mattermost.SkipTLSVerify
b.mc.NoTLS = b.Config.Mattermost.NoTLS
flog.mm.Infof("Trying login %s (team: %s) on %s", b.Config.Mattermost.Login, b.Config.Mattermost.Team, b.Config.Mattermost.Server)
err := b.mc.Login()
if err != nil {
flog.mm.Fatal("Can not connect", err)
}
flog.mm.Info("Login ok")
b.mc.JoinChannel(b.Config.Mattermost.Channel)
if len(b.Config.Channel) > 0 {
for _, val := range b.Config.Channel {
b.mc.JoinChannel(val.Mattermost)
}
}
go b.mc.WsReceiver()
}
flog.irc.Info("Trying IRC connection")
b.i = b.createIRC(name)
flog.irc.Info("Connection succeeded")
go b.handleMatter()
return b
}
func (b *Bridge) createIRC(name string) *irc.Connection {
i := irc.IRC(b.Config.IRC.Nick, b.Config.IRC.Nick)
i.UseTLS = b.Config.IRC.UseTLS
i.TLSConfig = &tls.Config{InsecureSkipVerify: b.Config.IRC.SkipTLSVerify}
if b.Config.IRC.Password != "" {
i.Password = b.Config.IRC.Password
}
i.AddCallback(ircm.RPL_WELCOME, b.handleNewConnection)
i.Connect(b.Config.IRC.Server + ":" + strconv.Itoa(b.Config.IRC.Port))
return i
}
func (b *Bridge) handleNewConnection(event *irc.Event) {
flog.irc.Info("Registering callbacks")
i := b.i
b.ircNick = event.Arguments[0]
i.AddCallback("PRIVMSG", b.handlePrivMsg)
i.AddCallback("CTCP_ACTION", b.handlePrivMsg)
i.AddCallback(ircm.RPL_ENDOFNAMES, b.endNames)
i.AddCallback(ircm.RPL_NAMREPLY, b.storeNames)
i.AddCallback(ircm.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
i.AddCallback(ircm.NOTICE, b.handleNotice)
i.AddCallback(ircm.RPL_MYINFO, func(e *irc.Event) { flog.irc.Infof("%s: %s", e.Code, strings.Join(e.Arguments[1:], " ")) })
i.AddCallback("PING", func(e *irc.Event) {
i.SendRaw("PONG :" + e.Message())
flog.irc.Debugf("PING/PONG")
})
if b.Config.Mattermost.ShowJoinPart {
i.AddCallback("JOIN", b.handleJoinPart)
i.AddCallback("PART", b.handleJoinPart)
}
i.AddCallback("*", b.handleOther)
b.setupChannels()
}
func (b *Bridge) setupChannels() {
i := b.i
if b.Config.IRC.Channel != "" {
flog.irc.Infof("Joining %s as %s", b.Config.IRC.Channel, b.ircNick)
i.Join(b.Config.IRC.Channel)
}
if b.kind == Legacy {
for _, val := range b.Config.Token {
flog.irc.Infof("Joining %s as %s", val.IRCChannel, b.ircNick)
i.Join(val.IRCChannel)
}
} else {
for _, val := range b.Config.Channel {
flog.irc.Infof("Joining %s as %s", val.IRC, b.ircNick)
i.Join(val.IRC)
}
}
}
func (b *Bridge) handleIrcBotCommand(event *irc.Event) bool {
parts := strings.Fields(event.Message())
exp, _ := regexp.Compile("[:,]+$")
channel := event.Arguments[0]
command := ""
if len(parts) == 2 {
command = parts[1]
}
if exp.ReplaceAllString(parts[0], "") == b.ircNick {
switch command {
case "users":
usernames := b.mc.UsernamesInChannel(b.getMMChannel(channel))
sort.Strings(usernames)
b.i.Privmsg(channel, "Users on Mattermost: "+strings.Join(usernames, ", "))
default:
b.i.Privmsg(channel, "Valid commands are: [users, help]")
}
return true
}
return false
}
func (b *Bridge) ircNickFormat(nick string) string {
if nick == b.ircNick {
return nick
}
if b.Config.Mattermost.RemoteNickFormat == nil {
return "irc-" + nick
}
return strings.Replace(*b.Config.Mattermost.RemoteNickFormat, "{NICK}", nick, -1)
}
func (b *Bridge) handlePrivMsg(event *irc.Event) {
if b.ignoreMessage(event.Nick, event.Message(), "irc") {
return
}
if b.handleIrcBotCommand(event) {
return
}
msg := ""
if event.Code == "CTCP_ACTION" {
msg = event.Nick + " "
}
msg += event.Message()
b.Send(b.ircNickFormat(event.Nick), msg, b.getMMChannel(event.Arguments[0]))
}
func (b *Bridge) handleJoinPart(event *irc.Event) {
b.Send(b.ircNick, b.ircNickFormat(event.Nick)+" "+strings.ToLower(event.Code)+"s "+event.Message(), b.getMMChannel(event.Arguments[0]))
}
func (b *Bridge) handleNotice(event *irc.Event) {
if strings.Contains(event.Message(), "This nickname is registered") {
b.i.Privmsg(b.Config.IRC.NickServNick, "IDENTIFY "+b.Config.IRC.NickServPassword)
}
}
func (b *Bridge) nicksPerRow() int {
if b.Config.Mattermost.NicksPerRow < 1 {
return 4
}
return b.Config.Mattermost.NicksPerRow
}
func (b *Bridge) formatnicks(nicks []string, continued bool) string {
switch b.Config.Mattermost.NickFormatter {
case "table":
return tableformatter(nicks, b.nicksPerRow(), continued)
default:
return plainformatter(nicks, b.nicksPerRow())
}
}
func (b *Bridge) storeNames(event *irc.Event) {
channel := event.Arguments[2]
b.MMirc.names[channel] = append(
b.MMirc.names[channel],
strings.Split(strings.TrimSpace(event.Message()), " ")...)
}
func (b *Bridge) endNames(event *irc.Event) {
channel := event.Arguments[1]
sort.Strings(b.MMirc.names[channel])
maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow()
continued := false
for len(b.MMirc.names[channel]) > maxNamesPerPost {
b.Send(
b.ircNick,
b.formatnicks(b.MMirc.names[channel][0:maxNamesPerPost], continued),
b.getMMChannel(channel))
b.MMirc.names[channel] = b.MMirc.names[channel][maxNamesPerPost:]
continued = true
}
b.Send(b.ircNick, b.formatnicks(b.MMirc.names[channel], continued), b.getMMChannel(channel))
b.MMirc.names[channel] = nil
}
func (b *Bridge) handleTopicWhoTime(event *irc.Event) {
parts := strings.Split(event.Arguments[2], "!")
t, err := strconv.ParseInt(event.Arguments[3], 10, 64)
if err != nil {
flog.irc.Errorf("Invalid time stamp: %s", event.Arguments[3])
}
user := parts[0]
if len(parts) > 1 {
user += " [" + parts[1] + "]"
}
flog.irc.Infof("%s: Topic set by %s [%s]", event.Code, user, time.Unix(t, 0))
}
func (b *Bridge) handleOther(event *irc.Event) {
flog.irc.Debugf("%#v", event)
}
func (b *Bridge) Send(nick string, message string, channel string) error {
return b.SendType(nick, message, channel, "")
}
func (b *Bridge) SendType(nick string, message string, channel string, mtype string) error {
if b.Config.Mattermost.PrefixMessagesWithNick {
if IsMarkup(message) {
message = nick + "\n\n" + message
} else {
message = nick + " " + message
}
}
if b.kind == Legacy {
matterMessage := matterhook.OMessage{IconURL: b.Config.Mattermost.IconURL}
matterMessage.Channel = channel
matterMessage.UserName = nick
matterMessage.Type = mtype
matterMessage.Text = message
err := b.mh.Send(matterMessage)
if err != nil {
flog.mm.Info(err)
return err
}
return nil
}
flog.mm.Debug("->mattermost channel: ", channel, " ", message)
b.mc.PostMessage(channel, message)
return nil
}
func (b *Bridge) handleMatterHook(mchan chan *MMMessage) {
for {
message := b.mh.Receive()
m := &MMMessage{}
m.Username = message.UserName
m.Text = message.Text
m.Channel = message.Token
mchan <- m
}
}
func (b *Bridge) handleMatterClient(mchan chan *MMMessage) {
for message := range b.mc.MessageChan {
// do not post our own messages back to irc
if message.Raw.Action == "posted" && b.mc.User.Username != message.Username {
m := &MMMessage{}
m.Username = message.Username
m.Channel = message.Channel
m.Text = message.Text
flog.mm.Debugf("<-mattermost channel: %s %#v %#v", message.Channel, message.Post, message.Raw)
mchan <- m
}
}
}
func (b *Bridge) handleMatter() {
flog.mm.Infof("Choosing Mattermost connection type %s", b.kind)
mchan := make(chan *MMMessage)
if b.kind == Legacy {
go b.handleMatterHook(mchan)
} else {
go b.handleMatterClient(mchan)
}
flog.mm.Info("Start listening for Mattermost messages")
for message := range mchan {
var username string
if b.ignoreMessage(message.Username, message.Text, "mattermost") {
continue
}
username = message.Username + ": "
if b.Config.IRC.RemoteNickFormat != "" {
username = strings.Replace(b.Config.IRC.RemoteNickFormat, "{NICK}", message.Username, -1)
} else if b.Config.IRC.UseSlackCircumfix {
username = "<" + message.Username + "> "
}
cmds := strings.Fields(message.Text)
// empty message
if len(cmds) == 0 {
continue
}
cmd := cmds[0]
switch cmd {
case "!users":
flog.mm.Info("Received !users from ", message.Username)
b.i.SendRaw("NAMES " + b.getIRCChannel(message.Channel))
continue
case "!gif":
message.Text = b.giphyRandom(strings.Fields(strings.Replace(message.Text, "!gif ", "", 1)))
b.Send(b.ircNick, message.Text, b.getIRCChannel(message.Channel))
continue
}
texts := strings.Split(message.Text, "\n")
for _, text := range texts {
flog.mm.Debug("Sending message from " + message.Username + " to " + message.Channel)
b.i.Privmsg(b.getIRCChannel(message.Channel), username+text)
}
}
}
func (b *Bridge) giphyRandom(query []string) string {
g := giphy.DefaultClient
if b.Config.General.GiphyAPIKey != "" {
g.APIKey = b.Config.General.GiphyAPIKey
}
res, err := g.Random(query)
if err != nil {
return "error"
}
return res.Data.FixedHeightDownsampledURL
}
func (b *Bridge) getMMChannel(ircChannel string) string {
mmchannel, ok := b.ircMap[ircChannel]
if !ok {
mmchannel = b.Config.Mattermost.Channel
}
return mmchannel
}
func (b *Bridge) getIRCChannel(channel string) string {
if b.kind == Legacy {
ircchannel := b.Config.IRC.Channel
_, ok := b.Config.Token[channel]
if ok {
ircchannel = b.Config.Token[channel].IRCChannel
}
return ircchannel
}
ircchannel, ok := b.mmMap[channel]
if !ok {
ircchannel = b.Config.IRC.Channel
}
return ircchannel
}
func (b *Bridge) ignoreMessage(nick string, message string, protocol string) bool {
var ignoreNicks = b.mmIgnoreNicks
if protocol == "irc" {
ignoreNicks = b.ircIgnoreNicks
}
// should we discard messages ?
for _, entry := range ignoreNicks {
if nick == entry {
return true
}
}
return false
}

View File

@ -0,0 +1,68 @@
package bridge
import (
"gopkg.in/gcfg.v1"
"io/ioutil"
"log"
)
type Config struct {
IRC struct {
UseTLS bool
SkipTLSVerify bool
Server string
Port int
Nick string
Password string
Channel string
UseSlackCircumfix bool
NickServNick string
NickServPassword string
RemoteNickFormat string
IgnoreNicks string
}
Mattermost struct {
URL string
Port int
ShowJoinPart bool
Token string
IconURL string
SkipTLSVerify bool
BindAddress string
Channel string
PrefixMessagesWithNick bool
NicksPerRow int
NickFormatter string
Server string
Team string
Login string
Password string
RemoteNickFormat *string
IgnoreNicks string
NoTLS bool
}
Token map[string]*struct {
IRCChannel string
MMChannel string
}
Channel map[string]*struct {
IRC string
Mattermost string
}
General struct {
GiphyAPIKey string
}
}
func NewConfig(cfgfile string) *Config {
var cfg Config
content, err := ioutil.ReadFile(cfgfile)
if err != nil {
log.Fatal(err)
}
err = gcfg.ReadStringInto(&cfg, string(content))
if err != nil {
log.Fatal("Failed to parse "+cfgfile+":", err)
}
return &cfg
}

View File

@ -0,0 +1,59 @@
package bridge
import (
"strings"
)
func tableformatter(nicks []string, nicksPerRow int, continued bool) string {
result := "|IRC users"
if continued {
result = "|(continued)"
}
for i := 0; i < 2; i++ {
for j := 1; j <= nicksPerRow && j <= len(nicks); j++ {
if i == 0 {
result += "|"
} else {
result += ":-|"
}
}
result += "\r\n|"
}
result += nicks[0] + "|"
for i := 1; i < len(nicks); i++ {
if i%nicksPerRow == 0 {
result += "\r\n|" + nicks[i] + "|"
} else {
result += nicks[i] + "|"
}
}
return result
}
func plainformatter(nicks []string, nicksPerRow int) string {
return strings.Join(nicks, ", ") + " currently on IRC"
}
func IsMarkup(message string) bool {
switch message[0] {
case '|':
fallthrough
case '#':
fallthrough
case '_':
fallthrough
case '*':
fallthrough
case '~':
fallthrough
case '-':
fallthrough
case ':':
fallthrough
case '>':
fallthrough
case '=':
return true
}
return false
}

View File

@ -199,3 +199,4 @@
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.

View File

@ -0,0 +1,441 @@
package matterclient
import (
"crypto/tls"
"errors"
log "github.com/Sirupsen/logrus"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/jpillora/backoff"
"github.com/mattermost/platform/model"
)
type Credentials struct {
Login string
Team string
Pass string
Server string
NoTLS bool
SkipTLSVerify bool
}
type Message struct {
Raw *model.Message
Post *model.Post
Team string
Channel string
Username string
Text string
}
type MMClient struct {
*Credentials
Client *model.Client
WsClient *websocket.Conn
WsQuit bool
WsAway bool
Channels *model.ChannelList
MoreChannels *model.ChannelList
User *model.User
Users map[string]*model.User
MessageChan chan *Message
Team *model.Team
log *log.Entry
}
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)}
mmclient.log = log.WithFields(log.Fields{"module": "matterclient"})
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
return mmclient
}
func (m *MMClient) SetLogLevel(level string) {
l, err := log.ParseLevel(level)
if err != nil {
log.SetLevel(log.InfoLevel)
return
}
log.SetLevel(l)
}
func (m *MMClient) Login() error {
if m.WsQuit {
return nil
}
b := &backoff.Backoff{
Min: time.Second,
Max: 5 * time.Minute,
Jitter: true,
}
uriScheme := "https://"
wsScheme := "wss://"
if m.NoTLS {
uriScheme = "http://"
wsScheme = "ws://"
}
// login to mattermost
m.Client = model.NewClient(uriScheme + m.Credentials.Server)
m.Client.HttpClient.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}}
var myinfo *model.Result
var appErr *model.AppError
var logmsg = "trying login"
for {
m.log.Debugf("%s %s %s %s", logmsg, m.Credentials.Team, m.Credentials.Login, m.Credentials.Server)
if strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN) {
m.log.Debugf(logmsg+" with ", model.SESSION_COOKIE_TOKEN)
token := strings.Split(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN+"=")
m.Client.HttpClient.Jar = m.createCookieJar(token[1])
m.Client.MockSession(token[1])
myinfo, appErr = m.Client.GetMe("")
if myinfo.Data.(*model.User) == nil {
m.log.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass)
return errors.New("invalid " + model.SESSION_COOKIE_TOKEN)
}
} else {
myinfo, appErr = m.Client.Login(m.Credentials.Login, m.Credentials.Pass)
}
if appErr != nil {
d := b.Duration()
m.log.Debug(appErr.DetailedError)
if !strings.Contains(appErr.DetailedError, "connection refused") &&
!strings.Contains(appErr.DetailedError, "invalid character") {
if appErr.Message == "" {
return errors.New(appErr.DetailedError)
}
return errors.New(appErr.Message)
}
m.log.Debugf("LOGIN: %s, reconnecting in %s", appErr, d)
time.Sleep(d)
logmsg = "retrying login"
continue
}
break
}
// reset timer
b.Reset()
initLoad, _ := m.Client.GetInitialLoad()
initData := initLoad.Data.(*model.InitialLoad)
m.User = initData.User
for _, v := range initData.Teams {
m.log.Debugf("trying %s (id: %s)", v.Name, v.Id)
if v.Name == m.Credentials.Team {
m.Client.SetTeamId(v.Id)
m.Team = v
m.log.Debugf("GetallTeamListings: found id %s for team %s", v.Id, v.Name)
break
}
}
if m.Team == nil {
return errors.New("team not found")
}
// setup websocket connection
wsurl := wsScheme + m.Credentials.Server + "/api/v3/users/websocket"
header := http.Header{}
header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken)
m.log.Debug("WsClient: making connection")
var err error
for {
wsDialer := &websocket.Dialer{Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}}
m.WsClient, _, err = wsDialer.Dial(wsurl, header)
if err != nil {
d := b.Duration()
m.log.Debugf("WSS: %s, reconnecting in %s", err, d)
time.Sleep(d)
continue
}
break
}
b.Reset()
// populating users
m.UpdateUsers()
// populating channels
m.UpdateChannels()
return nil
}
func (m *MMClient) WsReceiver() {
var rmsg model.Message
for {
if m.WsQuit {
m.log.Debug("exiting WsReceiver")
return
}
if err := m.WsClient.ReadJSON(&rmsg); err != nil {
m.log.Error("error:", err)
// reconnect
m.Login()
}
if rmsg.Action == "ping" {
m.handleWsPing()
continue
}
msg := &Message{Raw: &rmsg, Team: m.Credentials.Team}
m.parseMessage(msg)
m.MessageChan <- msg
}
}
func (m *MMClient) handleWsPing() {
m.log.Debug("Ws PING")
if !m.WsQuit && !m.WsAway {
m.log.Debug("Ws PONG")
m.WsClient.WriteMessage(websocket.PongMessage, []byte{})
}
}
func (m *MMClient) parseMessage(rmsg *Message) {
switch rmsg.Raw.Action {
case model.ACTION_POSTED:
m.parseActionPost(rmsg)
/*
case model.ACTION_USER_REMOVED:
m.handleWsActionUserRemoved(&rmsg)
case model.ACTION_USER_ADDED:
m.handleWsActionUserAdded(&rmsg)
*/
}
}
func (m *MMClient) parseActionPost(rmsg *Message) {
data := model.PostFromJson(strings.NewReader(rmsg.Raw.Props["post"]))
// log.Println("receiving userid", data.UserId)
// we don't have the user, refresh the userlist
if m.Users[data.UserId] == nil {
m.UpdateUsers()
}
rmsg.Username = m.Users[data.UserId].Username
rmsg.Channel = m.GetChannelName(data.ChannelId)
// direct message
if strings.Contains(rmsg.Channel, "__") {
//log.Println("direct message")
rcvusers := strings.Split(rmsg.Channel, "__")
if rcvusers[0] != m.User.Id {
rmsg.Channel = m.Users[rcvusers[0]].Username
} else {
rmsg.Channel = m.Users[rcvusers[1]].Username
}
}
rmsg.Text = data.Message
rmsg.Post = data
return
}
func (m *MMClient) UpdateUsers() error {
mmusers, _ := m.Client.GetProfilesForDirectMessageList(m.Team.Id)
m.Users = mmusers.Data.(map[string]*model.User)
return nil
}
func (m *MMClient) UpdateChannels() error {
mmchannels, _ := m.Client.GetChannels("")
m.Channels = mmchannels.Data.(*model.ChannelList)
mmchannels, _ = m.Client.GetMoreChannels("")
m.MoreChannels = mmchannels.Data.(*model.ChannelList)
return nil
}
func (m *MMClient) GetChannelName(id string) string {
for _, channel := range append(m.Channels.Channels, m.MoreChannels.Channels...) {
if channel.Id == id {
return channel.Name
}
}
// not found? could be a new direct message from mattermost. Try to update and check again
m.UpdateChannels()
for _, channel := range append(m.Channels.Channels, m.MoreChannels.Channels...) {
if channel.Id == id {
return channel.Name
}
}
return ""
}
func (m *MMClient) GetChannelId(name string) string {
for _, channel := range append(m.Channels.Channels, m.MoreChannels.Channels...) {
if channel.Name == name {
return channel.Id
}
}
return ""
}
func (m *MMClient) GetChannelHeader(id string) string {
for _, channel := range append(m.Channels.Channels, m.MoreChannels.Channels...) {
if channel.Id == id {
return channel.Header
}
}
return ""
}
func (m *MMClient) PostMessage(channel string, text string) {
post := &model.Post{ChannelId: m.GetChannelId(channel), Message: text}
m.Client.CreatePost(post)
}
func (m *MMClient) JoinChannel(channel string) error {
cleanChan := strings.Replace(channel, "#", "", 1)
if m.GetChannelId(cleanChan) == "" {
return errors.New("failed to join")
}
for _, c := range m.Channels.Channels {
if c.Name == cleanChan {
m.log.Debug("Not joining ", cleanChan, " already joined.")
return nil
}
}
m.log.Debug("Joining ", cleanChan)
_, err := m.Client.JoinChannel(m.GetChannelId(cleanChan))
if err != nil {
return errors.New("failed to join")
}
// m.SyncChannel(m.getMMChannelId(strings.Replace(channel, "#", "", 1)), strings.Replace(channel, "#", "", 1))
return nil
}
func (m *MMClient) GetPostsSince(channelId string, time int64) *model.PostList {
res, err := m.Client.GetPostsSince(channelId, time)
if err != nil {
return nil
}
return res.Data.(*model.PostList)
}
func (m *MMClient) SearchPosts(query string) *model.PostList {
res, err := m.Client.SearchPosts(query, false)
if err != nil {
return nil
}
return res.Data.(*model.PostList)
}
func (m *MMClient) GetPosts(channelId string, limit int) *model.PostList {
res, err := m.Client.GetPosts(channelId, 0, limit, "")
if err != nil {
return nil
}
return res.Data.(*model.PostList)
}
func (m *MMClient) GetPublicLink(filename string) string {
res, err := m.Client.GetPublicLink(filename)
if err != nil {
return ""
}
return res.Data.(string)
}
func (m *MMClient) GetPublicLinks(filenames []string) []string {
var output []string
for _, f := range filenames {
res, err := m.Client.GetPublicLink(f)
if err != nil {
continue
}
output = append(output, res.Data.(string))
}
return output
}
func (m *MMClient) UpdateChannelHeader(channelId string, header string) {
data := make(map[string]string)
data["channel_id"] = channelId
data["channel_header"] = header
m.log.Debugf("updating channelheader %#v, %#v", channelId, header)
_, err := m.Client.UpdateChannelHeader(data)
if err != nil {
log.Error(err)
}
}
func (m *MMClient) UpdateLastViewed(channelId string) {
m.log.Debugf("posting lastview %#v", channelId)
_, err := m.Client.UpdateLastViewedAt(channelId)
if err != nil {
m.log.Error(err)
}
}
func (m *MMClient) UsernamesInChannel(channelName string) []string {
ceiRes, err := m.Client.GetChannelExtraInfo(m.GetChannelId(channelName), 5000, "")
if err != nil {
m.log.Errorf("UsernamesInChannel(%s) failed: %s", channelName, err)
return []string{}
}
extra := ceiRes.Data.(*model.ChannelExtra)
result := []string{}
for _, member := range extra.Members {
result = append(result, member.Username)
}
return result
}
func (m *MMClient) createCookieJar(token string) *cookiejar.Jar {
var cookies []*http.Cookie
jar, _ := cookiejar.New(nil)
firstCookie := &http.Cookie{
Name: "MMAUTHTOKEN",
Value: token,
Path: "/",
Domain: m.Credentials.Server,
}
cookies = append(cookies, firstCookie)
cookieURL, _ := url.Parse("https://" + m.Credentials.Server)
jar.SetCookies(cookieURL, cookies)
return jar
}
func (m *MMClient) GetOtherUserDM(channel string) *model.User {
m.UpdateUsers()
var rcvuser *model.User
if strings.Contains(channel, "__") {
rcvusers := strings.Split(channel, "__")
if rcvusers[0] != m.User.Id {
rcvuser = m.Users[rcvusers[0]]
} else {
rcvuser = m.Users[rcvusers[1]]
}
}
return rcvuser
}
func (m *MMClient) SendDirectMessage(toUserId string, msg string) {
m.log.Debugf("SendDirectMessage to %s, msg %s", toUserId, msg)
var channel string
// We don't have a DM with this user yet.
if m.GetChannelId(toUserId+"__"+m.User.Id) == "" && m.GetChannelId(m.User.Id+"__"+toUserId) == "" {
// create DM channel
_, err := m.Client.CreateDirectChannel(toUserId)
if err != nil {
m.log.Debugf("SendDirectMessage to %#v failed: %s", toUserId, err)
}
// update our channels
mmchannels, _ := m.Client.GetChannels("")
m.Channels = mmchannels.Data.(*model.ChannelList)
}
// build the channel name
if toUserId > m.User.Id {
channel = m.User.Id + "__" + toUserId
} else {
channel = toUserId + "__" + m.User.Id
}
// build & send the message
msg = strings.Replace(msg, "\r", "", -1)
post := &model.Post{ChannelId: m.GetChannelId(channel), Message: msg}
m.Client.CreatePost(post)
}

View File

@ -4,7 +4,7 @@ files via reflection. There is also support for delaying decoding with
the Primitive type, and querying the set of keys in a TOML document with the
MetaData type.
The specification implemented: https://github.com/toml-lang/toml
The specification implemented: https://github.com/mojombo/toml
The sub-command github.com/BurntSushi/toml/cmd/tomlv can be used to verify
whether a file is a valid TOML document. It can also be used to print the

View File

@ -241,7 +241,7 @@ func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
func (enc *Encoder) eTable(key Key, rv reflect.Value) {
panicIfInvalidKey(key)
if len(key) == 1 {
// Output an extra newline between top-level tables.
// Output an extra new line between top-level tables.
// (The newline isn't written if nothing else has been written though.)
enc.newline()
}

View File

@ -30,28 +30,24 @@ const (
itemArrayTableEnd
itemKeyStart
itemCommentStart
itemInlineTableStart
itemInlineTableEnd
)
const (
eof = 0
comma = ','
tableStart = '['
tableEnd = ']'
arrayTableStart = '['
arrayTableEnd = ']'
tableSep = '.'
keySep = '='
arrayStart = '['
arrayEnd = ']'
commentStart = '#'
stringStart = '"'
stringEnd = '"'
rawStringStart = '\''
rawStringEnd = '\''
inlineTableStart = '{'
inlineTableEnd = '}'
eof = 0
tableStart = '['
tableEnd = ']'
arrayTableStart = '['
arrayTableEnd = ']'
tableSep = '.'
keySep = '='
arrayStart = '['
arrayEnd = ']'
arrayValTerm = ','
commentStart = '#'
stringStart = '"'
stringEnd = '"'
rawStringStart = '\''
rawStringEnd = '\''
)
type stateFn func(lx *lexer) stateFn
@ -60,18 +56,11 @@ type lexer struct {
input string
start int
pos int
width int
line int
state stateFn
items chan item
// Allow for backing up up to three runes.
// This is necessary because TOML contains 3-rune tokens (""" and ''').
prevWidths [3]int
nprev int // how many of prevWidths are in use
// If we emit an eof, we can still back up, but it is not OK to call
// next again.
atEOF bool
// A stack of state functions used to maintain context.
// The idea is to reuse parts of the state machine in various places.
// For example, values can appear at the top level or within arbitrarily
@ -99,7 +88,7 @@ func (lx *lexer) nextItem() item {
func lex(input string) *lexer {
lx := &lexer{
input: input,
input: input + "\n",
state: lexTop,
line: 1,
items: make(chan item, 10),
@ -114,7 +103,7 @@ func (lx *lexer) push(state stateFn) {
func (lx *lexer) pop() stateFn {
if len(lx.stack) == 0 {
return lx.errorf("BUG in lexer: no states to pop")
return lx.errorf("BUG in lexer: no states to pop.")
}
last := lx.stack[len(lx.stack)-1]
lx.stack = lx.stack[0 : len(lx.stack)-1]
@ -136,25 +125,16 @@ func (lx *lexer) emitTrim(typ itemType) {
}
func (lx *lexer) next() (r rune) {
if lx.atEOF {
panic("next called after EOF")
}
if lx.pos >= len(lx.input) {
lx.atEOF = true
lx.width = 0
return eof
}
if lx.input[lx.pos] == '\n' {
lx.line++
}
lx.prevWidths[2] = lx.prevWidths[1]
lx.prevWidths[1] = lx.prevWidths[0]
if lx.nprev < 3 {
lx.nprev++
}
r, w := utf8.DecodeRuneInString(lx.input[lx.pos:])
lx.prevWidths[0] = w
lx.pos += w
r, lx.width = utf8.DecodeRuneInString(lx.input[lx.pos:])
lx.pos += lx.width
return r
}
@ -163,20 +143,9 @@ func (lx *lexer) ignore() {
lx.start = lx.pos
}
// backup steps back one rune. Can be called only twice between calls to next.
// backup steps back one rune. Can be called only once per call of next.
func (lx *lexer) backup() {
if lx.atEOF {
lx.atEOF = false
return
}
if lx.nprev < 1 {
panic("backed up too far")
}
w := lx.prevWidths[0]
lx.prevWidths[0] = lx.prevWidths[1]
lx.prevWidths[1] = lx.prevWidths[2]
lx.nprev--
lx.pos -= w
lx.pos -= lx.width
if lx.pos < len(lx.input) && lx.input[lx.pos] == '\n' {
lx.line--
}
@ -213,7 +182,7 @@ func (lx *lexer) skip(pred func(rune) bool) {
// errorf stops all lexing by emitting an error and returning `nil`.
// Note that any value that is a character is escaped if it's a special
// character (newlines, tabs, etc.).
// character (new lines, tabs, etc.).
func (lx *lexer) errorf(format string, values ...interface{}) stateFn {
lx.items <- item{
itemError,
@ -229,6 +198,7 @@ func lexTop(lx *lexer) stateFn {
if isWhitespace(r) || isNL(r) {
return lexSkip(lx, lexTop)
}
switch r {
case commentStart:
lx.push(lexTop)
@ -237,7 +207,7 @@ func lexTop(lx *lexer) stateFn {
return lexTableStart
case eof:
if lx.pos > lx.start {
return lx.errorf("unexpected EOF")
return lx.errorf("Unexpected EOF.")
}
lx.emit(itemEOF)
return nil
@ -252,12 +222,12 @@ func lexTop(lx *lexer) stateFn {
// lexTopEnd is entered whenever a top-level item has been consumed. (A value
// or a table.) It must see only whitespace, and will turn back to lexTop
// upon a newline. If it sees EOF, it will quit the lexer successfully.
// upon a new line. If it sees EOF, it will quit the lexer successfully.
func lexTopEnd(lx *lexer) stateFn {
r := lx.next()
switch {
case r == commentStart:
// a comment will read to a newline for us.
// a comment will read to a new line for us.
lx.push(lexTop)
return lexCommentStart
case isWhitespace(r):
@ -266,11 +236,11 @@ func lexTopEnd(lx *lexer) stateFn {
lx.ignore()
return lexTop
case r == eof:
lx.emit(itemEOF)
return nil
lx.ignore()
return lexTop
}
return lx.errorf("expected a top-level item to end with a newline, "+
"comment, or EOF, but got %q instead", r)
return lx.errorf("Expected a top-level item to end with a new line, "+
"comment or EOF, but got %q instead.", r)
}
// lexTable lexes the beginning of a table. Namely, it makes sure that
@ -297,8 +267,8 @@ func lexTableEnd(lx *lexer) stateFn {
func lexArrayTableEnd(lx *lexer) stateFn {
if r := lx.next(); r != arrayTableEnd {
return lx.errorf("expected end of table array name delimiter %q, "+
"but got %q instead", arrayTableEnd, r)
return lx.errorf("Expected end of table array name delimiter %q, "+
"but got %q instead.", arrayTableEnd, r)
}
lx.emit(itemArrayTableEnd)
return lexTopEnd
@ -308,11 +278,11 @@ func lexTableNameStart(lx *lexer) stateFn {
lx.skip(isWhitespace)
switch r := lx.peek(); {
case r == tableEnd || r == eof:
return lx.errorf("unexpected end of table name " +
"(table names cannot be empty)")
return lx.errorf("Unexpected end of table name. (Table names cannot " +
"be empty.)")
case r == tableSep:
return lx.errorf("unexpected table separator " +
"(table names cannot be empty)")
return lx.errorf("Unexpected table separator. (Table names cannot " +
"be empty.)")
case r == stringStart || r == rawStringStart:
lx.ignore()
lx.push(lexTableNameEnd)
@ -347,8 +317,8 @@ func lexTableNameEnd(lx *lexer) stateFn {
case r == tableEnd:
return lx.pop()
default:
return lx.errorf("expected '.' or ']' to end table name, "+
"but got %q instead", r)
return lx.errorf("Expected '.' or ']' to end table name, but got %q "+
"instead.", r)
}
}
@ -358,7 +328,7 @@ func lexKeyStart(lx *lexer) stateFn {
r := lx.peek()
switch {
case r == keySep:
return lx.errorf("unexpected key separator %q", keySep)
return lx.errorf("Unexpected key separator %q.", keySep)
case isWhitespace(r) || isNL(r):
lx.next()
return lexSkip(lx, lexKeyStart)
@ -389,7 +359,7 @@ func lexBareKey(lx *lexer) stateFn {
lx.emit(itemText)
return lexKeyEnd
default:
return lx.errorf("bare keys cannot contain %q", r)
return lx.errorf("Bare keys cannot contain %q.", r)
}
}
@ -402,7 +372,7 @@ func lexKeyEnd(lx *lexer) stateFn {
case isWhitespace(r):
return lexSkip(lx, lexKeyEnd)
default:
return lx.errorf("expected key separator %q, but got %q instead",
return lx.errorf("Expected key separator %q, but got %q instead.",
keySep, r)
}
}
@ -411,8 +381,9 @@ func lexKeyEnd(lx *lexer) stateFn {
// lexValue will ignore whitespace.
// After a value is lexed, the last state on the next is popped and returned.
func lexValue(lx *lexer) stateFn {
// We allow whitespace to precede a value, but NOT newlines.
// In array syntax, the array states are responsible for ignoring newlines.
// We allow whitespace to precede a value, but NOT new lines.
// In array syntax, the array states are responsible for ignoring new
// lines.
r := lx.next()
switch {
case isWhitespace(r):
@ -426,10 +397,6 @@ func lexValue(lx *lexer) stateFn {
lx.ignore()
lx.emit(itemArray)
return lexArrayValue
case inlineTableStart:
lx.ignore()
lx.emit(itemInlineTableStart)
return lexInlineTableValue
case stringStart:
if lx.accept(stringStart) {
if lx.accept(stringStart) {
@ -453,7 +420,7 @@ func lexValue(lx *lexer) stateFn {
case '+', '-':
return lexNumberStart
case '.': // special error case, be kind to users
return lx.errorf("floats must start with a digit, not '.'")
return lx.errorf("Floats must start with a digit, not '.'.")
}
if unicode.IsLetter(r) {
// Be permissive here; lexBool will give a nice error if the
@ -463,11 +430,11 @@ func lexValue(lx *lexer) stateFn {
lx.backup()
return lexBool
}
return lx.errorf("expected value but found %q instead", r)
return lx.errorf("Expected value but found %q instead.", r)
}
// lexArrayValue consumes one value in an array. It assumes that '[' or ','
// have already been consumed. All whitespace and newlines are ignored.
// have already been consumed. All whitespace and new lines are ignored.
func lexArrayValue(lx *lexer) stateFn {
r := lx.next()
switch {
@ -476,11 +443,10 @@ func lexArrayValue(lx *lexer) stateFn {
case r == commentStart:
lx.push(lexArrayValue)
return lexCommentStart
case r == comma:
return lx.errorf("unexpected comma")
case r == arrayValTerm:
return lx.errorf("Unexpected array value terminator %q.",
arrayValTerm)
case r == arrayEnd:
// NOTE(caleb): The spec isn't clear about whether you can have
// a trailing comma or not, so we'll allow it.
return lexArrayEnd
}
@ -489,9 +455,8 @@ func lexArrayValue(lx *lexer) stateFn {
return lexValue
}
// lexArrayValueEnd consumes everything between the end of an array value and
// the next value (or the end of the array): it ignores whitespace and newlines
// and expects either a ',' or a ']'.
// lexArrayValueEnd consumes the cruft between values of an array. Namely,
// it ignores whitespace and expects either a ',' or a ']'.
func lexArrayValueEnd(lx *lexer) stateFn {
r := lx.next()
switch {
@ -500,88 +465,31 @@ func lexArrayValueEnd(lx *lexer) stateFn {
case r == commentStart:
lx.push(lexArrayValueEnd)
return lexCommentStart
case r == comma:
case r == arrayValTerm:
lx.ignore()
return lexArrayValue // move on to the next value
case r == arrayEnd:
return lexArrayEnd
}
return lx.errorf(
"expected a comma or array terminator %q, but got %q instead",
arrayEnd, r,
)
return lx.errorf("Expected an array value terminator %q or an array "+
"terminator %q, but got %q instead.", arrayValTerm, arrayEnd, r)
}
// lexArrayEnd finishes the lexing of an array.
// It assumes that a ']' has just been consumed.
// lexArrayEnd finishes the lexing of an array. It assumes that a ']' has
// just been consumed.
func lexArrayEnd(lx *lexer) stateFn {
lx.ignore()
lx.emit(itemArrayEnd)
return lx.pop()
}
// lexInlineTableValue consumes one key/value pair in an inline table.
// It assumes that '{' or ',' have already been consumed. Whitespace is ignored.
func lexInlineTableValue(lx *lexer) stateFn {
r := lx.next()
switch {
case isWhitespace(r):
return lexSkip(lx, lexInlineTableValue)
case isNL(r):
return lx.errorf("newlines not allowed within inline tables")
case r == commentStart:
lx.push(lexInlineTableValue)
return lexCommentStart
case r == comma:
return lx.errorf("unexpected comma")
case r == inlineTableEnd:
return lexInlineTableEnd
}
lx.backup()
lx.push(lexInlineTableValueEnd)
return lexKeyStart
}
// lexInlineTableValueEnd consumes everything between the end of an inline table
// key/value pair and the next pair (or the end of the table):
// it ignores whitespace and expects either a ',' or a '}'.
func lexInlineTableValueEnd(lx *lexer) stateFn {
r := lx.next()
switch {
case isWhitespace(r):
return lexSkip(lx, lexInlineTableValueEnd)
case isNL(r):
return lx.errorf("newlines not allowed within inline tables")
case r == commentStart:
lx.push(lexInlineTableValueEnd)
return lexCommentStart
case r == comma:
lx.ignore()
return lexInlineTableValue
case r == inlineTableEnd:
return lexInlineTableEnd
}
return lx.errorf("expected a comma or an inline table terminator %q, "+
"but got %q instead", inlineTableEnd, r)
}
// lexInlineTableEnd finishes the lexing of an inline table.
// It assumes that a '}' has just been consumed.
func lexInlineTableEnd(lx *lexer) stateFn {
lx.ignore()
lx.emit(itemInlineTableEnd)
return lx.pop()
}
// lexString consumes the inner contents of a string. It assumes that the
// beginning '"' has already been consumed and ignored.
func lexString(lx *lexer) stateFn {
r := lx.next()
switch {
case r == eof:
return lx.errorf("unexpected EOF")
case isNL(r):
return lx.errorf("strings cannot contain newlines")
return lx.errorf("Strings cannot contain new lines.")
case r == '\\':
lx.push(lexString)
return lexStringEscape
@ -598,12 +506,11 @@ func lexString(lx *lexer) stateFn {
// lexMultilineString consumes the inner contents of a string. It assumes that
// the beginning '"""' has already been consumed and ignored.
func lexMultilineString(lx *lexer) stateFn {
switch lx.next() {
case eof:
return lx.errorf("unexpected EOF")
case '\\':
r := lx.next()
switch {
case r == '\\':
return lexMultilineStringEscape
case stringEnd:
case r == stringEnd:
if lx.accept(stringEnd) {
if lx.accept(stringEnd) {
lx.backup()
@ -627,10 +534,8 @@ func lexMultilineString(lx *lexer) stateFn {
func lexRawString(lx *lexer) stateFn {
r := lx.next()
switch {
case r == eof:
return lx.errorf("unexpected EOF")
case isNL(r):
return lx.errorf("strings cannot contain newlines")
return lx.errorf("Strings cannot contain new lines.")
case r == rawStringEnd:
lx.backup()
lx.emit(itemRawString)
@ -642,13 +547,12 @@ func lexRawString(lx *lexer) stateFn {
}
// lexMultilineRawString consumes a raw string. Nothing can be escaped in such
// a string. It assumes that the beginning "'''" has already been consumed and
// a string. It assumes that the beginning "'" has already been consumed and
// ignored.
func lexMultilineRawString(lx *lexer) stateFn {
switch lx.next() {
case eof:
return lx.errorf("unexpected EOF")
case rawStringEnd:
r := lx.next()
switch {
case r == rawStringEnd:
if lx.accept(rawStringEnd) {
if lx.accept(rawStringEnd) {
lx.backup()
@ -701,9 +605,10 @@ func lexStringEscape(lx *lexer) stateFn {
case 'U':
return lexLongUnicodeEscape
}
return lx.errorf("invalid escape character %q; only the following "+
return lx.errorf("Invalid escape character %q. Only the following "+
"escape characters are allowed: "+
`\b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX`, r)
"\\b, \\t, \\n, \\f, \\r, \\\", \\/, \\\\, "+
"\\uXXXX and \\UXXXXXXXX.", r)
}
func lexShortUnicodeEscape(lx *lexer) stateFn {
@ -711,8 +616,8 @@ func lexShortUnicodeEscape(lx *lexer) stateFn {
for i := 0; i < 4; i++ {
r = lx.next()
if !isHexadecimal(r) {
return lx.errorf(`expected four hexadecimal digits after '\u', `+
"but got %q instead", lx.current())
return lx.errorf("Expected four hexadecimal digits after '\\u', "+
"but got '%s' instead.", lx.current())
}
}
return lx.pop()
@ -723,8 +628,8 @@ func lexLongUnicodeEscape(lx *lexer) stateFn {
for i := 0; i < 8; i++ {
r = lx.next()
if !isHexadecimal(r) {
return lx.errorf(`expected eight hexadecimal digits after '\U', `+
"but got %q instead", lx.current())
return lx.errorf("Expected eight hexadecimal digits after '\\U', "+
"but got '%s' instead.", lx.current())
}
}
return lx.pop()
@ -742,9 +647,9 @@ func lexNumberOrDateStart(lx *lexer) stateFn {
case 'e', 'E':
return lexFloat
case '.':
return lx.errorf("floats must start with a digit, not '.'")
return lx.errorf("Floats must start with a digit, not '.'.")
}
return lx.errorf("expected a digit but got %q", r)
return lx.errorf("Expected a digit but got %q.", r)
}
// lexNumberOrDate consumes either an integer, float or datetime.
@ -792,9 +697,9 @@ func lexNumberStart(lx *lexer) stateFn {
r := lx.next()
if !isDigit(r) {
if r == '.' {
return lx.errorf("floats must start with a digit, not '.'")
return lx.errorf("Floats must start with a digit, not '.'.")
}
return lx.errorf("expected a digit but got %q", r)
return lx.errorf("Expected a digit but got %q.", r)
}
return lexNumber
}
@ -852,7 +757,7 @@ func lexBool(lx *lexer) stateFn {
lx.emit(itemBool)
return lx.pop()
}
return lx.errorf("expected value but found %q instead", s)
return lx.errorf("Expected value but found %q instead.", s)
}
// lexCommentStart begins the lexing of a comment. It will emit
@ -864,7 +769,7 @@ func lexCommentStart(lx *lexer) stateFn {
}
// lexComment lexes an entire comment. It assumes that '#' has been consumed.
// It will consume *up to* the first newline character, and pass control
// It will consume *up to* the first new line character, and pass control
// back to the last state on the stack.
func lexComment(lx *lexer) stateFn {
r := lx.peek()

View File

@ -269,41 +269,6 @@ func (p *parser) value(it item) (interface{}, tomlType) {
types = append(types, typ)
}
return array, p.typeOfArray(types)
case itemInlineTableStart:
var (
hash = make(map[string]interface{})
outerContext = p.context
outerKey = p.currentKey
)
p.context = append(p.context, p.currentKey)
p.currentKey = ""
for it := p.next(); it.typ != itemInlineTableEnd; it = p.next() {
if it.typ != itemKeyStart {
p.bug("Expected key start but instead found %q, around line %d",
it.val, p.approxLine)
}
if it.typ == itemCommentStart {
p.expect(itemText)
continue
}
// retrieve key
k := p.next()
p.approxLine = k.line
kname := p.keyString(k)
// retrieve value
p.currentKey = kname
val, typ := p.value(p.next())
// make sure we keep metadata up to date
p.setType(kname, typ)
p.ordered = append(p.ordered, p.context.add(p.currentKey))
hash[kname] = val
}
p.context = outerContext
p.currentKey = outerKey
return hash, tomlHash
}
p.bug("Unexpected value type: %s", it.typ)
panic("unreachable")

View File

@ -1,22 +0,0 @@
Copyright (c) 2013, Geert-Johan Riemer
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,138 +0,0 @@
package rice
import (
"archive/zip"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/daaku/go.zipexe"
"github.com/kardianos/osext"
)
// appendedBox defines an appended box
type appendedBox struct {
Name string // box name
Files map[string]*appendedFile // appended files (*zip.File) by full path
}
type appendedFile struct {
zipFile *zip.File
dir bool
dirInfo *appendedDirInfo
children []*appendedFile
content []byte
}
// appendedBoxes is a public register of appendes boxes
var appendedBoxes = make(map[string]*appendedBox)
func init() {
// find if exec is appended
thisFile, err := osext.Executable()
if err != nil {
return // not appended or cant find self executable
}
closer, rd, err := zipexe.OpenCloser(thisFile)
if err != nil {
return // not appended
}
defer closer.Close()
for _, f := range rd.File {
// get box and file name from f.Name
fileParts := strings.SplitN(strings.TrimLeft(filepath.ToSlash(f.Name), "/"), "/", 2)
boxName := fileParts[0]
var fileName string
if len(fileParts) > 1 {
fileName = fileParts[1]
}
// find box or create new one if doesn't exist
box := appendedBoxes[boxName]
if box == nil {
box = &appendedBox{
Name: boxName,
Files: make(map[string]*appendedFile),
}
appendedBoxes[boxName] = box
}
// create and add file to box
af := &appendedFile{
zipFile: f,
}
if f.Comment == "dir" {
af.dir = true
af.dirInfo = &appendedDirInfo{
name: filepath.Base(af.zipFile.Name),
//++ TODO: use zip modtime when that is set correctly: af.zipFile.ModTime()
time: time.Now(),
}
} else {
// this is a file, we need it's contents so we can create a bytes.Reader when the file is opened
// make a new byteslice
af.content = make([]byte, af.zipFile.FileInfo().Size())
// ignore reading empty files from zip (empty file still is a valid file to be read though!)
if len(af.content) > 0 {
// open io.ReadCloser
rc, err := af.zipFile.Open()
if err != nil {
af.content = nil // this will cause an error when the file is being opened or seeked (which is good)
// TODO: it's quite blunt to just log this stuff. but this is in init, so rice.Debug can't be changed yet..
log.Printf("error opening appended file %s: %v", af.zipFile.Name, err)
} else {
_, err = rc.Read(af.content)
rc.Close()
if err != nil {
af.content = nil // this will cause an error when the file is being opened or seeked (which is good)
// TODO: it's quite blunt to just log this stuff. but this is in init, so rice.Debug can't be changed yet..
log.Printf("error reading data for appended file %s: %v", af.zipFile.Name, err)
}
}
}
}
// add appendedFile to box file list
box.Files[fileName] = af
// add to parent dir (if any)
dirName := filepath.Dir(fileName)
if dirName == "." {
dirName = ""
}
if fileName != "" { // don't make box root dir a child of itself
if dir := box.Files[dirName]; dir != nil {
dir.children = append(dir.children, af)
}
}
}
}
// implements os.FileInfo.
// used for Readdir()
type appendedDirInfo struct {
name string
time time.Time
}
func (adi *appendedDirInfo) Name() string {
return adi.name
}
func (adi *appendedDirInfo) Size() int64 {
return 0
}
func (adi *appendedDirInfo) Mode() os.FileMode {
return os.ModeDir
}
func (adi *appendedDirInfo) ModTime() time.Time {
return adi.time
}
func (adi *appendedDirInfo) IsDir() bool {
return true
}
func (adi *appendedDirInfo) Sys() interface{} {
return nil
}

View File

@ -1,337 +0,0 @@
package rice
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/GeertJohan/go.rice/embedded"
)
// Box abstracts a directory for resources/files.
// It can either load files from disk, or from embedded code (when `rice --embed` was ran).
type Box struct {
name string
absolutePath string
embed *embedded.EmbeddedBox
appendd *appendedBox
}
var defaultLocateOrder = []LocateMethod{LocateEmbedded, LocateAppended, LocateFS}
func findBox(name string, order []LocateMethod) (*Box, error) {
b := &Box{name: name}
// no support for absolute paths since gopath can be different on different machines.
// therefore, required box must be located relative to package requiring it.
if filepath.IsAbs(name) {
return nil, errors.New("given name/path is absolute")
}
var err error
for _, method := range order {
switch method {
case LocateEmbedded:
if embed := embedded.EmbeddedBoxes[name]; embed != nil {
b.embed = embed
return b, nil
}
case LocateAppended:
appendedBoxName := strings.Replace(name, `/`, `-`, -1)
if appendd := appendedBoxes[appendedBoxName]; appendd != nil {
b.appendd = appendd
return b, nil
}
case LocateFS:
// resolve absolute directory path
err := b.resolveAbsolutePathFromCaller()
if err != nil {
continue
}
// check if absolutePath exists on filesystem
info, err := os.Stat(b.absolutePath)
if err != nil {
continue
}
// check if absolutePath is actually a directory
if !info.IsDir() {
err = errors.New("given name/path is not a directory")
continue
}
return b, nil
case LocateWorkingDirectory:
// resolve absolute directory path
err := b.resolveAbsolutePathFromWorkingDirectory()
if err != nil {
continue
}
// check if absolutePath exists on filesystem
info, err := os.Stat(b.absolutePath)
if err != nil {
continue
}
// check if absolutePath is actually a directory
if !info.IsDir() {
err = errors.New("given name/path is not a directory")
continue
}
return b, nil
}
}
if err == nil {
err = fmt.Errorf("could not locate box %q", name)
}
return nil, err
}
// FindBox returns a Box instance for given name.
// When the given name is a relative path, it's base path will be the calling pkg/cmd's source root.
// When the given name is absolute, it's absolute. derp.
// Make sure the path doesn't contain any sensitive information as it might be placed into generated go source (embedded).
func FindBox(name string) (*Box, error) {
return findBox(name, defaultLocateOrder)
}
// MustFindBox returns a Box instance for given name, like FindBox does.
// It does not return an error, instead it panics when an error occurs.
func MustFindBox(name string) *Box {
box, err := findBox(name, defaultLocateOrder)
if err != nil {
panic(err)
}
return box
}
// This is injected as a mutable function literal so that we can mock it out in
// tests and return a fixed test file.
var resolveAbsolutePathFromCaller = func(name string, nStackFrames int) (string, error) {
_, callingGoFile, _, ok := runtime.Caller(nStackFrames)
if !ok {
return "", errors.New("couldn't find caller on stack")
}
// resolve to proper path
pkgDir := filepath.Dir(callingGoFile)
// fix for go cover
const coverPath = "_test/_obj_test"
if !filepath.IsAbs(pkgDir) {
if i := strings.Index(pkgDir, coverPath); i >= 0 {
pkgDir = pkgDir[:i] + pkgDir[i+len(coverPath):] // remove coverPath
pkgDir = filepath.Join(os.Getenv("GOPATH"), "src", pkgDir) // make absolute
}
}
return filepath.Join(pkgDir, name), nil
}
func (b *Box) resolveAbsolutePathFromCaller() error {
path, err := resolveAbsolutePathFromCaller(b.name, 4)
if err != nil {
return err
}
b.absolutePath = path
return nil
}
func (b *Box) resolveAbsolutePathFromWorkingDirectory() error {
path, err := os.Getwd()
if err != nil {
return err
}
b.absolutePath = filepath.Join(path, b.name)
return nil
}
// IsEmbedded indicates wether this box was embedded into the application
func (b *Box) IsEmbedded() bool {
return b.embed != nil
}
// IsAppended indicates wether this box was appended to the application
func (b *Box) IsAppended() bool {
return b.appendd != nil
}
// Time returns how actual the box is.
// When the box is embedded, it's value is saved in the embedding code.
// When the box is live, this methods returns time.Now()
func (b *Box) Time() time.Time {
if b.IsEmbedded() {
return b.embed.Time
}
//++ TODO: return time for appended box
return time.Now()
}
// Open opens a File from the box
// If there is an error, it will be of type *os.PathError.
func (b *Box) Open(name string) (*File, error) {
if Debug {
fmt.Printf("Open(%s)\n", name)
}
if b.IsEmbedded() {
if Debug {
fmt.Println("Box is embedded")
}
// trim prefix (paths are relative to box)
name = strings.TrimLeft(name, "/")
if Debug {
fmt.Printf("Trying %s\n", name)
}
// search for file
ef := b.embed.Files[name]
if ef == nil {
if Debug {
fmt.Println("Didn't find file in embed")
}
// file not found, try dir
ed := b.embed.Dirs[name]
if ed == nil {
if Debug {
fmt.Println("Didn't find dir in embed")
}
// dir not found, error out
return nil, &os.PathError{
Op: "open",
Path: name,
Err: os.ErrNotExist,
}
}
if Debug {
fmt.Println("Found dir. Returning virtual dir")
}
vd := newVirtualDir(ed)
return &File{virtualD: vd}, nil
}
// box is embedded
if Debug {
fmt.Println("Found file. Returning virtual file")
}
vf := newVirtualFile(ef)
return &File{virtualF: vf}, nil
}
if b.IsAppended() {
// trim prefix (paths are relative to box)
name = strings.TrimLeft(name, "/")
// search for file
appendedFile := b.appendd.Files[name]
if appendedFile == nil {
return nil, &os.PathError{
Op: "open",
Path: name,
Err: os.ErrNotExist,
}
}
// create new file
f := &File{
appendedF: appendedFile,
}
// if this file is a directory, we want to be able to read and seek
if !appendedFile.dir {
// looks like malformed data in zip, error now
if appendedFile.content == nil {
return nil, &os.PathError{
Op: "open",
Path: "name",
Err: errors.New("error reading data from zip file"),
}
}
// create new bytes.Reader
f.appendedFileReader = bytes.NewReader(appendedFile.content)
}
// all done
return f, nil
}
// perform os open
if Debug {
fmt.Printf("Using os.Open(%s)", filepath.Join(b.absolutePath, name))
}
file, err := os.Open(filepath.Join(b.absolutePath, name))
if err != nil {
return nil, err
}
return &File{realF: file}, nil
}
// Bytes returns the content of the file with given name as []byte.
func (b *Box) Bytes(name string) ([]byte, error) {
file, err := b.Open(name)
if err != nil {
return nil, err
}
defer file.Close()
content, err := ioutil.ReadAll(file)
if err != nil {
return nil, err
}
return content, nil
}
// MustBytes returns the content of the file with given name as []byte.
// panic's on error.
func (b *Box) MustBytes(name string) []byte {
bts, err := b.Bytes(name)
if err != nil {
panic(err)
}
return bts
}
// String returns the content of the file with given name as string.
func (b *Box) String(name string) (string, error) {
// check if box is embedded, optimized fast path
if b.IsEmbedded() {
// find file in embed
ef := b.embed.Files[name]
if ef == nil {
return "", os.ErrNotExist
}
// return as string
return ef.Content, nil
}
bts, err := b.Bytes(name)
if err != nil {
return "", err
}
return string(bts), nil
}
// MustString returns the content of the file with given name as string.
// panic's on error.
func (b *Box) MustString(name string) string {
str, err := b.String(name)
if err != nil {
panic(err)
}
return str
}
// Name returns the name of the box
func (b *Box) Name() string {
return b.name
}

View File

@ -1,39 +0,0 @@
package rice
// LocateMethod defines how a box is located.
type LocateMethod int
const (
LocateFS = LocateMethod(iota) // Locate on the filesystem according to package path.
LocateAppended // Locate boxes appended to the executable.
LocateEmbedded // Locate embedded boxes.
LocateWorkingDirectory // Locate on the binary working directory
)
// Config allows customizing the box lookup behavior.
type Config struct {
// LocateOrder defines the priority order that boxes are searched for. By
// default, the package global FindBox searches for embedded boxes first,
// then appended boxes, and then finally boxes on the filesystem. That
// search order may be customized by provided the ordered list here. Leaving
// out a particular method will omit that from the search space. For
// example, []LocateMethod{LocateEmbedded, LocateAppended} will never search
// the filesystem for boxes.
LocateOrder []LocateMethod
}
// FindBox searches for boxes using the LocateOrder of the config.
func (c *Config) FindBox(boxName string) (*Box, error) {
return findBox(boxName, c.LocateOrder)
}
// MustFindBox searches for boxes using the LocateOrder of the config, like
// FindBox does. It does not return an error, instead it panics when an error
// occurs.
func (c *Config) MustFindBox(boxName string) *Box {
box, err := findBox(boxName, c.LocateOrder)
if err != nil {
panic(err)
}
return box
}

View File

@ -1,4 +0,0 @@
package rice
// Debug can be set to true to enable debugging.
var Debug = false

View File

@ -1,90 +0,0 @@
package rice
import (
"os"
"time"
"github.com/GeertJohan/go.rice/embedded"
)
// re-type to make exported methods invisible to user (godoc)
// they're not required for the user
// embeddedDirInfo implements os.FileInfo
type embeddedDirInfo embedded.EmbeddedDir
// Name returns the base name of the directory
// (implementing os.FileInfo)
func (ed *embeddedDirInfo) Name() string {
return ed.Filename
}
// Size always returns 0
// (implementing os.FileInfo)
func (ed *embeddedDirInfo) Size() int64 {
return 0
}
// Mode returns the file mode bits
// (implementing os.FileInfo)
func (ed *embeddedDirInfo) Mode() os.FileMode {
return os.FileMode(0555 | os.ModeDir) // dr-xr-xr-x
}
// ModTime returns the modification time
// (implementing os.FileInfo)
func (ed *embeddedDirInfo) ModTime() time.Time {
return ed.DirModTime
}
// IsDir returns the abbreviation for Mode().IsDir() (always true)
// (implementing os.FileInfo)
func (ed *embeddedDirInfo) IsDir() bool {
return true
}
// Sys returns the underlying data source (always nil)
// (implementing os.FileInfo)
func (ed *embeddedDirInfo) Sys() interface{} {
return nil
}
// re-type to make exported methods invisible to user (godoc)
// they're not required for the user
// embeddedFileInfo implements os.FileInfo
type embeddedFileInfo embedded.EmbeddedFile
// Name returns the base name of the file
// (implementing os.FileInfo)
func (ef *embeddedFileInfo) Name() string {
return ef.Filename
}
// Size returns the length in bytes for regular files; system-dependent for others
// (implementing os.FileInfo)
func (ef *embeddedFileInfo) Size() int64 {
return int64(len(ef.Content))
}
// Mode returns the file mode bits
// (implementing os.FileInfo)
func (ef *embeddedFileInfo) Mode() os.FileMode {
return os.FileMode(0555) // r-xr-xr-x
}
// ModTime returns the modification time
// (implementing os.FileInfo)
func (ef *embeddedFileInfo) ModTime() time.Time {
return ef.FileModTime
}
// IsDir returns the abbreviation for Mode().IsDir() (always false)
// (implementing os.FileInfo)
func (ef *embeddedFileInfo) IsDir() bool {
return false
}
// Sys returns the underlying data source (always nil)
// (implementing os.FileInfo)
func (ef *embeddedFileInfo) Sys() interface{} {
return nil
}

View File

@ -1,80 +0,0 @@
// Package embedded defines embedded data types that are shared between the go.rice package and generated code.
package embedded
import (
"fmt"
"path/filepath"
"strings"
"time"
)
const (
EmbedTypeGo = 0
EmbedTypeSyso = 1
)
// EmbeddedBox defines an embedded box
type EmbeddedBox struct {
Name string // box name
Time time.Time // embed time
EmbedType int // kind of embedding
Files map[string]*EmbeddedFile // ALL embedded files by full path
Dirs map[string]*EmbeddedDir // ALL embedded dirs by full path
}
// Link creates the ChildDirs and ChildFiles links in all EmbeddedDir's
func (e *EmbeddedBox) Link() {
for path, ed := range e.Dirs {
fmt.Println(path)
ed.ChildDirs = make([]*EmbeddedDir, 0)
ed.ChildFiles = make([]*EmbeddedFile, 0)
}
for path, ed := range e.Dirs {
parentDirpath, _ := filepath.Split(path)
if strings.HasSuffix(parentDirpath, "/") {
parentDirpath = parentDirpath[:len(parentDirpath)-1]
}
parentDir := e.Dirs[parentDirpath]
if parentDir == nil {
panic("parentDir `" + parentDirpath + "` is missing in embedded box")
}
parentDir.ChildDirs = append(parentDir.ChildDirs, ed)
}
for path, ef := range e.Files {
dirpath, _ := filepath.Split(path)
if strings.HasSuffix(dirpath, "/") {
dirpath = dirpath[:len(dirpath)-1]
}
dir := e.Dirs[dirpath]
if dir == nil {
panic("dir `" + dirpath + "` is missing in embedded box")
}
dir.ChildFiles = append(dir.ChildFiles, ef)
}
}
// EmbeddedDir is instanced in the code generated by the rice tool and contains all necicary information about an embedded file
type EmbeddedDir struct {
Filename string
DirModTime time.Time
ChildDirs []*EmbeddedDir // direct childs, as returned by virtualDir.Readdir()
ChildFiles []*EmbeddedFile // direct childs, as returned by virtualDir.Readdir()
}
// EmbeddedFile is instanced in the code generated by the rice tool and contains all necicary information about an embedded file
type EmbeddedFile struct {
Filename string // filename
FileModTime time.Time
Content string
}
// EmbeddedBoxes is a public register of embedded boxes
var EmbeddedBoxes = make(map[string]*EmbeddedBox)
// RegisterEmbeddedBox registers an EmbeddedBox
func RegisterEmbeddedBox(name string, box *EmbeddedBox) {
if _, exists := EmbeddedBoxes[name]; exists {
panic(fmt.Sprintf("EmbeddedBox with name `%s` exists already", name))
}
EmbeddedBoxes[name] = box
}

View File

@ -1,69 +0,0 @@
package main
import (
"encoding/hex"
"fmt"
"log"
"net/http"
"os"
"text/template"
"github.com/GeertJohan/go.rice"
"github.com/davecgh/go-spew/spew"
)
func main() {
conf := rice.Config{
LocateOrder: []rice.LocateMethod{rice.LocateEmbedded, rice.LocateAppended, rice.LocateFS},
}
box, err := conf.FindBox("example-files")
if err != nil {
log.Fatalf("error opening rice.Box: %s\n", err)
}
// spew.Dump(box)
contentString, err := box.String("file.txt")
if err != nil {
log.Fatalf("could not read file contents as string: %s\n", err)
}
log.Printf("Read some file contents as string:\n%s\n", contentString)
contentBytes, err := box.Bytes("file.txt")
if err != nil {
log.Fatalf("could not read file contents as byteSlice: %s\n", err)
}
log.Printf("Read some file contents as byteSlice:\n%s\n", hex.Dump(contentBytes))
file, err := box.Open("file.txt")
if err != nil {
log.Fatalf("could not open file: %s\n", err)
}
spew.Dump(file)
// find/create a rice.Box
templateBox, err := rice.FindBox("example-templates")
if err != nil {
log.Fatal(err)
}
// get file contents as string
templateString, err := templateBox.String("message.tmpl")
if err != nil {
log.Fatal(err)
}
// parse and execute the template
tmplMessage, err := template.New("message").Parse(templateString)
if err != nil {
log.Fatal(err)
}
tmplMessage.Execute(os.Stdout, map[string]string{"Message": "Hello, world!"})
http.Handle("/", http.FileServer(box.HTTPBox()))
go func() {
fmt.Println("Serving files on :8080, press ctrl-C to exit")
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatalf("error serving files: %v", err)
}
}()
select {}
}

View File

@ -1,144 +0,0 @@
package rice
import (
"bytes"
"errors"
"os"
"path/filepath"
)
// File implements the io.Reader, io.Seeker, io.Closer and http.File interfaces
type File struct {
// File abstracts file methods so the user doesn't see the difference between rice.virtualFile, rice.virtualDir and os.File
// TODO: maybe use internal File interface and four implementations: *os.File, appendedFile, virtualFile, virtualDir
// real file on disk
realF *os.File
// when embedded (go)
virtualF *virtualFile
virtualD *virtualDir
// when appended (zip)
appendedF *appendedFile
appendedFileReader *bytes.Reader
// TODO: is appendedFileReader subject of races? Might need a lock here..
}
// Close is like (*os.File).Close()
// Visit http://golang.org/pkg/os/#File.Close for more information
func (f *File) Close() error {
if f.appendedF != nil {
if f.appendedFileReader == nil {
return errors.New("already closed")
}
f.appendedFileReader = nil
return nil
}
if f.virtualF != nil {
return f.virtualF.close()
}
if f.virtualD != nil {
return f.virtualD.close()
}
return f.realF.Close()
}
// Stat is like (*os.File).Stat()
// Visit http://golang.org/pkg/os/#File.Stat for more information
func (f *File) Stat() (os.FileInfo, error) {
if f.appendedF != nil {
if f.appendedF.dir {
return f.appendedF.dirInfo, nil
}
if f.appendedFileReader == nil {
return nil, errors.New("file is closed")
}
return f.appendedF.zipFile.FileInfo(), nil
}
if f.virtualF != nil {
return f.virtualF.stat()
}
if f.virtualD != nil {
return f.virtualD.stat()
}
return f.realF.Stat()
}
// Readdir is like (*os.File).Readdir()
// Visit http://golang.org/pkg/os/#File.Readdir for more information
func (f *File) Readdir(count int) ([]os.FileInfo, error) {
if f.appendedF != nil {
if f.appendedF.dir {
fi := make([]os.FileInfo, 0, len(f.appendedF.children))
for _, childAppendedFile := range f.appendedF.children {
if childAppendedFile.dir {
fi = append(fi, childAppendedFile.dirInfo)
} else {
fi = append(fi, childAppendedFile.zipFile.FileInfo())
}
}
return fi, nil
}
//++ TODO: is os.ErrInvalid the correct error for Readdir on file?
return nil, os.ErrInvalid
}
if f.virtualF != nil {
return f.virtualF.readdir(count)
}
if f.virtualD != nil {
return f.virtualD.readdir(count)
}
return f.realF.Readdir(count)
}
// Read is like (*os.File).Read()
// Visit http://golang.org/pkg/os/#File.Read for more information
func (f *File) Read(bts []byte) (int, error) {
if f.appendedF != nil {
if f.appendedFileReader == nil {
return 0, &os.PathError{
Op: "read",
Path: filepath.Base(f.appendedF.zipFile.Name),
Err: errors.New("file is closed"),
}
}
if f.appendedF.dir {
return 0, &os.PathError{
Op: "read",
Path: filepath.Base(f.appendedF.zipFile.Name),
Err: errors.New("is a directory"),
}
}
return f.appendedFileReader.Read(bts)
}
if f.virtualF != nil {
return f.virtualF.read(bts)
}
if f.virtualD != nil {
return f.virtualD.read(bts)
}
return f.realF.Read(bts)
}
// Seek is like (*os.File).Seek()
// Visit http://golang.org/pkg/os/#File.Seek for more information
func (f *File) Seek(offset int64, whence int) (int64, error) {
if f.appendedF != nil {
if f.appendedFileReader == nil {
return 0, &os.PathError{
Op: "seek",
Path: filepath.Base(f.appendedF.zipFile.Name),
Err: errors.New("file is closed"),
}
}
return f.appendedFileReader.Seek(offset, whence)
}
if f.virtualF != nil {
return f.virtualF.seek(offset, whence)
}
if f.virtualD != nil {
return f.virtualD.seek(offset, whence)
}
return f.realF.Seek(offset, whence)
}

View File

@ -1,21 +0,0 @@
package rice
import (
"net/http"
)
// HTTPBox implements http.FileSystem which allows the use of Box with a http.FileServer.
// e.g.: http.Handle("/", http.FileServer(rice.MustFindBox("http-files").HTTPBox()))
type HTTPBox struct {
*Box
}
// HTTPBox creates a new HTTPBox from an existing Box
func (b *Box) HTTPBox() *HTTPBox {
return &HTTPBox{b}
}
// Open returns a File using the http.File interface
func (hb *HTTPBox) Open(name string) (http.File, error) {
return hb.Box.Open(name)
}

View File

@ -1,172 +0,0 @@
package main
import (
"archive/zip"
"fmt"
"go/build"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/daaku/go.zipexe"
)
func operationAppend(pkgs []*build.Package) {
if runtime.GOOS == "windows" {
_, err := exec.LookPath("zip")
if err != nil {
fmt.Println("#### WARNING ! ####")
fmt.Println("`rice append` is known not to work under windows because the `zip` command is not available. Please let me know if you got this to work (and how).")
}
}
// MARKED FOR DELETION
// This is actually not required, the append command now has the option --exec required.
// // check if package is a command
// if !pkg.IsCommand() {
// fmt.Println("Error: can not append to non-main package. Please follow instructions at github.com/GeertJohan/go.rice")
// os.Exit(1)
// }
// create tmp zipfile
tmpZipfileName := filepath.Join(os.TempDir(), fmt.Sprintf("ricebox-%d-%s.zip", time.Now().Unix(), randomString(10)))
verbosef("Will create tmp zipfile: %s\n", tmpZipfileName)
tmpZipfile, err := os.Create(tmpZipfileName)
if err != nil {
fmt.Printf("Error creating tmp zipfile: %s\n", err)
os.Exit(1)
}
defer func() {
tmpZipfile.Close()
os.Remove(tmpZipfileName)
}()
// find abs path for binary file
binfileName, err := filepath.Abs(flags.Append.Executable)
if err != nil {
fmt.Printf("Error finding absolute path for executable to append: %s\n", err)
os.Exit(1)
}
verbosef("Will append to file: %s\n", binfileName)
// check that command doesn't already have zip appended
if rd, _ := zipexe.Open(binfileName); rd != nil {
fmt.Printf("Cannot append to already appended executable. Please remove %s and build a fresh one.\n", binfileName)
os.Exit(1)
}
// open binfile
binfile, err := os.OpenFile(binfileName, os.O_WRONLY, os.ModeAppend)
if err != nil {
fmt.Printf("Error: unable to open executable file: %s\n", err)
os.Exit(1)
}
// create zip.Writer
zipWriter := zip.NewWriter(tmpZipfile)
for _, pkg := range pkgs {
// find boxes for this command
boxMap := findBoxes(pkg)
// notify user when no calls to rice.FindBox are made (is this an error and therefore os.Exit(1) ?
if len(boxMap) == 0 {
fmt.Printf("no calls to rice.FindBox() or rice.MustFindBox() found in import path `%s`\n", pkg.ImportPath)
continue
}
verbosef("\n")
for boxname := range boxMap {
appendedBoxName := strings.Replace(boxname, `/`, `-`, -1)
// walk box path's and insert files
boxPath := filepath.Clean(filepath.Join(pkg.Dir, boxname))
filepath.Walk(boxPath, func(path string, info os.FileInfo, err error) error {
if info == nil {
fmt.Printf("Error: box \"%s\" not found on disk\n", path)
os.Exit(1)
}
// create zipFilename
zipFileName := filepath.Join(appendedBoxName, strings.TrimPrefix(path, boxPath))
// write directories as empty file with comment "dir"
if info.IsDir() {
_, err := zipWriter.CreateHeader(&zip.FileHeader{
Name: zipFileName,
Comment: "dir",
})
if err != nil {
fmt.Printf("Error creating dir in tmp zip: %s\n", err)
os.Exit(1)
}
return nil
}
// create zipFileWriter
zipFileHeader, err := zip.FileInfoHeader(info)
if err != nil {
fmt.Printf("Error creating zip FileHeader: %v\n", err)
os.Exit(1)
}
zipFileHeader.Name = zipFileName
zipFileWriter, err := zipWriter.CreateHeader(zipFileHeader)
if err != nil {
fmt.Printf("Error creating file in tmp zip: %s\n", err)
os.Exit(1)
}
srcFile, err := os.Open(path)
if err != nil {
fmt.Printf("Error opening file to append: %s\n", err)
os.Exit(1)
}
_, err = io.Copy(zipFileWriter, srcFile)
if err != nil {
fmt.Printf("Error copying file contents to zip: %s\n", err)
os.Exit(1)
}
srcFile.Close()
return nil
})
}
}
err = zipWriter.Close()
if err != nil {
fmt.Printf("Error closing tmp zipfile: %s\n", err)
os.Exit(1)
}
err = tmpZipfile.Sync()
if err != nil {
fmt.Printf("Error syncing tmp zipfile: %s\n", err)
os.Exit(1)
}
_, err = tmpZipfile.Seek(0, 0)
if err != nil {
fmt.Printf("Error seeking tmp zipfile: %s\n", err)
os.Exit(1)
}
_, err = binfile.Seek(0, 2)
if err != nil {
fmt.Printf("Error seeking bin file: %s\n", err)
os.Exit(1)
}
_, err = io.Copy(binfile, tmpZipfile)
if err != nil {
fmt.Printf("Error appending zipfile to executable: %s\n", err)
os.Exit(1)
}
zipA := exec.Command("zip", "-A", binfileName)
err = zipA.Run()
if err != nil {
fmt.Printf("Error setting zip offset: %s\n", err)
os.Exit(1)
}
}

View File

@ -1,33 +0,0 @@
package main
import (
"fmt"
"go/build"
"os"
"path/filepath"
"strings"
)
func operationClean(pkg *build.Package) {
filepath.Walk(pkg.Dir, func(filename string, info os.FileInfo, err error) error {
if err != nil {
fmt.Printf("error walking pkg dir to clean files: %v\n", err)
os.Exit(1)
}
if info.IsDir() {
return nil
}
verbosef("checking file '%s'\n", filename)
if filepath.Base(filename) == "rice-box.go" ||
strings.HasSuffix(filename, ".rice-box.go") ||
strings.HasSuffix(filename, ".rice-box.syso") {
err := os.Remove(filename)
if err != nil {
fmt.Printf("error removing file (%s): %s\n", filename, err)
os.Exit(-1)
}
verbosef("removed file '%s'\n", filename)
}
return nil
})
}

View File

@ -1,158 +0,0 @@
package main
import (
"bytes"
"fmt"
"go/build"
"go/format"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
)
const boxFilename = "rice-box.go"
func operationEmbedGo(pkg *build.Package) {
boxMap := findBoxes(pkg)
// notify user when no calls to rice.FindBox are made (is this an error and therefore os.Exit(1) ?
if len(boxMap) == 0 {
fmt.Println("no calls to rice.FindBox() found")
return
}
verbosef("\n")
var boxes []*boxDataType
for boxname := range boxMap {
// find path and filename for this box
boxPath := filepath.Join(pkg.Dir, boxname)
// Check to see if the path for the box is a symbolic link. If so, simply
// box what the symbolic link points to. Note: the filepath.Walk function
// will NOT follow any nested symbolic links. This only handles the case
// where the root of the box is a symbolic link.
symPath, serr := os.Readlink(boxPath)
if serr == nil {
boxPath = symPath
}
// verbose info
verbosef("embedding box '%s' to '%s'\n", boxname, boxFilename)
// read box metadata
boxInfo, ierr := os.Stat(boxPath)
if ierr != nil {
fmt.Printf("Error: unable to access box at %s\n", boxPath)
os.Exit(1)
}
// create box datastructure (used by template)
box := &boxDataType{
BoxName: boxname,
UnixNow: boxInfo.ModTime().Unix(),
Files: make([]*fileDataType, 0),
Dirs: make(map[string]*dirDataType),
}
if !boxInfo.IsDir() {
fmt.Printf("Error: Box %s must point to a directory but points to %s instead\n",
boxname, boxPath)
os.Exit(1)
}
// fill box datastructure with file data
filepath.Walk(boxPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
fmt.Printf("error walking box: %s\n", err)
os.Exit(1)
}
filename := strings.TrimPrefix(path, boxPath)
filename = strings.Replace(filename, "\\", "/", -1)
filename = strings.TrimPrefix(filename, "/")
if info.IsDir() {
dirData := &dirDataType{
Identifier: "dir" + nextIdentifier(),
FileName: filename,
ModTime: info.ModTime().Unix(),
ChildFiles: make([]*fileDataType, 0),
ChildDirs: make([]*dirDataType, 0),
}
verbosef("\tincludes dir: '%s'\n", dirData.FileName)
box.Dirs[dirData.FileName] = dirData
// add tree entry (skip for root, it'll create a recursion)
if dirData.FileName != "" {
pathParts := strings.Split(dirData.FileName, "/")
parentDir := box.Dirs[strings.Join(pathParts[:len(pathParts)-1], "/")]
parentDir.ChildDirs = append(parentDir.ChildDirs, dirData)
}
} else {
fileData := &fileDataType{
Identifier: "file" + nextIdentifier(),
FileName: filename,
ModTime: info.ModTime().Unix(),
}
verbosef("\tincludes file: '%s'\n", fileData.FileName)
fileData.Content, err = ioutil.ReadFile(path)
if err != nil {
fmt.Printf("error reading file content while walking box: %s\n", err)
os.Exit(1)
}
box.Files = append(box.Files, fileData)
// add tree entry
pathParts := strings.Split(fileData.FileName, "/")
parentDir := box.Dirs[strings.Join(pathParts[:len(pathParts)-1], "/")]
if parentDir == nil {
fmt.Printf("Error: parent of %s is not within the box\n", path)
os.Exit(1)
}
parentDir.ChildFiles = append(parentDir.ChildFiles, fileData)
}
return nil
})
boxes = append(boxes, box)
}
embedSourceUnformated := bytes.NewBuffer(make([]byte, 0))
// execute template to buffer
err := tmplEmbeddedBox.Execute(
embedSourceUnformated,
embedFileDataType{pkg.Name, boxes},
)
if err != nil {
log.Printf("error writing embedded box to file (template execute): %s\n", err)
os.Exit(1)
}
// format the source code
embedSource, err := format.Source(embedSourceUnformated.Bytes())
if err != nil {
log.Printf("error formatting embedSource: %s\n", err)
os.Exit(1)
}
// create go file for box
boxFile, err := os.Create(filepath.Join(pkg.Dir, boxFilename))
if err != nil {
log.Printf("error creating embedded box file: %s\n", err)
os.Exit(1)
}
defer boxFile.Close()
// write source to file
_, err = io.Copy(boxFile, bytes.NewBuffer(embedSource))
if err != nil {
log.Printf("error writing embedSource to file: %s\n", err)
os.Exit(1)
}
}

View File

@ -1,204 +0,0 @@
package main
import (
"bytes"
"encoding/gob"
"fmt"
"go/build"
"io"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"text/template"
"github.com/GeertJohan/go.rice/embedded"
"github.com/akavel/rsrc/coff"
)
type sizedReader struct {
*bytes.Reader
}
func (s sizedReader) Size() int64 {
return int64(s.Len())
}
var tmplEmbeddedSysoHelper *template.Template
func init() {
var err error
tmplEmbeddedSysoHelper, err = template.New("embeddedSysoHelper").Parse(`package {{.Package}}
// ############# GENERATED CODE #####################
// ## This file was generated by the rice tool.
// ## Do not edit unless you know what you're doing.
// ##################################################
// extern char _bricebox_{{.Symname}}[], _ericebox_{{.Symname}};
// int get_{{.Symname}}_length() {
// return &_ericebox_{{.Symname}} - _bricebox_{{.Symname}};
// }
import "C"
import (
"bytes"
"encoding/gob"
"github.com/GeertJohan/go.rice/embedded"
"unsafe"
)
func init() {
ptr := unsafe.Pointer(&C._bricebox_{{.Symname}})
bts := C.GoBytes(ptr, C.get_{{.Symname}}_length())
embeddedBox := &embedded.EmbeddedBox{}
err := gob.NewDecoder(bytes.NewReader(bts)).Decode(embeddedBox)
if err != nil {
panic("error decoding embedded box: "+err.Error())
}
embeddedBox.Link()
embedded.RegisterEmbeddedBox(embeddedBox.Name, embeddedBox)
}`)
if err != nil {
panic("could not parse template embeddedSysoHelper: " + err.Error())
}
}
type embeddedSysoHelperData struct {
Package string
Symname string
}
func operationEmbedSyso(pkg *build.Package) {
regexpSynameReplacer := regexp.MustCompile(`[^a-z0-9_]`)
boxMap := findBoxes(pkg)
// notify user when no calls to rice.FindBox are made (is this an error and therefore os.Exit(1) ?
if len(boxMap) == 0 {
fmt.Println("no calls to rice.FindBox() found")
return
}
verbosef("\n")
for boxname := range boxMap {
// find path and filename for this box
boxPath := filepath.Join(pkg.Dir, boxname)
boxFilename := strings.Replace(boxname, "/", "-", -1)
boxFilename = strings.Replace(boxFilename, "..", "back", -1)
boxFilename = strings.Replace(boxFilename, ".", "-", -1)
// verbose info
verbosef("embedding box '%s'\n", boxname)
verbosef("\tto file %s\n", boxFilename)
// read box metadata
boxInfo, ierr := os.Stat(boxPath)
if ierr != nil {
fmt.Printf("Error: unable to access box at %s\n", boxPath)
os.Exit(1)
}
// create box datastructure (used by template)
box := &embedded.EmbeddedBox{
Name: boxname,
Time: boxInfo.ModTime(),
EmbedType: embedded.EmbedTypeSyso,
Files: make(map[string]*embedded.EmbeddedFile),
Dirs: make(map[string]*embedded.EmbeddedDir),
}
// fill box datastructure with file data
filepath.Walk(boxPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
fmt.Printf("error walking box: %s\n", err)
os.Exit(1)
}
filename := strings.TrimPrefix(path, boxPath)
filename = strings.Replace(filename, "\\", "/", -1)
filename = strings.TrimPrefix(filename, "/")
if info.IsDir() {
embeddedDir := &embedded.EmbeddedDir{
Filename: filename,
DirModTime: info.ModTime(),
}
verbosef("\tincludes dir: '%s'\n", embeddedDir.Filename)
box.Dirs[embeddedDir.Filename] = embeddedDir
// add tree entry (skip for root, it'll create a recursion)
if embeddedDir.Filename != "" {
pathParts := strings.Split(embeddedDir.Filename, "/")
parentDir := box.Dirs[strings.Join(pathParts[:len(pathParts)-1], "/")]
parentDir.ChildDirs = append(parentDir.ChildDirs, embeddedDir)
}
} else {
embeddedFile := &embedded.EmbeddedFile{
Filename: filename,
FileModTime: info.ModTime(),
Content: "",
}
verbosef("\tincludes file: '%s'\n", embeddedFile.Filename)
contentBytes, err := ioutil.ReadFile(path)
if err != nil {
fmt.Printf("error reading file content while walking box: %s\n", err)
os.Exit(1)
}
embeddedFile.Content = string(contentBytes)
box.Files[embeddedFile.Filename] = embeddedFile
}
return nil
})
// encode embedded box to gob file
boxGobBuf := &bytes.Buffer{}
err := gob.NewEncoder(boxGobBuf).Encode(box)
if err != nil {
fmt.Printf("error encoding box to gob: %v\n", err)
os.Exit(1)
}
verbosef("gob-encoded embeddedBox is %d bytes large\n", boxGobBuf.Len())
// write coff
symname := regexpSynameReplacer.ReplaceAllString(boxname, "_")
createCoffSyso(boxname, symname, "386", boxGobBuf.Bytes())
createCoffSyso(boxname, symname, "amd64", boxGobBuf.Bytes())
// write go
sysoHelperData := embeddedSysoHelperData{
Package: pkg.Name,
Symname: symname,
}
fileSysoHelper, err := os.Create(boxFilename + ".rice-box.go")
if err != nil {
fmt.Printf("error creating syso helper: %v\n", err)
os.Exit(1)
}
err = tmplEmbeddedSysoHelper.Execute(fileSysoHelper, sysoHelperData)
if err != nil {
fmt.Printf("error executing tmplEmbeddedSysoHelper: %v\n", err)
os.Exit(1)
}
}
}
func createCoffSyso(boxFilename string, symname string, arch string, data []byte) {
boxCoff := coff.NewRDATA()
switch arch {
case "386":
case "amd64":
boxCoff.FileHeader.Machine = 0x8664
default:
panic("invalid arch")
}
boxCoff.AddData("_bricebox_"+symname, sizedReader{bytes.NewReader(data)})
boxCoff.AddData("_ericebox_"+symname, io.NewSectionReader(strings.NewReader("\000\000"), 0, 2)) // TODO: why? copied from rsrc, which copied it from as-generated
boxCoff.Freeze()
err := writeCoff(boxCoff, boxFilename+"_"+arch+".rice-box.syso")
if err != nil {
fmt.Printf("error writing %s coff/.syso: %v\n", arch, err)
os.Exit(1)
}
}

View File

@ -1,150 +0,0 @@
package main
import (
"fmt"
"go/ast"
"go/build"
"go/parser"
"go/token"
"os"
"path/filepath"
"strings"
)
func badArgument(fileset *token.FileSet, p token.Pos) {
pos := fileset.Position(p)
filename := pos.Filename
base, err := os.Getwd()
if err == nil {
rpath, perr := filepath.Rel(base, pos.Filename)
if perr == nil {
filename = rpath
}
}
msg := fmt.Sprintf("%s:%d: Error: found call to rice.FindBox, "+
"but argument must be a string literal.\n", filename, pos.Line)
fmt.Println(msg)
os.Exit(1)
}
func findBoxes(pkg *build.Package) map[string]bool {
// create map of boxes to embed
var boxMap = make(map[string]bool)
// create one list of files for this package
filenames := make([]string, 0, len(pkg.GoFiles)+len(pkg.CgoFiles))
filenames = append(filenames, pkg.GoFiles...)
filenames = append(filenames, pkg.CgoFiles...)
// loop over files, search for rice.FindBox(..) calls
for _, filename := range filenames {
// find full filepath
fullpath := filepath.Join(pkg.Dir, filename)
if strings.HasSuffix(filename, "rice-box.go") {
// Ignore *.rice-box.go files
verbosef("skipping file %q\n", fullpath)
continue
}
verbosef("scanning file %q\n", fullpath)
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, fullpath, nil, 0)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
var riceIsImported bool
ricePkgName := "rice"
for _, imp := range f.Imports {
if strings.HasSuffix(imp.Path.Value, "go.rice\"") {
if imp.Name != nil {
ricePkgName = imp.Name.Name
}
riceIsImported = true
break
}
}
if !riceIsImported {
// Rice wasn't imported, so we won't find a box.
continue
}
if ricePkgName == "_" {
// Rice pkg is unnamed, so we won't find a box.
continue
}
// Inspect AST, looking for calls to (Must)?FindBox.
// First parameter of the func must be a basic literal.
// Identifiers won't be resolved.
var nextIdentIsBoxFunc bool
var nextBasicLitParamIsBoxName bool
var boxCall token.Pos
var variableToRemember string
var validVariablesForBoxes map[string]bool = make(map[string]bool)
ast.Inspect(f, func(node ast.Node) bool {
if node == nil {
return false
}
switch x := node.(type) {
// this case fixes the var := func() style assignments, not assignments to vars declared separately from the assignment.
case *ast.AssignStmt:
var assign = node.(*ast.AssignStmt)
name, found := assign.Lhs[0].(*ast.Ident)
if found {
variableToRemember = name.Name
composite, first := assign.Rhs[0].(*ast.CompositeLit)
if first {
riceSelector, second := composite.Type.(*ast.SelectorExpr)
if second {
callCorrect := riceSelector.Sel.Name == "Config"
packageName, third := riceSelector.X.(*ast.Ident)
if third && callCorrect && packageName.Name == ricePkgName {
validVariablesForBoxes[name.Name] = true
verbosef("\tfound variable, saving to scan for boxes: %q\n", name.Name)
}
}
}
}
case *ast.Ident:
if nextIdentIsBoxFunc || ricePkgName == "." {
nextIdentIsBoxFunc = false
if x.Name == "FindBox" || x.Name == "MustFindBox" {
nextBasicLitParamIsBoxName = true
boxCall = x.Pos()
}
} else {
if x.Name == ricePkgName || validVariablesForBoxes[x.Name] {
nextIdentIsBoxFunc = true
}
}
case *ast.BasicLit:
if nextBasicLitParamIsBoxName {
if x.Kind == token.STRING {
nextBasicLitParamIsBoxName = false
// trim "" or ``
name := x.Value[1 : len(x.Value)-1]
boxMap[name] = true
verbosef("\tfound box %q\n", name)
} else {
badArgument(fset, boxCall)
}
}
default:
if nextIdentIsBoxFunc {
nextIdentIsBoxFunc = false
}
if nextBasicLitParamIsBoxName {
badArgument(fset, boxCall)
}
}
return true
})
}
return boxMap
}

View File

@ -1,80 +0,0 @@
package main
import (
"fmt"
"go/build"
"os"
goflags "github.com/jessevdk/go-flags" // rename import to `goflags` (file scope) so we can use `var flags` (package scope)
)
// flags
var flags struct {
Verbose bool `long:"verbose" short:"v" description:"Show verbose debug information"`
ImportPaths []string `long:"import-path" short:"i" description:"Import path(s) to use. Using PWD when left empty. Specify multiple times for more import paths to append"`
Append struct {
Executable string `long:"exec" description:"Executable to append" required:"true"`
} `command:"append"`
EmbedGo struct{} `command:"embed-go" alias:"embed"`
EmbedSyso struct{} `command:"embed-syso"`
Clean struct{} `command:"clean"`
}
// flags parser
var flagsParser *goflags.Parser
// initFlags parses the given flags.
// when the user asks for help (-h or --help): the application exists with status 0
// when unexpected flags is given: the application exits with status 1
func parseArguments() {
// create flags parser in global var, for flagsParser.Active.Name (operation)
flagsParser = goflags.NewParser(&flags, goflags.Default)
// parse flags
args, err := flagsParser.Parse()
if err != nil {
// assert the err to be a flags.Error
flagError := err.(*goflags.Error)
if flagError.Type == goflags.ErrHelp {
// user asked for help on flags.
// program can exit successfully
os.Exit(0)
}
if flagError.Type == goflags.ErrUnknownFlag {
fmt.Println("Use --help to view available options.")
os.Exit(1)
}
if flagError.Type == goflags.ErrRequired {
os.Exit(1)
}
fmt.Printf("Error parsing flags: %s\n", err)
os.Exit(1)
}
// error on left-over arguments
if len(args) > 0 {
fmt.Printf("Unexpected arguments: %s\nUse --help to view available options.", args)
os.Exit(1)
}
// default ImportPath to pwd when not set
if len(flags.ImportPaths) == 0 {
pwd, err := os.Getwd()
if err != nil {
fmt.Printf("error getting pwd: %s\n", err)
os.Exit(1)
}
verbosef("using pwd as import path\n")
// find non-absolute path for this pwd
pkg, err := build.ImportDir(pwd, build.FindOnly)
if err != nil {
fmt.Printf("error using current directory as import path: %s\n", err)
os.Exit(1)
}
flags.ImportPaths = append(flags.ImportPaths, pkg.ImportPath)
verbosef("using import paths: %s\n", flags.ImportPaths)
return
}
}

View File

@ -1,14 +0,0 @@
package main
import (
"strconv"
"github.com/GeertJohan/go.incremental"
)
var identifierCount incremental.Uint64
func nextIdentifier() string {
num := identifierCount.Next()
return strconv.FormatUint(num, 36) // 0123456789abcdefghijklmnopqrstuvwxyz
}

View File

@ -1,68 +0,0 @@
package main
import (
"fmt"
"go/build"
"log"
"os"
)
func main() {
// parser arguments
parseArguments()
// find package for path
var pkgs []*build.Package
for _, importPath := range flags.ImportPaths {
pkg := pkgForPath(importPath)
pkgs = append(pkgs, pkg)
}
// switch on the operation to perform
switch flagsParser.Active.Name {
case "embed", "embed-go":
for _, pkg := range pkgs {
operationEmbedGo(pkg)
}
case "embed-syso":
log.Println("WARNING: embedding .syso is experimental..")
for _, pkg := range pkgs {
operationEmbedSyso(pkg)
}
case "append":
operationAppend(pkgs)
case "clean":
for _, pkg := range pkgs {
operationClean(pkg)
}
}
// all done
verbosef("\n")
verbosef("rice finished successfully\n")
}
// helper function to get *build.Package for given path
func pkgForPath(path string) *build.Package {
// get pwd for relative imports
pwd, err := os.Getwd()
if err != nil {
fmt.Printf("error getting pwd (required for relative imports): %s\n", err)
os.Exit(1)
}
// read full package information
pkg, err := build.Import(path, pwd, 0)
if err != nil {
fmt.Printf("error reading package: %s\n", err)
os.Exit(1)
}
return pkg
}
func verbosef(format string, stuff ...interface{}) {
if flags.Verbose {
log.Printf(format, stuff...)
}
}

View File

@ -1,98 +0,0 @@
package main
import (
"fmt"
"os"
"text/template"
)
var tmplEmbeddedBox *template.Template
func init() {
var err error
// parse embedded box template
tmplEmbeddedBox, err = template.New("embeddedBox").Parse(`package {{.Package}}
import (
"github.com/GeertJohan/go.rice/embedded"
"time"
)
{{range .Boxes}}
func init() {
// define files
{{range .Files}}{{.Identifier}} := &embedded.EmbeddedFile{
Filename: ` + "`" + `{{.FileName}}` + "`" + `,
FileModTime: time.Unix({{.ModTime}}, 0),
Content: string({{.Content | printf "%q"}}),
}
{{end}}
// define dirs
{{range .Dirs}}{{.Identifier}} := &embedded.EmbeddedDir{
Filename: ` + "`" + `{{.FileName}}` + "`" + `,
DirModTime: time.Unix({{.ModTime}}, 0),
ChildFiles: []*embedded.EmbeddedFile{
{{range .ChildFiles}}{{.Identifier}}, // {{.FileName}}
{{end}}
},
}
{{end}}
// link ChildDirs
{{range .Dirs}}{{.Identifier}}.ChildDirs = []*embedded.EmbeddedDir{
{{range .ChildDirs}}{{.Identifier}}, // {{.FileName}}
{{end}}
}
{{end}}
// register embeddedBox
embedded.RegisterEmbeddedBox(` + "`" + `{{.BoxName}}` + "`" + `, &embedded.EmbeddedBox{
Name: ` + "`" + `{{.BoxName}}` + "`" + `,
Time: time.Unix({{.UnixNow}}, 0),
Dirs: map[string]*embedded.EmbeddedDir{
{{range .Dirs}}"{{.FileName}}": {{.Identifier}},
{{end}}
},
Files: map[string]*embedded.EmbeddedFile{
{{range .Files}}"{{.FileName}}": {{.Identifier}},
{{end}}
},
})
}
{{end}}`)
if err != nil {
fmt.Printf("error parsing embedded box template: %s\n", err)
os.Exit(-1)
}
}
type embedFileDataType struct {
Package string
Boxes []*boxDataType
}
type boxDataType struct {
BoxName string
UnixNow int64
Files []*fileDataType
Dirs map[string]*dirDataType
}
type fileDataType struct {
Identifier string
FileName string
Content []byte
ModTime int64
}
type dirDataType struct {
Identifier string
FileName string
Content []byte
ModTime int64
ChildDirs []*dirDataType
ChildFiles []*fileDataType
}

View File

@ -1,22 +0,0 @@
package main
import (
"math/rand"
"time"
)
// randomString generates a pseudo-random alpha-numeric string with given length.
func randomString(length int) string {
rand.Seed(time.Now().UnixNano())
k := make([]rune, length)
for i := 0; i < length; i++ {
c := rand.Intn(35)
if c < 10 {
c += 48 // numbers (0-9) (0+48 == 48 == '0', 9+48 == 57 == '9')
} else {
c += 87 // lower case alphabets (a-z) (10+87 == 97 == 'a', 35+87 == 122 = 'z')
}
k[i] = rune(c)
}
return string(k)
}

View File

@ -1,42 +0,0 @@
package main
import (
"fmt"
"os"
"reflect"
"github.com/akavel/rsrc/binutil"
"github.com/akavel/rsrc/coff"
)
// copied from github.com/akavel/rsrc
// LICENSE: MIT
// Copyright 2013-2014 The rsrc Authors. (https://github.com/akavel/rsrc/blob/master/AUTHORS)
func writeCoff(coff *coff.Coff, fnameout string) error {
out, err := os.Create(fnameout)
if err != nil {
return err
}
defer out.Close()
w := binutil.Writer{W: out}
// write the resulting file to disk
binutil.Walk(coff, func(v reflect.Value, path string) error {
if binutil.Plain(v.Kind()) {
w.WriteLE(v.Interface())
return nil
}
vv, ok := v.Interface().(binutil.SizedReader)
if ok {
w.WriteFromSized(vv)
return binutil.WALK_SKIP
}
return nil
})
if w.Err != nil {
return fmt.Errorf("Error writing output file: %s", w.Err)
}
return nil
}

View File

@ -1,19 +0,0 @@
package rice
import "os"
// SortByName allows an array of os.FileInfo objects
// to be easily sorted by filename using sort.Sort(SortByName(array))
type SortByName []os.FileInfo
func (f SortByName) Len() int { return len(f) }
func (f SortByName) Less(i, j int) bool { return f[i].Name() < f[j].Name() }
func (f SortByName) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
// SortByModified allows an array of os.FileInfo objects
// to be easily sorted by modified date using sort.Sort(SortByModified(array))
type SortByModified []os.FileInfo
func (f SortByModified) Len() int { return len(f) }
func (f SortByModified) Less(i, j int) bool { return f[i].ModTime().Unix() > f[j].ModTime().Unix() }
func (f SortByModified) Swap(i, j int) { f[i], f[j] = f[j], f[i] }

View File

@ -1,252 +0,0 @@
package rice
import (
"errors"
"io"
"os"
"path/filepath"
"sort"
"github.com/GeertJohan/go.rice/embedded"
)
//++ TODO: IDEA: merge virtualFile and virtualDir, this decreases work done by rice.File
// Error indicating some function is not implemented yet (but available to satisfy an interface)
var ErrNotImplemented = errors.New("not implemented yet")
// virtualFile is a 'stateful' virtual file.
// virtualFile wraps an *EmbeddedFile for a call to Box.Open() and virtualizes 'read cursor' (offset) and 'closing'.
// virtualFile is only internally visible and should be exposed through rice.File
type virtualFile struct {
*embedded.EmbeddedFile // the actual embedded file, embedded to obtain methods
offset int64 // read position on the virtual file
closed bool // closed when true
}
// create a new virtualFile for given EmbeddedFile
func newVirtualFile(ef *embedded.EmbeddedFile) *virtualFile {
vf := &virtualFile{
EmbeddedFile: ef,
offset: 0,
closed: false,
}
return vf
}
//++ TODO check for nil pointers in all these methods. When so: return os.PathError with Err: os.ErrInvalid
func (vf *virtualFile) close() error {
if vf.closed {
return &os.PathError{
Op: "close",
Path: vf.EmbeddedFile.Filename,
Err: errors.New("already closed"),
}
}
vf.EmbeddedFile = nil
vf.closed = true
return nil
}
func (vf *virtualFile) stat() (os.FileInfo, error) {
if vf.closed {
return nil, &os.PathError{
Op: "stat",
Path: vf.EmbeddedFile.Filename,
Err: errors.New("bad file descriptor"),
}
}
return (*embeddedFileInfo)(vf.EmbeddedFile), nil
}
func (vf *virtualFile) readdir(count int) ([]os.FileInfo, error) {
if vf.closed {
return nil, &os.PathError{
Op: "readdir",
Path: vf.EmbeddedFile.Filename,
Err: errors.New("bad file descriptor"),
}
}
//TODO: return proper error for a readdir() call on a file
return nil, ErrNotImplemented
}
func (vf *virtualFile) read(bts []byte) (int, error) {
if vf.closed {
return 0, &os.PathError{
Op: "read",
Path: vf.EmbeddedFile.Filename,
Err: errors.New("bad file descriptor"),
}
}
end := vf.offset + int64(len(bts))
if end >= int64(len(vf.Content)) {
// end of file, so return what we have + EOF
n := copy(bts, vf.Content[vf.offset:])
vf.offset = 0
return n, io.EOF
}
n := copy(bts, vf.Content[vf.offset:end])
vf.offset += int64(n)
return n, nil
}
func (vf *virtualFile) seek(offset int64, whence int) (int64, error) {
if vf.closed {
return 0, &os.PathError{
Op: "seek",
Path: vf.EmbeddedFile.Filename,
Err: errors.New("bad file descriptor"),
}
}
var e error
//++ TODO: check if this is correct implementation for seek
switch whence {
case os.SEEK_SET:
//++ check if new offset isn't out of bounds, set e when it is, then break out of switch
vf.offset = offset
case os.SEEK_CUR:
//++ check if new offset isn't out of bounds, set e when it is, then break out of switch
vf.offset += offset
case os.SEEK_END:
//++ check if new offset isn't out of bounds, set e when it is, then break out of switch
vf.offset = int64(len(vf.EmbeddedFile.Content)) - offset
}
if e != nil {
return 0, &os.PathError{
Op: "seek",
Path: vf.Filename,
Err: e,
}
}
return vf.offset, nil
}
// virtualDir is a 'stateful' virtual directory.
// virtualDir wraps an *EmbeddedDir for a call to Box.Open() and virtualizes 'closing'.
// virtualDir is only internally visible and should be exposed through rice.File
type virtualDir struct {
*embedded.EmbeddedDir
offset int // readdir position on the directory
closed bool
}
// create a new virtualDir for given EmbeddedDir
func newVirtualDir(ed *embedded.EmbeddedDir) *virtualDir {
vd := &virtualDir{
EmbeddedDir: ed,
offset: 0,
closed: false,
}
return vd
}
func (vd *virtualDir) close() error {
//++ TODO: needs sync mutex?
if vd.closed {
return &os.PathError{
Op: "close",
Path: vd.EmbeddedDir.Filename,
Err: errors.New("already closed"),
}
}
vd.closed = true
return nil
}
func (vd *virtualDir) stat() (os.FileInfo, error) {
if vd.closed {
return nil, &os.PathError{
Op: "stat",
Path: vd.EmbeddedDir.Filename,
Err: errors.New("bad file descriptor"),
}
}
return (*embeddedDirInfo)(vd.EmbeddedDir), nil
}
func (vd *virtualDir) readdir(n int) (fi []os.FileInfo, err error) {
if vd.closed {
return nil, &os.PathError{
Op: "readdir",
Path: vd.EmbeddedDir.Filename,
Err: errors.New("bad file descriptor"),
}
}
// Build up the array of our contents
var files []os.FileInfo
// Add the child directories
for _, child := range vd.ChildDirs {
child.Filename = filepath.Base(child.Filename)
files = append(files, (*embeddedDirInfo)(child))
}
// Add the child files
for _, child := range vd.ChildFiles {
child.Filename = filepath.Base(child.Filename)
files = append(files, (*embeddedFileInfo)(child))
}
// Sort it by filename (lexical order)
sort.Sort(SortByName(files))
// Return all contents if that's what is requested
if n <= 0 {
vd.offset = 0
return files, nil
}
// If user has requested past the end of our list
// return what we can and send an EOF
if vd.offset+n >= len(files) {
offset := vd.offset
vd.offset = 0
return files[offset:], io.EOF
}
offset := vd.offset
vd.offset += n
return files[offset : offset+n], nil
}
func (vd *virtualDir) read(bts []byte) (int, error) {
if vd.closed {
return 0, &os.PathError{
Op: "read",
Path: vd.EmbeddedDir.Filename,
Err: errors.New("bad file descriptor"),
}
}
return 0, &os.PathError{
Op: "read",
Path: vd.EmbeddedDir.Filename,
Err: errors.New("is a directory"),
}
}
func (vd *virtualDir) seek(offset int64, whence int) (int64, error) {
if vd.closed {
return 0, &os.PathError{
Op: "seek",
Path: vd.EmbeddedDir.Filename,
Err: errors.New("bad file descriptor"),
}
}
return 0, &os.PathError{
Op: "seek",
Path: vd.Filename,
Err: errors.New("is a directory"),
}
}

View File

@ -1,122 +0,0 @@
package rice
import (
"os"
"path/filepath"
"sort"
"strings"
)
// Walk is like filepath.Walk()
// Visit http://golang.org/pkg/path/filepath/#Walk for more information
func (b *Box) Walk(path string, walkFn filepath.WalkFunc) error {
pathFile, err := b.Open(path)
if err != nil {
return err
}
defer pathFile.Close()
pathInfo, err := pathFile.Stat()
if err != nil {
return err
}
if b.IsAppended() || b.IsEmbedded() {
return b.walk(path, pathInfo, walkFn)
}
// We don't have any embedded or appended box so use live filesystem mode
return filepath.Walk(b.absolutePath+string(os.PathSeparator)+path, func(path string, info os.FileInfo, err error) error {
// Strip out the box name from the returned paths
path = strings.TrimPrefix(path, b.absolutePath+string(os.PathSeparator))
return walkFn(path, info, err)
})
}
// walk recursively descends path.
// See walk() in $GOROOT/src/pkg/path/filepath/path.go
func (b *Box) walk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
err := walkFn(path, info, nil)
if err != nil {
if info.IsDir() && err == filepath.SkipDir {
return nil
}
return err
}
if !info.IsDir() {
return nil
}
names, err := b.readDirNames(path)
if err != nil {
return walkFn(path, info, err)
}
for _, name := range names {
filename := filepath.Join(path, name)
fileObject, err := b.Open(filename)
if err != nil {
return err
}
defer fileObject.Close()
fileInfo, err := fileObject.Stat()
if err != nil {
if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir {
return err
}
} else {
err = b.walk(filename, fileInfo, walkFn)
if err != nil {
if !fileInfo.IsDir() || err != filepath.SkipDir {
return err
}
}
}
}
return nil
}
// readDirNames reads the directory named by path and returns a sorted list of directory entries.
// See readDirNames() in $GOROOT/pkg/path/filepath/path.go
func (b *Box) readDirNames(path string) ([]string, error) {
f, err := b.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
stat, err := f.Stat()
if err != nil {
return nil, err
}
if !stat.IsDir() {
return nil, nil
}
infos, err := f.Readdir(0)
if err != nil {
return nil, err
}
var names []string
for _, info := range infos {
names = append(names, info.Name())
}
sort.Strings(names)
return names, nil
}

View File

@ -1,26 +0,0 @@
Copyright (c) 2014 The go-steam Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* The names of its contributors may not be used to endorse or promote
products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,178 +0,0 @@
package steam
import (
"crypto/sha1"
. "github.com/Philipp15b/go-steam/protocol"
. "github.com/Philipp15b/go-steam/protocol/protobuf"
. "github.com/Philipp15b/go-steam/protocol/steamlang"
. "github.com/Philipp15b/go-steam/steamid"
"github.com/golang/protobuf/proto"
"sync/atomic"
"time"
)
type Auth struct {
client *Client
details *LogOnDetails
}
type SentryHash []byte
type LogOnDetails struct {
Username string
Password string
AuthCode string
TwoFactorCode string
SentryFileHash SentryHash
}
// Log on with the given details. You must always specify username and
// password. For the first login, don't set an authcode or a hash and you'll receive an error
// and Steam will send you an authcode. Then you have to login again, this time with the authcode.
// Shortly after logging in, you'll receive a MachineAuthUpdateEvent with a hash which allows
// you to login without using an authcode in the future.
//
// If you don't use Steam Guard, username and password are enough.
func (a *Auth) LogOn(details *LogOnDetails) {
if len(details.Username) == 0 || len(details.Password) == 0 {
panic("Username and password must be set!")
}
logon := new(CMsgClientLogon)
logon.AccountName = &details.Username
logon.Password = &details.Password
if details.AuthCode != "" {
logon.AuthCode = proto.String(details.AuthCode)
}
if details.TwoFactorCode != "" {
logon.TwoFactorCode = proto.String(details.TwoFactorCode)
}
logon.ClientLanguage = proto.String("english")
logon.ProtocolVersion = proto.Uint32(MsgClientLogon_CurrentProtocol)
logon.ShaSentryfile = details.SentryFileHash
atomic.StoreUint64(&a.client.steamId, uint64(NewIdAdv(0, 1, int32(EUniverse_Public), int32(EAccountType_Individual))))
a.client.Write(NewClientMsgProtobuf(EMsg_ClientLogon, logon))
}
func (a *Auth) HandlePacket(packet *Packet) {
switch packet.EMsg {
case EMsg_ClientLogOnResponse:
a.handleLogOnResponse(packet)
case EMsg_ClientNewLoginKey:
a.handleLoginKey(packet)
case EMsg_ClientSessionToken:
case EMsg_ClientLoggedOff:
a.handleLoggedOff(packet)
case EMsg_ClientUpdateMachineAuth:
a.handleUpdateMachineAuth(packet)
case EMsg_ClientAccountInfo:
a.handleAccountInfo(packet)
case EMsg_ClientWalletInfoUpdate:
case EMsg_ClientRequestWebAPIAuthenticateUserNonceResponse:
case EMsg_ClientMarketingMessageUpdate:
}
}
func (a *Auth) handleLogOnResponse(packet *Packet) {
if !packet.IsProto {
a.client.Fatalf("Got non-proto logon response!")
return
}
body := new(CMsgClientLogonResponse)
msg := packet.ReadProtoMsg(body)
result := EResult(body.GetEresult())
if result == EResult_OK {
atomic.StoreInt32(&a.client.sessionId, msg.Header.Proto.GetClientSessionid())
atomic.StoreUint64(&a.client.steamId, msg.Header.Proto.GetSteamid())
a.client.Web.webLoginKey = *body.WebapiAuthenticateUserNonce
go a.client.heartbeatLoop(time.Duration(body.GetOutOfGameHeartbeatSeconds()))
a.client.Emit(&LoggedOnEvent{
Result: EResult(body.GetEresult()),
ExtendedResult: EResult(body.GetEresultExtended()),
OutOfGameSecsPerHeartbeat: body.GetOutOfGameHeartbeatSeconds(),
InGameSecsPerHeartbeat: body.GetInGameHeartbeatSeconds(),
PublicIp: body.GetPublicIp(),
ServerTime: body.GetRtime32ServerTime(),
AccountFlags: EAccountFlags(body.GetAccountFlags()),
ClientSteamId: SteamId(body.GetClientSuppliedSteamid()),
EmailDomain: body.GetEmailDomain(),
CellId: body.GetCellId(),
CellIdPingThreshold: body.GetCellIdPingThreshold(),
Steam2Ticket: body.GetSteam2Ticket(),
UsePics: body.GetUsePics(),
WebApiUserNonce: body.GetWebapiAuthenticateUserNonce(),
IpCountryCode: body.GetIpCountryCode(),
VanityUrl: body.GetVanityUrl(),
NumLoginFailuresToMigrate: body.GetCountLoginfailuresToMigrate(),
NumDisconnectsToMigrate: body.GetCountDisconnectsToMigrate(),
})
} else if result == EResult_Fail || result == EResult_ServiceUnavailable || result == EResult_TryAnotherCM {
// some error on Steam's side, we'll get an EOF later
} else {
a.client.Emit(&LogOnFailedEvent{
Result: EResult(body.GetEresult()),
})
a.client.Disconnect()
}
}
func (a *Auth) handleLoginKey(packet *Packet) {
body := new(CMsgClientNewLoginKey)
packet.ReadProtoMsg(body)
a.client.Write(NewClientMsgProtobuf(EMsg_ClientNewLoginKeyAccepted, &CMsgClientNewLoginKeyAccepted{
UniqueId: proto.Uint32(body.GetUniqueId()),
}))
a.client.Emit(&LoginKeyEvent{
UniqueId: body.GetUniqueId(),
LoginKey: body.GetLoginKey(),
})
}
func (a *Auth) handleLoggedOff(packet *Packet) {
result := EResult_Invalid
if packet.IsProto {
body := new(CMsgClientLoggedOff)
packet.ReadProtoMsg(body)
result = EResult(body.GetEresult())
} else {
body := new(MsgClientLoggedOff)
packet.ReadClientMsg(body)
result = body.Result
}
a.client.Emit(&LoggedOffEvent{Result: result})
}
func (a *Auth) handleUpdateMachineAuth(packet *Packet) {
body := new(CMsgClientUpdateMachineAuth)
packet.ReadProtoMsg(body)
hash := sha1.New()
hash.Write(packet.Data)
sha := hash.Sum(nil)
msg := NewClientMsgProtobuf(EMsg_ClientUpdateMachineAuthResponse, &CMsgClientUpdateMachineAuthResponse{
ShaFile: sha,
})
msg.SetTargetJobId(packet.SourceJobId)
a.client.Write(msg)
a.client.Emit(&MachineAuthUpdateEvent{sha})
}
func (a *Auth) handleAccountInfo(packet *Packet) {
body := new(CMsgClientAccountInfo)
packet.ReadProtoMsg(body)
a.client.Emit(&AccountInfoEvent{
PersonaName: body.GetPersonaName(),
Country: body.GetIpCountry(),
CountAuthedComputers: body.GetCountAuthedComputers(),
AccountFlags: EAccountFlags(body.GetAccountFlags()),
FacebookId: body.GetFacebookId(),
FacebookName: body.GetFacebookName(),
})
}

View File

@ -1,53 +0,0 @@
package steam
import (
. "github.com/Philipp15b/go-steam/protocol/steamlang"
. "github.com/Philipp15b/go-steam/steamid"
)
type LoggedOnEvent struct {
Result EResult
ExtendedResult EResult
OutOfGameSecsPerHeartbeat int32
InGameSecsPerHeartbeat int32
PublicIp uint32
ServerTime uint32
AccountFlags EAccountFlags
ClientSteamId SteamId `json:",string"`
EmailDomain string
CellId uint32
CellIdPingThreshold uint32
Steam2Ticket []byte
UsePics bool
WebApiUserNonce string
IpCountryCode string
VanityUrl string
NumLoginFailuresToMigrate int32
NumDisconnectsToMigrate int32
}
type LogOnFailedEvent struct {
Result EResult
}
type LoginKeyEvent struct {
UniqueId uint32
LoginKey string
}
type LoggedOffEvent struct {
Result EResult
}
type MachineAuthUpdateEvent struct {
Hash []byte
}
type AccountInfoEvent struct {
PersonaName string
Country string
CountAuthedComputers int32
AccountFlags EAccountFlags
FacebookId uint64 `json:",string"`
FacebookName string
}

View File

@ -1,383 +0,0 @@
package steam
import (
"bytes"
"compress/gzip"
"crypto/rand"
"encoding/binary"
"fmt"
"hash/crc32"
"io/ioutil"
"net"
"sync"
"sync/atomic"
"time"
"github.com/Philipp15b/go-steam/cryptoutil"
"github.com/Philipp15b/go-steam/netutil"
. "github.com/Philipp15b/go-steam/protocol"
. "github.com/Philipp15b/go-steam/protocol/protobuf"
. "github.com/Philipp15b/go-steam/protocol/steamlang"
. "github.com/Philipp15b/go-steam/steamid"
)
// Represents a client to the Steam network.
// Always poll events from the channel returned by Events() or receiving messages will stop.
// All access, unless otherwise noted, should be threadsafe.
//
// When a FatalErrorEvent is emitted, the connection is automatically closed. The same client can be used to reconnect.
// Other errors don't have any effect.
type Client struct {
// these need to be 64 bit aligned for sync/atomic on 32bit
sessionId int32
_ uint32
steamId uint64
currentJobId uint64
Auth *Auth
Social *Social
Web *Web
Notifications *Notifications
Trading *Trading
GC *GameCoordinator
events chan interface{}
handlers []PacketHandler
handlersMutex sync.RWMutex
tempSessionKey []byte
ConnectionTimeout time.Duration
mutex sync.RWMutex // guarding conn and writeChan
conn connection
writeChan chan IMsg
writeBuf *bytes.Buffer
heartbeat *time.Ticker
}
type PacketHandler interface {
HandlePacket(*Packet)
}
func NewClient() *Client {
client := &Client{
events: make(chan interface{}, 3),
writeBuf: new(bytes.Buffer),
}
client.Auth = &Auth{client: client}
client.RegisterPacketHandler(client.Auth)
client.Social = newSocial(client)
client.RegisterPacketHandler(client.Social)
client.Web = &Web{client: client}
client.RegisterPacketHandler(client.Web)
client.Notifications = newNotifications(client)
client.RegisterPacketHandler(client.Notifications)
client.Trading = &Trading{client: client}
client.RegisterPacketHandler(client.Trading)
client.GC = newGC(client)
client.RegisterPacketHandler(client.GC)
return client
}
// Get the event channel. By convention all events are pointers, except for errors.
// It is never closed.
func (c *Client) Events() <-chan interface{} {
return c.events
}
func (c *Client) Emit(event interface{}) {
c.events <- event
}
// Emits a FatalErrorEvent formatted with fmt.Errorf and disconnects.
func (c *Client) Fatalf(format string, a ...interface{}) {
c.Emit(FatalErrorEvent(fmt.Errorf(format, a...)))
c.Disconnect()
}
// Emits an error formatted with fmt.Errorf.
func (c *Client) Errorf(format string, a ...interface{}) {
c.Emit(fmt.Errorf(format, a...))
}
// Registers a PacketHandler that receives all incoming packets.
func (c *Client) RegisterPacketHandler(handler PacketHandler) {
c.handlersMutex.Lock()
defer c.handlersMutex.Unlock()
c.handlers = append(c.handlers, handler)
}
func (c *Client) GetNextJobId() JobId {
return JobId(atomic.AddUint64(&c.currentJobId, 1))
}
func (c *Client) SteamId() SteamId {
return SteamId(atomic.LoadUint64(&c.steamId))
}
func (c *Client) SessionId() int32 {
return atomic.LoadInt32(&c.sessionId)
}
func (c *Client) Connected() bool {
c.mutex.RLock()
defer c.mutex.RUnlock()
return c.conn != nil
}
// Connects to a random Steam server and returns its address.
// If this client is already connected, it is disconnected first.
// This method tries to use an address from the Steam Directory and falls
// back to the built-in server list if the Steam Directory can't be reached.
// If you want to connect to a specific server, use `ConnectTo`.
func (c *Client) Connect() *netutil.PortAddr {
var server *netutil.PortAddr
if steamDirectoryCache.IsInitialized() {
server = steamDirectoryCache.GetRandomCM()
} else {
server = GetRandomCM()
}
c.ConnectTo(server)
return server
}
// Connects to a specific server.
// You may want to use one of the `GetRandom*CM()` functions in this package.
// If this client is already connected, it is disconnected first.
func (c *Client) ConnectTo(addr *netutil.PortAddr) {
c.ConnectToBind(addr, nil)
}
// Connects to a specific server, and binds to a specified local IP
// If this client is already connected, it is disconnected first.
func (c *Client) ConnectToBind(addr *netutil.PortAddr, local *net.TCPAddr) {
c.Disconnect()
conn, err := dialTCP(local, addr.ToTCPAddr())
if err != nil {
c.Fatalf("Connect failed: %v", err)
return
}
c.conn = conn
c.writeChan = make(chan IMsg, 5)
go c.readLoop()
go c.writeLoop()
}
func (c *Client) Disconnect() {
c.mutex.Lock()
defer c.mutex.Unlock()
if c.conn == nil {
return
}
c.conn.Close()
c.conn = nil
if c.heartbeat != nil {
c.heartbeat.Stop()
}
close(c.writeChan)
c.Emit(&DisconnectedEvent{})
}
// Adds a message to the send queue. Modifications to the given message after
// writing are not allowed (possible race conditions).
//
// Writes to this client when not connected are ignored.
func (c *Client) Write(msg IMsg) {
if cm, ok := msg.(IClientMsg); ok {
cm.SetSessionId(c.SessionId())
cm.SetSteamId(c.SteamId())
}
c.mutex.RLock()
defer c.mutex.RUnlock()
if c.conn == nil {
return
}
c.writeChan <- msg
}
func (c *Client) readLoop() {
for {
// This *should* be atomic on most platforms, but the Go spec doesn't guarantee it
c.mutex.RLock()
conn := c.conn
c.mutex.RUnlock()
if conn == nil {
return
}
packet, err := conn.Read()
if err != nil {
c.Fatalf("Error reading from the connection: %v", err)
return
}
c.handlePacket(packet)
}
}
func (c *Client) writeLoop() {
for {
c.mutex.RLock()
conn := c.conn
c.mutex.RUnlock()
if conn == nil {
return
}
msg, ok := <-c.writeChan
if !ok {
return
}
err := msg.Serialize(c.writeBuf)
if err != nil {
c.writeBuf.Reset()
c.Fatalf("Error serializing message %v: %v", msg, err)
return
}
err = conn.Write(c.writeBuf.Bytes())
c.writeBuf.Reset()
if err != nil {
c.Fatalf("Error writing message %v: %v", msg, err)
return
}
}
}
func (c *Client) heartbeatLoop(seconds time.Duration) {
if c.heartbeat != nil {
c.heartbeat.Stop()
}
c.heartbeat = time.NewTicker(seconds * time.Second)
for {
_, ok := <-c.heartbeat.C
if !ok {
break
}
c.Write(NewClientMsgProtobuf(EMsg_ClientHeartBeat, new(CMsgClientHeartBeat)))
}
c.heartbeat = nil
}
func (c *Client) handlePacket(packet *Packet) {
switch packet.EMsg {
case EMsg_ChannelEncryptRequest:
c.handleChannelEncryptRequest(packet)
case EMsg_ChannelEncryptResult:
c.handleChannelEncryptResult(packet)
case EMsg_Multi:
c.handleMulti(packet)
case EMsg_ClientCMList:
c.handleClientCMList(packet)
}
c.handlersMutex.RLock()
defer c.handlersMutex.RUnlock()
for _, handler := range c.handlers {
handler.HandlePacket(packet)
}
}
func (c *Client) handleChannelEncryptRequest(packet *Packet) {
body := NewMsgChannelEncryptRequest()
packet.ReadMsg(body)
if body.Universe != EUniverse_Public {
c.Fatalf("Invalid univserse %v!", body.Universe)
}
c.tempSessionKey = make([]byte, 32)
rand.Read(c.tempSessionKey)
encryptedKey := cryptoutil.RSAEncrypt(GetPublicKey(EUniverse_Public), c.tempSessionKey)
payload := new(bytes.Buffer)
payload.Write(encryptedKey)
binary.Write(payload, binary.LittleEndian, crc32.ChecksumIEEE(encryptedKey))
payload.WriteByte(0)
payload.WriteByte(0)
payload.WriteByte(0)
payload.WriteByte(0)
c.Write(NewMsg(NewMsgChannelEncryptResponse(), payload.Bytes()))
}
func (c *Client) handleChannelEncryptResult(packet *Packet) {
body := NewMsgChannelEncryptResult()
packet.ReadMsg(body)
if body.Result != EResult_OK {
c.Fatalf("Encryption failed: %v", body.Result)
return
}
c.conn.SetEncryptionKey(c.tempSessionKey)
c.tempSessionKey = nil
c.Emit(&ConnectedEvent{})
}
func (c *Client) handleMulti(packet *Packet) {
body := new(CMsgMulti)
packet.ReadProtoMsg(body)
payload := body.GetMessageBody()
if body.GetSizeUnzipped() > 0 {
r, err := gzip.NewReader(bytes.NewReader(payload))
if err != nil {
c.Errorf("handleMulti: Error while decompressing: %v", err)
return
}
payload, err = ioutil.ReadAll(r)
if err != nil {
c.Errorf("handleMulti: Error while decompressing: %v", err)
return
}
}
pr := bytes.NewReader(payload)
for pr.Len() > 0 {
var length uint32
binary.Read(pr, binary.LittleEndian, &length)
packetData := make([]byte, length)
pr.Read(packetData)
p, err := NewPacket(packetData)
if err != nil {
c.Errorf("Error reading packet in Multi msg %v: %v", packet, err)
continue
}
c.handlePacket(p)
}
}
func (c *Client) handleClientCMList(packet *Packet) {
body := new(CMsgClientCMList)
packet.ReadProtoMsg(body)
l := make([]*netutil.PortAddr, 0)
for i, ip := range body.GetCmAddresses() {
l = append(l, &netutil.PortAddr{
readIp(ip),
uint16(body.GetCmPorts()[i]),
})
}
c.Emit(&ClientCMListEvent{l})
}
func readIp(ip uint32) net.IP {
r := make(net.IP, 4)
r[3] = byte(ip)
r[2] = byte(ip >> 8)
r[1] = byte(ip >> 16)
r[0] = byte(ip >> 24)
return r
}

View File

@ -1,20 +0,0 @@
package steam
import (
"github.com/Philipp15b/go-steam/netutil"
)
// When this event is emitted by the Client, the connection is automatically closed.
// This may be caused by a network error, for example.
type FatalErrorEvent error
type ConnectedEvent struct{}
type DisconnectedEvent struct{}
// A list of connection manager addresses to connect to in the future.
// You should always save them and then select one of these
// instead of the builtin ones for the next connection.
type ClientCMListEvent struct {
Addresses []*netutil.PortAddr
}

View File

@ -1,35 +0,0 @@
package community
import (
"net/http"
"net/http/cookiejar"
"net/url"
)
const cookiePath = "https://steamcommunity.com/"
func SetCookies(client *http.Client, sessionId, steamLogin, steamLoginSecure string) {
if client.Jar == nil {
client.Jar, _ = cookiejar.New(new(cookiejar.Options))
}
base, err := url.Parse(cookiePath)
if err != nil {
panic(err)
}
client.Jar.SetCookies(base, []*http.Cookie{
// It seems that, for some reason, Steam tries to URL-decode the cookie.
&http.Cookie{
Name: "sessionid",
Value: url.QueryEscape(sessionId),
},
// steamLogin is already URL-encoded.
&http.Cookie{
Name: "steamLogin",
Value: steamLogin,
},
&http.Cookie{
Name: "steamLoginSecure",
Value: steamLoginSecure,
},
})
}

View File

@ -1,127 +0,0 @@
package steam
import (
"crypto/aes"
"crypto/cipher"
"encoding/binary"
"fmt"
"io"
"net"
"sync"
"github.com/Philipp15b/go-steam/cryptoutil"
. "github.com/Philipp15b/go-steam/protocol"
)
type connection interface {
Read() (*Packet, error)
Write([]byte) error
Close() error
SetEncryptionKey([]byte)
IsEncrypted() bool
}
const tcpConnectionMagic uint32 = 0x31305456 // "VT01"
type tcpConnection struct {
conn *net.TCPConn
ciph cipher.Block
cipherMutex sync.RWMutex
}
func dialTCP(laddr, raddr *net.TCPAddr) (*tcpConnection, error) {
conn, err := net.DialTCP("tcp", laddr, raddr)
if err != nil {
return nil, err
}
return &tcpConnection{
conn: conn,
}, nil
}
func (c *tcpConnection) Read() (*Packet, error) {
// All packets begin with a packet length
var packetLen uint32
err := binary.Read(c.conn, binary.LittleEndian, &packetLen)
if err != nil {
return nil, err
}
// A magic value follows for validation
var packetMagic uint32
err = binary.Read(c.conn, binary.LittleEndian, &packetMagic)
if err != nil {
return nil, err
}
if packetMagic != tcpConnectionMagic {
return nil, fmt.Errorf("Invalid connection magic! Expected %d, got %d!", tcpConnectionMagic, packetMagic)
}
buf := make([]byte, packetLen, packetLen)
_, err = io.ReadFull(c.conn, buf)
if err == io.ErrUnexpectedEOF {
return nil, io.EOF
}
if err != nil {
return nil, err
}
// Packets after ChannelEncryptResult are encrypted
c.cipherMutex.RLock()
if c.ciph != nil {
buf = cryptoutil.SymmetricDecrypt(c.ciph, buf)
}
c.cipherMutex.RUnlock()
return NewPacket(buf)
}
// Writes a message. This may only be used by one goroutine at a time.
func (c *tcpConnection) Write(message []byte) error {
c.cipherMutex.RLock()
if c.ciph != nil {
message = cryptoutil.SymmetricEncrypt(c.ciph, message)
}
c.cipherMutex.RUnlock()
err := binary.Write(c.conn, binary.LittleEndian, uint32(len(message)))
if err != nil {
return err
}
err = binary.Write(c.conn, binary.LittleEndian, tcpConnectionMagic)
if err != nil {
return err
}
_, err = c.conn.Write(message)
return err
}
func (c *tcpConnection) Close() error {
return c.conn.Close()
}
func (c *tcpConnection) SetEncryptionKey(key []byte) {
c.cipherMutex.Lock()
defer c.cipherMutex.Unlock()
if key == nil {
c.ciph = nil
return
}
if len(key) != 32 {
panic("Connection AES key is not 32 bytes long!")
}
var err error
c.ciph, err = aes.NewCipher(key)
if err != nil {
panic(err)
}
}
func (c *tcpConnection) IsEncrypted() bool {
c.cipherMutex.RLock()
defer c.cipherMutex.RUnlock()
return c.ciph != nil
}

View File

@ -1,38 +0,0 @@
package cryptoutil
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
)
// Performs an encryption using AES/CBC/PKCS7
// with a random IV prepended using AES/ECB/None.
func SymmetricEncrypt(ciph cipher.Block, src []byte) []byte {
// get a random IV and ECB encrypt it
iv := make([]byte, aes.BlockSize, aes.BlockSize)
_, err := rand.Read(iv)
if err != nil {
panic(err)
}
encryptedIv := make([]byte, aes.BlockSize, aes.BlockSize)
newECBEncrypter(ciph).CryptBlocks(encryptedIv, iv)
// pad it, copy the IV to the first 16 bytes and encrypt the rest with CBC
encrypted := padPKCS7WithIV(src)
copy(encrypted, encryptedIv)
cipher.NewCBCEncrypter(ciph, iv).CryptBlocks(encrypted[aes.BlockSize:], encrypted[aes.BlockSize:])
return encrypted
}
// Decrypts data from the reader using AES/CBC/PKCS7 with an IV
// prepended using AES/ECB/None. The src slice may not be used anymore.
func SymmetricDecrypt(ciph cipher.Block, src []byte) []byte {
iv := src[:aes.BlockSize]
newECBDecrypter(ciph).CryptBlocks(iv, iv)
data := src[aes.BlockSize:]
cipher.NewCBCDecrypter(ciph, iv).CryptBlocks(data, data)
return unpadPKCS7(data)
}

View File

@ -1,68 +0,0 @@
package cryptoutil
import (
"crypto/cipher"
)
// From this code review: https://codereview.appspot.com/7860047/
// by fasmat for the Go crypto/cipher package
type ecb struct {
b cipher.Block
blockSize int
}
func newECB(b cipher.Block) *ecb {
return &ecb{
b: b,
blockSize: b.BlockSize(),
}
}
type ecbEncrypter ecb
// NewECBEncrypter returns a BlockMode which encrypts in electronic code book
// mode, using the given Block.
func newECBEncrypter(b cipher.Block) cipher.BlockMode {
return (*ecbEncrypter)(newECB(b))
}
func (x *ecbEncrypter) BlockSize() int { return x.blockSize }
func (x *ecbEncrypter) CryptBlocks(dst, src []byte) {
if len(src)%x.blockSize != 0 {
panic("cryptoutil/ecb: input not full blocks")
}
if len(dst) < len(src) {
panic("cryptoutil/ecb: output smaller than input")
}
for len(src) > 0 {
x.b.Encrypt(dst, src[:x.blockSize])
src = src[x.blockSize:]
dst = dst[x.blockSize:]
}
}
type ecbDecrypter ecb
// newECBDecrypter returns a BlockMode which decrypts in electronic code book
// mode, using the given Block.
func newECBDecrypter(b cipher.Block) cipher.BlockMode {
return (*ecbDecrypter)(newECB(b))
}
func (x *ecbDecrypter) BlockSize() int { return x.blockSize }
func (x *ecbDecrypter) CryptBlocks(dst, src []byte) {
if len(src)%x.blockSize != 0 {
panic("cryptoutil/ecb: input not full blocks")
}
if len(dst) < len(src) {
panic("cryptoutil/ecb: output smaller than input")
}
for len(src) > 0 {
x.b.Decrypt(dst, src[:x.blockSize])
src = src[x.blockSize:]
dst = dst[x.blockSize:]
}
}

View File

@ -1,25 +0,0 @@
package cryptoutil
import (
"crypto/aes"
)
// Returns a new byte array padded with PKCS7 and prepended
// with empty space of the AES block size (16 bytes) for the IV.
func padPKCS7WithIV(src []byte) []byte {
missing := aes.BlockSize - (len(src) % aes.BlockSize)
newSize := len(src) + aes.BlockSize + missing
dest := make([]byte, newSize, newSize)
copy(dest[aes.BlockSize:], src)
padding := byte(missing)
for i := newSize - missing; i < newSize; i++ {
dest[i] = padding
}
return dest
}
func unpadPKCS7(src []byte) []byte {
padLen := src[len(src)-1]
return src[:len(src)-int(padLen)]
}

View File

@ -1,31 +0,0 @@
package cryptoutil
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"errors"
)
// Parses a DER encoded RSA public key
func ParseASN1RSAPublicKey(derBytes []byte) (*rsa.PublicKey, error) {
key, err := x509.ParsePKIXPublicKey(derBytes)
if err != nil {
return nil, err
}
pubKey, ok := key.(*rsa.PublicKey)
if !ok {
return nil, errors.New("not an RSA public key")
}
return pubKey, nil
}
// Encrypts a message with the given public key using RSA-OAEP and the sha1 hash function.
func RSAEncrypt(pub *rsa.PublicKey, msg []byte) []byte {
b, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, pub, msg, nil)
if err != nil {
panic(err)
}
return b
}

View File

@ -1,53 +0,0 @@
/*
This package allows you to automate actions on Valve's Steam network. It is a Go port of SteamKit.
To login, you'll have to create a new Client first. Then connect to the Steam network
and wait for a ConnectedCallback. Then you may call the Login method in the Auth module
with your login information. This is covered in more detail in the method's documentation. After you've
received the LoggedOnEvent, you should set your persona state to online to receive friend lists etc.
Example code
You can also find a running example in the `gsbot` package.
package main
import (
"io/ioutil"
"log"
"github.com/Philipp15b/go-steam"
"github.com/Philipp15b/go-steam/protocol/steamlang"
)
func main() {
myLoginInfo := new(steam.LogOnDetails)
myLoginInfo.Username = "Your username"
myLoginInfo.Password = "Your password"
client := steam.NewClient()
client.Connect()
for event := range client.Events() {
switch e := event.(type) {
case *steam.ConnectedEvent:
client.Auth.LogOn(myLoginInfo)
case *steam.MachineAuthUpdateEvent:
ioutil.WriteFile("sentry", e.Hash, 0666)
case *steam.LoggedOnEvent:
client.Social.SetPersonaState(steamlang.EPersonaState_Online)
case steam.FatalErrorEvent:
log.Print(e)
case error:
log.Print(e)
}
}
}
Events
go-steam emits events that can be read via Client.Events(). Although the channel has the type interface{},
only types from this package ending with "Event" and errors will be emitted.
*/
package steam

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,579 +0,0 @@
// Code generated by protoc-gen-go.
// source: gcsystemmsgs.proto
// DO NOT EDIT!
package protobuf
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package protobuf is being compiled against.
const _ = proto.ProtoPackageIsVersion1
type EGCSystemMsg int32
const (
EGCSystemMsg_k_EGCMsgInvalid EGCSystemMsg = 0
EGCSystemMsg_k_EGCMsgMulti EGCSystemMsg = 1
EGCSystemMsg_k_EGCMsgGenericReply EGCSystemMsg = 10
EGCSystemMsg_k_EGCMsgSystemBase EGCSystemMsg = 50
EGCSystemMsg_k_EGCMsgAchievementAwarded EGCSystemMsg = 51
EGCSystemMsg_k_EGCMsgConCommand EGCSystemMsg = 52
EGCSystemMsg_k_EGCMsgStartPlaying EGCSystemMsg = 53
EGCSystemMsg_k_EGCMsgStopPlaying EGCSystemMsg = 54
EGCSystemMsg_k_EGCMsgStartGameserver EGCSystemMsg = 55
EGCSystemMsg_k_EGCMsgStopGameserver EGCSystemMsg = 56
EGCSystemMsg_k_EGCMsgWGRequest EGCSystemMsg = 57
EGCSystemMsg_k_EGCMsgWGResponse EGCSystemMsg = 58
EGCSystemMsg_k_EGCMsgGetUserGameStatsSchema EGCSystemMsg = 59
EGCSystemMsg_k_EGCMsgGetUserGameStatsSchemaResponse EGCSystemMsg = 60
EGCSystemMsg_k_EGCMsgGetUserStatsDEPRECATED EGCSystemMsg = 61
EGCSystemMsg_k_EGCMsgGetUserStatsResponse EGCSystemMsg = 62
EGCSystemMsg_k_EGCMsgAppInfoUpdated EGCSystemMsg = 63
EGCSystemMsg_k_EGCMsgValidateSession EGCSystemMsg = 64
EGCSystemMsg_k_EGCMsgValidateSessionResponse EGCSystemMsg = 65
EGCSystemMsg_k_EGCMsgLookupAccountFromInput EGCSystemMsg = 66
EGCSystemMsg_k_EGCMsgSendHTTPRequest EGCSystemMsg = 67
EGCSystemMsg_k_EGCMsgSendHTTPRequestResponse EGCSystemMsg = 68
EGCSystemMsg_k_EGCMsgPreTestSetup EGCSystemMsg = 69
EGCSystemMsg_k_EGCMsgRecordSupportAction EGCSystemMsg = 70
EGCSystemMsg_k_EGCMsgGetAccountDetails_DEPRECATED EGCSystemMsg = 71
EGCSystemMsg_k_EGCMsgReceiveInterAppMessage EGCSystemMsg = 73
EGCSystemMsg_k_EGCMsgFindAccounts EGCSystemMsg = 74
EGCSystemMsg_k_EGCMsgPostAlert EGCSystemMsg = 75
EGCSystemMsg_k_EGCMsgGetLicenses EGCSystemMsg = 76
EGCSystemMsg_k_EGCMsgGetUserStats EGCSystemMsg = 77
EGCSystemMsg_k_EGCMsgGetCommands EGCSystemMsg = 78
EGCSystemMsg_k_EGCMsgGetCommandsResponse EGCSystemMsg = 79
EGCSystemMsg_k_EGCMsgAddFreeLicense EGCSystemMsg = 80
EGCSystemMsg_k_EGCMsgAddFreeLicenseResponse EGCSystemMsg = 81
EGCSystemMsg_k_EGCMsgGetIPLocation EGCSystemMsg = 82
EGCSystemMsg_k_EGCMsgGetIPLocationResponse EGCSystemMsg = 83
EGCSystemMsg_k_EGCMsgSystemStatsSchema EGCSystemMsg = 84
EGCSystemMsg_k_EGCMsgGetSystemStats EGCSystemMsg = 85
EGCSystemMsg_k_EGCMsgGetSystemStatsResponse EGCSystemMsg = 86
EGCSystemMsg_k_EGCMsgSendEmail EGCSystemMsg = 87
EGCSystemMsg_k_EGCMsgSendEmailResponse EGCSystemMsg = 88
EGCSystemMsg_k_EGCMsgGetEmailTemplate EGCSystemMsg = 89
EGCSystemMsg_k_EGCMsgGetEmailTemplateResponse EGCSystemMsg = 90
EGCSystemMsg_k_EGCMsgGrantGuestPass EGCSystemMsg = 91
EGCSystemMsg_k_EGCMsgGrantGuestPassResponse EGCSystemMsg = 92
EGCSystemMsg_k_EGCMsgGetAccountDetails EGCSystemMsg = 93
EGCSystemMsg_k_EGCMsgGetAccountDetailsResponse EGCSystemMsg = 94
EGCSystemMsg_k_EGCMsgGetPersonaNames EGCSystemMsg = 95
EGCSystemMsg_k_EGCMsgGetPersonaNamesResponse EGCSystemMsg = 96
EGCSystemMsg_k_EGCMsgMultiplexMsg EGCSystemMsg = 97
EGCSystemMsg_k_EGCMsgWebAPIRegisterInterfaces EGCSystemMsg = 101
EGCSystemMsg_k_EGCMsgWebAPIJobRequest EGCSystemMsg = 102
EGCSystemMsg_k_EGCMsgWebAPIJobRequestHttpResponse EGCSystemMsg = 104
EGCSystemMsg_k_EGCMsgWebAPIJobRequestForwardResponse EGCSystemMsg = 105
EGCSystemMsg_k_EGCMsgMemCachedGet EGCSystemMsg = 200
EGCSystemMsg_k_EGCMsgMemCachedGetResponse EGCSystemMsg = 201
EGCSystemMsg_k_EGCMsgMemCachedSet EGCSystemMsg = 202
EGCSystemMsg_k_EGCMsgMemCachedDelete EGCSystemMsg = 203
EGCSystemMsg_k_EGCMsgMemCachedStats EGCSystemMsg = 204
EGCSystemMsg_k_EGCMsgMemCachedStatsResponse EGCSystemMsg = 205
EGCSystemMsg_k_EGCMsgSQLStats EGCSystemMsg = 210
EGCSystemMsg_k_EGCMsgSQLStatsResponse EGCSystemMsg = 211
EGCSystemMsg_k_EGCMsgMasterSetDirectory EGCSystemMsg = 220
EGCSystemMsg_k_EGCMsgMasterSetDirectoryResponse EGCSystemMsg = 221
EGCSystemMsg_k_EGCMsgMasterSetWebAPIRouting EGCSystemMsg = 222
EGCSystemMsg_k_EGCMsgMasterSetWebAPIRoutingResponse EGCSystemMsg = 223
EGCSystemMsg_k_EGCMsgMasterSetClientMsgRouting EGCSystemMsg = 224
EGCSystemMsg_k_EGCMsgMasterSetClientMsgRoutingResponse EGCSystemMsg = 225
EGCSystemMsg_k_EGCMsgSetOptions EGCSystemMsg = 226
EGCSystemMsg_k_EGCMsgSetOptionsResponse EGCSystemMsg = 227
EGCSystemMsg_k_EGCMsgSystemBase2 EGCSystemMsg = 500
EGCSystemMsg_k_EGCMsgGetPurchaseTrustStatus EGCSystemMsg = 501
EGCSystemMsg_k_EGCMsgGetPurchaseTrustStatusResponse EGCSystemMsg = 502
EGCSystemMsg_k_EGCMsgUpdateSession EGCSystemMsg = 503
EGCSystemMsg_k_EGCMsgGCAccountVacStatusChange EGCSystemMsg = 504
EGCSystemMsg_k_EGCMsgCheckFriendship EGCSystemMsg = 505
EGCSystemMsg_k_EGCMsgCheckFriendshipResponse EGCSystemMsg = 506
EGCSystemMsg_k_EGCMsgGetPartnerAccountLink EGCSystemMsg = 507
EGCSystemMsg_k_EGCMsgGetPartnerAccountLinkResponse EGCSystemMsg = 508
EGCSystemMsg_k_EGCMsgVSReportedSuspiciousActivity EGCSystemMsg = 509
EGCSystemMsg_k_EGCMsgDPPartnerMicroTxns EGCSystemMsg = 512
EGCSystemMsg_k_EGCMsgDPPartnerMicroTxnsResponse EGCSystemMsg = 513
EGCSystemMsg_k_EGCMsgGetIPASN EGCSystemMsg = 514
EGCSystemMsg_k_EGCMsgGetIPASNResponse EGCSystemMsg = 515
EGCSystemMsg_k_EGCMsgGetAppFriendsList EGCSystemMsg = 516
EGCSystemMsg_k_EGCMsgGetAppFriendsListResponse EGCSystemMsg = 517
)
var EGCSystemMsg_name = map[int32]string{
0: "k_EGCMsgInvalid",
1: "k_EGCMsgMulti",
10: "k_EGCMsgGenericReply",
50: "k_EGCMsgSystemBase",
51: "k_EGCMsgAchievementAwarded",
52: "k_EGCMsgConCommand",
53: "k_EGCMsgStartPlaying",
54: "k_EGCMsgStopPlaying",
55: "k_EGCMsgStartGameserver",
56: "k_EGCMsgStopGameserver",
57: "k_EGCMsgWGRequest",
58: "k_EGCMsgWGResponse",
59: "k_EGCMsgGetUserGameStatsSchema",
60: "k_EGCMsgGetUserGameStatsSchemaResponse",
61: "k_EGCMsgGetUserStatsDEPRECATED",
62: "k_EGCMsgGetUserStatsResponse",
63: "k_EGCMsgAppInfoUpdated",
64: "k_EGCMsgValidateSession",
65: "k_EGCMsgValidateSessionResponse",
66: "k_EGCMsgLookupAccountFromInput",
67: "k_EGCMsgSendHTTPRequest",
68: "k_EGCMsgSendHTTPRequestResponse",
69: "k_EGCMsgPreTestSetup",
70: "k_EGCMsgRecordSupportAction",
71: "k_EGCMsgGetAccountDetails_DEPRECATED",
73: "k_EGCMsgReceiveInterAppMessage",
74: "k_EGCMsgFindAccounts",
75: "k_EGCMsgPostAlert",
76: "k_EGCMsgGetLicenses",
77: "k_EGCMsgGetUserStats",
78: "k_EGCMsgGetCommands",
79: "k_EGCMsgGetCommandsResponse",
80: "k_EGCMsgAddFreeLicense",
81: "k_EGCMsgAddFreeLicenseResponse",
82: "k_EGCMsgGetIPLocation",
83: "k_EGCMsgGetIPLocationResponse",
84: "k_EGCMsgSystemStatsSchema",
85: "k_EGCMsgGetSystemStats",
86: "k_EGCMsgGetSystemStatsResponse",
87: "k_EGCMsgSendEmail",
88: "k_EGCMsgSendEmailResponse",
89: "k_EGCMsgGetEmailTemplate",
90: "k_EGCMsgGetEmailTemplateResponse",
91: "k_EGCMsgGrantGuestPass",
92: "k_EGCMsgGrantGuestPassResponse",
93: "k_EGCMsgGetAccountDetails",
94: "k_EGCMsgGetAccountDetailsResponse",
95: "k_EGCMsgGetPersonaNames",
96: "k_EGCMsgGetPersonaNamesResponse",
97: "k_EGCMsgMultiplexMsg",
101: "k_EGCMsgWebAPIRegisterInterfaces",
102: "k_EGCMsgWebAPIJobRequest",
104: "k_EGCMsgWebAPIJobRequestHttpResponse",
105: "k_EGCMsgWebAPIJobRequestForwardResponse",
200: "k_EGCMsgMemCachedGet",
201: "k_EGCMsgMemCachedGetResponse",
202: "k_EGCMsgMemCachedSet",
203: "k_EGCMsgMemCachedDelete",
204: "k_EGCMsgMemCachedStats",
205: "k_EGCMsgMemCachedStatsResponse",
210: "k_EGCMsgSQLStats",
211: "k_EGCMsgSQLStatsResponse",
220: "k_EGCMsgMasterSetDirectory",
221: "k_EGCMsgMasterSetDirectoryResponse",
222: "k_EGCMsgMasterSetWebAPIRouting",
223: "k_EGCMsgMasterSetWebAPIRoutingResponse",
224: "k_EGCMsgMasterSetClientMsgRouting",
225: "k_EGCMsgMasterSetClientMsgRoutingResponse",
226: "k_EGCMsgSetOptions",
227: "k_EGCMsgSetOptionsResponse",
500: "k_EGCMsgSystemBase2",
501: "k_EGCMsgGetPurchaseTrustStatus",
502: "k_EGCMsgGetPurchaseTrustStatusResponse",
503: "k_EGCMsgUpdateSession",
504: "k_EGCMsgGCAccountVacStatusChange",
505: "k_EGCMsgCheckFriendship",
506: "k_EGCMsgCheckFriendshipResponse",
507: "k_EGCMsgGetPartnerAccountLink",
508: "k_EGCMsgGetPartnerAccountLinkResponse",
509: "k_EGCMsgVSReportedSuspiciousActivity",
512: "k_EGCMsgDPPartnerMicroTxns",
513: "k_EGCMsgDPPartnerMicroTxnsResponse",
514: "k_EGCMsgGetIPASN",
515: "k_EGCMsgGetIPASNResponse",
516: "k_EGCMsgGetAppFriendsList",
517: "k_EGCMsgGetAppFriendsListResponse",
}
var EGCSystemMsg_value = map[string]int32{
"k_EGCMsgInvalid": 0,
"k_EGCMsgMulti": 1,
"k_EGCMsgGenericReply": 10,
"k_EGCMsgSystemBase": 50,
"k_EGCMsgAchievementAwarded": 51,
"k_EGCMsgConCommand": 52,
"k_EGCMsgStartPlaying": 53,
"k_EGCMsgStopPlaying": 54,
"k_EGCMsgStartGameserver": 55,
"k_EGCMsgStopGameserver": 56,
"k_EGCMsgWGRequest": 57,
"k_EGCMsgWGResponse": 58,
"k_EGCMsgGetUserGameStatsSchema": 59,
"k_EGCMsgGetUserGameStatsSchemaResponse": 60,
"k_EGCMsgGetUserStatsDEPRECATED": 61,
"k_EGCMsgGetUserStatsResponse": 62,
"k_EGCMsgAppInfoUpdated": 63,
"k_EGCMsgValidateSession": 64,
"k_EGCMsgValidateSessionResponse": 65,
"k_EGCMsgLookupAccountFromInput": 66,
"k_EGCMsgSendHTTPRequest": 67,
"k_EGCMsgSendHTTPRequestResponse": 68,
"k_EGCMsgPreTestSetup": 69,
"k_EGCMsgRecordSupportAction": 70,
"k_EGCMsgGetAccountDetails_DEPRECATED": 71,
"k_EGCMsgReceiveInterAppMessage": 73,
"k_EGCMsgFindAccounts": 74,
"k_EGCMsgPostAlert": 75,
"k_EGCMsgGetLicenses": 76,
"k_EGCMsgGetUserStats": 77,
"k_EGCMsgGetCommands": 78,
"k_EGCMsgGetCommandsResponse": 79,
"k_EGCMsgAddFreeLicense": 80,
"k_EGCMsgAddFreeLicenseResponse": 81,
"k_EGCMsgGetIPLocation": 82,
"k_EGCMsgGetIPLocationResponse": 83,
"k_EGCMsgSystemStatsSchema": 84,
"k_EGCMsgGetSystemStats": 85,
"k_EGCMsgGetSystemStatsResponse": 86,
"k_EGCMsgSendEmail": 87,
"k_EGCMsgSendEmailResponse": 88,
"k_EGCMsgGetEmailTemplate": 89,
"k_EGCMsgGetEmailTemplateResponse": 90,
"k_EGCMsgGrantGuestPass": 91,
"k_EGCMsgGrantGuestPassResponse": 92,
"k_EGCMsgGetAccountDetails": 93,
"k_EGCMsgGetAccountDetailsResponse": 94,
"k_EGCMsgGetPersonaNames": 95,
"k_EGCMsgGetPersonaNamesResponse": 96,
"k_EGCMsgMultiplexMsg": 97,
"k_EGCMsgWebAPIRegisterInterfaces": 101,
"k_EGCMsgWebAPIJobRequest": 102,
"k_EGCMsgWebAPIJobRequestHttpResponse": 104,
"k_EGCMsgWebAPIJobRequestForwardResponse": 105,
"k_EGCMsgMemCachedGet": 200,
"k_EGCMsgMemCachedGetResponse": 201,
"k_EGCMsgMemCachedSet": 202,
"k_EGCMsgMemCachedDelete": 203,
"k_EGCMsgMemCachedStats": 204,
"k_EGCMsgMemCachedStatsResponse": 205,
"k_EGCMsgSQLStats": 210,
"k_EGCMsgSQLStatsResponse": 211,
"k_EGCMsgMasterSetDirectory": 220,
"k_EGCMsgMasterSetDirectoryResponse": 221,
"k_EGCMsgMasterSetWebAPIRouting": 222,
"k_EGCMsgMasterSetWebAPIRoutingResponse": 223,
"k_EGCMsgMasterSetClientMsgRouting": 224,
"k_EGCMsgMasterSetClientMsgRoutingResponse": 225,
"k_EGCMsgSetOptions": 226,
"k_EGCMsgSetOptionsResponse": 227,
"k_EGCMsgSystemBase2": 500,
"k_EGCMsgGetPurchaseTrustStatus": 501,
"k_EGCMsgGetPurchaseTrustStatusResponse": 502,
"k_EGCMsgUpdateSession": 503,
"k_EGCMsgGCAccountVacStatusChange": 504,
"k_EGCMsgCheckFriendship": 505,
"k_EGCMsgCheckFriendshipResponse": 506,
"k_EGCMsgGetPartnerAccountLink": 507,
"k_EGCMsgGetPartnerAccountLinkResponse": 508,
"k_EGCMsgVSReportedSuspiciousActivity": 509,
"k_EGCMsgDPPartnerMicroTxns": 512,
"k_EGCMsgDPPartnerMicroTxnsResponse": 513,
"k_EGCMsgGetIPASN": 514,
"k_EGCMsgGetIPASNResponse": 515,
"k_EGCMsgGetAppFriendsList": 516,
"k_EGCMsgGetAppFriendsListResponse": 517,
}
func (x EGCSystemMsg) Enum() *EGCSystemMsg {
p := new(EGCSystemMsg)
*p = x
return p
}
func (x EGCSystemMsg) String() string {
return proto.EnumName(EGCSystemMsg_name, int32(x))
}
func (x *EGCSystemMsg) UnmarshalJSON(data []byte) error {
value, err := proto.UnmarshalJSONEnum(EGCSystemMsg_value, data, "EGCSystemMsg")
if err != nil {
return err
}
*x = EGCSystemMsg(value)
return nil
}
func (EGCSystemMsg) EnumDescriptor() ([]byte, []int) { return system_fileDescriptor0, []int{0} }
type ESOMsg int32
const (
ESOMsg_k_ESOMsg_Create ESOMsg = 21
ESOMsg_k_ESOMsg_Update ESOMsg = 22
ESOMsg_k_ESOMsg_Destroy ESOMsg = 23
ESOMsg_k_ESOMsg_CacheSubscribed ESOMsg = 24
ESOMsg_k_ESOMsg_CacheUnsubscribed ESOMsg = 25
ESOMsg_k_ESOMsg_UpdateMultiple ESOMsg = 26
ESOMsg_k_ESOMsg_CacheSubscriptionRefresh ESOMsg = 28
ESOMsg_k_ESOMsg_CacheSubscribedUpToDate ESOMsg = 29
)
var ESOMsg_name = map[int32]string{
21: "k_ESOMsg_Create",
22: "k_ESOMsg_Update",
23: "k_ESOMsg_Destroy",
24: "k_ESOMsg_CacheSubscribed",
25: "k_ESOMsg_CacheUnsubscribed",
26: "k_ESOMsg_UpdateMultiple",
28: "k_ESOMsg_CacheSubscriptionRefresh",
29: "k_ESOMsg_CacheSubscribedUpToDate",
}
var ESOMsg_value = map[string]int32{
"k_ESOMsg_Create": 21,
"k_ESOMsg_Update": 22,
"k_ESOMsg_Destroy": 23,
"k_ESOMsg_CacheSubscribed": 24,
"k_ESOMsg_CacheUnsubscribed": 25,
"k_ESOMsg_UpdateMultiple": 26,
"k_ESOMsg_CacheSubscriptionRefresh": 28,
"k_ESOMsg_CacheSubscribedUpToDate": 29,
}
func (x ESOMsg) Enum() *ESOMsg {
p := new(ESOMsg)
*p = x
return p
}
func (x ESOMsg) String() string {
return proto.EnumName(ESOMsg_name, int32(x))
}
func (x *ESOMsg) UnmarshalJSON(data []byte) error {
value, err := proto.UnmarshalJSONEnum(ESOMsg_value, data, "ESOMsg")
if err != nil {
return err
}
*x = ESOMsg(value)
return nil
}
func (ESOMsg) EnumDescriptor() ([]byte, []int) { return system_fileDescriptor0, []int{1} }
type EGCBaseClientMsg int32
const (
EGCBaseClientMsg_k_EMsgGCPingRequest EGCBaseClientMsg = 3001
EGCBaseClientMsg_k_EMsgGCPingResponse EGCBaseClientMsg = 3002
EGCBaseClientMsg_k_EMsgGCClientWelcome EGCBaseClientMsg = 4004
EGCBaseClientMsg_k_EMsgGCServerWelcome EGCBaseClientMsg = 4005
EGCBaseClientMsg_k_EMsgGCClientHello EGCBaseClientMsg = 4006
EGCBaseClientMsg_k_EMsgGCServerHello EGCBaseClientMsg = 4007
EGCBaseClientMsg_k_EMsgGCClientConnectionStatus EGCBaseClientMsg = 4009
EGCBaseClientMsg_k_EMsgGCServerConnectionStatus EGCBaseClientMsg = 4010
)
var EGCBaseClientMsg_name = map[int32]string{
3001: "k_EMsgGCPingRequest",
3002: "k_EMsgGCPingResponse",
4004: "k_EMsgGCClientWelcome",
4005: "k_EMsgGCServerWelcome",
4006: "k_EMsgGCClientHello",
4007: "k_EMsgGCServerHello",
4009: "k_EMsgGCClientConnectionStatus",
4010: "k_EMsgGCServerConnectionStatus",
}
var EGCBaseClientMsg_value = map[string]int32{
"k_EMsgGCPingRequest": 3001,
"k_EMsgGCPingResponse": 3002,
"k_EMsgGCClientWelcome": 4004,
"k_EMsgGCServerWelcome": 4005,
"k_EMsgGCClientHello": 4006,
"k_EMsgGCServerHello": 4007,
"k_EMsgGCClientConnectionStatus": 4009,
"k_EMsgGCServerConnectionStatus": 4010,
}
func (x EGCBaseClientMsg) Enum() *EGCBaseClientMsg {
p := new(EGCBaseClientMsg)
*p = x
return p
}
func (x EGCBaseClientMsg) String() string {
return proto.EnumName(EGCBaseClientMsg_name, int32(x))
}
func (x *EGCBaseClientMsg) UnmarshalJSON(data []byte) error {
value, err := proto.UnmarshalJSONEnum(EGCBaseClientMsg_value, data, "EGCBaseClientMsg")
if err != nil {
return err
}
*x = EGCBaseClientMsg(value)
return nil
}
func (EGCBaseClientMsg) EnumDescriptor() ([]byte, []int) { return system_fileDescriptor0, []int{2} }
type EGCToGCMsg int32
const (
EGCToGCMsg_k_EGCToGCMsgMasterAck EGCToGCMsg = 150
EGCToGCMsg_k_EGCToGCMsgMasterAckResponse EGCToGCMsg = 151
EGCToGCMsg_k_EGCToGCMsgRouted EGCToGCMsg = 152
EGCToGCMsg_k_EGCToGCMsgRoutedReply EGCToGCMsg = 153
EGCToGCMsg_k_EMsgGCUpdateSubGCSessionInfo EGCToGCMsg = 154
EGCToGCMsg_k_EMsgGCRequestSubGCSessionInfo EGCToGCMsg = 155
EGCToGCMsg_k_EMsgGCRequestSubGCSessionInfoResponse EGCToGCMsg = 156
EGCToGCMsg_k_EGCToGCMsgMasterStartupComplete EGCToGCMsg = 157
EGCToGCMsg_k_EMsgGCToGCSOCacheSubscribe EGCToGCMsg = 158
EGCToGCMsg_k_EMsgGCToGCSOCacheUnsubscribe EGCToGCMsg = 159
EGCToGCMsg_k_EMsgGCToGCLoadSessionSOCache EGCToGCMsg = 160
EGCToGCMsg_k_EMsgGCToGCLoadSessionSOCacheResponse EGCToGCMsg = 161
EGCToGCMsg_k_EMsgGCToGCUpdateSessionStats EGCToGCMsg = 162
)
var EGCToGCMsg_name = map[int32]string{
150: "k_EGCToGCMsgMasterAck",
151: "k_EGCToGCMsgMasterAckResponse",
152: "k_EGCToGCMsgRouted",
153: "k_EGCToGCMsgRoutedReply",
154: "k_EMsgGCUpdateSubGCSessionInfo",
155: "k_EMsgGCRequestSubGCSessionInfo",
156: "k_EMsgGCRequestSubGCSessionInfoResponse",
157: "k_EGCToGCMsgMasterStartupComplete",
158: "k_EMsgGCToGCSOCacheSubscribe",
159: "k_EMsgGCToGCSOCacheUnsubscribe",
160: "k_EMsgGCToGCLoadSessionSOCache",
161: "k_EMsgGCToGCLoadSessionSOCacheResponse",
162: "k_EMsgGCToGCUpdateSessionStats",
}
var EGCToGCMsg_value = map[string]int32{
"k_EGCToGCMsgMasterAck": 150,
"k_EGCToGCMsgMasterAckResponse": 151,
"k_EGCToGCMsgRouted": 152,
"k_EGCToGCMsgRoutedReply": 153,
"k_EMsgGCUpdateSubGCSessionInfo": 154,
"k_EMsgGCRequestSubGCSessionInfo": 155,
"k_EMsgGCRequestSubGCSessionInfoResponse": 156,
"k_EGCToGCMsgMasterStartupComplete": 157,
"k_EMsgGCToGCSOCacheSubscribe": 158,
"k_EMsgGCToGCSOCacheUnsubscribe": 159,
"k_EMsgGCToGCLoadSessionSOCache": 160,
"k_EMsgGCToGCLoadSessionSOCacheResponse": 161,
"k_EMsgGCToGCUpdateSessionStats": 162,
}
func (x EGCToGCMsg) Enum() *EGCToGCMsg {
p := new(EGCToGCMsg)
*p = x
return p
}
func (x EGCToGCMsg) String() string {
return proto.EnumName(EGCToGCMsg_name, int32(x))
}
func (x *EGCToGCMsg) UnmarshalJSON(data []byte) error {
value, err := proto.UnmarshalJSONEnum(EGCToGCMsg_value, data, "EGCToGCMsg")
if err != nil {
return err
}
*x = EGCToGCMsg(value)
return nil
}
func (EGCToGCMsg) EnumDescriptor() ([]byte, []int) { return system_fileDescriptor0, []int{3} }
func init() {
proto.RegisterEnum("EGCSystemMsg", EGCSystemMsg_name, EGCSystemMsg_value)
proto.RegisterEnum("ESOMsg", ESOMsg_name, ESOMsg_value)
proto.RegisterEnum("EGCBaseClientMsg", EGCBaseClientMsg_name, EGCBaseClientMsg_value)
proto.RegisterEnum("EGCToGCMsg", EGCToGCMsg_name, EGCToGCMsg_value)
}
var system_fileDescriptor0 = []byte{
// 1475 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x84, 0x57, 0x59, 0x73, 0x1b, 0xc5,
0x13, 0xcf, 0x96, 0xfc, 0xff, 0x3f, 0x4c, 0x41, 0xd1, 0x99, 0xc4, 0x47, 0x12, 0x27, 0x4a, 0x42,
0x0e, 0x62, 0xa8, 0x3c, 0x84, 0xfb, 0x46, 0x91, 0x64, 0x5b, 0x41, 0x8e, 0x15, 0x4b, 0xb6, 0xb9,
0xcd, 0x7a, 0x35, 0xb6, 0xb6, 0x2c, 0xed, 0x2c, 0x33, 0xbb, 0x26, 0x7e, 0x0b, 0xd7, 0x57, 0xe0,
0xbe, 0x8b, 0xa3, 0xe0, 0x1b, 0xc0, 0x27, 0xe0, 0x7c, 0x81, 0x57, 0xee, 0x7c, 0x01, 0x1e, 0xb8,
0x21, 0x55, 0xf4, 0xee, 0xce, 0xce, 0xce, 0x4a, 0xb2, 0x79, 0x93, 0xe6, 0xd7, 0xdd, 0xd3, 0xdd,
0xd3, 0xfd, 0xeb, 0x5e, 0x42, 0xd7, 0x1d, 0xb9, 0x25, 0x03, 0xd6, 0xeb, 0xc9, 0x75, 0x79, 0xda,
0x17, 0x3c, 0xe0, 0x53, 0x97, 0x47, 0xc9, 0x55, 0xd5, 0x99, 0x72, 0x33, 0x3e, 0x9f, 0x93, 0xeb,
0x74, 0x0f, 0xb9, 0x66, 0x63, 0x05, 0x4f, 0xf0, 0x77, 0xcd, 0xdb, 0xb4, 0xbb, 0x6e, 0x1b, 0x76,
0xd1, 0xdd, 0xe4, 0xea, 0xf4, 0x70, 0x2e, 0xec, 0x06, 0x2e, 0x58, 0x74, 0x82, 0xec, 0x4d, 0x8f,
0x66, 0x98, 0xc7, 0x84, 0xeb, 0x2c, 0x30, 0xbf, 0xbb, 0x05, 0x84, 0x8e, 0x11, 0x9a, 0x22, 0x89,
0xd9, 0xb3, 0xb6, 0x64, 0x70, 0x86, 0x1e, 0x22, 0xfb, 0xd3, 0xf3, 0x92, 0xd3, 0x71, 0xd9, 0x26,
0xeb, 0x31, 0x2f, 0x28, 0x3d, 0x69, 0x8b, 0x36, 0x6b, 0xc3, 0x8d, 0xa6, 0x5e, 0x99, 0x7b, 0x65,
0xde, 0xeb, 0xd9, 0x5e, 0x1b, 0x6e, 0x32, 0x6f, 0x6a, 0x06, 0xb6, 0x08, 0x1a, 0x5d, 0x7b, 0xcb,
0xf5, 0xd6, 0xe1, 0x66, 0x3a, 0x4e, 0xf6, 0x64, 0x08, 0xf7, 0x53, 0xe0, 0x16, 0x7a, 0x80, 0x8c,
0xe7, 0x54, 0x66, 0xec, 0x1e, 0x93, 0x4c, 0x6c, 0x32, 0x01, 0xb7, 0xd2, 0xfd, 0x64, 0xcc, 0xd4,
0x32, 0xb0, 0xdb, 0xe8, 0x28, 0xd9, 0x9d, 0x62, 0xcb, 0x33, 0x0b, 0xec, 0x89, 0x90, 0xc9, 0x00,
0x6e, 0x37, 0x5d, 0x8b, 0x8e, 0xa5, 0xcf, 0x3d, 0x0c, 0xe9, 0x0e, 0x7a, 0x94, 0x1c, 0xca, 0x92,
0x10, 0x2c, 0xa2, 0x99, 0xc8, 0x1a, 0x5e, 0x19, 0xc8, 0xa6, 0xd3, 0x61, 0x3d, 0x1b, 0xee, 0xa4,
0x53, 0xe4, 0xc4, 0xce, 0x32, 0xda, 0xde, 0x5d, 0x43, 0xec, 0xc5, 0x72, 0x95, 0x6a, 0x63, 0xa1,
0x5a, 0x2e, 0xb5, 0xaa, 0x15, 0xb8, 0x9b, 0x1e, 0x26, 0x93, 0xc3, 0x64, 0xb4, 0x95, 0x7b, 0xcc,
0x00, 0x4b, 0xbe, 0x5f, 0xf3, 0xd6, 0xf8, 0xa2, 0xdf, 0xb6, 0x03, 0x4c, 0xf2, 0xbd, 0x66, 0x66,
0x96, 0xa2, 0xc7, 0xc5, 0xe3, 0x26, 0x93, 0xd2, 0xe5, 0x1e, 0xdc, 0x47, 0xaf, 0x25, 0xc5, 0x6d,
0x40, 0x6d, 0xbd, 0x64, 0xfa, 0x58, 0xe7, 0x7c, 0x23, 0xf4, 0x4b, 0x8e, 0xc3, 0x43, 0x2f, 0x98,
0x16, 0xbc, 0x57, 0xf3, 0xfc, 0x30, 0x80, 0xb3, 0xb9, 0xfc, 0x33, 0xaf, 0x3d, 0xdb, 0x6a, 0x35,
0xd2, 0x64, 0x96, 0xcd, 0x5b, 0xfa, 0x40, 0x7d, 0x4b, 0xc5, 0x7c, 0xf4, 0x86, 0x60, 0x2d, 0x04,
0x9b, 0x2c, 0x08, 0x7d, 0xa8, 0xd2, 0x22, 0x39, 0x90, 0x22, 0x0b, 0xcc, 0xe1, 0xa2, 0xdd, 0x0c,
0x7d, 0x9f, 0x8b, 0xa0, 0xe4, 0x04, 0x51, 0x14, 0xd3, 0xf4, 0x3a, 0x72, 0xcc, 0x48, 0x90, 0xf2,
0xae, 0xc2, 0x02, 0xdb, 0xed, 0xca, 0x15, 0x23, 0x95, 0x33, 0x66, 0x28, 0x68, 0x8a, 0xb9, 0x9b,
0xac, 0xe6, 0x05, 0x4c, 0x60, 0xd2, 0xe6, 0x30, 0x6c, 0x7b, 0x9d, 0x41, 0xcd, 0x74, 0x64, 0xda,
0xf5, 0xda, 0xca, 0x9c, 0x84, 0x73, 0x66, 0xad, 0x34, 0xb8, 0x0c, 0x4a, 0x5d, 0x26, 0x02, 0xb8,
0xdf, 0x2c, 0x4a, 0xbc, 0xbe, 0xee, 0x3a, 0x0c, 0x23, 0x92, 0x50, 0xcf, 0x77, 0x4c, 0xf6, 0x70,
0x30, 0xd7, 0xa7, 0xa2, 0x2a, 0x5f, 0xc2, 0x79, 0x33, 0x56, 0x03, 0xd0, 0x69, 0x9a, 0xcf, 0x3d,
0x75, 0xbb, 0x3d, 0x2d, 0x18, 0x53, 0x17, 0x42, 0xc3, 0x8c, 0x2e, 0x8f, 0x69, 0xfd, 0x0b, 0x74,
0x1f, 0x19, 0x35, 0x2e, 0xa8, 0x35, 0xea, 0xdc, 0xb1, 0xe3, 0x34, 0x2e, 0xd0, 0x23, 0xe4, 0xe0,
0x50, 0x48, 0x6b, 0x37, 0xe9, 0x41, 0xb2, 0x2f, 0xdf, 0xe9, 0x66, 0xe5, 0xb7, 0x4c, 0xe7, 0xd0,
0x82, 0x21, 0x01, 0x8b, 0x7d, 0x95, 0x6e, 0x60, 0xda, 0xfc, 0x92, 0x99, 0xe0, 0xa8, 0x50, 0xaa,
0x3d, 0x7c, 0x41, 0x58, 0xce, 0xdd, 0x9a, 0x1e, 0x6b, 0xad, 0x07, 0xe8, 0x24, 0x99, 0x30, 0x2c,
0xc7, 0x68, 0x8b, 0xf5, 0xfc, 0x2e, 0x16, 0x33, 0x3c, 0x48, 0x8f, 0x91, 0xc3, 0xdb, 0xa1, 0xda,
0xc6, 0x43, 0x39, 0xcf, 0x85, 0xed, 0x05, 0x33, 0x51, 0x75, 0x36, 0x6c, 0x29, 0xe1, 0xe1, 0x9c,
0xe7, 0x39, 0x4c, 0xeb, 0x3f, 0x62, 0xba, 0x38, 0x50, 0x82, 0xf0, 0x28, 0x3d, 0x4e, 0x8e, 0x6c,
0x0b, 0x6b, 0x2b, 0x8f, 0x99, 0x5d, 0x84, 0x62, 0x0d, 0x26, 0x24, 0xf7, 0xec, 0xf3, 0x11, 0x5d,
0xc1, 0x8a, 0xd9, 0x45, 0x7d, 0xa0, 0xb6, 0xf0, 0xb8, 0x59, 0x72, 0x31, 0x6f, 0xfb, 0x5d, 0x76,
0x11, 0x7f, 0x83, 0x6d, 0xe6, 0x61, 0x99, 0xad, 0x96, 0x1a, 0xb5, 0x05, 0xb6, 0xee, 0xe2, 0x23,
0x88, 0xb8, 0x03, 0xd6, 0x6c, 0x07, 0x2f, 0x61, 0x66, 0x2e, 0x13, 0xa9, 0x73, 0x7c, 0x35, 0x6d,
0xe4, 0x35, 0xb3, 0xd1, 0xfa, 0xd1, 0xd9, 0x20, 0xf0, 0xb5, 0x1f, 0x1d, 0x7a, 0x3d, 0x39, 0xb9,
0x9d, 0xe4, 0x34, 0x17, 0xd1, 0x04, 0xd0, 0xc2, 0x2e, 0xd6, 0x64, 0xe6, 0x34, 0xeb, 0x95, 0x6d,
0x2c, 0xa7, 0x36, 0x86, 0x08, 0x9f, 0x58, 0x58, 0x93, 0x93, 0xc3, 0x20, 0xad, 0xfc, 0xa9, 0x35,
0x54, 0x1b, 0xa9, 0x03, 0x3e, 0xb3, 0x30, 0x9a, 0xf1, 0x01, 0xa8, 0xc2, 0xba, 0x0c, 0x0b, 0xe3,
0x73, 0x0b, 0xb3, 0x3d, 0x36, 0xa8, 0x18, 0x57, 0xeb, 0x17, 0x16, 0x66, 0xfb, 0xd0, 0x70, 0x50,
0x5f, 0xfd, 0xa5, 0x85, 0xf5, 0x0a, 0xba, 0x30, 0x2f, 0xd4, 0x13, 0xdd, 0xaf, 0x2c, 0x2c, 0x86,
0x89, 0xfe, 0x63, 0xad, 0xf5, 0xb5, 0x85, 0x3d, 0xae, 0xc7, 0xe2, 0x9c, 0x1d, 0xbd, 0x00, 0x7a,
0x5b, 0x71, 0x05, 0x73, 0x02, 0x2e, 0xb6, 0xe0, 0x1b, 0x8b, 0x9e, 0x24, 0x47, 0xb7, 0x17, 0xd0,
0x96, 0xbe, 0xcd, 0x3b, 0x99, 0x0a, 0xaa, 0xc7, 0xe5, 0x61, 0x10, 0x4d, 0xc6, 0xef, 0x2c, 0x7c,
0x8a, 0x13, 0x3b, 0x0b, 0x69, 0x8b, 0xdf, 0x5b, 0xf4, 0x44, 0x56, 0xa8, 0x5a, 0xb8, 0xdc, 0x75,
0x71, 0x6c, 0x47, 0x94, 0xa9, 0x8c, 0xfe, 0x60, 0xd1, 0xd3, 0xe4, 0xd4, 0x7f, 0xca, 0x69, 0xbb,
0x3f, 0x5a, 0x48, 0x78, 0xd9, 0x8a, 0xc0, 0x82, 0x79, 0x3f, 0xe2, 0x15, 0x09, 0x3f, 0xe5, 0x92,
0x91, 0x01, 0x5a, 0xf3, 0x72, 0xb4, 0x76, 0xec, 0x19, 0x5c, 0x2e, 0xce, 0xc0, 0x2f, 0x05, 0x33,
0xfa, 0xa8, 0x21, 0x42, 0xe1, 0x74, 0x10, 0x6a, 0x89, 0x10, 0x47, 0x07, 0xe6, 0x3c, 0x94, 0xf0,
0x6b, 0xc1, 0x8c, 0x7e, 0xb8, 0x90, 0xbe, 0xeb, 0xb7, 0x02, 0xb2, 0x80, 0x26, 0xc7, 0x64, 0x80,
0xa6, 0x93, 0xf2, 0xf7, 0x02, 0xb6, 0x70, 0xc6, 0x23, 0x65, 0xd5, 0xc1, 0x4b, 0xb6, 0x93, 0x18,
0x29, 0x77, 0x6c, 0x0f, 0x87, 0xc7, 0x1f, 0x05, 0xb3, 0xe4, 0xca, 0x1d, 0xe6, 0x6c, 0x4c, 0x0b,
0x4c, 0x4a, 0x5b, 0x76, 0x5c, 0x1f, 0xfe, 0x2c, 0x60, 0x13, 0x16, 0xb7, 0x41, 0xb5, 0x1b, 0x7f,
0x15, 0x90, 0x70, 0x4c, 0x22, 0x6e, 0xe0, 0x3a, 0x83, 0xeb, 0x96, 0xba, 0xb2, 0xee, 0x7a, 0x1b,
0xf0, 0x77, 0x01, 0x97, 0x8c, 0xe3, 0x3b, 0xca, 0x68, 0x7b, 0xff, 0x14, 0xe8, 0xa9, 0xac, 0x6d,
0x97, 0x9a, 0xb8, 0xb4, 0xe1, 0xec, 0xc4, 0x62, 0x0e, 0xa5, 0xef, 0x3a, 0x2e, 0x0f, 0x65, 0x34,
0x47, 0x37, 0xdd, 0x60, 0x0b, 0xae, 0x14, 0xcc, 0xe7, 0xa8, 0x34, 0x94, 0xd5, 0x39, 0xd7, 0x11,
0xbc, 0x75, 0x11, 0xdf, 0xeb, 0xd2, 0x88, 0x59, 0x9b, 0x83, 0x02, 0xfa, 0xd2, 0xa7, 0x46, 0xcc,
0xde, 0x88, 0xa7, 0x49, 0xa9, 0x79, 0x1e, 0x9e, 0x1e, 0x31, 0x7b, 0x23, 0x3d, 0xd6, 0x5a, 0xcf,
0x8c, 0xe0, 0xca, 0x98, 0xe3, 0x51, 0xdf, 0x57, 0x19, 0xaa, 0x23, 0x55, 0xc1, 0xb3, 0x23, 0x66,
0x7d, 0x0e, 0xe0, 0xda, 0xce, 0x73, 0x23, 0x53, 0x3f, 0x5b, 0xe4, 0xff, 0xd5, 0xe6, 0x7c, 0xb6,
0xdf, 0xc6, 0xbf, 0x57, 0xca, 0x82, 0x45, 0x53, 0x61, 0x34, 0x77, 0x98, 0x3c, 0x35, 0x8c, 0xd1,
0xbd, 0xb1, 0xcb, 0xc9, 0x61, 0x05, 0x99, 0x4a, 0xf0, 0x2d, 0x18, 0x57, 0x94, 0xa8, 0xf4, 0x23,
0x1e, 0x68, 0x86, 0xab, 0xd2, 0x11, 0xee, 0x2a, 0xae, 0x57, 0x13, 0x6a, 0xc7, 0x35, 0xd0, 0x45,
0x4f, 0x66, 0xf8, 0x3e, 0x45, 0xe9, 0xe6, 0x45, 0x29, 0x2f, 0xc3, 0x7e, 0x35, 0x16, 0x06, 0x4d,
0xfb, 0xc9, 0xd8, 0x5d, 0x13, 0x4c, 0x76, 0x60, 0x52, 0x51, 0xf7, 0x50, 0x0f, 0x16, 0xfd, 0x16,
0xaf, 0x44, 0xde, 0x1f, 0x9c, 0xba, 0x62, 0x11, 0xc0, 0xcc, 0x44, 0xed, 0xa1, 0x3b, 0x51, 0x75,
0x4f, 0x5c, 0xb3, 0x8d, 0xb8, 0x25, 0x13, 0x2a, 0xff, 0x68, 0x5c, 0xd1, 0xa6, 0x81, 0xa8, 0xe4,
0x7d, 0x3c, 0xae, 0xda, 0x20, 0x86, 0x12, 0x4b, 0xcb, 0xac, 0xeb, 0xf0, 0x1e, 0x83, 0x77, 0x8a,
0x26, 0xd6, 0x8c, 0x77, 0xe8, 0x14, 0x7b, 0xb7, 0x68, 0x5e, 0x96, 0xe8, 0xcd, 0xb2, 0x6e, 0x97,
0xc3, 0x7b, 0x39, 0x24, 0xd1, 0x4a, 0x90, 0xf7, 0x8b, 0xaa, 0x89, 0x0d, 0x1d, 0xfc, 0x12, 0xf0,
0x58, 0xbc, 0xd9, 0xa9, 0x26, 0xfe, 0x20, 0x27, 0x94, 0xa8, 0x0f, 0x08, 0x7d, 0x58, 0x9c, 0xba,
0x5c, 0x20, 0x04, 0xe3, 0x6f, 0xf1, 0xb8, 0x3a, 0x74, 0x2f, 0xab, 0xff, 0x09, 0x4b, 0x95, 0x9c,
0x0d, 0x78, 0xde, 0xd2, 0x0d, 0xd6, 0x8f, 0xe9, 0x24, 0xbc, 0x90, 0x31, 0x96, 0x92, 0x89, 0x38,
0x0d, 0x1f, 0xf4, 0xc5, 0x6c, 0xa8, 0xe4, 0x80, 0xe4, 0x53, 0xe8, 0x25, 0xcb, 0x74, 0x55, 0x51,
0x48, 0xb8, 0x1a, 0x79, 0x1d, 0xf3, 0x48, 0xb4, 0x99, 0xc3, 0xcb, 0x96, 0xa2, 0x81, 0x58, 0x48,
0xbd, 0xc8, 0x80, 0xd4, 0x2b, 0x16, 0xbd, 0x21, 0x9e, 0xa1, 0x3b, 0x49, 0x69, 0x7f, 0x5f, 0xcd,
0x98, 0x3b, 0x17, 0x53, 0xfc, 0x2d, 0x14, 0xfa, 0xb8, 0x47, 0xfa, 0xf1, 0xd4, 0x7b, 0x2d, 0x9d,
0xa8, 0xb1, 0xd5, 0x48, 0xb4, 0x39, 0x9f, 0xaf, 0x28, 0x78, 0x3d, 0x17, 0x83, 0x21, 0x62, 0x14,
0x36, 0xbc, 0x31, 0x20, 0x54, 0xe7, 0x76, 0x5b, 0x79, 0xa6, 0xe4, 0xe1, 0xcd, 0x74, 0xf6, 0xec,
0x20, 0xa4, 0x23, 0x78, 0x6b, 0xc0, 0x62, 0x8e, 0x81, 0x93, 0xd9, 0xfa, 0xb6, 0x75, 0xf6, 0x7f,
0xb3, 0xd6, 0x25, 0x6b, 0xd7, 0xbf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x05, 0xab, 0xaf, 0x14, 0xda,
0x0e, 0x00, 0x00,
}

View File

@ -1,188 +0,0 @@
/*
Includes inventory types as used in the trade package
*/
package inventory
import (
"bytes"
"encoding/json"
"fmt"
"github.com/Philipp15b/go-steam/jsont"
"strconv"
)
type GenericInventory map[uint32]map[uint64]*Inventory
func NewGenericInventory() GenericInventory {
iMap := make(map[uint32]map[uint64]*Inventory)
return GenericInventory(iMap)
}
// Get inventory for specified AppId and ContextId
func (i *GenericInventory) Get(appId uint32, contextId uint64) (*Inventory, error) {
iMap := (map[uint32]map[uint64]*Inventory)(*i)
iMap2, ok := iMap[appId]
if !ok {
return nil, fmt.Errorf("inventory for specified appId not found")
}
inv, ok := iMap2[contextId]
if !ok {
return nil, fmt.Errorf("inventory for specified contextId not found")
}
return inv, nil
}
func (i *GenericInventory) Add(appId uint32, contextId uint64, inv *Inventory) {
iMap := (map[uint32]map[uint64]*Inventory)(*i)
iMap2, ok := iMap[appId]
if !ok {
iMap2 = make(map[uint64]*Inventory)
iMap[appId] = iMap2
}
iMap2[contextId] = inv
}
type Inventory struct {
Items Items `json:"rgInventory"`
Currencies Currencies `json:"rgCurrency"`
Descriptions Descriptions `json:"rgDescriptions"`
AppInfo *AppInfo `json:"rgAppInfo"`
}
// Items key is an AssetId
type Items map[string]*Item
func (i *Items) ToMap() map[string]*Item {
return (map[string]*Item)(*i)
}
func (i *Items) Get(assetId uint64) (*Item, error) {
iMap := (map[string]*Item)(*i)
if item, ok := iMap[strconv.FormatUint(assetId, 10)]; ok {
return item, nil
}
return nil, fmt.Errorf("item not found")
}
func (i *Items) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte("[]")) {
return nil
}
return json.Unmarshal(data, (*map[string]*Item)(i))
}
type Currencies map[string]*Currency
func (c *Currencies) ToMap() map[string]*Currency {
return (map[string]*Currency)(*c)
}
func (c *Currencies) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte("[]")) {
return nil
}
return json.Unmarshal(data, (*map[string]*Currency)(c))
}
// Descriptions key format is %d_%d, first %d is ClassId, second is InstanceId
type Descriptions map[string]*Description
func (d *Descriptions) ToMap() map[string]*Description {
return (map[string]*Description)(*d)
}
func (d *Descriptions) Get(classId uint64, instanceId uint64) (*Description, error) {
dMap := (map[string]*Description)(*d)
descId := fmt.Sprintf("%v_%v", classId, instanceId)
if desc, ok := dMap[descId]; ok {
return desc, nil
}
return nil, fmt.Errorf("description not found")
}
func (d *Descriptions) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte("[]")) {
return nil
}
return json.Unmarshal(data, (*map[string]*Description)(d))
}
type Item struct {
Id uint64 `json:",string"`
ClassId uint64 `json:",string"`
InstanceId uint64 `json:",string"`
Amount uint64 `json:",string"`
Pos uint32
}
type Currency struct {
Id uint64 `json:",string"`
ClassId uint64 `json:",string"`
IsCurrency bool `json:"is_currency"`
Pos uint32
}
type Description struct {
AppId uint32 `json:",string"`
ClassId uint64 `json:",string"`
InstanceId uint64 `json:",string"`
IconUrl string `json:"icon_url"`
IconUrlLarge string `json:"icon_url_large"`
IconDragUrl string `json:"icon_drag_url"`
Name string
MarketName string `json:"market_name"`
MarketHashName string `json:"market_hash_name"`
// Colors in hex, for example `B2B2B2`
NameColor string `json:"name_color"`
BackgroundColor string `json:"background_color"`
Type string
Tradable jsont.UintBool
Marketable jsont.UintBool
Commodity jsont.UintBool
MarketTradableRestriction uint32 `json:"market_tradable_restriction,string"`
Descriptions DescriptionLines
Actions []*Action
// Application-specific data, like "def_index" and "quality" for TF2
AppData map[string]string
Tags []*Tag
}
type DescriptionLines []*DescriptionLine
func (d *DescriptionLines) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte(`""`)) {
return nil
}
return json.Unmarshal(data, (*[]*DescriptionLine)(d))
}
type DescriptionLine struct {
Value string
Type *string // Is `html` for HTML descriptions
Color *string
}
type Action struct {
Name string
Link string
}
type AppInfo struct {
AppId uint32
Name string
Icon string
Link string
}
type Tag struct {
InternalName string `json:internal_name`
Name string
Category string
CategoryName string `json:category_name`
}

View File

@ -1,79 +0,0 @@
package inventory
import (
"encoding/json"
"fmt"
"github.com/Philipp15b/go-steam/steamid"
"io/ioutil"
"net/http"
"regexp"
"strconv"
)
type InventoryApps map[string]*InventoryApp
func (i *InventoryApps) Get(appId uint32) (*InventoryApp, error) {
iMap := (map[string]*InventoryApp)(*i)
if inventoryApp, ok := iMap[strconv.FormatUint(uint64(appId), 10)]; ok {
return inventoryApp, nil
}
return nil, fmt.Errorf("inventory app not found")
}
func (i *InventoryApps) ToMap() map[string]*InventoryApp {
return (map[string]*InventoryApp)(*i)
}
type InventoryApp struct {
AppId uint32
Name string
Icon string
Link string
AssetCount uint32 `json:"asset_count"`
InventoryLogo string `json:"inventory_logo"`
TradePermissions string `json:"trade_permissions"`
Contexts Contexts `json:"rgContexts"`
}
type Contexts map[string]*Context
func (c *Contexts) Get(contextId uint64) (*Context, error) {
cMap := (map[string]*Context)(*c)
if context, ok := cMap[strconv.FormatUint(contextId, 10)]; ok {
return context, nil
}
return nil, fmt.Errorf("context not found")
}
func (c *Contexts) ToMap() map[string]*Context {
return (map[string]*Context)(*c)
}
type Context struct {
ContextId uint64 `json:"id,string"`
AssetCount uint32 `json:"asset_count"`
Name string
}
func GetInventoryApps(client *http.Client, steamId steamid.SteamId) (InventoryApps, error) {
resp, err := http.Get("http://steamcommunity.com/profiles/" + steamId.ToString() + "/inventory/")
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
reg := regexp.MustCompile("var g_rgAppContextData = (.*?);")
inventoryAppsMatches := reg.FindSubmatch(respBody)
if inventoryAppsMatches == nil {
return nil, fmt.Errorf("profile inventory not found in steam response")
}
var inventoryApps InventoryApps
if err = json.Unmarshal(inventoryAppsMatches[1], &inventoryApps); err != nil {
return nil, err
}
return inventoryApps, nil
}

View File

@ -1,28 +0,0 @@
package inventory
import (
"fmt"
"net/http"
"strconv"
)
func GetPartialOwnInventory(client *http.Client, contextId uint64, appId uint32, start *uint) (*PartialInventory, error) {
// TODO: the "trading" parameter can be left off to return non-tradable items too
url := fmt.Sprintf("http://steamcommunity.com/my/inventory/json/%d/%d?trading=1", appId, contextId)
if start != nil {
url += "&start=" + strconv.FormatUint(uint64(*start), 10)
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
panic(err)
}
return DoInventoryRequest(client, req)
}
func GetOwnInventory(client *http.Client, contextId uint64, appId uint32) (*Inventory, error) {
return GetFullInventory(func() (*PartialInventory, error) {
return GetPartialOwnInventory(client, contextId, appId, nil)
}, func(start uint) (*PartialInventory, error) {
return GetPartialOwnInventory(client, contextId, appId, &start)
})
}

View File

@ -1,91 +0,0 @@
package inventory
import (
"bytes"
"encoding/json"
"errors"
"net/http"
)
// A partial inventory as sent by the Steam API.
type PartialInventory struct {
Success bool
Error string
Inventory
More bool
MoreStart MoreStart `json:"more_start"`
}
type MoreStart uint
func (m *MoreStart) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte("false")) {
return nil
}
return json.Unmarshal(data, (*uint)(m))
}
func DoInventoryRequest(client *http.Client, req *http.Request) (*PartialInventory, error) {
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
inv := new(PartialInventory)
err = json.NewDecoder(resp.Body).Decode(inv)
if err != nil {
return nil, err
}
return inv, nil
}
func GetFullInventory(getFirst func() (*PartialInventory, error), getNext func(start uint) (*PartialInventory, error)) (*Inventory, error) {
first, err := getFirst()
if err != nil {
return nil, err
}
if !first.Success {
return nil, errors.New("GetFullInventory API call failed: " + first.Error)
}
result := &first.Inventory
var next *PartialInventory
for latest := first; latest.More; latest = next {
next, err := getNext(uint(latest.MoreStart))
if err != nil {
return nil, err
}
if !next.Success {
return nil, errors.New("GetFullInventory API call failed: " + next.Error)
}
result = Merge(result, &next.Inventory)
}
return result, nil
}
// Merges the given Inventory into a single Inventory.
// The given slice must have at least one element. The first element of the slice is used
// and modified.
func Merge(p ...*Inventory) *Inventory {
inv := p[0]
for idx, i := range p {
if idx == 0 {
continue
}
for key, value := range i.Items {
inv.Items[key] = value
}
for key, value := range i.Descriptions {
inv.Descriptions[key] = value
}
for key, value := range i.Currencies {
inv.Currencies[key] = value
}
}
return inv
}

View File

@ -1,79 +0,0 @@
package steam
import (
"bytes"
. "github.com/Philipp15b/go-steam/protocol"
. "github.com/Philipp15b/go-steam/protocol/gamecoordinator"
. "github.com/Philipp15b/go-steam/protocol/protobuf"
. "github.com/Philipp15b/go-steam/protocol/steamlang"
"github.com/golang/protobuf/proto"
)
type GameCoordinator struct {
client *Client
handlers []GCPacketHandler
}
func newGC(client *Client) *GameCoordinator {
return &GameCoordinator{
client: client,
handlers: make([]GCPacketHandler, 0),
}
}
type GCPacketHandler interface {
HandleGCPacket(*GCPacket)
}
func (g *GameCoordinator) RegisterPacketHandler(handler GCPacketHandler) {
g.handlers = append(g.handlers, handler)
}
func (g *GameCoordinator) HandlePacket(packet *Packet) {
if packet.EMsg != EMsg_ClientFromGC {
return
}
msg := new(CMsgGCClient)
packet.ReadProtoMsg(msg)
p, err := NewGCPacket(msg)
if err != nil {
g.client.Errorf("Error reading GC message: %v", err)
return
}
for _, handler := range g.handlers {
handler.HandleGCPacket(p)
}
}
func (g *GameCoordinator) Write(msg IGCMsg) {
buf := new(bytes.Buffer)
msg.Serialize(buf)
msgType := msg.GetMsgType()
if msg.IsProto() {
msgType = msgType | 0x80000000 // mask with protoMask
}
g.client.Write(NewClientMsgProtobuf(EMsg_ClientToGC, &CMsgGCClient{
Msgtype: proto.Uint32(msgType),
Appid: proto.Uint32(msg.GetAppId()),
Payload: buf.Bytes(),
}))
}
// Sets you in the given games. Specify none to quit all games.
func (g *GameCoordinator) SetGamesPlayed(appIds ...uint64) {
games := make([]*CMsgClientGamesPlayed_GamePlayed, 0)
for _, appId := range appIds {
games = append(games, &CMsgClientGamesPlayed_GamePlayed{
GameId: proto.Uint64(appId),
})
}
g.client.Write(NewClientMsgProtobuf(EMsg_ClientGamesPlayed, &CMsgClientGamesPlayed{
GamesPlayed: games,
}))
}

View File

@ -1,295 +0,0 @@
/*
This program generates the protobuf and SteamLanguage files from the SteamKit data.
*/
package main
import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/token"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
)
var printCommands = false
func main() {
args := strings.Join(os.Args[1:], " ")
found := false
if strings.Contains(args, "clean") {
clean()
found = true
}
if strings.Contains(args, "steamlang") {
buildSteamLanguage()
found = true
}
if strings.Contains(args, "proto") {
buildProto()
found = true
}
if !found {
os.Stderr.WriteString("Invalid target!\nAvailable targets: clean, proto, steamlang\n")
os.Exit(1)
}
}
func clean() {
print("# Cleaning")
cleanGlob("../protocol/**/*.pb.go")
cleanGlob("../tf2/protocol/**/*.pb.go")
cleanGlob("../dota/protocol/**/*.pb.go")
os.Remove("../protocol/steamlang/enums.go")
os.Remove("../protocol/steamlang/messages.go")
}
func cleanGlob(pattern string) {
protos, _ := filepath.Glob(pattern)
for _, proto := range protos {
err := os.Remove(proto)
if err != nil {
panic(err)
}
}
}
func buildSteamLanguage() {
print("# Building Steam Language")
exePath := "./GoSteamLanguageGenerator/bin/Debug/GoSteamLanguageGenerator.exe"
if runtime.GOOS != "windows" {
execute("mono", exePath, "./SteamKit", "../protocol/steamlang")
} else {
execute(exePath, "./SteamKit", "../protocol/steamlang")
}
execute("gofmt", "-w", "../protocol/steamlang/enums.go", "../protocol/steamlang/messages.go")
}
func buildProto() {
print("# Building Protobufs")
buildProtoMap("steamclient", clientProtoFiles, "../protocol/protobuf")
buildProtoMap("tf", tf2ProtoFiles, "../tf2/protocol/protobuf")
buildProtoMap("dota", dotaProtoFiles, "../dota/protocol/protobuf")
}
func buildProtoMap(srcSubdir string, files map[string]string, outDir string) {
os.MkdirAll(outDir, os.ModePerm)
for proto, out := range files {
full := filepath.Join(outDir, out)
compileProto("SteamKit/Resources/Protobufs", srcSubdir, proto, full)
fixProto(full)
}
}
// Maps the proto files to their target files.
// See `SteamKit/Resources/Protobufs/steamclient/generate-base.bat` for reference.
var clientProtoFiles = map[string]string{
"steammessages_base.proto": "base.pb.go",
"encrypted_app_ticket.proto": "app_ticket.pb.go",
"steammessages_clientserver.proto": "client_server.pb.go",
"steammessages_clientserver_2.proto": "client_server_2.pb.go",
"content_manifest.proto": "content_manifest.pb.go",
"steammessages_unified_base.steamclient.proto": "unified/base.pb.go",
"steammessages_cloud.steamclient.proto": "unified/cloud.pb.go",
"steammessages_credentials.steamclient.proto": "unified/credentials.pb.go",
"steammessages_deviceauth.steamclient.proto": "unified/deviceauth.pb.go",
"steammessages_gamenotifications.steamclient.proto": "unified/gamenotifications.pb.go",
"steammessages_offline.steamclient.proto": "unified/offline.pb.go",
"steammessages_parental.steamclient.proto": "unified/parental.pb.go",
"steammessages_partnerapps.steamclient.proto": "unified/partnerapps.pb.go",
"steammessages_player.steamclient.proto": "unified/player.pb.go",
"steammessages_publishedfile.steamclient.proto": "unified/publishedfile.pb.go",
}
var tf2ProtoFiles = map[string]string{
"base_gcmessages.proto": "base.pb.go",
"econ_gcmessages.proto": "econ.pb.go",
"gcsdk_gcmessages.proto": "gcsdk.pb.go",
"tf_gcmessages.proto": "tf.pb.go",
"gcsystemmsgs.proto": "system.pb.go",
}
var dotaProtoFiles = map[string]string{
"base_gcmessages.proto": "base.pb.go",
"econ_gcmessages.proto": "econ.pb.go",
"gcsdk_gcmessages.proto": "gcsdk.pb.go",
"dota_gcmessages_common.proto": "dota_common.pb.go",
"dota_gcmessages_client.proto": "dota_client.pb.go",
"dota_gcmessages_client_fantasy.proto": "dota_client_fantasy.pb.go",
"gcsystemmsgs.proto": "system.pb.go",
}
func compileProto(srcBase, srcSubdir, proto, target string) {
outDir, _ := filepath.Split(target)
err := os.MkdirAll(outDir, os.ModePerm)
if err != nil {
panic(err)
}
execute("protoc", "--go_out="+outDir, "-I="+srcBase+"/"+srcSubdir, "-I="+srcBase, filepath.Join(srcBase, srcSubdir, proto))
out := strings.Replace(filepath.Join(outDir, proto), ".proto", ".pb.go", 1)
err = forceRename(out, target)
if err != nil {
panic(err)
}
}
func forceRename(from, to string) error {
if from != to {
os.Remove(to)
}
return os.Rename(from, to)
}
var pkgRegex = regexp.MustCompile(`(package \w+)`)
var pkgCommentRegex = regexp.MustCompile(`(?s)(\/\*.*?\*\/\n)package`)
var unusedImportCommentRegex = regexp.MustCompile("// discarding unused import .*\n")
var fileDescriptorVarRegex = regexp.MustCompile(`fileDescriptor\d+`)
func fixProto(path string) {
// goprotobuf is really bad at dependencies, so we must fix them manually...
// It tries to load each dependency of a file as a seperate package (but in a very, very wrong way).
// Because we want some files in the same package, we'll remove those imports to local files.
file, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, path, file, parser.ImportsOnly)
if err != nil {
panic("Error parsing " + path + ": " + err.Error())
}
importsToRemove := make([]*ast.ImportSpec, 0)
for _, i := range f.Imports {
// We remove all local imports
if i.Path.Value == "\".\"" {
importsToRemove = append(importsToRemove, i)
}
}
for _, itr := range importsToRemove {
// remove the package name from all types
file = bytes.Replace(file, []byte(itr.Name.Name+"."), []byte{}, -1)
// and remove the import itself
file = bytes.Replace(file, []byte(fmt.Sprintf("import %v %v\n", itr.Name.Name, itr.Path.Value)), []byte{}, -1)
}
// remove the package comment because it just includes a list of all messages and
// collides not only with the other compiled protobuf files, but also our own documentation.
file = cutAllSubmatch(pkgCommentRegex, file, 1)
// remove warnings
file = unusedImportCommentRegex.ReplaceAllLiteral(file, []byte{})
// fix the package name
file = pkgRegex.ReplaceAll(file, []byte("package "+inferPackageName(path)))
// fix the google dependency;
// we just reuse the one from protoc-gen-go
file = bytes.Replace(file, []byte("google/protobuf"), []byte("github.com/golang/protobuf/protoc-gen-go/descriptor"), -1)
// we need to prefix local variables created by protoc-gen-go so that they don't clash with others in the same package
filename := strings.Split(filepath.Base(path), ".")[0]
file = fileDescriptorVarRegex.ReplaceAllFunc(file, func(match []byte) []byte {
return []byte(filename + "_" + string(match))
})
err = ioutil.WriteFile(path, file, os.ModePerm)
if err != nil {
panic(err)
}
}
func inferPackageName(path string) string {
pieces := strings.Split(path, string(filepath.Separator))
return pieces[len(pieces)-2]
}
func cutAllSubmatch(r *regexp.Regexp, b []byte, n int) []byte {
i := r.FindSubmatchIndex(b)
return bytesCut(b, i[2*n], i[2*n+1])
}
// Removes the given section from the byte array
func bytesCut(b []byte, from, to int) []byte {
buf := new(bytes.Buffer)
buf.Write(b[:from])
buf.Write(b[to:])
return buf.Bytes()
}
func print(text string) { os.Stdout.WriteString(text + "\n") }
func printerr(text string) { os.Stderr.WriteString(text + "\n") }
// This writer appends a "> " after every newline so that the outpout appears quoted.
type QuotedWriter struct {
w io.Writer
started bool
}
func NewQuotedWriter(w io.Writer) *QuotedWriter {
return &QuotedWriter{w, false}
}
func (w *QuotedWriter) Write(p []byte) (n int, err error) {
if !w.started {
_, err = w.w.Write([]byte("> "))
if err != nil {
return n, err
}
w.started = true
}
for i, c := range p {
if c == '\n' {
nw, err := w.w.Write(p[n : i+1])
n += nw
if err != nil {
return n, err
}
_, err = w.w.Write([]byte("> "))
if err != nil {
return n, err
}
}
}
if n != len(p) {
nw, err := w.w.Write(p[n:len(p)])
n += nw
return n, err
}
return
}
func execute(command string, args ...string) {
if printCommands {
print(command + " " + strings.Join(args, " "))
}
cmd := exec.Command(command, args...)
cmd.Stdout = NewQuotedWriter(os.Stdout)
cmd.Stderr = NewQuotedWriter(os.Stderr)
err := cmd.Run()
if err != nil {
printerr(err.Error())
os.Exit(1)
}
}

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