Skip to content

Commit

Permalink
Add mautrix-facebook DB migration utility. Fixes #12
Browse files Browse the repository at this point in the history
  • Loading branch information
tulir committed Mar 18, 2024
1 parent 4e987b5 commit fc1de15
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 8 deletions.
2 changes: 1 addition & 1 deletion cmd/lscli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/tidwall/gjson v1.17.1
github.com/zyedidia/clipboard v1.0.4
go.mau.fi/mautrix-meta v0.1.0
go.mau.fi/util v0.4.1
go.mau.fi/util v0.4.2-0.20240318211948-d27d5a4cda9e
)

require (
Expand Down
4 changes: 2 additions & 2 deletions cmd/lscli/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ github.com/zyedidia/clipboard v1.0.4 h1:r6GUQOyPtIaApRLeD56/U+2uJbXis6ANGbKWCljU
github.com/zyedidia/clipboard v1.0.4/go.mod h1:zykFnZUXX0ErxqvYLUFEq7QDJKId8rmh2FgD0/Y8cjA=
go.mau.fi/libsignal v0.1.0 h1:vAKI/nJ5tMhdzke4cTK1fb0idJzz1JuEIpmjprueC+c=
go.mau.fi/libsignal v0.1.0/go.mod h1:R8ovrTezxtUNzCQE5PH30StOQWWeBskBsWE55vMfY9I=
go.mau.fi/util v0.4.1 h1:3EC9KxIXo5+h869zDGf5OOZklRd/FjeVnimTwtm3owg=
go.mau.fi/util v0.4.1/go.mod h1:GjkTEBsehYZbSh2LlE6cWEn+6ZIZTGrTMM/5DMNlmFY=
go.mau.fi/util v0.4.2-0.20240318211948-d27d5a4cda9e h1:2jdYsZTTIwSo4TGmVrqLgeCqaxexJ9nY2Tuj1MzDIwc=
go.mau.fi/util v0.4.2-0.20240318211948-d27d5a4cda9e/go.mod h1:GjkTEBsehYZbSh2LlE6cWEn+6ZIZTGrTMM/5DMNlmFY=
go.mau.fi/whatsmeow v0.0.0-20240316104858-18372a0653fa h1:ifQivrTMLAAkBkIqdE/D56uAZyc7ziRdhUU59VfNPFg=
go.mau.fi/whatsmeow v0.0.0-20240316104858-18372a0653fa/go.mod h1:kNI5foyzqd77d5HaWc1Jico6/rxtZ/UE8nr80hIsbIk=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
Expand Down
233 changes: 233 additions & 0 deletions database/legacymigrate/migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
// mautrix-meta - A Matrix-Facebook Messenger and Instagram DM puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// 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 legacymigrate

import (
"context"
"database/sql"
"fmt"
"time"

"github.com/rs/zerolog"
"go.mau.fi/util/dbutil"
"go.mau.fi/util/exerrors"
"maunium.net/go/mautrix/id"

"go.mau.fi/mautrix-meta/database"
"go.mau.fi/mautrix-meta/messagix/table"
)

type Insertable interface {
Insert(context.Context) error
}

type ToNewable[T Insertable] interface {
ToNew(*database.Database) T
}

type LegacyUser struct {
MXID id.UserID
NoticeRoom id.RoomID
}

func (lu *LegacyUser) ToNew(db *database.Database) *database.User {
dbUser := db.User.New()
dbUser.MXID = lu.MXID
dbUser.ManagementRoom = lu.NoticeRoom
return dbUser
}

type LegacyPortal struct {
FBID int64
FBReceiver int64
FBType string
MXID sql.NullString
Name sql.NullString
PhotoID sql.NullString
AvatarURL sql.NullString
Encrypted bool
NameSet bool
AvatarSet bool
}

func (lp *LegacyPortal) ToNew(db *database.Database) *database.Portal {
dbPortal := db.Portal.New()
dbPortal.PortalKey.ThreadID = lp.FBID
dbPortal.PortalKey.Receiver = lp.FBReceiver
switch lp.FBType {
case "USER":
dbPortal.ThreadType = table.ONE_TO_ONE
case "GROUP":
dbPortal.ThreadType = table.GROUP_THREAD
default:
dbPortal.ThreadType = table.UNKNOWN_THREAD_TYPE
}
dbPortal.MXID = id.RoomID(lp.MXID.String)
dbPortal.Name = lp.Name.String
dbPortal.AvatarID = lp.PhotoID.String
dbPortal.AvatarURL, _ = id.ParseContentURI(lp.AvatarURL.String)
dbPortal.Encrypted = lp.Encrypted
dbPortal.NameSet = lp.NameSet
dbPortal.AvatarSet = lp.AvatarSet
return dbPortal
}

type LegacyPuppet struct {
FBID int64
Name sql.NullString
PhotoID sql.NullString
PhotoMXC sql.NullString
NameSet bool
AvatarSet bool
}

func (lp *LegacyPuppet) ToNew(db *database.Database) *database.Puppet {
dbPuppet := db.Puppet.New()
dbPuppet.ID = lp.FBID
dbPuppet.Name = lp.Name.String
dbPuppet.AvatarID = lp.PhotoID.String
dbPuppet.AvatarURL, _ = id.ParseContentURI(lp.PhotoMXC.String)
dbPuppet.NameSet = lp.NameSet
dbPuppet.AvatarSet = lp.AvatarSet
return dbPuppet
}

type LegacyMessage struct {
FBID string
FBTxnID sql.NullInt64
Index int
FBChat int64
FBReceiver int64
FBSender int64
Timestamp int64
MXID id.EventID
MXRoom id.RoomID
}

func (lm *LegacyMessage) ToNew(db *database.Database) *database.Message {
dbMessage := db.Message.New()
dbMessage.ID = lm.FBID
dbMessage.OTID = lm.FBTxnID.Int64
dbMessage.PartIndex = lm.Index
dbMessage.ThreadID = lm.FBChat
dbMessage.ThreadReceiver = lm.FBReceiver
dbMessage.Sender = lm.FBSender
dbMessage.Timestamp = time.UnixMilli(lm.Timestamp)
dbMessage.MXID = lm.MXID
dbMessage.RoomID = lm.MXRoom
return dbMessage
}

type LegacyReaction struct {
FBMsgID string
FBThreadID int64
FBReceiver int64
FBSender int64
Reaction string
MXID id.EventID
MXRoom id.RoomID
}

func (lr *LegacyReaction) ToNew(db *database.Database) *database.Reaction {
dbReaction := db.Reaction.New()
dbReaction.MessageID = lr.FBMsgID
dbReaction.ThreadID = lr.FBThreadID
dbReaction.ThreadReceiver = lr.FBReceiver
dbReaction.Sender = lr.FBSender
dbReaction.Emoji = lr.Reaction
dbReaction.MXID = lr.MXID
dbReaction.RoomID = lr.MXRoom
return dbReaction
}

type reinserter[T ToNewable[I], I Insertable] struct {
db *database.Database
ctx context.Context
}

func (r reinserter[T, I]) do(m T) (bool, error) {
return true, m.ToNew(r.db).Insert(r.ctx)
}

func Migrate(ctx context.Context, targetDB *database.Database, sourceDialect, sourceURI string) {
log := zerolog.Ctx(ctx)
sourceDB := exerrors.Must(dbutil.NewWithDialect(sourceURI, sourceDialect))
sourceDB.Log = dbutil.ZeroLoggerPtr(log)

oldDBOwner := exerrors.Must(dbutil.ScanSingleColumn[string](sourceDB.QueryRow(ctx, "SELECT owner FROM database_owner")))
if oldDBOwner != "mautrix-facebook" {
panic(fmt.Errorf("source database is %s, not mautrix-facebook", oldDBOwner))
}
oldDBVersion := exerrors.Must(dbutil.ScanSingleColumn[int](sourceDB.QueryRow(ctx, "SELECT version FROM version")))
if oldDBVersion != 12 {
panic(fmt.Errorf("source database is not on latest version (got %d, expected 12)", oldDBVersion))
}
log.Debug().
Str("owner", oldDBOwner).
Int("version", oldDBVersion).
Msg("Source database version confirmed")

log.Info().Msg("Upgrading target database")
exerrors.PanicIfNotNil(targetDB.Upgrade(ctx))

log.Info().Msg("Migrating data")
origCtx := ctx
exerrors.PanicIfNotNil(targetDB.DoTxn(ctx, nil, func(ctx context.Context) error {
err := dbutil.NewSimpleReflectRowIter[LegacyUser](sourceDB.Query(origCtx, `
SELECT mxid, notice_room FROM "user" WHERE notice_room<>''
`)).Iter(reinserter[*LegacyUser, *database.User]{targetDB, ctx}.do)
if err != nil {
log.Error().Msg("Failed to copy users")
return err
}
err = dbutil.NewSimpleReflectRowIter[LegacyPortal](sourceDB.Query(origCtx, `
SELECT fbid, fb_receiver, fb_type, mxid, name, photo_id, avatar_url, encrypted, name_set, avatar_set FROM portal
`)).Iter(reinserter[*LegacyPortal, *database.Portal]{targetDB, ctx}.do)
if err != nil {
log.Error().Msg("Failed to copy portals")
return err
}
err = dbutil.NewSimpleReflectRowIter[LegacyPuppet](sourceDB.Query(origCtx, `
SELECT fbid, name, photo_id, photo_mxc, name_set, avatar_set FROM puppet
`)).Iter(reinserter[*LegacyPuppet, *database.Puppet]{targetDB, ctx}.do)
if err != nil {
log.Error().Msg("Failed to copy puppets")
return err
}
err = dbutil.NewSimpleReflectRowIter[LegacyMessage](sourceDB.Query(origCtx, `
SELECT fbid, fb_txn_id, index, fb_chat, fb_receiver, fb_sender, timestamp, mxid, mx_room
FROM message WHERE fbid<>'' AND fb_sender<>0
`)).Iter(reinserter[*LegacyMessage, *database.Message]{targetDB, ctx}.do)
if err != nil {
log.Error().Msg("Failed to copy messages")
return err
}
err = dbutil.NewSimpleReflectRowIter[LegacyReaction](sourceDB.Query(origCtx, `
SELECT reaction.fb_msgid, message.fb_chat, reaction.fb_receiver,
reaction.fb_sender, reaction.reaction, reaction.mxid, reaction.mx_room
FROM reaction
JOIN message ON reaction.fb_msgid=message.fbid AND message.index=0
WHERE reaction.fb_sender<>0 AND message.fb_chat<>0 AND reaction.fb_msgid='mid.$gABC3ypFJHACT3lHXb2NrQyNVgAAq'
`)).Iter(reinserter[*LegacyReaction, *database.Reaction]{targetDB, ctx}.do)
if err != nil {
log.Error().Msg("Failed to copy reactions")
return err
}
return nil
}))
log.Info().Msg("Migration complete")
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ require (
github.com/mattn/go-sqlite3 v1.14.22
github.com/rs/zerolog v1.32.0
go.mau.fi/libsignal v0.1.0
go.mau.fi/util v0.4.1
go.mau.fi/util v0.4.2-0.20240318211948-d27d5a4cda9e
go.mau.fi/whatsmeow v0.0.0-20240316104858-18372a0653fa
golang.org/x/crypto v0.21.0
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f
golang.org/x/image v0.15.0
golang.org/x/net v0.22.0
google.golang.org/protobuf v1.33.0
maunium.net/go/mauflag v1.0.0
maunium.net/go/mautrix v0.18.0
)

Expand All @@ -37,5 +38,4 @@ require (
golang.org/x/sys v0.18.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
maunium.net/go/mauflag v1.0.0 // indirect
)
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
go.mau.fi/libsignal v0.1.0 h1:vAKI/nJ5tMhdzke4cTK1fb0idJzz1JuEIpmjprueC+c=
go.mau.fi/libsignal v0.1.0/go.mod h1:R8ovrTezxtUNzCQE5PH30StOQWWeBskBsWE55vMfY9I=
go.mau.fi/util v0.4.1 h1:3EC9KxIXo5+h869zDGf5OOZklRd/FjeVnimTwtm3owg=
go.mau.fi/util v0.4.1/go.mod h1:GjkTEBsehYZbSh2LlE6cWEn+6ZIZTGrTMM/5DMNlmFY=
go.mau.fi/util v0.4.2-0.20240318211948-d27d5a4cda9e h1:2jdYsZTTIwSo4TGmVrqLgeCqaxexJ9nY2Tuj1MzDIwc=
go.mau.fi/util v0.4.2-0.20240318211948-d27d5a4cda9e/go.mod h1:GjkTEBsehYZbSh2LlE6cWEn+6ZIZTGrTMM/5DMNlmFY=
go.mau.fi/whatsmeow v0.0.0-20240316104858-18372a0653fa h1:ifQivrTMLAAkBkIqdE/D56uAZyc7ziRdhUU59VfNPFg=
go.mau.fi/whatsmeow v0.0.0-20240316104858-18372a0653fa/go.mod h1:kNI5foyzqd77d5HaWc1Jico6/rxtZ/UE8nr80hIsbIk=
go.mau.fi/zeroconfig v0.1.2 h1:DKOydWnhPMn65GbXZOafgkPm11BvFashZWLct0dGFto=
Expand Down
25 changes: 24 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ import (
"context"
_ "embed"
"fmt"
"os"
"strings"
"sync"

"github.com/rs/zerolog"
"go.mau.fi/util/configupgrade"
flag "maunium.net/go/mauflag"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridge"
"maunium.net/go/mautrix/bridge/commands"
Expand All @@ -36,6 +39,7 @@ import (

"go.mau.fi/mautrix-meta/config"
"go.mau.fi/mautrix-meta/database"
"go.mau.fi/mautrix-meta/database/legacymigrate"
"go.mau.fi/mautrix-meta/messagix/socket"
"go.mau.fi/mautrix-meta/messagix/table"
"go.mau.fi/mautrix-meta/messagix/types"
Expand All @@ -53,6 +57,11 @@ var (
BuildTime = "unknown"
)

var migrateLegacyFrom = flag.Make().
LongKey("db-migrate-from").
Usage("Migrate from a legacy mautrix-facebook database").
String()

type MetaBridge struct {
bridge.Bridge

Expand Down Expand Up @@ -101,6 +110,21 @@ func (br *MetaBridge) ValidateConfig() error {
}

func (br *MetaBridge) Init() {
br.DB = database.New(br.Bridge.DB)
if *migrateLegacyFrom != "" {
if br.Config.Meta.Mode.IsInstagram() {
br.ZLog.Fatal().Msg("Instagram database can't be migrated")
}
dialect := "sqlite3"
if strings.HasPrefix(*migrateLegacyFrom, "postgres") {
dialect = "postgres"
}
br.ZLog.Info().Str("legacy_db_dialect", dialect).Msg("Database migration requested")
legacymigrate.Migrate(br.ZLog.WithContext(context.Background()), br.DB, dialect, *migrateLegacyFrom)
_ = br.DB.Close()
os.Exit(0)
}

var defaultCommandPrefix string
switch br.Config.Meta.Mode {
case config.ModeInstagram:
Expand Down Expand Up @@ -137,7 +161,6 @@ func (br *MetaBridge) Init() {
br.CommandProcessor = commands.NewProcessor(&br.Bridge)
br.RegisterCommands()

br.DB = database.New(br.Bridge.DB)
br.DeviceStore = sqlstore.NewWithDB(br.DB.RawDB, br.DB.Dialect.String(), waLog.Zerolog(br.ZLog.With().Str("db_section", "whatsmeow").Logger()))

ss := br.Config.Bridge.Provisioning.SharedSecret
Expand Down

0 comments on commit fc1de15

Please sign in to comment.