@@ -94,6 +94,7 @@ type Protocol struct {
|
||||
Charset string // irc
|
||||
ClientID string // msteams
|
||||
ColorNicks bool // only irc for now
|
||||
DataDir string // status
|
||||
Debug bool // general
|
||||
DebugLevel int // only for irc now
|
||||
DisableWebPagePreview bool // telegram
|
||||
@@ -107,6 +108,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
|
||||
LogFile string // general
|
||||
MediaDownloadBlackList []string
|
||||
@@ -161,7 +164,7 @@ type Protocol struct {
|
||||
Team string // mattermost, keybase
|
||||
TeamID string // msteams
|
||||
TenantID string // msteams
|
||||
Token string // gitter, slack, discord, api, matrix
|
||||
Token string // gitter, slack, discord, api, matrix, status
|
||||
Topic string // zulip
|
||||
URL string // mattermost, slack // DEPRECATED
|
||||
UseAPI bool // mattermost, slack
|
||||
@@ -220,6 +223,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
|
||||
|
||||
451
bridge/status/status.go
Normal file
451
bridge/status/status.go
Normal file
@@ -0,0 +1,451 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
crypto "github.com/ethereum/go-ethereum/crypto"
|
||||
api "github.com/status-im/status-go/api"
|
||||
"github.com/status-im/status-go/appdatabase"
|
||||
gethbridge "github.com/status-im/status-go/eth-node/bridge/geth"
|
||||
"github.com/status-im/status-go/eth-node/types"
|
||||
"github.com/status-im/status-go/multiaccounts"
|
||||
"github.com/status-im/status-go/multiaccounts/accounts"
|
||||
"github.com/status-im/status-go/multiaccounts/settings"
|
||||
gonode "github.com/status-im/status-go/node"
|
||||
params "github.com/status-im/status-go/params"
|
||||
|
||||
status "github.com/status-im/status-go/protocol"
|
||||
"github.com/status-im/status-go/protocol/common"
|
||||
"github.com/status-im/status-go/protocol/communities"
|
||||
"github.com/status-im/status-go/protocol/identity/alias"
|
||||
"github.com/status-im/status-go/protocol/protobuf"
|
||||
"github.com/status-im/status-go/protocol/requests"
|
||||
"github.com/status-im/status-go/services/ext/mailservers"
|
||||
mailserversDB "github.com/status-im/status-go/services/mailservers"
|
||||
|
||||
"github.com/status-im/status-go/common/dbsetup"
|
||||
"github.com/status-im/status-go/walletdatabase"
|
||||
)
|
||||
|
||||
type Bstatus struct {
|
||||
*bridge.Config
|
||||
|
||||
// message fetching loop controls
|
||||
fetchInterval time.Duration
|
||||
fetchDone chan bool
|
||||
|
||||
// node settings
|
||||
statusListenPort int
|
||||
statusListenAddr string
|
||||
|
||||
statusDataDir string
|
||||
|
||||
privateKey *ecdsa.PrivateKey
|
||||
nodeConfig *params.NodeConfig
|
||||
statusNode *gonode.StatusNode
|
||||
messenger *status.Messenger
|
||||
|
||||
joinedCommunities []string
|
||||
}
|
||||
|
||||
func New(cfg *bridge.Config) bridge.Bridger {
|
||||
return &Bstatus{
|
||||
Config: cfg,
|
||||
fetchDone: make(chan bool),
|
||||
statusListenPort: 30303,
|
||||
statusListenAddr: "0.0.0.0",
|
||||
statusDataDir: cfg.GetString("DataDir"),
|
||||
fetchInterval: 500 * time.Millisecond,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a sane configuration for a Status Node
|
||||
func (b *Bstatus) generateNodeConfig() (*params.NodeConfig, error) {
|
||||
options := []params.Option{
|
||||
b.withListenAddr(),
|
||||
}
|
||||
configFiles := []string{"./fleet.json"}
|
||||
config, err := params.NewNodeConfigWithDefaultsAndFiles(
|
||||
b.statusDataDir,
|
||||
params.MainNetworkID,
|
||||
options,
|
||||
configFiles,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
infuraToken := os.Getenv("STATUS_BUILD_INFURA_TOKEN")
|
||||
if len(infuraToken) == 0 {
|
||||
return nil, fmt.Errorf("STATUS_BUILD_INFURA_TOKEN env variable not set")
|
||||
}
|
||||
|
||||
createAccRequest := &requests.CreateAccount{
|
||||
WalletSecretsConfig: requests.WalletSecretsConfig{
|
||||
InfuraToken: infuraToken,
|
||||
},
|
||||
}
|
||||
config.Networks = api.BuildDefaultNetworks(createAccRequest)
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// get or generate new settings
|
||||
func (b *Bstatus) getOrGenerateSettings(nodeConfig *params.NodeConfig, appDB *sql.DB) (*settings.Settings, error) {
|
||||
|
||||
accdb, err := accounts.NewDB(appDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prevSettings, err := accdb.GetSettings()
|
||||
if err == nil {
|
||||
// return settings if exists
|
||||
return &prevSettings, nil
|
||||
}
|
||||
|
||||
// create new settings
|
||||
s := &settings.Settings{}
|
||||
|
||||
// needed values
|
||||
s.BackupEnabled = false
|
||||
s.InstallationID = uuid.New().String()
|
||||
s.AutoMessageEnabled = false
|
||||
s.UseMailservers = true
|
||||
|
||||
// other
|
||||
networks := make([]map[string]string, 0)
|
||||
networksJSON, err := json.Marshal(networks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
networkRawMessage := json.RawMessage(networksJSON)
|
||||
s.Networks = &networkRawMessage
|
||||
|
||||
err = accdb.CreateSettings(*s, *nodeConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (b *Bstatus) withListenAddr() params.Option {
|
||||
if addr := b.GetString("ListenAddr"); addr != "" {
|
||||
b.statusListenAddr = addr
|
||||
}
|
||||
if port := b.GetInt("ListenPort"); port != 0 {
|
||||
b.statusListenPort = port
|
||||
}
|
||||
return func(c *params.NodeConfig) error {
|
||||
c.ListenAddr = fmt.Sprintf("%s:%d", b.statusListenAddr, b.statusListenPort)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
b.propagateMessage(msg)
|
||||
}
|
||||
case <-b.fetchDone:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bstatus) stopMessagesLoop() {
|
||||
close(b.fetchDone)
|
||||
}
|
||||
|
||||
func (b *Bstatus) getDisplayName(c *status.Contact) string {
|
||||
if c.ENSVerified && c.EnsName != "" {
|
||||
return c.EnsName
|
||||
}
|
||||
if c.DisplayName != "" {
|
||||
return c.DisplayName
|
||||
}
|
||||
if c.PrimaryName() != "" {
|
||||
return c.PrimaryName()
|
||||
}
|
||||
return c.Alias
|
||||
}
|
||||
|
||||
func (b *Bstatus) propagateMessage(msg *common.Message) {
|
||||
var username string
|
||||
contact := b.messenger.GetContactByID(msg.From)
|
||||
if contact == nil {
|
||||
threeWordsName, err := alias.GenerateFromPublicKeyString(msg.From)
|
||||
if err != nil {
|
||||
username = msg.From
|
||||
} else {
|
||||
username = threeWordsName
|
||||
}
|
||||
} else {
|
||||
username = b.getDisplayName(contact)
|
||||
}
|
||||
|
||||
// Send message for processing
|
||||
b.Remote <- config.Message{
|
||||
Timestamp: time.Unix(int64(msg.WhisperTimestamp), 0),
|
||||
Username: username,
|
||||
Text: msg.Text,
|
||||
Channel: msg.ChatId,
|
||||
ID: msg.ID,
|
||||
Account: b.Account,
|
||||
}
|
||||
}
|
||||
|
||||
// Converts a bridge message into a Status message
|
||||
func (b *Bstatus) toStatusMsg(msg config.Message) *common.Message {
|
||||
message := common.NewMessage()
|
||||
message.ChatId = msg.Channel
|
||||
message.ContentType = protobuf.ChatMessage_BRIDGE_MESSAGE
|
||||
message.Payload = &protobuf.ChatMessage_BridgeMessage{
|
||||
BridgeMessage: &protobuf.BridgeMessage{
|
||||
BridgeName: msg.Protocol,
|
||||
UserName: msg.Username,
|
||||
UserAvatar: msg.Avatar,
|
||||
UserID: msg.UserID,
|
||||
Content: msg.Text,
|
||||
MessageID: msg.ID,
|
||||
ParentMessageID: msg.ParentID,
|
||||
},
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
func (b *Bstatus) connected() bool {
|
||||
return b.statusNode.IsRunning() && b.messenger.Online()
|
||||
}
|
||||
|
||||
func (b *Bstatus) getCommunityIdFromFullChatId(chatId string) string {
|
||||
const communityIdLength = 68
|
||||
if len(chatId) <= communityIdLength {
|
||||
return ""
|
||||
}
|
||||
return chatId[0:communityIdLength]
|
||||
}
|
||||
|
||||
func (b *Bstatus) createMultiAccount(privKey *ecdsa.PrivateKey) multiaccounts.Account {
|
||||
keyUID := sha256.Sum256(crypto.FromECDSAPub(&privKey.PublicKey))
|
||||
keyUIDHex := types.EncodeHex(keyUID[:])
|
||||
return multiaccounts.Account{
|
||||
KeyUID: keyUIDHex,
|
||||
}
|
||||
}
|
||||
|
||||
// i-face functions
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
b.Log.Debugf("=> Sending message %#v", msg)
|
||||
|
||||
_, err := b.messenger.SendChatMessage(context.Background(), b.toStatusMsg(msg))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to send message")
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (b *Bstatus) Connect() error {
|
||||
if len(b.statusDataDir) == 0 {
|
||||
b.statusDataDir = os.TempDir() + "/matterbridge-status-data"
|
||||
}
|
||||
err := os.Mkdir(b.statusDataDir, 0750)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return errors.Wrap(err, "Failed to create status directory")
|
||||
}
|
||||
|
||||
keyHex := strings.TrimPrefix(b.GetString("Token"), "0x")
|
||||
if privKey, err := crypto.HexToECDSA(keyHex); err != nil {
|
||||
return errors.Wrap(err, "Failed to parse private key in Token field")
|
||||
} else {
|
||||
b.privateKey = privKey
|
||||
}
|
||||
|
||||
b.nodeConfig, err = b.generateNodeConfig()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to generate node config")
|
||||
}
|
||||
|
||||
backend := api.NewGethStatusBackend()
|
||||
b.statusNode = backend.StatusNode()
|
||||
|
||||
walletDB, err := walletdatabase.InitializeDB(b.statusDataDir+"/"+"wallet.db", "", dbsetup.ReducedKDFIterationsNumber)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to initialize wallet db")
|
||||
}
|
||||
b.statusNode.SetWalletDB(walletDB)
|
||||
|
||||
appDB, err := appdatabase.InitializeDB(b.statusDataDir+"/"+"status.db", "", dbsetup.ReducedKDFIterationsNumber)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to initialize app db")
|
||||
}
|
||||
|
||||
settings, err := b.getOrGenerateSettings(b.nodeConfig, appDB)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to generate settings")
|
||||
}
|
||||
installationID := settings.InstallationID
|
||||
b.nodeConfig.ShhextConfig.InstallationID = installationID
|
||||
|
||||
multiaccountsDB, err := multiaccounts.InitializeDB(b.statusDataDir + "/" + "accounts.db")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to initialize accounts db")
|
||||
}
|
||||
multiAcc := b.createMultiAccount(b.privateKey)
|
||||
multiaccountsDB.SaveAccount(multiAcc)
|
||||
|
||||
b.statusNode.SetAppDB(appDB)
|
||||
b.statusNode.SetMultiaccountsDB(multiaccountsDB)
|
||||
|
||||
err = backend.StartNode(b.nodeConfig)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to start status node")
|
||||
}
|
||||
|
||||
// Create a custom logger to suppress DEBUG messages
|
||||
logger, _ := zap.NewProduction()
|
||||
|
||||
options := []status.Option{
|
||||
status.WithDatabase(appDB),
|
||||
status.WithWalletDatabase(walletDB),
|
||||
status.WithCustomLogger(logger),
|
||||
status.WithMailserversDatabase(mailserversDB.NewDB(appDB)),
|
||||
status.WithClusterConfig(b.nodeConfig.ClusterConfig),
|
||||
status.WithCheckingForBackupDisabled(),
|
||||
status.WithAutoMessageDisabled(),
|
||||
status.WithMultiAccounts(multiaccountsDB),
|
||||
status.WithAccount(&multiAcc),
|
||||
status.WithCommunityTokensService(b.statusNode.CommunityTokensService()),
|
||||
status.WithAccountManager(backend.AccountManager()),
|
||||
}
|
||||
|
||||
ldb, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
cache := mailservers.NewCache(ldb)
|
||||
peerStore := mailservers.NewPeerStore(cache)
|
||||
|
||||
messenger, err := status.NewMessenger(
|
||||
"status bridge messenger",
|
||||
b.privateKey,
|
||||
gethbridge.NewNodeBridge(b.statusNode.GethNode(), nil, b.statusNode.WakuV2Service()),
|
||||
installationID,
|
||||
peerStore,
|
||||
options...,
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to create Messenger")
|
||||
}
|
||||
|
||||
messenger.SetP2PServer(b.statusNode.GethNode().Server())
|
||||
messenger.EnableBackedupMessagesProcessing()
|
||||
|
||||
if err := messenger.Init(); err != nil {
|
||||
return errors.Wrap(err, "Failed to init Messenger")
|
||||
}
|
||||
|
||||
if _, err := messenger.Start(); err != nil {
|
||||
return errors.Wrap(err, "Failed to start Messenger")
|
||||
}
|
||||
b.messenger = messenger
|
||||
|
||||
startTime := time.Now()
|
||||
for !b.connected() && time.Since(startTime) < 10*time.Second {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
if !b.connected() {
|
||||
return fmt.Errorf("failed to create Messenger")
|
||||
}
|
||||
|
||||
// Start fetching messages
|
||||
go b.fetchMessagesLoop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type BridgeTimeSource struct{}
|
||||
|
||||
func (t *BridgeTimeSource) GetCurrentTime() uint64 {
|
||||
return uint64(time.Now().Unix()) * 1000
|
||||
}
|
||||
|
||||
func (b *Bstatus) JoinChannel(channel config.ChannelInfo) error {
|
||||
return b.joinCommunityChannel(channel)
|
||||
}
|
||||
|
||||
func (b *Bstatus) joinCommunityChannel(channel config.ChannelInfo) error {
|
||||
chatID := channel.Name
|
||||
communityID := b.getCommunityIdFromFullChatId(chatID)
|
||||
|
||||
if communityID == "" {
|
||||
return fmt.Errorf("wrong chat id %v", chatID)
|
||||
}
|
||||
|
||||
if slices.Contains(b.joinedCommunities, communityID) {
|
||||
return nil
|
||||
}
|
||||
b.joinedCommunities = append(b.joinedCommunities, communityID)
|
||||
|
||||
_, err := b.messenger.FetchCommunity(&status.FetchCommunityRequest{
|
||||
CommunityKey: communityID,
|
||||
Shard: nil,
|
||||
TryDatabase: true,
|
||||
WaitForResponse: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to fetch community")
|
||||
}
|
||||
|
||||
_, err = b.messenger.JoinCommunity(context.Background(), types.Hex2Bytes(communityID), true)
|
||||
if err != nil && err != communities.ErrOrgAlreadyJoined {
|
||||
return errors.Wrap(err, "Failed to join community")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bstatus) Disconnect() error {
|
||||
b.stopMessagesLoop()
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user