forked from mirror/mautrix-discord
End to bridge encryption implementation
So far this is passing my basic tests, but could use some testing from people that are much more familiar with how this is supposed to work. Refs #27
This commit is contained in:
parent
145c0cc2cb
commit
c5f58afe71
22 changed files with 848 additions and 58 deletions
|
@ -48,6 +48,8 @@ type Bridge struct {
|
|||
puppetsLock sync.Mutex
|
||||
|
||||
StateStore *database.SQLStateStore
|
||||
|
||||
crypto Crypto
|
||||
}
|
||||
|
||||
func New(cfg *config.Config) (*Bridge, error) {
|
||||
|
@ -104,6 +106,8 @@ func New(cfg *config.Config) (*Bridge, error) {
|
|||
StateStore: stateStore,
|
||||
}
|
||||
|
||||
bridge.crypto = NewCryptoHelper(bridge)
|
||||
|
||||
if cfg.Appservice.Provisioning.Enabled() {
|
||||
bridge.provisioning = newProvisioningAPI(bridge)
|
||||
}
|
||||
|
@ -151,6 +155,13 @@ func (b *Bridge) Start() error {
|
|||
return err
|
||||
}
|
||||
|
||||
if b.crypto != nil {
|
||||
if err := b.crypto.Init(); err != nil {
|
||||
b.log.Fatalln("Error initializing end-to-bridge encryption:", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
b.log.Debugln("Starting application service HTTP server")
|
||||
go b.as.Start()
|
||||
|
||||
|
@ -159,6 +170,10 @@ func (b *Bridge) Start() error {
|
|||
|
||||
go b.updateBotProfile()
|
||||
|
||||
if b.crypto != nil {
|
||||
go b.crypto.Start()
|
||||
}
|
||||
|
||||
go b.startUsers()
|
||||
|
||||
// Finally tell the appservice we're ready
|
||||
|
@ -168,5 +183,21 @@ func (b *Bridge) Start() error {
|
|||
}
|
||||
|
||||
func (b *Bridge) Stop() {
|
||||
if b.crypto != nil {
|
||||
b.crypto.Stop()
|
||||
}
|
||||
|
||||
b.as.Stop()
|
||||
b.eventProcessor.Stop()
|
||||
|
||||
for _, user := range b.usersByMXID {
|
||||
if user.Session == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
b.log.Debugln("Disconnecting", user.MXID)
|
||||
user.Session.Close()
|
||||
}
|
||||
|
||||
b.log.Infoln("Bridge stopped")
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package bridge
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
|
||||
|
@ -33,11 +34,17 @@ func (g *globals) reply(msg string) {
|
|||
content.MsgType = event.MsgNotice
|
||||
intent := g.bot
|
||||
|
||||
if g.portal != nil && g.portal.IsPrivateChat() {
|
||||
if g.portal == nil {
|
||||
g.handler.log.Errorfln("we don't have a portal for this command")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if g.portal.IsPrivateChat() {
|
||||
intent = g.portal.MainIntent()
|
||||
}
|
||||
|
||||
_, err := intent.SendMessageEvent(g.roomID, event.EventMessage, content)
|
||||
_, err := g.portal.sendMatrixMessage(intent, event.EventMessage, &content, nil, time.Now().UTC().UnixMilli())
|
||||
if err != nil {
|
||||
g.handler.log.Warnfln("Failed to reply to command from %q: %v", g.user.MXID, err)
|
||||
}
|
||||
|
|
339
bridge/crypto.go
Normal file
339
bridge/crypto.go
Normal file
|
@ -0,0 +1,339 @@
|
|||
package bridge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"maunium.net/go/maulogger/v2"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/crypto"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"gitlab.com/beeper/discord/database"
|
||||
)
|
||||
|
||||
var NoSessionFound = crypto.NoSessionFound
|
||||
|
||||
var levelTrace = maulogger.Level{
|
||||
Name: "TRACE",
|
||||
Severity: -10,
|
||||
Color: -1,
|
||||
}
|
||||
|
||||
type Crypto interface {
|
||||
HandleMemberEvent(*event.Event)
|
||||
Decrypt(*event.Event) (*event.Event, error)
|
||||
Encrypt(id.RoomID, event.Type, event.Content) (*event.EncryptedEventContent, error)
|
||||
WaitForSession(id.RoomID, id.SenderKey, id.SessionID, time.Duration) bool
|
||||
RequestSession(id.RoomID, id.SenderKey, id.SessionID, id.UserID, id.DeviceID)
|
||||
ResetSession(id.RoomID)
|
||||
Init() error
|
||||
Start()
|
||||
Stop()
|
||||
}
|
||||
|
||||
type CryptoHelper struct {
|
||||
bridge *Bridge
|
||||
client *mautrix.Client
|
||||
mach *crypto.OlmMachine
|
||||
store *database.SQLCryptoStore
|
||||
log maulogger.Logger
|
||||
baseLog maulogger.Logger
|
||||
}
|
||||
|
||||
func NewCryptoHelper(bridge *Bridge) Crypto {
|
||||
if !bridge.Config.Bridge.Encryption.Allow {
|
||||
bridge.log.Debugln("Bridge built with end-to-bridge encryption, but disabled in config")
|
||||
return nil
|
||||
}
|
||||
|
||||
baseLog := bridge.log.Sub("Crypto")
|
||||
return &CryptoHelper{
|
||||
bridge: bridge,
|
||||
log: baseLog.Sub("Helper"),
|
||||
baseLog: baseLog,
|
||||
}
|
||||
}
|
||||
|
||||
func (helper *CryptoHelper) Init() error {
|
||||
helper.log.Debugln("Initializing end-to-bridge encryption...")
|
||||
|
||||
helper.store = database.NewSQLCryptoStore(helper.bridge.db, helper.bridge.as.BotMXID(),
|
||||
fmt.Sprintf("@%s:%s", helper.bridge.Config.Bridge.FormatUsername("%"), helper.bridge.as.HomeserverDomain))
|
||||
|
||||
var err error
|
||||
helper.client, err = helper.loginBot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
helper.log.Debugln("Logged in as bridge bot with device ID", helper.client.DeviceID)
|
||||
|
||||
logger := &cryptoLogger{helper.baseLog}
|
||||
stateStore := &cryptoStateStore{helper.bridge}
|
||||
helper.mach = crypto.NewOlmMachine(helper.client, logger, helper.store, stateStore)
|
||||
helper.mach.AllowKeyShare = helper.allowKeyShare
|
||||
|
||||
helper.client.Syncer = &cryptoSyncer{helper.mach}
|
||||
helper.client.Store = &cryptoClientStore{helper.store}
|
||||
|
||||
return helper.mach.Load()
|
||||
}
|
||||
|
||||
func (helper *CryptoHelper) allowKeyShare(device *crypto.DeviceIdentity, info event.RequestedKeyInfo) *crypto.KeyShareRejection {
|
||||
cfg := helper.bridge.Config.Bridge.Encryption.KeySharing
|
||||
if !cfg.Allow {
|
||||
return &crypto.KeyShareRejectNoResponse
|
||||
} else if device.Trust == crypto.TrustStateBlacklisted {
|
||||
return &crypto.KeyShareRejectBlacklisted
|
||||
} else if device.Trust == crypto.TrustStateVerified || !cfg.RequireVerification {
|
||||
portal := helper.bridge.GetPortalByMXID(info.RoomID)
|
||||
if portal == nil {
|
||||
helper.log.Debugfln("Rejecting key request for %s from %s/%s: room is not a portal", info.SessionID, device.UserID, device.DeviceID)
|
||||
|
||||
return &crypto.KeyShareRejection{Code: event.RoomKeyWithheldUnavailable, Reason: "Requested room is not a portal room"}
|
||||
}
|
||||
user := helper.bridge.GetUserByMXID(device.UserID)
|
||||
// FIXME reimplement IsInPortal
|
||||
if !user.Admin /*&& !user.IsInPortal(portal.Key)*/ {
|
||||
helper.log.Debugfln("Rejecting key request for %s from %s/%s: user is not in portal", info.SessionID, device.UserID, device.DeviceID)
|
||||
|
||||
return &crypto.KeyShareRejection{Code: event.RoomKeyWithheldUnauthorized, Reason: "You're not in that portal"}
|
||||
}
|
||||
helper.log.Debugfln("Accepting key request for %s from %s/%s", info.SessionID, device.UserID, device.DeviceID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return &crypto.KeyShareRejectUnverified
|
||||
}
|
||||
|
||||
func (helper *CryptoHelper) loginBot() (*mautrix.Client, error) {
|
||||
deviceID := helper.store.FindDeviceID()
|
||||
if len(deviceID) > 0 {
|
||||
helper.log.Debugln("Found existing device ID for bot in database:", deviceID)
|
||||
}
|
||||
|
||||
client, err := mautrix.NewClient(helper.bridge.as.HomeserverURL, "", "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize client: %w", err)
|
||||
}
|
||||
|
||||
client.Logger = helper.baseLog.Sub("Bot")
|
||||
client.Client = helper.bridge.as.HTTPClient
|
||||
client.DefaultHTTPRetries = helper.bridge.as.DefaultHTTPRetries
|
||||
flows, err := client.GetLoginFlows()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get supported login flows: %w", err)
|
||||
}
|
||||
|
||||
flow := flows.FirstFlowOfType(mautrix.AuthTypeAppservice, mautrix.AuthTypeHalfyAppservice)
|
||||
if flow == nil {
|
||||
return nil, fmt.Errorf("homeserver does not support appservice login")
|
||||
}
|
||||
|
||||
// We set the API token to the AS token here to authenticate the appservice login
|
||||
// It'll get overridden after the login
|
||||
client.AccessToken = helper.bridge.as.Registration.AppToken
|
||||
resp, err := client.Login(&mautrix.ReqLogin{
|
||||
Type: flow.Type,
|
||||
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(helper.bridge.as.BotMXID())},
|
||||
DeviceID: deviceID,
|
||||
InitialDeviceDisplayName: "Discord Bridge",
|
||||
StoreCredentials: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to log in as bridge bot: %w", err)
|
||||
}
|
||||
|
||||
helper.store.DeviceID = resp.DeviceID
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (helper *CryptoHelper) Start() {
|
||||
helper.log.Debugln("Starting syncer for receiving to-device messages")
|
||||
|
||||
err := helper.client.Sync()
|
||||
if err != nil {
|
||||
helper.log.Errorln("Fatal error syncing:", err)
|
||||
} else {
|
||||
helper.log.Infoln("Bridge bot to-device syncer stopped without error")
|
||||
}
|
||||
}
|
||||
|
||||
func (helper *CryptoHelper) Stop() {
|
||||
helper.log.Debugln("CryptoHelper.Stop() called, stopping bridge bot sync")
|
||||
helper.client.StopSync()
|
||||
}
|
||||
|
||||
func (helper *CryptoHelper) Decrypt(evt *event.Event) (*event.Event, error) {
|
||||
return helper.mach.DecryptMegolmEvent(evt)
|
||||
}
|
||||
|
||||
func (helper *CryptoHelper) Encrypt(roomID id.RoomID, evtType event.Type, content event.Content) (*event.EncryptedEventContent, error) {
|
||||
encrypted, err := helper.mach.EncryptMegolmEvent(roomID, evtType, &content)
|
||||
|
||||
if err != nil {
|
||||
if err != crypto.SessionExpired && err != crypto.SessionNotShared && err != crypto.NoGroupSession {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
helper.log.Debugfln("Got %v while encrypting event for %s, sharing group session and trying again...", err, roomID)
|
||||
users, err := helper.store.GetRoomMembers(roomID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get room member list: %w", err)
|
||||
}
|
||||
|
||||
err = helper.mach.ShareGroupSession(roomID, users)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to share group session: %w", err)
|
||||
}
|
||||
|
||||
encrypted, err = helper.mach.EncryptMegolmEvent(roomID, evtType, &content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt event after re-sharing group session: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
func (helper *CryptoHelper) WaitForSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, timeout time.Duration) bool {
|
||||
return helper.mach.WaitForSession(roomID, senderKey, sessionID, timeout)
|
||||
}
|
||||
|
||||
func (helper *CryptoHelper) RequestSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, userID id.UserID, deviceID id.DeviceID) {
|
||||
err := helper.mach.SendRoomKeyRequest(roomID, senderKey, sessionID, "", map[id.UserID][]id.DeviceID{userID: {deviceID}})
|
||||
if err != nil {
|
||||
helper.log.Warnfln("Failed to send key request to %s/%s for %s in %s: %v", userID, deviceID, sessionID, roomID, err)
|
||||
} else {
|
||||
helper.log.Debugfln("Sent key request to %s/%s for %s in %s", userID, deviceID, sessionID, roomID)
|
||||
}
|
||||
}
|
||||
|
||||
func (helper *CryptoHelper) ResetSession(roomID id.RoomID) {
|
||||
err := helper.mach.CryptoStore.RemoveOutboundGroupSession(roomID)
|
||||
if err != nil {
|
||||
helper.log.Debugfln("Error manually removing outbound group session in %s: %v", roomID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (helper *CryptoHelper) HandleMemberEvent(evt *event.Event) {
|
||||
helper.mach.HandleMemberEvent(evt)
|
||||
}
|
||||
|
||||
type cryptoSyncer struct {
|
||||
*crypto.OlmMachine
|
||||
}
|
||||
|
||||
func (syncer *cryptoSyncer) ProcessResponse(resp *mautrix.RespSync, since string) error {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
syncer.Log.Error("Processing sync response (%s) panicked: %v\n%s", since, err, debug.Stack())
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
syncer.Log.Trace("Starting sync response handling (%s)", since)
|
||||
syncer.ProcessSyncResponse(resp, since)
|
||||
syncer.Log.Trace("Successfully handled sync response (%s)", since)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(30 * time.Second):
|
||||
syncer.Log.Warn("Handling sync response (%s) is taking unusually long", since)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (syncer *cryptoSyncer) OnFailedSync(_ *mautrix.RespSync, err error) (time.Duration, error) {
|
||||
syncer.Log.Error("Error /syncing, waiting 10 seconds: %v", err)
|
||||
|
||||
return 10 * time.Second, nil
|
||||
}
|
||||
|
||||
func (syncer *cryptoSyncer) GetFilterJSON(_ id.UserID) *mautrix.Filter {
|
||||
everything := []event.Type{{Type: "*"}}
|
||||
|
||||
return &mautrix.Filter{
|
||||
Presence: mautrix.FilterPart{NotTypes: everything},
|
||||
AccountData: mautrix.FilterPart{NotTypes: everything},
|
||||
Room: mautrix.RoomFilter{
|
||||
IncludeLeave: false,
|
||||
Ephemeral: mautrix.FilterPart{NotTypes: everything},
|
||||
AccountData: mautrix.FilterPart{NotTypes: everything},
|
||||
State: mautrix.FilterPart{NotTypes: everything},
|
||||
Timeline: mautrix.FilterPart{NotTypes: everything},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type cryptoLogger struct {
|
||||
int maulogger.Logger
|
||||
}
|
||||
|
||||
func (c *cryptoLogger) Error(message string, args ...interface{}) {
|
||||
c.int.Errorfln(message, args...)
|
||||
}
|
||||
|
||||
func (c *cryptoLogger) Warn(message string, args ...interface{}) {
|
||||
c.int.Warnfln(message, args...)
|
||||
}
|
||||
|
||||
func (c *cryptoLogger) Debug(message string, args ...interface{}) {
|
||||
c.int.Debugfln(message, args...)
|
||||
}
|
||||
|
||||
func (c *cryptoLogger) Trace(message string, args ...interface{}) {
|
||||
c.int.Logfln(levelTrace, message, args...)
|
||||
}
|
||||
|
||||
type cryptoClientStore struct {
|
||||
int *database.SQLCryptoStore
|
||||
}
|
||||
|
||||
func (c cryptoClientStore) SaveFilterID(_ id.UserID, _ string) {}
|
||||
func (c cryptoClientStore) LoadFilterID(_ id.UserID) string { return "" }
|
||||
func (c cryptoClientStore) SaveRoom(_ *mautrix.Room) {}
|
||||
func (c cryptoClientStore) LoadRoom(_ id.RoomID) *mautrix.Room { return nil }
|
||||
|
||||
func (c cryptoClientStore) SaveNextBatch(_ id.UserID, nextBatchToken string) {
|
||||
c.int.PutNextBatch(nextBatchToken)
|
||||
}
|
||||
|
||||
func (c cryptoClientStore) LoadNextBatch(_ id.UserID) string {
|
||||
return c.int.GetNextBatch()
|
||||
}
|
||||
|
||||
var _ mautrix.Storer = (*cryptoClientStore)(nil)
|
||||
|
||||
type cryptoStateStore struct {
|
||||
bridge *Bridge
|
||||
}
|
||||
|
||||
var _ crypto.StateStore = (*cryptoStateStore)(nil)
|
||||
|
||||
func (c *cryptoStateStore) IsEncrypted(id id.RoomID) bool {
|
||||
portal := c.bridge.GetPortalByMXID(id)
|
||||
if portal != nil {
|
||||
return portal.Encrypted
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *cryptoStateStore) FindSharedRooms(id id.UserID) []id.RoomID {
|
||||
return c.bridge.StateStore.FindSharedRooms(id)
|
||||
}
|
||||
|
||||
func (c *cryptoStateStore) GetEncryptionEvent(id.RoomID) *event.EncryptionEventContent {
|
||||
// TODO implement
|
||||
return nil
|
||||
}
|
119
bridge/matrix.go
119
bridge/matrix.go
|
@ -1,7 +1,10 @@
|
|||
package bridge
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"maunium.net/go/maulogger/v2"
|
||||
"maunium.net/go/mautrix"
|
||||
|
@ -29,9 +32,11 @@ func (b *Bridge) setupEvents() {
|
|||
}
|
||||
|
||||
b.eventProcessor.On(event.EventMessage, b.matrixHandler.handleMessage)
|
||||
b.eventProcessor.On(event.EventEncrypted, b.matrixHandler.handleEncrypted)
|
||||
b.eventProcessor.On(event.EventReaction, b.matrixHandler.handleReaction)
|
||||
b.eventProcessor.On(event.EventRedaction, b.matrixHandler.handleRedaction)
|
||||
b.eventProcessor.On(event.StateMember, b.matrixHandler.handleMembership)
|
||||
b.eventProcessor.On(event.StateEncryption, b.matrixHandler.handleEncryption)
|
||||
}
|
||||
|
||||
func (mh *matrixHandler) join(evt *event.Event, intent *appservice.IntentAPI) *mautrix.RespJoinedMembers {
|
||||
|
@ -185,6 +190,10 @@ func (mh *matrixHandler) handleMembership(evt *event.Event) {
|
|||
return
|
||||
}
|
||||
|
||||
if mh.bridge.crypto != nil {
|
||||
mh.bridge.crypto.HandleMemberEvent(evt)
|
||||
}
|
||||
|
||||
// Grab the content of the event.
|
||||
content := evt.Content.AsMember()
|
||||
|
||||
|
@ -255,3 +264,113 @@ func (mh *matrixHandler) handleRedaction(evt *event.Event) {
|
|||
portal.handleMatrixRedaction(evt)
|
||||
}
|
||||
}
|
||||
|
||||
func (mh *matrixHandler) handleEncryption(evt *event.Event) {
|
||||
if evt.Content.AsEncryption().Algorithm != id.AlgorithmMegolmV1 {
|
||||
return
|
||||
}
|
||||
|
||||
portal := mh.bridge.GetPortalByMXID(evt.RoomID)
|
||||
if portal != nil && !portal.Encrypted {
|
||||
mh.log.Debugfln("%s enabled encryption in %s", evt.Sender, evt.RoomID)
|
||||
portal.Encrypted = true
|
||||
portal.Update()
|
||||
}
|
||||
}
|
||||
|
||||
const sessionWaitTimeout = 5 * time.Second
|
||||
|
||||
func (mh *matrixHandler) handleEncrypted(evt *event.Event) {
|
||||
if mh.ignoreEvent(evt) || mh.bridge.crypto == nil {
|
||||
return
|
||||
}
|
||||
|
||||
decrypted, err := mh.bridge.crypto.Decrypt(evt)
|
||||
decryptionRetryCount := 0
|
||||
if errors.Is(err, NoSessionFound) {
|
||||
content := evt.Content.AsEncrypted()
|
||||
mh.log.Debugfln("Couldn't find session %s trying to decrypt %s, waiting %d seconds...", content.SessionID, evt.ID, int(sessionWaitTimeout.Seconds()))
|
||||
mh.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, err, false, decryptionRetryCount)
|
||||
decryptionRetryCount++
|
||||
|
||||
if mh.bridge.crypto.WaitForSession(evt.RoomID, content.SenderKey, content.SessionID, sessionWaitTimeout) {
|
||||
mh.log.Debugfln("Got session %s after waiting, trying to decrypt %s again", content.SessionID, evt.ID)
|
||||
decrypted, err = mh.bridge.crypto.Decrypt(evt)
|
||||
} else {
|
||||
mh.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, fmt.Errorf("didn't receive encryption keys"), false, decryptionRetryCount)
|
||||
|
||||
go mh.waitLongerForSession(evt)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
mh.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, err, true, decryptionRetryCount)
|
||||
|
||||
mh.log.Warnfln("Failed to decrypt %s: %v", evt.ID, err)
|
||||
_, _ = mh.bridge.bot.SendNotice(evt.RoomID, fmt.Sprintf(
|
||||
"\u26a0 Your message was not bridged: %v", err))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
mh.as.SendMessageSendCheckpoint(decrypted, appservice.StepDecrypted, decryptionRetryCount)
|
||||
mh.bridge.eventProcessor.Dispatch(decrypted)
|
||||
}
|
||||
|
||||
func (mh *matrixHandler) waitLongerForSession(evt *event.Event) {
|
||||
const extendedTimeout = sessionWaitTimeout * 3
|
||||
|
||||
content := evt.Content.AsEncrypted()
|
||||
mh.log.Debugfln("Couldn't find session %s trying to decrypt %s, waiting %d more seconds...",
|
||||
content.SessionID, evt.ID, int(extendedTimeout.Seconds()))
|
||||
|
||||
go mh.bridge.crypto.RequestSession(evt.RoomID, content.SenderKey, content.SessionID, evt.Sender, content.DeviceID)
|
||||
|
||||
resp, err := mh.bridge.bot.SendNotice(evt.RoomID, fmt.Sprintf(
|
||||
"\u26a0 Your message was not bridged: the bridge hasn't received the decryption keys. "+
|
||||
"The bridge will retry for %d seconds. If this error keeps happening, try restarting your client.",
|
||||
int(extendedTimeout.Seconds())))
|
||||
if err != nil {
|
||||
mh.log.Errorfln("Failed to send decryption error to %s: %v", evt.RoomID, err)
|
||||
}
|
||||
|
||||
update := event.MessageEventContent{MsgType: event.MsgNotice}
|
||||
|
||||
if mh.bridge.crypto.WaitForSession(evt.RoomID, content.SenderKey, content.SessionID, extendedTimeout) {
|
||||
mh.log.Debugfln("Got session %s after waiting more, trying to decrypt %s again", content.SessionID, evt.ID)
|
||||
|
||||
decrypted, err := mh.bridge.crypto.Decrypt(evt)
|
||||
if err == nil {
|
||||
mh.as.SendMessageSendCheckpoint(decrypted, appservice.StepDecrypted, 2)
|
||||
mh.bridge.eventProcessor.Dispatch(decrypted)
|
||||
_, _ = mh.bridge.bot.RedactEvent(evt.RoomID, resp.EventID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
mh.log.Warnfln("Failed to decrypt %s: %v", evt.ID, err)
|
||||
mh.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, err, true, 2)
|
||||
update.Body = fmt.Sprintf("\u26a0 Your message was not bridged: %v", err)
|
||||
} else {
|
||||
mh.log.Debugfln("Didn't get %s, giving up on %s", content.SessionID, evt.ID)
|
||||
mh.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, fmt.Errorf("didn't receive encryption keys"), true, 2)
|
||||
update.Body = "\u26a0 Your message was not bridged: the bridge hasn't received the decryption keys. " +
|
||||
"If this error keeps happening, try restarting your client."
|
||||
}
|
||||
|
||||
newContent := update
|
||||
update.NewContent = &newContent
|
||||
if resp != nil {
|
||||
update.RelatesTo = &event.RelatesTo{
|
||||
Type: event.RelReplace,
|
||||
EventID: resp.EventID,
|
||||
}
|
||||
}
|
||||
|
||||
_, err = mh.bridge.bot.SendMessageEvent(evt.RoomID, event.EventMessage, &update)
|
||||
if err != nil {
|
||||
mh.log.Debugfln("Failed to update decryption error notice %s: %v", resp.EventID, err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ type Portal struct {
|
|||
log log.Logger
|
||||
|
||||
roomCreateLock sync.Mutex
|
||||
encryptLock sync.Mutex
|
||||
|
||||
discordMessages chan portalDiscordMessage
|
||||
matrixMessages chan portalMatrixMessage
|
||||
|
@ -144,7 +145,7 @@ func (p *Portal) handleMatrixInvite(sender *User, evt *event.Event) {
|
|||
p.log.Infoln("no puppet for %v", sender)
|
||||
// Open a conversation on the discord side?
|
||||
}
|
||||
p.log.Infoln("puppet:", puppet)
|
||||
p.log.Infoln("matrixInvite: puppet:", puppet)
|
||||
}
|
||||
|
||||
func (p *Portal) messageLoop() {
|
||||
|
@ -212,14 +213,25 @@ func (p *Portal) createMatrixRoom(user *User, channel *discordgo.Channel) error
|
|||
|
||||
var invite []id.UserID
|
||||
|
||||
if p.IsPrivateChat() {
|
||||
invite = append(invite, p.bridge.bot.UserID)
|
||||
if p.bridge.Config.Bridge.Encryption.Default {
|
||||
initialState = append(initialState, &event.Event{
|
||||
Type: event.StateEncryption,
|
||||
Content: event.Content{
|
||||
Parsed: event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1},
|
||||
},
|
||||
})
|
||||
p.Encrypted = true
|
||||
|
||||
if p.IsPrivateChat() {
|
||||
invite = append(invite, p.bridge.bot.UserID)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := intent.CreateRoom(&mautrix.ReqCreateRoom{
|
||||
Visibility: "private",
|
||||
Name: p.Name,
|
||||
Topic: p.Topic,
|
||||
Invite: invite,
|
||||
Preset: "private_chat",
|
||||
IsDirect: p.IsPrivateChat(),
|
||||
InitialState: initialState,
|
||||
|
@ -325,7 +337,7 @@ func (p *Portal) sendMediaFailedMessage(intent *appservice.IntentAPI, bridgeErr
|
|||
MsgType: event.MsgNotice,
|
||||
}
|
||||
|
||||
_, err := intent.SendMessageEvent(p.MXID, event.EventMessage, content)
|
||||
_, err := p.sendMatrixMessage(intent, event.EventMessage, content, nil, time.Now().UTC().UnixMilli())
|
||||
if err != nil {
|
||||
p.log.Warnfln("failed to send error message to matrix: %v", err)
|
||||
}
|
||||
|
@ -379,7 +391,7 @@ func (p *Portal) handleDiscordAttachment(intent *appservice.IntentAPI, msgID str
|
|||
return
|
||||
}
|
||||
|
||||
resp, err := intent.SendMessageEvent(p.MXID, event.EventMessage, content)
|
||||
resp, err := p.sendMatrixMessage(intent, event.EventMessage, content, nil, time.Now().UTC().UnixMilli())
|
||||
if err != nil {
|
||||
p.log.Warnfln("failed to send media message to matrix: %v", err)
|
||||
}
|
||||
|
@ -426,7 +438,7 @@ func (p *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Message)
|
|||
}
|
||||
}
|
||||
|
||||
resp, err := intent.SendMessageEvent(p.MXID, event.EventMessage, content)
|
||||
resp, err := p.sendMatrixMessage(intent, event.EventMessage, content, nil, time.Now().UTC().UnixMilli())
|
||||
if err != nil {
|
||||
p.log.Warnfln("failed to send message %q to matrix: %v", msg.ID, err)
|
||||
|
||||
|
@ -498,7 +510,7 @@ func (p *Portal) handleDiscordMessagesUpdate(user *User, msg *discordgo.Message)
|
|||
|
||||
content.SetEdit(existing.MatrixID)
|
||||
|
||||
resp, err := intent.SendMessageEvent(p.MXID, event.EventMessage, content)
|
||||
resp, err := p.sendMatrixMessage(intent, event.EventMessage, content, nil, time.Now().UTC().UnixMilli())
|
||||
if err != nil {
|
||||
p.log.Warnfln("failed to send message %q to matrix: %v", msg.ID, err)
|
||||
|
||||
|
@ -567,6 +579,57 @@ func (p *Portal) syncParticipants(source *User, participants []*discordgo.User)
|
|||
}
|
||||
}
|
||||
|
||||
func (portal *Portal) encrypt(content *event.Content, eventType event.Type) (event.Type, error) {
|
||||
if portal.Encrypted && portal.bridge.crypto != nil {
|
||||
// TODO maybe the locking should be inside mautrix-go?
|
||||
portal.encryptLock.Lock()
|
||||
encrypted, err := portal.bridge.crypto.Encrypt(portal.MXID, eventType, *content)
|
||||
portal.encryptLock.Unlock()
|
||||
if err != nil {
|
||||
return eventType, fmt.Errorf("failed to encrypt event: %w", err)
|
||||
}
|
||||
eventType = event.EventEncrypted
|
||||
content.Parsed = encrypted
|
||||
}
|
||||
return eventType, nil
|
||||
}
|
||||
|
||||
const doublePuppetKey = "fi.mau.double_puppet_source"
|
||||
const doublePuppetValue = "mautrix-discord"
|
||||
|
||||
func (portal *Portal) sendMatrixMessage(intent *appservice.IntentAPI, eventType event.Type, content *event.MessageEventContent, extraContent map[string]interface{}, timestamp int64) (*mautrix.RespSendEvent, error) {
|
||||
wrappedContent := event.Content{Parsed: content, Raw: extraContent}
|
||||
if timestamp != 0 && intent.IsCustomPuppet {
|
||||
if wrappedContent.Raw == nil {
|
||||
wrappedContent.Raw = map[string]interface{}{}
|
||||
}
|
||||
if intent.IsCustomPuppet {
|
||||
wrappedContent.Raw[doublePuppetKey] = doublePuppetValue
|
||||
}
|
||||
}
|
||||
var err error
|
||||
eventType, err = portal.encrypt(&wrappedContent, eventType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if eventType == event.EventEncrypted {
|
||||
// Clear other custom keys if the event was encrypted, but keep the double puppet identifier
|
||||
if intent.IsCustomPuppet {
|
||||
wrappedContent.Raw = map[string]interface{}{doublePuppetKey: doublePuppetValue}
|
||||
} else {
|
||||
wrappedContent.Raw = nil
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = intent.UserTyping(portal.MXID, false, 0)
|
||||
if timestamp == 0 {
|
||||
return intent.SendMessageEvent(portal.MXID, eventType, &wrappedContent)
|
||||
} else {
|
||||
return intent.SendMassagedMessageEvent(portal.MXID, eventType, &wrappedContent, timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Portal) handleMatrixMessages(msg portalMatrixMessage) {
|
||||
switch msg.evt.Type {
|
||||
case event.EventMessage:
|
||||
|
|
|
@ -32,6 +32,9 @@ type User struct {
|
|||
bridge *Bridge
|
||||
log log.Logger
|
||||
|
||||
// TODO finish implementing
|
||||
Admin bool
|
||||
|
||||
guilds map[string]*database.Guild
|
||||
guildsLock sync.Mutex
|
||||
|
||||
|
|
|
@ -30,6 +30,8 @@ type bridge struct {
|
|||
DoublePuppetAllowDiscovery bool `yaml:"double_puppet_allow_discovery"`
|
||||
LoginSharedSecretMap map[string]string `yaml:"login_shared_secret_map"`
|
||||
|
||||
Encryption encryption `yaml:"encryption"`
|
||||
|
||||
usernameTemplate *template.Template `yaml:"-"`
|
||||
displaynameTemplate *template.Template `yaml:"-"`
|
||||
channelnameTemplate *template.Template `yaml:"-"`
|
||||
|
|
29
config/encryption.go
Normal file
29
config/encryption.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package config
|
||||
|
||||
type encryption struct {
|
||||
Allow bool `yaml:"allow"`
|
||||
Default bool `yaml:"default"`
|
||||
|
||||
KeySharing struct {
|
||||
Allow bool `yaml:"allow"`
|
||||
RequireCrossSigning bool `yaml:"require_cross_signing"`
|
||||
RequireVerification bool `yaml:"require_verification"`
|
||||
} `yaml:"key_sharing"`
|
||||
}
|
||||
|
||||
func (e *encryption) validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *encryption) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawEncryption encryption
|
||||
|
||||
raw := rawEncryption{}
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*e = encryption(raw)
|
||||
|
||||
return e.validate()
|
||||
}
|
97
database/cryptostore.go
Normal file
97
database/cryptostore.go
Normal file
|
@ -0,0 +1,97 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
log "maunium.net/go/maulogger/v2"
|
||||
|
||||
"maunium.net/go/mautrix/crypto"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
type SQLCryptoStore struct {
|
||||
*crypto.SQLCryptoStore
|
||||
UserID id.UserID
|
||||
GhostIDFormat string
|
||||
}
|
||||
|
||||
var _ crypto.Store = (*SQLCryptoStore)(nil)
|
||||
|
||||
func NewSQLCryptoStore(db *Database, userID id.UserID, ghostIDFormat string) *SQLCryptoStore {
|
||||
return &SQLCryptoStore{
|
||||
SQLCryptoStore: crypto.NewSQLCryptoStore(db.DB, db.dialect, "", "",
|
||||
[]byte("maunium.net/go/mautrix-whatsapp"),
|
||||
&cryptoLogger{db.log.Sub("CryptoStore")}),
|
||||
UserID: userID,
|
||||
GhostIDFormat: ghostIDFormat,
|
||||
}
|
||||
}
|
||||
|
||||
func (store *SQLCryptoStore) FindDeviceID() id.DeviceID {
|
||||
var deviceID id.DeviceID
|
||||
|
||||
query := `SELECT device_id FROM crypto_account WHERE account_id=$1`
|
||||
err := store.DB.QueryRow(query, store.AccountID).Scan(&deviceID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
store.Log.Warn("Failed to scan device ID: %v", err)
|
||||
}
|
||||
|
||||
return deviceID
|
||||
}
|
||||
|
||||
func (store *SQLCryptoStore) GetRoomMembers(roomID id.RoomID) ([]id.UserID, error) {
|
||||
query := `
|
||||
SELECT user_id FROM mx_user_profile
|
||||
WHERE room_id=$1
|
||||
AND (membership='join' OR membership='invite')
|
||||
AND user_id<>$2
|
||||
AND user_id NOT LIKE $3
|
||||
`
|
||||
|
||||
members := []id.UserID{}
|
||||
|
||||
rows, err := store.DB.Query(query, roomID, store.UserID, store.GhostIDFormat)
|
||||
if err != nil {
|
||||
return members, err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var userID id.UserID
|
||||
err := rows.Scan(&userID)
|
||||
if err != nil {
|
||||
store.Log.Warn("Failed to scan member in %s: %v", roomID, err)
|
||||
return members, err
|
||||
}
|
||||
|
||||
members = append(members, userID)
|
||||
}
|
||||
|
||||
return members, nil
|
||||
}
|
||||
|
||||
// TODO merge this with the one in the parent package
|
||||
type cryptoLogger struct {
|
||||
int log.Logger
|
||||
}
|
||||
|
||||
var levelTrace = log.Level{
|
||||
Name: "TRACE",
|
||||
Severity: -10,
|
||||
Color: -1,
|
||||
}
|
||||
|
||||
func (c *cryptoLogger) Error(message string, args ...interface{}) {
|
||||
c.int.Errorfln(message, args...)
|
||||
}
|
||||
|
||||
func (c *cryptoLogger) Warn(message string, args ...interface{}) {
|
||||
c.int.Warnfln(message, args...)
|
||||
}
|
||||
|
||||
func (c *cryptoLogger) Debug(message string, args ...interface{}) {
|
||||
c.int.Debugfln(message, args...)
|
||||
}
|
||||
|
||||
func (c *cryptoLogger) Trace(message string, args ...interface{}) {
|
||||
c.int.Logfln(levelTrace, message, args...)
|
||||
}
|
3
database/migrations/08-add-crypto-store-to-database.sql
Normal file
3
database/migrations/08-add-crypto-store-to-database.sql
Normal file
|
@ -0,0 +1,3 @@
|
|||
-- This migration is implemented in migrations.go as it comes from
|
||||
-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 0
|
||||
-- which is described as "Add crypto store to database".
|
|
@ -0,0 +1,3 @@
|
|||
-- This migration is implemented in migrations.go as it comes from
|
||||
-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 1
|
||||
-- which is described as "Add account_id to crypto store".
|
|
@ -0,0 +1,3 @@
|
|||
-- This migration is implemented in migrations.go as it comes from
|
||||
-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 2
|
||||
-- which is described as "Add megolm withheld data to crypto store".
|
|
@ -0,0 +1,3 @@
|
|||
-- This migration is implemented in migrations.go as it comes from
|
||||
-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 3
|
||||
-- which is described as "Add cross-signing keys to crypto store".
|
|
@ -0,0 +1,4 @@
|
|||
-- This migration is implemented in migrations.go as it comes from
|
||||
-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 4
|
||||
-- which is described as "Replace VARCHAR(255) with TEXT in the crypto
|
||||
-- database".
|
|
@ -0,0 +1,4 @@
|
|||
-- This migration is implemented in migrations.go as it comes from
|
||||
-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 5
|
||||
-- which is described as "Split last_used into last_encrypted and
|
||||
-- last_decrypted in crypto store".
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE portal ADD COLUMN encrypted BOOLEAN NOT NULL DEFAULT false;
|
|
@ -3,37 +3,18 @@ package migrations
|
|||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"sort"
|
||||
|
||||
"github.com/lopezator/migrator"
|
||||
log "maunium.net/go/maulogger/v2"
|
||||
"maunium.net/go/mautrix/crypto/sql_store_upgrade"
|
||||
)
|
||||
|
||||
//go:embed *.sql
|
||||
var embeddedMigrations embed.FS
|
||||
|
||||
var (
|
||||
commonMigrations = []string{
|
||||
"01-initial.sql",
|
||||
"02-attachments.sql",
|
||||
"03-emoji.sql",
|
||||
"04-custom-puppet.sql",
|
||||
"05-additional-puppet-fields.sql",
|
||||
"07-guilds.sql",
|
||||
}
|
||||
|
||||
sqliteMigrations = []string{
|
||||
"06-remove-unique-user-constraint.sqlite.sql",
|
||||
}
|
||||
|
||||
postgresMigrations = []string{
|
||||
"06-remove-unique-user-constraint.postgres.sql",
|
||||
}
|
||||
)
|
||||
|
||||
func migrationFromFile(filename string) *migrator.Migration {
|
||||
func migrationFromFile(description, filename string) *migrator.Migration {
|
||||
return &migrator.Migration{
|
||||
Name: filename,
|
||||
Name: description,
|
||||
Func: func(tx *sql.Tx) error {
|
||||
data, err := embeddedMigrations.ReadFile(filename)
|
||||
if err != nil {
|
||||
|
@ -49,31 +30,83 @@ func migrationFromFile(filename string) *migrator.Migration {
|
|||
}
|
||||
}
|
||||
|
||||
func migrationFromFileWithDialect(dialect, description, sqliteFile, postgresFile string) *migrator.Migration {
|
||||
switch dialect {
|
||||
case "sqlite3":
|
||||
return migrationFromFile(description, sqliteFile)
|
||||
case "postgres":
|
||||
return migrationFromFile(description, postgresFile)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func Run(db *sql.DB, baseLog log.Logger, dialect string) error {
|
||||
subLogger := baseLog.Sub("Migrations")
|
||||
logger := migrator.LoggerFunc(func(msg string, args ...interface{}) {
|
||||
subLogger.Infof(msg, args...)
|
||||
})
|
||||
|
||||
migrationNames := commonMigrations
|
||||
switch dialect {
|
||||
case "sqlite3":
|
||||
migrationNames = append(migrationNames, sqliteMigrations...)
|
||||
case "postgres":
|
||||
migrationNames = append(migrationNames, postgresMigrations...)
|
||||
}
|
||||
|
||||
sort.Strings(migrationNames)
|
||||
|
||||
migrations := make([]interface{}, len(migrationNames))
|
||||
for idx, name := range migrationNames {
|
||||
migrations[idx] = migrationFromFile(name)
|
||||
}
|
||||
|
||||
m, err := migrator.New(
|
||||
migrator.TableName("version"),
|
||||
migrator.WithLogger(logger),
|
||||
migrator.Migrations(migrations...),
|
||||
migrator.Migrations(
|
||||
migrationFromFile("Initial Schema", "01-initial.sql"),
|
||||
migrationFromFile("Attachments", "02-attachments.sql"),
|
||||
migrationFromFile("Emoji", "03-emoji.sql"),
|
||||
migrationFromFile("Custom Puppets", "04-custom-puppet.sql"),
|
||||
migrationFromFile(
|
||||
"Additional puppet fields",
|
||||
"05-additional-puppet-fields.sql",
|
||||
),
|
||||
migrationFromFileWithDialect(
|
||||
dialect,
|
||||
"Remove unique user constraint",
|
||||
"06-remove-unique-user-constraint.sqlite.sql",
|
||||
"06-remove-unique-user-constraint.postgres.sql",
|
||||
),
|
||||
migrationFromFile("Guild Bridging", "07-guilds.sql"),
|
||||
&migrator.Migration{
|
||||
Name: "Add crypto store to database",
|
||||
Func: func(tx *sql.Tx) error {
|
||||
return sql_store_upgrade.Upgrades[0](tx, dialect)
|
||||
},
|
||||
},
|
||||
&migrator.Migration{
|
||||
Name: "Add account_id to crypto store",
|
||||
Func: func(tx *sql.Tx) error {
|
||||
return sql_store_upgrade.Upgrades[1](tx, dialect)
|
||||
},
|
||||
},
|
||||
&migrator.Migration{
|
||||
Name: "Add megolm withheld data to crypto store",
|
||||
Func: func(tx *sql.Tx) error {
|
||||
return sql_store_upgrade.Upgrades[2](tx, dialect)
|
||||
},
|
||||
},
|
||||
&migrator.Migration{
|
||||
Name: "Add cross-signing keys to crypto store",
|
||||
Func: func(tx *sql.Tx) error {
|
||||
return sql_store_upgrade.Upgrades[3](tx, dialect)
|
||||
},
|
||||
},
|
||||
&migrator.Migration{
|
||||
Name: "Replace VARCHAR(255) with TEXT in the crypto database",
|
||||
Func: func(tx *sql.Tx) error {
|
||||
return sql_store_upgrade.Upgrades[4](tx, dialect)
|
||||
},
|
||||
},
|
||||
&migrator.Migration{
|
||||
Name: "Split last_used into last_encrypted and last_decrypted in crypto store",
|
||||
Func: func(tx *sql.Tx) error {
|
||||
return sql_store_upgrade.Upgrades[5](tx, dialect)
|
||||
},
|
||||
},
|
||||
migrationFromFile(
|
||||
"Add encryption column to portal table",
|
||||
"14-add-encrypted-column-to-portal-table.sql",
|
||||
),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -19,6 +19,8 @@ type Portal struct {
|
|||
Name string
|
||||
Topic string
|
||||
|
||||
Encrypted bool
|
||||
|
||||
Avatar string
|
||||
AvatarURL id.ContentURI
|
||||
|
||||
|
@ -33,7 +35,8 @@ func (p *Portal) Scan(row Scannable) *Portal {
|
|||
var typ sql.NullInt32
|
||||
|
||||
err := row.Scan(&p.Key.ChannelID, &p.Key.Receiver, &mxid, &p.Name,
|
||||
&p.Topic, &p.Avatar, &avatarURL, &typ, &p.DMUser, &firstEventID)
|
||||
&p.Topic, &p.Avatar, &avatarURL, &typ, &p.DMUser, &firstEventID,
|
||||
&p.Encrypted)
|
||||
|
||||
if err != nil {
|
||||
if err != sql.ErrNoRows {
|
||||
|
@ -62,12 +65,12 @@ func (p *Portal) mxidPtr() *id.RoomID {
|
|||
func (p *Portal) Insert() {
|
||||
query := "INSERT INTO portal" +
|
||||
" (channel_id, receiver, mxid, name, topic, avatar, avatar_url," +
|
||||
" type, dmuser, first_event_id)" +
|
||||
" VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)"
|
||||
" type, dmuser, first_event_id, encrypted)" +
|
||||
" VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)"
|
||||
|
||||
_, err := p.db.Exec(query, p.Key.ChannelID, p.Key.Receiver, p.mxidPtr(),
|
||||
p.Name, p.Topic, p.Avatar, p.AvatarURL.String(), p.Type, p.DMUser,
|
||||
p.FirstEventID.String())
|
||||
p.FirstEventID.String(), p.Encrypted)
|
||||
|
||||
if err != nil {
|
||||
p.log.Warnfln("Failed to insert %s: %v", p.Key, err)
|
||||
|
@ -77,11 +80,12 @@ func (p *Portal) Insert() {
|
|||
func (p *Portal) Update() {
|
||||
query := "UPDATE portal SET" +
|
||||
" mxid=$1, name=$2, topic=$3, avatar=$4, avatar_url=$5, type=$6," +
|
||||
" dmuser=$7, first_event_id=$8" +
|
||||
" WHERE channel_id=$9 AND receiver=$10"
|
||||
" dmuser=$7, first_event_id=$8, encrypted=$9" +
|
||||
" WHERE channel_id=$10 AND receiver=$11"
|
||||
|
||||
_, err := p.db.Exec(query, p.mxidPtr(), p.Name, p.Topic, p.Avatar,
|
||||
p.AvatarURL.String(), p.Type, p.DMUser, p.FirstEventID.String(),
|
||||
p.Encrypted,
|
||||
p.Key.ChannelID, p.Key.Receiver)
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -6,6 +6,12 @@ import (
|
|||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
const (
|
||||
portalSelect = "SELECT channel_id, receiver, mxid, name, topic, avatar," +
|
||||
" avatar_url, type, dmuser, first_event_id, encrypted" +
|
||||
" FROM portal"
|
||||
)
|
||||
|
||||
type PortalQuery struct {
|
||||
db *Database
|
||||
log log.Logger
|
||||
|
@ -19,23 +25,23 @@ func (pq *PortalQuery) New() *Portal {
|
|||
}
|
||||
|
||||
func (pq *PortalQuery) GetAll() []*Portal {
|
||||
return pq.getAll("SELECT * FROM portal")
|
||||
return pq.getAll(portalSelect)
|
||||
}
|
||||
|
||||
func (pq *PortalQuery) GetByID(key PortalKey) *Portal {
|
||||
return pq.get("SELECT * FROM portal WHERE channel_id=$1 AND receiver=$2", key.ChannelID, key.Receiver)
|
||||
return pq.get(portalSelect+" WHERE channel_id=$1 AND receiver=$2", key.ChannelID, key.Receiver)
|
||||
}
|
||||
|
||||
func (pq *PortalQuery) GetByMXID(mxid id.RoomID) *Portal {
|
||||
return pq.get("SELECT * FROM portal WHERE mxid=$1", mxid)
|
||||
return pq.get(portalSelect+" WHERE mxid=$1", mxid)
|
||||
}
|
||||
|
||||
func (pq *PortalQuery) GetAllByID(id string) []*Portal {
|
||||
return pq.getAll("SELECT * FROM portal WHERE receiver=$1", id)
|
||||
return pq.getAll(portalSelect+" WHERE receiver=$1", id)
|
||||
}
|
||||
|
||||
func (pq *PortalQuery) FindPrivateChats(receiver string) []*Portal {
|
||||
query := "SELECT * FROM portal WHERE receiver=$1 AND type=$2;"
|
||||
query := portalSelect + " portal WHERE receiver=$1 AND type=$2;"
|
||||
|
||||
return pq.getAll(query, receiver, discordgo.ChannelTypeDM)
|
||||
}
|
||||
|
|
|
@ -272,3 +272,31 @@ func (s *SQLStateStore) HasPowerLevel(roomID id.RoomID, userID id.UserID, eventT
|
|||
|
||||
return s.GetPowerLevel(roomID, userID) >= s.GetPowerLevelRequirement(roomID, eventType)
|
||||
}
|
||||
|
||||
func (store *SQLStateStore) FindSharedRooms(userID id.UserID) []id.RoomID {
|
||||
query := "SELECT room_id FROM mx_user_profile" +
|
||||
"LEFT JOIN portal ON portal.mxid=mx_user_profile.room_id" +
|
||||
"WHERE user_id=$1 AND portal.encrypted=true"
|
||||
|
||||
rooms := []id.RoomID{}
|
||||
|
||||
rows, err := store.db.Query(query, userID)
|
||||
if err != nil {
|
||||
store.log.Warnfln("Failed to query shared rooms with %s: %v", userID, err)
|
||||
|
||||
return rooms
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var roomID id.RoomID
|
||||
|
||||
err = rows.Scan(&roomID)
|
||||
if err != nil {
|
||||
store.log.Warnfln("Failed to scan room ID: %v", err)
|
||||
} else {
|
||||
rooms = append(rooms, roomID)
|
||||
}
|
||||
}
|
||||
|
||||
return rooms
|
||||
}
|
||||
|
|
4
go.mod
4
go.mod
|
@ -20,6 +20,10 @@ require (
|
|||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/tidwall/gjson v1.14.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/sjson v1.2.4 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect
|
||||
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect
|
||||
golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 // indirect
|
||||
|
|
4
go.sum
4
go.sum
|
@ -35,9 +35,13 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w=
|
||||
github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.4 h1:cuiLzLnaMeBhRmEv00Lpk3tkYrcxpmbU81tAY4Dw0tc=
|
||||
github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM=
|
||||
gitlab.com/beeper/discordgo v0.23.3-0.20220219094025-13ff4cc63da7 h1:8ieR27GadHnShqhsvPrDzL1/ZOntavGGt4TXqafncYE=
|
||||
gitlab.com/beeper/discordgo v0.23.3-0.20220219094025-13ff4cc63da7/go.mod h1:Hwfv4M8yP/MDh47BN+4Z1WItJ1umLKUyplCH5KcQPgE=
|
||||
|
|
Loading…
Reference in a new issue