Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bridgev2/bridgeconfig/appservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ type BotUserConfig struct {
Username string `yaml:"username"`
Displayname string `yaml:"displayname"`
Avatar string `yaml:"avatar"`
AccessToken string `yaml:"access_token"`

ParsedAvatar id.ContentURI `yaml:"-"`
}
Expand Down
1 change: 1 addition & 0 deletions bridgev2/bridgeconfig/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func doUpgrade(helper up.Helper) {
helper.Copy(up.Str, "appservice", "bot", "username")
helper.Copy(up.Str, "appservice", "bot", "displayname")
helper.Copy(up.Str, "appservice", "bot", "avatar")
helper.Copy(up.Str, "appservice", "bot", "access_token")
helper.Copy(up.Bool, "appservice", "ephemeral_events")
helper.Copy(up.Bool, "appservice", "async_transactions")
helper.Copy(up.Str, "appservice", "as_token")
Expand Down
46 changes: 40 additions & 6 deletions bridgev2/matrix/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ type Crypto interface {
Client() *mautrix.Client
ShareKeys(context.Context) error
BeeperStreamPublisher() bridgev2.BeeperStreamPublisher
ProcessSyncResponse(ctx context.Context, resp *mautrix.RespSync, since string) bool
}

type Connector struct {
Expand Down Expand Up @@ -87,9 +88,14 @@ type Connector struct {
Capabilities *bridgev2.MatrixCapabilities
IgnoreUnsupportedServer bool

BotMode bool
stopBotSync context.CancelFunc
botSyncDone sync.WaitGroup

EventProcessor *appservice.EventProcessor

userIDRegex *regexp.Regexp
whoami *mautrix.RespWhoami

Websocket bool
wsStopPinger chan struct{}
Expand Down Expand Up @@ -131,6 +137,7 @@ func (br *Connector) Init(bridge *bridgev2.Bridge) {
br.AS = br.Config.MakeAppService()
br.AS.Log = bridge.Log
br.AS.StateStore = br.StateStore
br.Bot = br.AS.BotIntent()
br.EventProcessor = appservice.NewEventProcessor(br.AS)
if !br.Config.AppService.AsyncTransactions {
br.EventProcessor.ExecMode = appservice.Sync
Expand All @@ -148,11 +155,11 @@ func (br *Connector) Init(bridge *bridgev2.Bridge) {
event.StateMember,
event.StatePowerLevels,
event.StateRoomName,
event.BeeperSendState,
event.StateRoomAvatar,
event.StateTopic,
event.StateTombstone,
event.StateBeeperDisappearingTimer,
event.BeeperSendState,
event.BeeperDeleteChat,
event.BeeperAcceptMessageRequest,
event.EphemeralEventReceipt,
Expand All @@ -163,7 +170,6 @@ func (br *Connector) Init(bridge *bridgev2.Bridge) {
br.EventProcessor.On(event.EventEncrypted, br.handleEncryptedEvent)
br.EventProcessor.On(event.EphemeralEventReceipt, br.handleEphemeralEvent)
br.EventProcessor.On(event.EphemeralEventTyping, br.handleEphemeralEvent)
br.Bot = br.AS.BotIntent()
br.Crypto = NewCryptoHelper(br)
br.Bridge.Commands.(*commands.Processor).AddHandlers(
CommandDiscardMegolmSession, CommandSetPowerLevel,
Expand All @@ -172,6 +178,15 @@ func (br *Connector) Init(bridge *bridgev2.Bridge) {
br.Provisioning = &ProvisioningAPI{br: br}
br.DoublePuppet = newDoublePuppetUtil(br)
br.deterministicEventIDServer = "backfill." + br.Config.Homeserver.Domain
if br.Config.AppService.Bot.AccessToken != "" {
br.Bot.SetAppServiceUserID = false
br.Bot.SetAppServiceDeviceID = false
br.Bot.Registered = true
br.Bot.AccessToken = br.Config.AppService.Bot.AccessToken
br.AS.Registration.AppToken = ""
br.BotMode = true
br.configureRelaySyncer()
}
}

func (br *Connector) Start(ctx context.Context) error {
Expand Down Expand Up @@ -236,6 +251,9 @@ func (br *Connector) Start(ctx context.Context) error {
br.deterministicEventIDServer = strings.TrimPrefix(parsed.Hostname(), "www.")
}
br.AS.Ready = true
if br.BotMode {
go br.startRelaySyncer()
}
if br.Websocket && br.Config.Homeserver.WSPingInterval > 0 {
br.wsStopPinger = make(chan struct{}, 1)
go br.websocketServerPinger()
Expand Down Expand Up @@ -334,6 +352,9 @@ func (br *Connector) Stop() {
if br.Crypto != nil {
br.Crypto.Stop()
}
if br.BotMode {
br.stopRelaySyncer()
}
if wsStopChan := br.wsStopped; wsStopChan != nil {
select {
case <-wsStopChan:
Expand Down Expand Up @@ -419,16 +440,20 @@ func (br *Connector) ensureConnection(ctx context.Context) {
Msg("Unexpected user ID in whoami call")
os.Exit(17)
}
br.whoami = resp
if br.BotMode {
br.Bot.DeviceID = resp.DeviceID
}

if br.Websocket {
br.Log.Debug().Msg("Websocket mode: no need to check status of homeserver -> bridge connection")
return
} else if br.BotMode {
br.Log.Debug().Msg("Bot mode: no need to check status of homeserver -> bridge connection")
} else if !br.SpecVersions.Supports(mautrix.FeatureAppservicePing) {
br.Log.Debug().Msg("Homeserver does not support checking status of homeserver -> bridge connection")
return
} else {
br.Bot.EnsureAppserviceConnection(ctx)
}

br.Bot.EnsureAppserviceConnection(ctx)
}

func (br *Connector) fetchCapabilities(ctx context.Context) *mautrix.RespCapabilities {
Expand Down Expand Up @@ -502,6 +527,12 @@ func (br *Connector) UpdateBotProfile(ctx context.Context) {
}

func (br *Connector) GhostIntent(userID networkid.UserID) bridgev2.MatrixAPI {
if br.BotMode {
return &RelayIntent{
ASIntent: br.Bridge.Bot.(*ASIntent),
ID: userID,
}
}
return &ASIntent{
Matrix: br.AS.Intent(br.FormatGhostMXID(userID)),
Connector: br,
Expand Down Expand Up @@ -602,6 +633,9 @@ func (br *Connector) SendMessageCheckpoints(ctx context.Context, checkpoints []*
}

func (br *Connector) ParseGhostMXID(userID id.UserID) (networkid.UserID, bool) {
if br.BotMode {
return "", false
}
match := br.userIDRegex.FindStringSubmatch(string(userID))
if match == nil || userID == br.Bot.UserID {
return "", false
Expand Down
41 changes: 35 additions & 6 deletions bridgev2/matrix/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ func (helper *CryptoHelper) Init(ctx context.Context) error {
helper.bridge.Bridge.DB.Database,
dbutil.ZeroLogger(helper.bridge.Log.With().Str("db_section", "crypto").Logger()),
string(helper.bridge.Bridge.ID),
helper.bridge.AS.BotMXID(),
fmt.Sprintf("@%s:%s", strings.ReplaceAll(helper.bridge.Config.AppService.FormatUsername("%"), "_", `\_`), helper.bridge.AS.HomeserverDomain),
helper.bridge.Bot.UserID,
fmt.Sprintf("@%s:%s", strings.ReplaceAll(helper.bridge.Config.AppService.FormatUsername("%"), "_", `\_`), helper.bridge.Config.Homeserver.Domain),
helper.bridge.Config.Encryption.PickleKey,
)

Expand All @@ -91,7 +91,12 @@ func (helper *CryptoHelper) Init(ctx context.Context) error {
}

var isExistingDevice bool
helper.client, isExistingDevice, err = helper.loginBot(ctx)
if helper.bridge.BotMode {
helper.client = helper.bridge.Bot.Client
isExistingDevice, err = helper.checkBotDevice(ctx)
} else {
helper.client, isExistingDevice, err = helper.loginBot(ctx)
}
if err != nil {
return err
}
Expand Down Expand Up @@ -135,8 +140,10 @@ func (helper *CryptoHelper) Init(ctx context.Context) error {
return err
}
helper.streams = streams
helper.client.Syncer = &cryptoSyncer{OlmMachine: helper.mach, handleSyncResponse: streams.HandleSyncResponse}
helper.client.Store = helper.store
if !helper.bridge.BotMode {
helper.client.Syncer = &cryptoSyncer{OlmMachine: helper.mach, handleSyncResponse: streams.HandleSyncResponse}
helper.client.Store = helper.store
}

err = helper.mach.Load(ctx)
if err != nil {
Expand Down Expand Up @@ -287,6 +294,16 @@ func (helper *CryptoHelper) allowKeyShare(ctx context.Context, device *id.Device
return &crypto.KeyShareRejectUnverified
}
}
func (helper *CryptoHelper) checkBotDevice(ctx context.Context) (bool, error) {
deviceID, err := helper.store.FindDeviceID(ctx)
if err != nil {
return false, fmt.Errorf("failed to find existing device ID: %w", err)
} else if deviceID != "" && deviceID != helper.client.DeviceID {
return false, fmt.Errorf("device ID from database doesn't match bot's actual device ID: %s != %s", deviceID, helper.bridge.whoami.DeviceID)
}
helper.store.DeviceID = helper.client.DeviceID
return deviceID != "", nil
}

func (helper *CryptoHelper) loginBot(ctx context.Context) (*mautrix.Client, bool, error) {
deviceID, err := helper.store.FindDeviceID(ctx)
Expand Down Expand Up @@ -355,6 +372,9 @@ func (helper *CryptoHelper) verifyKeysAreOnServer(ctx context.Context) bool {
}

func (helper *CryptoHelper) Start() {
if helper.bridge.BotMode {
return
}
if helper.bridge.Config.Encryption.Appservice {
helper.log.Debug().Msg("End-to-bridge encryption is in appservice mode, registering event listeners and not starting syncer")
helper.bridge.AS.Registration.EphemeralEvents = true
Expand Down Expand Up @@ -383,7 +403,9 @@ func (helper *CryptoHelper) Start() {

func (helper *CryptoHelper) Stop() {
helper.log.Debug().Msg("CryptoHelper.Stop() called, stopping bridge bot sync")
helper.client.StopSync()
if !helper.bridge.BotMode {
helper.client.StopSync()
}
if helper.cancelSync != nil {
helper.cancelSync()
}
Expand Down Expand Up @@ -537,6 +559,13 @@ func (helper *CryptoHelper) BeeperStreamPublisher() bridgev2.BeeperStreamPublish
return helper.streams
}

func (helper *CryptoHelper) ProcessSyncResponse(ctx context.Context, resp *mautrix.RespSync, since string) bool {
helper.lock.RLock()
m := helper.mach
helper.lock.RUnlock()
return m.ProcessSyncResponse(ctx, resp, since)
}

type cryptoSyncer struct {
*crypto.OlmMachine
handleSyncResponse func(context.Context, *mautrix.RespSync) []*event.Event
Expand Down
2 changes: 1 addition & 1 deletion bridgev2/matrix/doublepuppet.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (dp *doublePuppetUtil) newClient(ctx context.Context, mxid id.UserID, acces
}
homeserverURL, found := dp.br.Config.DoublePuppet.Servers[homeserver]
if !found {
if homeserver == dp.br.AS.HomeserverDomain {
if homeserver == dp.br.Config.Homeserver.Domain {
homeserverURL = ""
} else if dp.br.Config.DoublePuppet.AllowDiscovery {
dp.discoveryCacheLock.Lock()
Expand Down
4 changes: 4 additions & 0 deletions bridgev2/matrix/intent.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type ASIntent struct {

var _ bridgev2.MatrixAPI = (*ASIntent)(nil)
var _ bridgev2.MarkAsDMMatrixAPI = (*ASIntent)(nil)
var _ bridgev2.MatrixAPIWithArbitraryRoomState = (*ASIntent)(nil)

func (as *ASIntent) SendMessage(ctx context.Context, roomID id.RoomID, eventType event.Type, content *event.Content, extra *bridgev2.MatrixSendExtra) (*mautrix.RespSendEvent, error) {
if extra == nil {
Expand Down Expand Up @@ -121,6 +122,9 @@ func (as *ASIntent) fillMemberEvent(ctx context.Context, roomID id.RoomID, userI

func (as *ASIntent) SendState(ctx context.Context, roomID id.RoomID, eventType event.Type, stateKey string, content *event.Content, ts time.Time) (resp *mautrix.RespSendEvent, err error) {
if eventType == event.StateMember {
if stateKey == "" {
return &mautrix.RespSendEvent{}, nil
}
as.fillMemberEvent(ctx, roomID, id.UserID(stateKey), content)
}
resp, err = as.Matrix.SendStateEvent(ctx, roomID, eventType, stateKey, content, mautrix.ReqSendEvent{Timestamp: ts.UnixMilli()})
Expand Down
2 changes: 1 addition & 1 deletion bridgev2/matrix/matrix.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
)

func (br *Connector) handleRoomEvent(ctx context.Context, evt *event.Event) {
if evt.Type == event.StateMember && br.Crypto != nil && !br.Bridge.IsGhostMXID(id.UserID(evt.GetStateKey())) {
if !br.BotMode && evt.Type == event.StateMember && br.Crypto != nil && !br.Bridge.IsGhostMXID(id.UserID(evt.GetStateKey())) {
br.Crypto.HandleMemberEvent(ctx, evt)
}
if br.shouldIgnoreEvent(evt) {
Expand Down
3 changes: 3 additions & 0 deletions bridgev2/matrix/mxmain/example-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ appservice:
# to leave display name/avatar as-is.
displayname: $<<.DisplayName>> bridge bot
avatar: $<<.NetworkIcon>>
# To use a single bot with per-message profiles instead of an appservice, specify the access token here.
# When set, as_token and hs_token will not be used.
access_token: null

# Whether to receive ephemeral events via appservice transactions.
ephemeral_events: true
Expand Down
12 changes: 10 additions & 2 deletions bridgev2/matrix/mxmain/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ func (br *BridgeMain) GenerateRegistration() {
} else if br.Config.Homeserver.Domain == "example.com" {
_, _ = fmt.Fprintln(os.Stderr, "Homeserver domain is not set")
os.Exit(20)
} else if br.Config.AppService.Bot.AccessToken != "" {
_, _ = fmt.Fprintln(os.Stderr, "Generating a registration is not necessary in bot mode")
_, _ = fmt.Fprintln(os.Stderr, "To use appservice mode instead of bot mode, remove appservice -> bot -> access_token")
os.Exit(20)
}
reg := br.Config.GenerateRegistration()
err := reg.Save(br.RegistrationPath)
Expand Down Expand Up @@ -295,16 +299,20 @@ func (br *BridgeMain) validateConfig() error {
return errors.New("homeserver.domain not configured")
case !bridgeconfig.AllowedHomeserverSoftware[br.Config.Homeserver.Software]:
return errors.New("invalid value for homeserver.software (use `standard` if you don't know what the field is for)")
case br.Config.AppService.ASToken == "This value is generated when generating the registration":
case br.Config.AppService.ASToken == "This value is generated when generating the registration" && br.Config.AppService.Bot.AccessToken == "":
return errors.New("appservice.as_token not configured. Did you forget to generate the registration? ")
case br.Config.AppService.HSToken == "This value is generated when generating the registration":
case br.Config.AppService.HSToken == "This value is generated when generating the registration" && br.Config.AppService.Bot.AccessToken == "":
return errors.New("appservice.hs_token not configured. Did you forget to generate the registration? ")
case br.Config.Database.URI == "postgres://user:password@host/database?sslmode=disable":
return errors.New("database.uri not configured")
case !br.Config.Bridge.Permissions.IsConfigured():
return errors.New("bridge.permissions not configured")
case !strings.Contains(br.Config.AppService.FormatUsername("1234567890"), "1234567890"):
return errors.New("username template is missing user ID placeholder")
case br.Config.AppService.Bot.AccessToken != "" && br.Config.Homeserver.Websocket:
return errors.New("appservice websockets cannot be used in single-bot mode")
case br.Config.AppService.Bot.AccessToken != "" && br.Config.Encryption.Appservice:
return errors.New("appservice encryption cannot be used in single-bot mode")
default:
cfgValidator, ok := br.Connector.(bridgev2.ConfigValidatingNetwork)
if ok {
Expand Down
Loading
Loading