add support for Status whisper protocol

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 2977a5957e
commit b60437d12e
No known key found for this signature in database
GPG Key ID: 4EF064D0E6D63020
8 changed files with 1055 additions and 15 deletions

View File

@ -1,16 +1,22 @@
FROM alpine:edge AS builder
COPY . /go/src/github.com/42wim/matterbridge
RUN apk update && apk add go git gcc musl-dev \
&& 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
RUN apk update && apk add go git gcc musl-dev linux-headers
COPY . /go/src/github.com/42wim/matterbridge
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
FROM alpine:edge
RUN apk --no-cache add ca-certificates mailcap
COPY --from=builder /bin/matterbridge /bin/matterbridge
RUN mkdir /etc/matterbridge \
&& touch /etc/matterbridge/matterbridge.toml \
&& ln -sf /matterbridge.toml /etc/matterbridge/matterbridge.toml
ENTRYPOINT ["/bin/matterbridge", "-conf", "/etc/matterbridge/matterbridge.toml"]
ENTRYPOINT ["/bin/matterbridge"]

View File

@ -92,6 +92,8 @@ type Protocol struct {
Jid string // xmpp
JoinDelay string // all protocols
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.
@ -143,6 +145,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
@ -200,6 +203,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

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

@ -0,0 +1,35 @@
# Description
This is an implementation of support for the [Status](https://status.im/) chat protocol.
It makes use of the [status-go](https://github.com/status-im/status-go).
# Configuration
Here's a basic config using a randomly generated `PrivateKey` for bridge identity:
```yaml
---
Nick: "bridge-bot"
status:
bridge:
Nick: 'mybridge.stateofus.eth'
PrivateKey: '0xeb87e5780fef3a83fa6a7f5c19fb206715a66e1c10aab50471686c6347b1ede4'
RemoteNickFormat: '**{NICK}**@*{PROTOCOL}*: '
gateway:
- name: "status-bridge-test"
enable: true
inout:
- account: "status.bridge"
channel: "test-channel-1"
- account: "status.bridge"
channel: "test-channel-2"
```
# TODO
* Drop usage of the SQLite database entirely
* Improve handling of logs for the Whisper node
* Handle properly verifying successful delivery

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
}

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

@ -0,0 +1,328 @@
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
// Maximum number of characters a username can have
maxUsernameLen int
// message fetching loop controls
fetchInterval time.Duration
fetchTimeout time.Duration
fetchDone chan bool
// Whisper node settings
whisperListenPort int
whisperListenAddr string
whisperDataDir string
// ENS settings
ensName string // Make sure the bridge account owns the ENS name
ensVerifyURL string // URL of Infura endpoint to call
ensContract string // Address of ENS resolving contract
ensDone chan bool // Control channel for stopping ENS checking loop
ensInterval time.Duration // Frequency of ENS verification checks
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
maxUsernameLen: 40,
whisperDataDir: "/tmp/matterbridge-status-data",
fetchTimeout: 500 * time.Millisecond,
fetchInterval: 500 * time.Millisecond,
// ENS checks are slow, also db has a lock
ensInterval: 30 * time.Second,
ensVerifyURL: "https://mainnet.infura.io/v3/f315575765b14720b32382a61a89341a",
ensContract: "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e",
}
}
func (b *Bstatus) Connect() error {
// Will be displayed to other users if registered
b.ensName = b.GetString("Nick")
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()
// Start a routine for periodically checking ENS names
go b.checkEnsNamesLoop()
return nil
}
func (b *Bstatus) Disconnect() error {
b.stopMessagesLoop()
b.stopEnsNamesLoop()
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, b.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 (b *Bstatus) genStatusMsg(msg config.Message) (sMsg *status.Message) {
sMsg = &status.Message{}
sMsg.EnsName = b.ensName
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) stopMessagesLoop() {
close(b.fetchDone)
}
func (b *Bstatus) stopEnsNamesLoop() {
close(b.ensDone)
}
// Main loop for fetching Status messages and relaying them to the bridge
func (b *Bstatus) fetchMessagesLoop() {
ticker := time.NewTicker(b.fetchInterval)
defer ticker.Stop()
for {
select {
case <-ticker.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) checkEnsNamesLoop() {
ticker := time.NewTicker(b.ensInterval)
defer ticker.Stop()
ctx, cancelVerifyENS := context.WithCancel(context.Background())
for {
select {
case <-ticker.C:
_, err := b.messenger.VerifyENSNames(ctx, b.ensVerifyURL, b.ensContract)
if err != nil {
b.Log.WithError(err).Error("Failed to validate ENS name")
continue
}
case <-b.ensDone:
cancelVerifyENS()
return
}
}
}
func (b *Bstatus) propagateMessage(msg *status.Message) {
pubKey := publicKeyToHex(msg.SigPubKey)
var username string
// Contact can have an ENS Name, but needs to be verified
contact, err := b.messenger.GetContactByID(pubKey)
if err != nil {
b.Log.WithError(err).Error("Not yet verified contact:", pubKey)
username, err = alias.GenerateFromPublicKeyString(pubKey)
if err != nil { // fallback to full public key
b.Log.WithError(err).Error("Failed to generate Chat name")
username = pubKey
}
} else if contact.ENSVerified { // trim our domain for brevity
username = strings.TrimSuffix(contact.Name, ".stateofus.eth")
} else { // fallback to 3-word chat name
username = contact.Alias
}
// Trim username in case of LONG ENS names
if len(username) > b.maxUsernameLen {
username = username[:b.maxUsernameLen]
}
// Send message for processing
b.Remote <- config.Message{
Timestamp: time.Unix(int64(msg.WhisperTimestamp), 0),
Username: username,
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

@ -0,0 +1,11 @@
// +build !nostatus
package bridgemap
import (
bstatus "github.com/42wim/matterbridge/bridge/status"
)
func init() {
FullMap["status"] = bstatus.New
}

10
go.mod
View File

@ -8,10 +8,12 @@ require (
github.com/Rhymen/go-whatsapp v0.1.1-0.20200421062035-31e8111ac334
github.com/d5/tengo/v2 v2.4.2
github.com/davecgh/go-spew v1.1.1
github.com/ethereum/go-ethereum v1.9.5
github.com/fsnotify/fsnotify v1.4.9
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.2
@ -36,10 +38,8 @@ require (
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
@ -47,12 +47,14 @@ require (
github.com/sirupsen/logrus v1.6.0
github.com/slack-go/slack v0.6.4
github.com/spf13/viper v1.7.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/writeas/go-strip-markdown v2.0.1+incompatible
github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
github.com/yaegashi/msgraph.go v0.1.2
github.com/zfjagann/golang-ring v0.0.0-20190304061218-d34796e0a6c2
go.uber.org/zap v1.13.0
golang.org/x/image v0.0.0-20200430140353-33d19683fad8
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
gopkg.in/fsnotify.v1 v1.4.7 // indirect
@ -62,4 +64,6 @@ require (
//replace github.com/bwmarrin/discordgo v0.20.2 => github.com/matterbridge/discordgo v0.18.1-0.20200109173909-ed873362fa43
replace github.com/ethereum/go-ethereum v1.9.5 => github.com/status-im/go-ethereum v1.9.5-status.9
go 1.13

634
go.sum

File diff suppressed because it is too large Load Diff