542 lines
15 KiB
Go
542 lines
15 KiB
Go
// 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 <https://www.gnu.org/licenses/>.
|
|
|
|
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()
|
|
}
|