// mautrix-groupme - A Matrix-GroupMe puppeting bridge. // Copyright (C) 2022 Sumner Evans, Karmanyaah Malhotra // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package main import ( "context" _ "embed" "fmt" "gitea.watsonlabs.net/watsonb8/groupme-lib" "github.com/beeper/groupme/groupmeext" "go.mau.fi/util/configupgrade" "maunium.net/go/mautrix/bridge/bridgeconfig" "maunium.net/go/mautrix/event" "regexp" "sync" "maunium.net/go/mautrix" "maunium.net/go/mautrix/bridge" "maunium.net/go/mautrix/bridge/commands" "maunium.net/go/mautrix/bridge/status" "maunium.net/go/mautrix/id" "github.com/beeper/groupme/config" "github.com/beeper/groupme/database" ) // Information to find out exactly which commit the bridge was built from. // These are filled at build time with the -X linker flag. var ( Tag = "unknown" Commit = "unknown" BuildTime = "unknown" ) //go:embed example-config.yaml var ExampleConfig string const unstableFeatureBatchSending = "org.matrix.msc2716" type GMBridge struct { bridge.Bridge Config *config.Config DB *database.Database Provisioning *ProvisioningAPI Metrics *MetricsHandler usersByMXID map[id.UserID]*User usersByGMID map[groupme.ID]*User usersLock sync.Mutex spaceRooms map[id.RoomID]*User spaceRoomsLock sync.Mutex managementRooms map[id.RoomID]*User managementRoomsLock sync.Mutex portalsByMXID map[id.RoomID]*Portal portalsByGMID map[database.PortalKey]*Portal portalsLock sync.Mutex puppets map[groupme.ID]*Puppet puppetsByCustomMXID map[id.UserID]*Puppet puppetsLock sync.Mutex } var ( TypeMSC3381PollStart = event.Type{Class: event.MessageEventType, Type: "org.matrix.msc3381.poll.start"} TypeMSC3381PollResponse = event.Type{Class: event.MessageEventType, Type: "org.matrix.msc3381.poll.response"} TypeMSC3381V2PollResponse = event.Type{Class: event.MessageEventType, Type: "org.matrix.msc3381.v2.poll.response"} ) func (br *GMBridge) Init() { br.CommandProcessor = commands.NewProcessor(&br.Bridge) br.RegisterCommands() matrixHTMLParser.PillConverter = br.pillConverter Segment.log = br.Log.Sub("Segment") Segment.key = br.Config.SegmentKey Segment.userID = br.Config.SegmentUserID if Segment.IsEnabled() { Segment.log.Infoln("Segment metrics are enabled") if Segment.userID != "" { Segment.log.Infoln("Overriding Segment user_id with %v", Segment.userID) } } br.DB = database.New(br.Bridge.DB, br.Log.Sub("Database")) ss := br.Config.Bridge.Provisioning.SharedSecret if len(ss) > 0 && ss != "disable" { br.Provisioning = &ProvisioningAPI{bridge: br} } br.Metrics = NewMetricsHandler(br.Config.Metrics.Listen, br.Log.Sub("Metrics"), br.DB) br.MatrixHandler.TrackEventDuration = br.Metrics.TrackMatrixEvent } func (br *GMBridge) Start() { if br.Provisioning != nil { br.Log.Debugln("Initializing provisioning API") br.Provisioning.Init() } go br.StartUsers() if br.Config.Metrics.Enabled { go br.Metrics.Start() } } func (br *GMBridge) StartUsers() { br.Log.Debugln("Starting users") foundAnySessions := false gmc := groupmeext.NewClient() conn := groupme.NewPushSubscription(context.Background()) conn.Connect(context.Background()) for _, user := range br.GetAllUsers() { if user.GMID.Valid() { foundAnySessions = true } go user.Connect(gmc, &conn) } if !foundAnySessions { br.SendGlobalBridgeState(status.BridgeState{StateEvent: status.StateUnconfigured}.Fill(nil)) } br.Log.Debugln("Starting custom puppets") for _, loopuppet := range br.GetAllPuppetsWithCustomMXID() { go func(puppet *Puppet) { puppet.log.Debugln("Starting custom puppet", puppet.CustomMXID) err := puppet.StartCustomMXID(true) if err != nil { puppet.log.Errorln("Failed to start custom puppet:", err) } }(loopuppet) } } func (br *GMBridge) Stop() { br.Metrics.Stop() // TODO anything needed to disconnect the users? for _, user := range br.usersByGMID { if user.Client == nil { continue } br.Log.Debugln("Disconnecting", user.MXID) } } func (br *GMBridge) GetExampleConfig() string { return ExampleConfig } func (br *GMBridge) GetConfigPtr() interface{} { br.Config = &config.Config{ BaseConfig: &br.Bridge.Config, } br.Config.BaseConfig.Bridge = &br.Config.Bridge return br.Config } func (bridge *GMBridge) GetPortalByGMID(key database.PortalKey) *Portal { bridge.portalsLock.Lock() defer bridge.portalsLock.Unlock() portal, ok := bridge.portalsByGMID[key] if !ok { dbPortal := bridge.DB.Portal.GetByGMID(key) return bridge.loadDBPortal(dbPortal, &key) } return portal } func (br *GMBridge) GetAllPortals() []*Portal { return br.dbPortalsToPortals(br.DB.Portal.GetAll()) } func (br *GMBridge) GetAllIPortals() (iportals []bridge.Portal) { portals := br.GetAllPortals() iportals = make([]bridge.Portal, len(portals)) for i, portal := range portals { iportals[i] = portal } return iportals } func (br *GMBridge) GetAllPortalsByGMID(gmid groupme.ID) []*Portal { return br.dbPortalsToPortals(br.DB.Portal.GetAllByGMID(gmid)) } func (bridge *GMBridge) GetPortalByMXID(mxid id.RoomID) *Portal { bridge.portalsLock.Lock() defer bridge.portalsLock.Unlock() portal, ok := bridge.portalsByMXID[mxid] if !ok { return bridge.loadDBPortal(bridge.DB.Portal.GetByMXID(mxid), nil) } return portal } func (br *GMBridge) GetIPortal(mxid id.RoomID) bridge.Portal { p := br.GetPortalByMXID(mxid) if p == nil { return nil } return p } func (br *GMBridge) getUserByMXID(userID id.UserID, onlyIfExists bool) *User { _, isPuppet := br.ParsePuppetMXID(userID) if isPuppet || userID == br.Bot.UserID { return nil } br.usersLock.Lock() defer br.usersLock.Unlock() user, ok := br.usersByMXID[userID] if !ok { userIDPtr := &userID if onlyIfExists { userIDPtr = nil } return br.loadDBUser(br.DB.User.GetByMXID(userID), userIDPtr) } return user } func (br *GMBridge) GetUserByMXID(userID id.UserID) *User { return br.getUserByMXID(userID, false) } func (br *GMBridge) GetIUser(userID id.UserID, create bool) bridge.User { u := br.getUserByMXID(userID, !create) if u == nil { return nil } return u } func (br *GMBridge) GetUserByMXIDIfExists(userID id.UserID) *User { return br.getUserByMXID(userID, true) } func (bridge *GMBridge) GetUserByGMID(gmid groupme.ID) *User { bridge.usersLock.Lock() defer bridge.usersLock.Unlock() user, ok := bridge.usersByGMID[gmid] if !ok { return bridge.loadDBUser(bridge.DB.User.GetByGMID(gmid), nil) } return user } func (br *GMBridge) GetAllUsers() []*User { br.usersLock.Lock() defer br.usersLock.Unlock() dbUsers := br.DB.User.GetAll() output := make([]*User, len(dbUsers)) for index, dbUser := range dbUsers { user, ok := br.usersByMXID[dbUser.MXID] if !ok { user = br.loadDBUser(dbUser, nil) } output[index] = user } return output } func (br *GMBridge) loadDBUser(dbUser *database.User, mxid *id.UserID) *User { if dbUser == nil { if mxid == nil { return nil } dbUser = br.DB.User.New() dbUser.MXID = *mxid dbUser.Insert() } user := br.NewUser(dbUser) br.usersByMXID[user.MXID] = user if len(user.GMID) > 0 { br.usersByGMID[user.GMID] = user } if len(user.ManagementRoom) > 0 { br.managementRooms[user.ManagementRoom] = user } return user } func (br *GMBridge) NewUser(dbUser *database.User) *User { user := &User{ User: dbUser, bridge: br, log: br.Log.Sub("User").Sub(string(dbUser.MXID)), chatListReceived: make(chan struct{}, 1), syncPortalsDone: make(chan struct{}, 1), syncStart: make(chan struct{}, 1), messageInput: make(chan PortalMessage), messageOutput: make(chan PortalMessage, br.Config.Bridge.PortalMessageBuffer), } user.PermissionLevel = user.bridge.Config.Bridge.Permissions.Get(user.MXID) user.Whitelisted = user.PermissionLevel >= bridgeconfig.PermissionLevelUser user.Admin = user.PermissionLevel >= bridgeconfig.PermissionLevelAdmin user.BridgeState = br.NewBridgeStateQueue(user) go user.handleMessageLoop() go user.runMessageRingBuffer() return user } func (bridge *GMBridge) GetAllPuppetsWithCustomMXID() []*Puppet { return bridge.dbPuppetsToPuppets(bridge.DB.Puppet.GetAllWithCustomMXID()) } func (bridge *GMBridge) GetAllPuppets() []*Puppet { return bridge.dbPuppetsToPuppets(bridge.DB.Puppet.GetAll()) } func (bridge *GMBridge) dbPuppetsToPuppets(dbPuppets []*database.Puppet) []*Puppet { bridge.puppetsLock.Lock() defer bridge.puppetsLock.Unlock() output := make([]*Puppet, len(dbPuppets)) for index, dbPuppet := range dbPuppets { if dbPuppet == nil { continue } puppet, ok := bridge.puppets[dbPuppet.GMID] if !ok { puppet = bridge.NewPuppet(dbPuppet) bridge.puppets[dbPuppet.GMID] = puppet if len(dbPuppet.CustomMXID) > 0 { bridge.puppetsByCustomMXID[dbPuppet.CustomMXID] = puppet } } output[index] = puppet } return output } func (bridge *GMBridge) FormatPuppetMXID(gmid groupme.ID) id.UserID { return id.NewUserID( bridge.Config.Bridge.FormatUsername(gmid.String()), bridge.Config.Homeserver.Domain) } func (bridge *GMBridge) NewPuppet(dbPuppet *database.Puppet) *Puppet { return &Puppet{ Puppet: dbPuppet, bridge: bridge, log: bridge.Log.Sub(fmt.Sprintf("Puppet/%s", dbPuppet.GMID)), MXID: bridge.FormatPuppetMXID(dbPuppet.GMID), } } func (br *GMBridge) IsGhost(id id.UserID) bool { _, ok := br.ParsePuppetMXID(id) return ok } func (br *GMBridge) GetIGhost(id id.UserID) bridge.Ghost { p := br.GetPuppetByMXID(id) if p == nil { return nil } return p } func (bridge *GMBridge) ParsePuppetMXID(mxid id.UserID) (groupme.ID, bool) { if userIDRegex == nil { userIDRegex = regexp.MustCompile(fmt.Sprintf("^@%s:%s$", bridge.Config.Bridge.FormatUsername("([0-9]+)"), bridge.Config.Homeserver.Domain)) } match := userIDRegex.FindStringSubmatch(string(mxid)) if match == nil || len(match) != 2 { return "", false } return groupme.ID(match[1]), true } func (bridge *GMBridge) GetPuppetByMXID(mxid id.UserID) *Puppet { gmid, ok := bridge.ParsePuppetMXID(mxid) if !ok { return nil } return bridge.GetPuppetByGMID(gmid) } func (bridge *GMBridge) GetPuppetByGMID(gmid groupme.ID) *Puppet { bridge.puppetsLock.Lock() defer bridge.puppetsLock.Unlock() puppet, ok := bridge.puppets[gmid] if !ok { dbPuppet := bridge.DB.Puppet.Get(gmid) if dbPuppet == nil { dbPuppet = bridge.DB.Puppet.New() dbPuppet.GMID = gmid dbPuppet.Insert() } puppet = bridge.NewPuppet(dbPuppet) bridge.puppets[puppet.GMID] = puppet if len(puppet.CustomMXID) > 0 { bridge.puppetsByCustomMXID[puppet.CustomMXID] = puppet } } return puppet } func (bridge *GMBridge) GetPuppetByCustomMXID(mxid id.UserID) *Puppet { bridge.puppetsLock.Lock() defer bridge.puppetsLock.Unlock() puppet, ok := bridge.puppetsByCustomMXID[mxid] if !ok { dbPuppet := bridge.DB.Puppet.GetByCustomMXID(mxid) if dbPuppet == nil { return nil } puppet = bridge.NewPuppet(dbPuppet) bridge.puppets[puppet.GMID] = puppet bridge.puppetsByCustomMXID[puppet.CustomMXID] = puppet } return puppet } func (bridge *GMBridge) dbPortalsToPortals(dbPortals []*database.Portal) []*Portal { bridge.portalsLock.Lock() defer bridge.portalsLock.Unlock() output := make([]*Portal, len(dbPortals)) for index, dbPortal := range dbPortals { if dbPortal == nil { continue } portal, ok := bridge.portalsByGMID[dbPortal.Key] if !ok { portal = bridge.loadDBPortal(dbPortal, nil) } output[index] = portal } return output } func (bridge *GMBridge) loadDBPortal(dbPortal *database.Portal, key *database.PortalKey) *Portal { if dbPortal == nil { if key == nil { return nil } dbPortal = bridge.DB.Portal.New() dbPortal.Key = *key dbPortal.Insert() } portal := bridge.NewPortal(dbPortal) bridge.portalsByGMID[portal.Key] = portal if len(portal.MXID) > 0 { bridge.portalsByMXID[portal.MXID] = portal } return portal } func (bridge *GMBridge) NewManualPortal(key database.PortalKey) *Portal { portal := &Portal{ Portal: bridge.DB.Portal.New(), bridge: bridge, log: bridge.Log.Sub(fmt.Sprintf("Portal/%s", key)), recentlyHandled: make([]string, recentlyHandledLength), messages: make(chan PortalMessage, bridge.Config.Bridge.PortalMessageBuffer), } portal.Key = key go portal.handleMessageLoop() return portal } func (bridge *GMBridge) NewPortal(dbPortal *database.Portal) *Portal { portal := &Portal{ Portal: dbPortal, bridge: bridge, log: bridge.Log.Sub(fmt.Sprintf("Portal/%s", dbPortal.Key)), recentlyHandled: make([]string, recentlyHandledLength), messages: make(chan PortalMessage, bridge.Config.Bridge.PortalMessageBuffer), matrixMessages: make(chan PortalMatrixMessage, bridge.Config.Bridge.PortalMessageBuffer), } go portal.handleMessageLoop() return portal } func (br *GMBridge) CheckFeatures(versions *mautrix.RespVersions) (string, bool) { if br.Config.Bridge.HistorySync.Backfill { supported, known := versions.UnstableFeatures[unstableFeatureBatchSending] if !known { return "Backfilling is enabled in bridge config, but homeserver does not support MSC2716 batch sending", false } else if !supported { return "Backfilling is enabled in bridge config, but MSC2716 batch sending is not enabled on homeserver", false } } return "", true } func main() { br := &GMBridge{ usersByMXID: make(map[id.UserID]*User), usersByGMID: make(map[groupme.ID]*User), spaceRooms: make(map[id.RoomID]*User), managementRooms: make(map[id.RoomID]*User), portalsByMXID: make(map[id.RoomID]*Portal), portalsByGMID: make(map[database.PortalKey]*Portal), puppets: make(map[groupme.ID]*Puppet), puppetsByCustomMXID: make(map[id.UserID]*Puppet), } br.Bridge = bridge.Bridge{ Name: "groupme-matrix", URL: "https://github.com/beeper/groupme", Description: "A Matrix-GroupMe puppeting bridge.", Version: "0.1.0", ProtocolName: "GroupMe", CryptoPickleKey: "github.com/beeper/groupme", ConfigUpgrader: &configupgrade.StructUpgrader{ SimpleUpgrader: configupgrade.SimpleUpgrader(config.DoUpgrade), Blocks: config.SpacedBlocks, Base: ExampleConfig, }, Child: br, } br.InitVersion(Tag, Commit, BuildTime) br.Main() }