add support for Status and whisper

Signed-off-by: Jakub Sokołowski <jakub@status.im>
This commit is contained in:
Jakub Sokołowski 2019-08-31 11:37:22 -04:00
parent 6b017b226a
commit b2be9a4797
No known key found for this signature in database
GPG Key ID: 4EF064D0E6D63020
8 changed files with 1018 additions and 21 deletions

View File

@ -1,11 +1,22 @@
FROM alpine:edge
ENTRYPOINT ["/bin/matterbridge"]
FROM alpine:edge AS builder
RUN apk update && apk add go git gcc musl-dev linux-headers
COPY . /go/src/github.com/42wim/matterbridge
RUN apk update && apk add go git gcc musl-dev ca-certificates mailcap \
&& 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
WORKDIR /go/src/github.com/42wim/matterbridge
ENV GOPATH /go
ENV CGOENABLE 1
ENV GO111MODULE on
RUN go get
RUN go build -x -ldflags "-X main.githash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge
FROM alpine:latest
RUN apk update && apk add ca-certificates
COPY --from=builder /bin/matterbridge /bin/matterbridge
ENTRYPOINT ["/bin/matterbridge"]

View File

@ -90,6 +90,8 @@ type Protocol struct {
IgnoreMessages string // all protocols
Jid string // xmpp
Label string // all protocols
ListenPort int // status
ListenAddr string // status
Login string // mattermost, matrix
MediaDownloadBlackList []string
MediaDownloadPath string // Basically MediaServerUpload, but instead of uploading it, just write it to a file on the same server.
@ -140,6 +142,7 @@ type Protocol struct {
TeamID string // msteams
TenantID string // msteams
Token string // gitter, slack, discord, api
PrivateKey string // status
Topic string // zulip
URL string // mattermost, slack // DEPRECATED
UseAPI bool // mattermost, slack
@ -197,6 +200,7 @@ type BridgeValues struct {
Matrix map[string]Protocol
Slack map[string]Protocol
SlackLegacy map[string]Protocol
Status map[string]Protocol
Steam map[string]Protocol
Gitter map[string]Protocol
XMPP map[string]Protocol

15
bridge/status/README.md Normal file
View File

@ -0,0 +1,15 @@
# Description
This is an implementation of support for the [Status](https://status.im/) chat protocol.
It mostly makes use of the [status-go](https://github.com/status-im/status-go) and [status-protocol-go](https://github.com/status-im/status-protocol-go) packages.
# TODO
* Drop usage of the SQLite database entirely
* Improve handling of logs for the Whisper node
* Handle properly verifying successful delivery
# Known Issues
* Sometimes the message doesn't show up in Status Desktop, just in notifications

18
bridge/status/helpers.go Normal file
View File

@ -0,0 +1,18 @@
package status
import (
"crypto/ecdsa"
"encoding/hex"
crypto "github.com/ethereum/go-ethereum/crypto"
)
func publicKeyToHex(pubkey *ecdsa.PublicKey) string {
return "0x" + hex.EncodeToString(crypto.FromECDSAPub(pubkey))
}
// isPubKeyEqual checks that two public keys are equal
func isPubKeyEqual(a, b *ecdsa.PublicKey) bool {
// the curve is always the same, just compare the points
return a.X.Cmp(b.X) == 0 && a.Y.Cmp(b.Y) == 0
}

268
bridge/status/status.go Normal file
View File

@ -0,0 +1,268 @@
package status
import (
"context"
"crypto/ecdsa"
"database/sql"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
"go.uber.org/zap"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
crypto "github.com/ethereum/go-ethereum/crypto"
gethbridge "github.com/status-im/status-go/eth-node/bridge/geth"
gonode "github.com/status-im/status-go/node"
params "github.com/status-im/status-go/params"
status "github.com/status-im/status-go/protocol"
alias "github.com/status-im/status-go/protocol/identity/alias"
"github.com/status-im/status-go/protocol/protobuf"
)
type Bstatus struct {
*bridge.Config
// message fetching loop controls
fetchInterval time.Duration
fetchTimeout time.Duration
fetchDone chan bool
// Whisper node settings
whisperListenPort int
whisperListenAddr string
whisperDataDir string
privateKey *ecdsa.PrivateKey // secret for Status chat identity
nodeConfig *params.NodeConfig // configuration for Whisper node
statusNode *gonode.StatusNode // Ethereum Whisper node to run in background
messenger *status.Messenger // Status messaging layer instance
}
func New(cfg *bridge.Config) bridge.Bridger {
return &Bstatus{
Config: cfg,
fetchDone: make(chan bool),
whisperListenPort: 30303,
whisperListenAddr: "0.0.0.0",
// TODO parametrize those
whisperDataDir: "/tmp/matterbridge-status-data",
fetchTimeout: 500 * time.Millisecond,
fetchInterval: 500 * time.Millisecond,
}
}
func (b *Bstatus) Connect() error {
keyHex := strings.TrimPrefix(b.GetString("PrivateKey"), "0x")
if privKey, err := crypto.HexToECDSA(keyHex); err != nil {
return errors.Wrap(err, "Failed to parse PrivateKey")
} else {
b.privateKey = privKey
}
b.nodeConfig = b.generateConfig()
b.statusNode = gonode.New()
accsMgr, _ := b.statusNode.AccountManager()
if err := b.statusNode.Start(b.nodeConfig, accsMgr); err != nil {
return errors.Wrap(err, "Failed to start Status node")
}
// Create a custom logger to suppress DEBUG messages
logger, _ := zap.NewProduction()
// Using an in-memory SQLite DB since we have nothing worth preserving
db, err := sql.Open("sqlite3", "file:mem?mode=memory&cache=shared")
if err != nil {
return errors.Wrap(err, "Failed to open sqlite database")
}
options := []status.Option{
status.WithDatabase(db),
status.WithCustomLogger(logger),
}
var instID string = uuid.New().String()
messenger, err := status.NewMessenger(
b.privateKey,
gethbridge.NewNodeBridge(b.statusNode.GethNode()),
instID,
options...,
)
if err != nil {
return errors.Wrap(err, "Failed to create Messenger")
}
if err := messenger.Start(); err != nil {
return errors.Wrap(err, "Failed to start Messenger")
}
if err := messenger.Init(); err != nil {
return errors.Wrap(err, "Failed to init Messenger")
}
b.messenger = messenger
// Start a routine for periodically fetching messages
go b.fetchMessagesLoop()
return nil
}
func (b *Bstatus) Disconnect() error {
b.stopMessagesLoops()
if err := b.messenger.Shutdown(); err != nil {
return errors.Wrap(err, "Failed to stop Status messenger")
}
if err := b.statusNode.Stop(); err != nil {
return errors.Wrap(err, "Failed to stop Status node")
}
return nil
}
func (b *Bstatus) JoinChannel(channel config.ChannelInfo) error {
chat := status.CreatePublicChat(channel.Name, b.messenger.Timesource())
b.messenger.Join(chat)
b.messenger.SaveChat(&chat)
return nil
}
func (b *Bstatus) Send(msg config.Message) (string, error) {
if !b.Connected() {
return "", fmt.Errorf("bridge %s not connected, dropping message %#v to bridge", b.Account, msg)
}
if skipBridgeMessage(msg) {
return "", nil
}
b.Log.Infof("=> Sending message %#v", msg)
// Use a timeout for sending messages
ctx, cancel := context.WithTimeout(context.Background(), b.fetchTimeout)
defer cancel()
msgHash, err := b.messenger.SendChatMessage(ctx, genStatusMsg(msg))
if err != nil {
return "", errors.Wrap(err, "failed to send message")
}
// TODO handle the delivery event?
return fmt.Sprintf("%#x", msgHash), nil
}
func (b *Bstatus) Connected() bool {
return b.statusNode.IsRunning()
}
// Converts a bridge message into a Status message
func genStatusMsg(msg config.Message) (sMsg *status.Message) {
sMsg = &status.Message{}
sMsg.ChatId = msg.Channel
sMsg.ContentType = protobuf.ChatMessage_TEXT_PLAIN
// We need to prefix messages with usernames
sMsg.Text = fmt.Sprintf("%s%s", msg.Username, msg.Text)
return
}
// Generate a sane configuration for a Status Node
func (b *Bstatus) generateConfig() *params.NodeConfig {
options := []params.Option{
params.WithFleet("eth.prod"),
b.withListenAddr(),
}
var configFiles []string
config, err := params.NewNodeConfigWithDefaultsAndFiles(
b.whisperDataDir,
params.MainNetworkID,
options,
configFiles,
)
if err != nil {
b.Log.WithError(err).Error("Failed to generate config")
}
return config
}
func (b *Bstatus) stopMessagesLoops() {
close(b.fetchDone)
}
// Main loop for fetching Status messages and relaying them to the bridge
func (b *Bstatus) fetchMessagesLoop() {
t := time.NewTicker(b.fetchInterval)
defer t.Stop()
for {
select {
case <-t.C:
mResp, err := b.messenger.RetrieveAll()
if err != nil {
b.Log.WithError(err).Error("Failed to retrieve messages")
continue
}
for _, msg := range mResp.Messages {
if b.skipStatusMessage(msg) {
continue
}
b.propagateMessage(msg)
}
case <-b.fetchDone:
return
}
}
}
func (b *Bstatus) propagateMessage(msg *status.Message) {
pubKey := publicKeyToHex(msg.SigPubKey)
alias, err := alias.GenerateFromPublicKeyString(pubKey)
if err != nil {
b.Log.WithError(err).Error("Failed to generate Chat name")
}
// Send message for processing
b.Remote <- config.Message{
Timestamp: time.Unix(int64(msg.WhisperTimestamp), 0),
Username: alias,
UserID: pubKey,
Text: msg.Text,
Channel: msg.ChatId,
ID: fmt.Sprintf("%#x", msg.ID),
Account: b.Account,
}
}
// skipStatusMessage defines which Status messages can be ignored
func (b *Bstatus) skipStatusMessage(msg *status.Message) bool {
// skip messages from ourselves
if isPubKeyEqual(msg.SigPubKey, &b.privateKey.PublicKey) {
return true
}
// skip empty messages
if msg.Text == "" {
return true
}
return false
}
// skipBridgeMessage defines which messages from the bridge should be ignored
func skipBridgeMessage(msg config.Message) bool {
// skip delete messages
if msg.Event == config.EventMsgDelete {
return true
}
return false
}
func (b *Bstatus) withListenAddr() params.Option {
if addr := b.GetString("ListenAddr"); addr != "" {
b.whisperListenAddr = addr
}
if port := b.GetInt("ListenPort"); port != 0 {
b.whisperListenPort = port
}
return func(c *params.NodeConfig) error {
c.ListenAddr = fmt.Sprintf("%s:%d", b.whisperListenAddr, b.whisperListenPort)
return nil
}
}

View File

@ -13,6 +13,7 @@ import (
brocketchat "github.com/42wim/matterbridge/bridge/rocketchat"
bslack "github.com/42wim/matterbridge/bridge/slack"
bsshchat "github.com/42wim/matterbridge/bridge/sshchat"
bstatus "github.com/42wim/matterbridge/bridge/status"
bsteam "github.com/42wim/matterbridge/bridge/steam"
btelegram "github.com/42wim/matterbridge/bridge/telegram"
bwhatsapp "github.com/42wim/matterbridge/bridge/whatsapp"
@ -33,6 +34,7 @@ var (
"slack": bslack.New,
"sshchat": bsshchat.New,
"steam": bsteam.New,
"status": bstatus.New,
"telegram": btelegram.New,
"whatsapp": bwhatsapp.New,
"xmpp": bxmpp.New,

22
go.mod
View File

@ -8,15 +8,16 @@ require (
github.com/Rhymen/go-whatsapp v0.1.0
github.com/d5/tengo/v2 v2.0.2
github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec
github.com/ethereum/go-ethereum v1.9.5
github.com/fsnotify/fsnotify v1.4.7
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible
github.com/gomarkdown/markdown v0.0.0-20200127000047-1813ea067497
github.com/google/gops v0.3.6
github.com/google/uuid v1.1.1
github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 // indirect
github.com/gorilla/schema v1.1.0
github.com/gorilla/websocket v1.4.1
github.com/hashicorp/golang-lru v0.5.3
github.com/hpcloud/tail v1.0.0 // indirect
github.com/jpillora/backoff v1.0.0
github.com/keybase/go-keybase-chat-bot v0.0.0-20200226211841-4e48f3eaef3e
github.com/labstack/echo/v4 v4.1.13
@ -32,15 +33,12 @@ require (
github.com/mattermost/mattermost-server v5.5.0+incompatible
github.com/mattn/go-runewidth v0.0.7 // indirect
github.com/mattn/godown v0.0.0-20180312012330-2e9e17e0ea51
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 // indirect
github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect
github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9
github.com/nicksnyder/go-i18n v1.4.0 // indirect
github.com/onsi/ginkgo v1.6.0 // indirect
github.com/onsi/gomega v1.4.1 // indirect
github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c
github.com/pborman/uuid v0.0.0-20160216163710-c55201b03606 // indirect
github.com/pkg/errors v0.9.1
github.com/rs/xid v1.2.1
github.com/russross/blackfriday v1.5.2
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca
@ -48,19 +46,19 @@ require (
github.com/sirupsen/logrus v1.4.2
github.com/slack-go/slack v0.6.3-0.20200228121756-f56d616d5901
github.com/spf13/viper v1.6.1
github.com/stretchr/testify v1.4.0
github.com/status-im/status-go v0.49.0
github.com/stretchr/testify v1.5.1
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
github.com/zfjagann/golang-ring v0.0.0-20190106091943-a88bb6aef447
go.uber.org/zap v1.13.0
golang.org/x/image v0.0.0-20191214001246-9130b4cfad52
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
gopkg.in/fsnotify.v1 v1.4.7 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
)
//replace github.com/bwmarrin/discordgo v0.20.2 => github.com/matterbridge/discordgo v0.18.1-0.20200109173909-ed873362fa43
replace github.com/nlopes/slack v0.6.0 => github.com/matterbridge/slack v0.1.1-0.20191208194820-95190f11bfb6
//replace github.com/yaegashi/msgraph.go => github.com/matterbridge/msgraph.go v0.0.0-20191226214848-9e5d9c08a4e1
replace github.com/bwmarrin/discordgo v0.19.0 => github.com/matterbridge/discordgo v0.0.0-20191026232317-01823f4ebba4
replace github.com/ethereum/go-ethereum v1.9.5 => github.com/status-im/go-ethereum v1.9.5-status.7
go 1.13

681
go.sum

File diff suppressed because it is too large Load Diff