diff options
author | Wim <wim@42.be> | 2022-01-31 00:27:37 +0100 |
---|---|---|
committer | Wim <wim@42.be> | 2022-03-20 14:57:48 +0100 |
commit | e3cafeaf9292f67459ff1d186f68283bfaedf2ae (patch) | |
tree | b69c39620aa91dba695b3b935c6651c0fb37ce75 /vendor/go.mau.fi/whatsmeow/store/sqlstore | |
parent | e7b193788a56ee7cdb02a87a9db0ad6724ef66d5 (diff) | |
download | matterbridge-msglm-e3cafeaf9292f67459ff1d186f68283bfaedf2ae.tar.gz matterbridge-msglm-e3cafeaf9292f67459ff1d186f68283bfaedf2ae.tar.bz2 matterbridge-msglm-e3cafeaf9292f67459ff1d186f68283bfaedf2ae.zip |
Add dependencies/vendor (whatsapp)
Diffstat (limited to 'vendor/go.mau.fi/whatsmeow/store/sqlstore')
-rw-r--r-- | vendor/go.mau.fi/whatsmeow/store/sqlstore/container.go | 245 | ||||
-rw-r--r-- | vendor/go.mau.fi/whatsmeow/store/sqlstore/store.go | 610 | ||||
-rw-r--r-- | vendor/go.mau.fi/whatsmeow/store/sqlstore/upgrade.go | 214 |
3 files changed, 1069 insertions, 0 deletions
diff --git a/vendor/go.mau.fi/whatsmeow/store/sqlstore/container.go b/vendor/go.mau.fi/whatsmeow/store/sqlstore/container.go new file mode 100644 index 00000000..30d91971 --- /dev/null +++ b/vendor/go.mau.fi/whatsmeow/store/sqlstore/container.go @@ -0,0 +1,245 @@ +// Copyright (c) 2021 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package sqlstore + +import ( + "crypto/rand" + "database/sql" + "errors" + "fmt" + mathRand "math/rand" + + waProto "go.mau.fi/whatsmeow/binary/proto" + "go.mau.fi/whatsmeow/store" + "go.mau.fi/whatsmeow/types" + "go.mau.fi/whatsmeow/util/keys" + waLog "go.mau.fi/whatsmeow/util/log" +) + +// Container is a wrapper for a SQL database that can contain multiple whatsmeow sessions. +type Container struct { + db *sql.DB + dialect string + log waLog.Logger +} + +var _ store.DeviceContainer = (*Container)(nil) + +// New connects to the given SQL database and wraps it in a Container. +// +// Only SQLite and Postgres are currently fully supported. +// +// The logger can be nil and will default to a no-op logger. +// +// When using SQLite, it's strongly recommended to enable foreign keys by adding `?_foreign_keys=true`: +// container, err := sqlstore.New("sqlite3", "file:yoursqlitefile.db?_foreign_keys=on", nil) +func New(dialect, address string, log waLog.Logger) (*Container, error) { + db, err := sql.Open(dialect, address) + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + container := NewWithDB(db, dialect, log) + err = container.Upgrade() + if err != nil { + return nil, fmt.Errorf("failed to upgrade database: %w", err) + } + return container, nil +} + +// NewWithDB wraps an existing SQL connection in a Container. +// +// Only SQLite and Postgres are currently fully supported. +// +// The logger can be nil and will default to a no-op logger. +// +// When using SQLite, it's strongly recommended to enable foreign keys by adding `?_foreign_keys=true`: +// db, err := sql.Open("sqlite3", "file:yoursqlitefile.db?_foreign_keys=on") +// if err != nil { +// panic(err) +// } +// container, err := sqlstore.NewWithDB(db, "sqlite3", nil) +func NewWithDB(db *sql.DB, dialect string, log waLog.Logger) *Container { + if log == nil { + log = waLog.Noop + } + return &Container{ + db: db, + dialect: dialect, + log: log, + } +} + +const getAllDevicesQuery = ` +SELECT jid, registration_id, noise_key, identity_key, + signed_pre_key, signed_pre_key_id, signed_pre_key_sig, + adv_key, adv_details, adv_account_sig, adv_device_sig, + platform, business_name, push_name +FROM whatsmeow_device +` + +const getDeviceQuery = getAllDevicesQuery + " WHERE jid=$1" + +type scannable interface { + Scan(dest ...interface{}) error +} + +func (c *Container) scanDevice(row scannable) (*store.Device, error) { + var device store.Device + device.Log = c.log + device.SignedPreKey = &keys.PreKey{} + var noisePriv, identityPriv, preKeyPriv, preKeySig []byte + var account waProto.ADVSignedDeviceIdentity + + err := row.Scan( + &device.ID, &device.RegistrationID, &noisePriv, &identityPriv, + &preKeyPriv, &device.SignedPreKey.KeyID, &preKeySig, + &device.AdvSecretKey, &account.Details, &account.AccountSignature, &account.DeviceSignature, + &device.Platform, &device.BusinessName, &device.PushName) + if err != nil { + return nil, fmt.Errorf("failed to scan session: %w", err) + } else if len(noisePriv) != 32 || len(identityPriv) != 32 || len(preKeyPriv) != 32 || len(preKeySig) != 64 { + return nil, ErrInvalidLength + } + + device.NoiseKey = keys.NewKeyPairFromPrivateKey(*(*[32]byte)(noisePriv)) + device.IdentityKey = keys.NewKeyPairFromPrivateKey(*(*[32]byte)(identityPriv)) + device.SignedPreKey.KeyPair = *keys.NewKeyPairFromPrivateKey(*(*[32]byte)(preKeyPriv)) + device.SignedPreKey.Signature = (*[64]byte)(preKeySig) + device.Account = &account + + innerStore := NewSQLStore(c, *device.ID) + device.Identities = innerStore + device.Sessions = innerStore + device.PreKeys = innerStore + device.SenderKeys = innerStore + device.AppStateKeys = innerStore + device.AppState = innerStore + device.Contacts = innerStore + device.ChatSettings = innerStore + device.Container = c + device.Initialized = true + + return &device, nil +} + +// GetAllDevices finds all the devices in the database. +func (c *Container) GetAllDevices() ([]*store.Device, error) { + res, err := c.db.Query(getAllDevicesQuery) + if err != nil { + return nil, fmt.Errorf("failed to query sessions: %w", err) + } + sessions := make([]*store.Device, 0) + for res.Next() { + sess, scanErr := c.scanDevice(res) + if scanErr != nil { + return sessions, scanErr + } + sessions = append(sessions, sess) + } + return sessions, nil +} + +// GetFirstDevice is a convenience method for getting the first device in the store. If there are +// no devices, then a new device will be created. You should only use this if you don't want to +// have multiple sessions simultaneously. +func (c *Container) GetFirstDevice() (*store.Device, error) { + devices, err := c.GetAllDevices() + if err != nil { + return nil, err + } + if len(devices) == 0 { + return c.NewDevice(), nil + } else { + return devices[0], nil + } +} + +// GetDevice finds the device with the specified JID in the database. +// +// If the device is not found, nil is returned instead. +// +// Note that the parameter usually must be an AD-JID. +func (c *Container) GetDevice(jid types.JID) (*store.Device, error) { + sess, err := c.scanDevice(c.db.QueryRow(getDeviceQuery, jid)) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return sess, err +} + +const ( + insertDeviceQuery = ` + INSERT INTO whatsmeow_device (jid, registration_id, noise_key, identity_key, + signed_pre_key, signed_pre_key_id, signed_pre_key_sig, + adv_key, adv_details, adv_account_sig, adv_device_sig, + platform, business_name, push_name) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) + ON CONFLICT (jid) DO UPDATE SET platform=$12, business_name=$13, push_name=$14 + ` + deleteDeviceQuery = `DELETE FROM whatsmeow_device WHERE jid=$1` +) + +// NewDevice creates a new device in this database. +// +// No data is actually stored before Save is called. However, the pairing process will automatically +// call Save after a successful pairing, so you most likely don't need to call it yourself. +func (c *Container) NewDevice() *store.Device { + device := &store.Device{ + Log: c.log, + Container: c, + + NoiseKey: keys.NewKeyPair(), + IdentityKey: keys.NewKeyPair(), + RegistrationID: mathRand.Uint32(), + AdvSecretKey: make([]byte, 32), + } + _, err := rand.Read(device.AdvSecretKey) + if err != nil { + panic(err) + } + device.SignedPreKey = device.IdentityKey.CreateSignedPreKey(1) + return device +} + +// ErrDeviceIDMustBeSet is the error returned by PutDevice if you try to save a device before knowing its JID. +var ErrDeviceIDMustBeSet = errors.New("device JID must be known before accessing database") + +// PutDevice stores the given device in this database. This should be called through Device.Save() +// (which usually doesn't need to be called manually, as the library does that automatically when relevant). +func (c *Container) PutDevice(device *store.Device) error { + if device.ID == nil { + return ErrDeviceIDMustBeSet + } + _, err := c.db.Exec(insertDeviceQuery, + device.ID.String(), device.RegistrationID, device.NoiseKey.Priv[:], device.IdentityKey.Priv[:], + device.SignedPreKey.Priv[:], device.SignedPreKey.KeyID, device.SignedPreKey.Signature[:], + device.AdvSecretKey, device.Account.Details, device.Account.AccountSignature, device.Account.DeviceSignature, + device.Platform, device.BusinessName, device.PushName) + + if !device.Initialized { + innerStore := NewSQLStore(c, *device.ID) + device.Identities = innerStore + device.Sessions = innerStore + device.PreKeys = innerStore + device.SenderKeys = innerStore + device.AppStateKeys = innerStore + device.AppState = innerStore + device.Contacts = innerStore + device.ChatSettings = innerStore + device.Initialized = true + } + return err +} + +// DeleteDevice deletes the given device from this database. This should be called through Device.Delete() +func (c *Container) DeleteDevice(store *store.Device) error { + if store.ID == nil { + return ErrDeviceIDMustBeSet + } + _, err := c.db.Exec(deleteDeviceQuery, store.ID.String()) + return err +} diff --git a/vendor/go.mau.fi/whatsmeow/store/sqlstore/store.go b/vendor/go.mau.fi/whatsmeow/store/sqlstore/store.go new file mode 100644 index 00000000..ad1eea7e --- /dev/null +++ b/vendor/go.mau.fi/whatsmeow/store/sqlstore/store.go @@ -0,0 +1,610 @@ +// Copyright (c) 2021 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package sqlstore contains an SQL-backed implementation of the interfaces in the store package. +package sqlstore + +import ( + "database/sql" + "database/sql/driver" + "errors" + "fmt" + "strings" + "sync" + "time" + + "go.mau.fi/whatsmeow/store" + "go.mau.fi/whatsmeow/types" + "go.mau.fi/whatsmeow/util/keys" +) + +// ErrInvalidLength is returned by some database getters if the database returned a byte array with an unexpected length. +// This should be impossible, as the database schema contains CHECK()s for all the relevant columns. +var ErrInvalidLength = errors.New("database returned byte array with illegal length") + +// PostgresArrayWrapper is a function to wrap array values before passing them to the sql package. +// +// When using github.com/lib/pq, you should set +// whatsmeow.PostgresArrayWrapper = pq.Array +var PostgresArrayWrapper func(interface{}) interface { + driver.Valuer + sql.Scanner +} + +type SQLStore struct { + *Container + JID string + + preKeyLock sync.Mutex + + contactCache map[types.JID]*types.ContactInfo + contactCacheLock sync.Mutex +} + +// NewSQLStore creates a new SQLStore with the given database container and user JID. +// It contains implementations of all the different stores in the store package. +// +// In general, you should use Container.NewDevice or Container.GetDevice instead of this. +func NewSQLStore(c *Container, jid types.JID) *SQLStore { + return &SQLStore{ + Container: c, + JID: jid.String(), + contactCache: make(map[types.JID]*types.ContactInfo), + } +} + +var _ store.IdentityStore = (*SQLStore)(nil) +var _ store.SessionStore = (*SQLStore)(nil) +var _ store.PreKeyStore = (*SQLStore)(nil) +var _ store.SenderKeyStore = (*SQLStore)(nil) +var _ store.AppStateSyncKeyStore = (*SQLStore)(nil) +var _ store.AppStateStore = (*SQLStore)(nil) +var _ store.ContactStore = (*SQLStore)(nil) + +const ( + putIdentityQuery = ` + INSERT INTO whatsmeow_identity_keys (our_jid, their_id, identity) VALUES ($1, $2, $3) + ON CONFLICT (our_jid, their_id) DO UPDATE SET identity=$3 + ` + deleteAllIdentitiesQuery = `DELETE FROM whatsmeow_identity_keys WHERE our_jid=$1 AND their_id LIKE $2` + deleteIdentityQuery = `DELETE FROM whatsmeow_identity_keys WHERE our_jid=$1 AND their_id=$2` + getIdentityQuery = `SELECT identity FROM whatsmeow_identity_keys WHERE our_jid=$1 AND their_id=$2` +) + +func (s *SQLStore) PutIdentity(address string, key [32]byte) error { + _, err := s.db.Exec(putIdentityQuery, s.JID, address, key[:]) + return err +} + +func (s *SQLStore) DeleteAllIdentities(phone string) error { + _, err := s.db.Exec(deleteAllIdentitiesQuery, s.JID, phone+":%") + return err +} + +func (s *SQLStore) DeleteIdentity(address string) error { + _, err := s.db.Exec(deleteAllIdentitiesQuery, s.JID, address) + return err +} + +func (s *SQLStore) IsTrustedIdentity(address string, key [32]byte) (bool, error) { + var existingIdentity []byte + err := s.db.QueryRow(getIdentityQuery, s.JID, address).Scan(&existingIdentity) + if errors.Is(err, sql.ErrNoRows) { + // Trust if not known, it'll be saved automatically later + return true, nil + } else if err != nil { + return false, err + } else if len(existingIdentity) != 32 { + return false, ErrInvalidLength + } + return *(*[32]byte)(existingIdentity) == key, nil +} + +const ( + getSessionQuery = `SELECT session FROM whatsmeow_sessions WHERE our_jid=$1 AND their_id=$2` + hasSessionQuery = `SELECT true FROM whatsmeow_sessions WHERE our_jid=$1 AND their_id=$2` + putSessionQuery = ` + INSERT INTO whatsmeow_sessions (our_jid, their_id, session) VALUES ($1, $2, $3) + ON CONFLICT (our_jid, their_id) DO UPDATE SET session=$3 + ` + deleteAllSessionsQuery = `DELETE FROM whatsmeow_sessions WHERE our_jid=$1 AND their_id LIKE $2` + deleteSessionQuery = `DELETE FROM whatsmeow_sessions WHERE our_jid=$1 AND their_id=$2` +) + +func (s *SQLStore) GetSession(address string) (session []byte, err error) { + err = s.db.QueryRow(getSessionQuery, s.JID, address).Scan(&session) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + return +} + +func (s *SQLStore) HasSession(address string) (has bool, err error) { + err = s.db.QueryRow(hasSessionQuery, s.JID, address).Scan(&has) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + return +} + +func (s *SQLStore) PutSession(address string, session []byte) error { + _, err := s.db.Exec(putSessionQuery, s.JID, address, session) + return err +} + +func (s *SQLStore) DeleteAllSessions(phone string) error { + _, err := s.db.Exec(deleteAllSessionsQuery, s.JID, phone+":%") + return err +} + +func (s *SQLStore) DeleteSession(address string) error { + _, err := s.db.Exec(deleteSessionQuery, s.JID, address) + return err +} + +const ( + getLastPreKeyIDQuery = `SELECT MAX(key_id) FROM whatsmeow_pre_keys WHERE jid=$1` + insertPreKeyQuery = `INSERT INTO whatsmeow_pre_keys (jid, key_id, key, uploaded) VALUES ($1, $2, $3, $4)` + getUnuploadedPreKeysQuery = `SELECT key_id, key FROM whatsmeow_pre_keys WHERE jid=$1 AND uploaded=false ORDER BY key_id LIMIT $2` + getPreKeyQuery = `SELECT key_id, key FROM whatsmeow_pre_keys WHERE jid=$1 AND key_id=$2` + deletePreKeyQuery = `DELETE FROM whatsmeow_pre_keys WHERE jid=$1 AND key_id=$2` + markPreKeysAsUploadedQuery = `UPDATE whatsmeow_pre_keys SET uploaded=true WHERE jid=$1 AND key_id<=$2` + getUploadedPreKeyCountQuery = `SELECT COUNT(*) FROM whatsmeow_pre_keys WHERE jid=$1 AND uploaded=true` +) + +func (s *SQLStore) genOnePreKey(id uint32, markUploaded bool) (*keys.PreKey, error) { + key := keys.NewPreKey(id) + _, err := s.db.Exec(insertPreKeyQuery, s.JID, key.KeyID, key.Priv[:], markUploaded) + return key, err +} + +func (s *SQLStore) getNextPreKeyID() (uint32, error) { + var lastKeyID sql.NullInt32 + err := s.db.QueryRow(getLastPreKeyIDQuery, s.JID).Scan(&lastKeyID) + if err != nil { + return 0, fmt.Errorf("failed to query next prekey ID: %w", err) + } + return uint32(lastKeyID.Int32) + 1, nil +} + +func (s *SQLStore) GenOnePreKey() (*keys.PreKey, error) { + s.preKeyLock.Lock() + defer s.preKeyLock.Unlock() + nextKeyID, err := s.getNextPreKeyID() + if err != nil { + return nil, err + } + return s.genOnePreKey(nextKeyID, true) +} + +func (s *SQLStore) GetOrGenPreKeys(count uint32) ([]*keys.PreKey, error) { + s.preKeyLock.Lock() + defer s.preKeyLock.Unlock() + + res, err := s.db.Query(getUnuploadedPreKeysQuery, s.JID, count) + if err != nil { + return nil, fmt.Errorf("failed to query existing prekeys: %w", err) + } + newKeys := make([]*keys.PreKey, count) + var existingCount uint32 + for res.Next() { + var key *keys.PreKey + key, err = scanPreKey(res) + if err != nil { + return nil, err + } else if key != nil { + newKeys[existingCount] = key + existingCount++ + } + } + + if existingCount < uint32(len(newKeys)) { + var nextKeyID uint32 + nextKeyID, err = s.getNextPreKeyID() + if err != nil { + return nil, err + } + for i := existingCount; i < count; i++ { + newKeys[i], err = s.genOnePreKey(nextKeyID, false) + if err != nil { + return nil, fmt.Errorf("failed to generate prekey: %w", err) + } + nextKeyID++ + } + } + + return newKeys, nil +} + +func scanPreKey(row scannable) (*keys.PreKey, error) { + var priv []byte + var id uint32 + err := row.Scan(&id, &priv) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } else if err != nil { + return nil, err + } else if len(priv) != 32 { + return nil, ErrInvalidLength + } + return &keys.PreKey{ + KeyPair: *keys.NewKeyPairFromPrivateKey(*(*[32]byte)(priv)), + KeyID: id, + }, nil +} + +func (s *SQLStore) GetPreKey(id uint32) (*keys.PreKey, error) { + return scanPreKey(s.db.QueryRow(getPreKeyQuery, s.JID, id)) +} + +func (s *SQLStore) RemovePreKey(id uint32) error { + _, err := s.db.Exec(deletePreKeyQuery, s.JID, id) + return err +} + +func (s *SQLStore) MarkPreKeysAsUploaded(upToID uint32) error { + _, err := s.db.Exec(markPreKeysAsUploadedQuery, s.JID, upToID) + return err +} + +func (s *SQLStore) UploadedPreKeyCount() (count int, err error) { + err = s.db.QueryRow(getUploadedPreKeyCountQuery, s.JID).Scan(&count) + return +} + +const ( + getSenderKeyQuery = `SELECT sender_key FROM whatsmeow_sender_keys WHERE our_jid=$1 AND chat_id=$2 AND sender_id=$3` + putSenderKeyQuery = ` + INSERT INTO whatsmeow_sender_keys (our_jid, chat_id, sender_id, sender_key) VALUES ($1, $2, $3, $4) + ON CONFLICT (our_jid, chat_id, sender_id) DO UPDATE SET sender_key=$4 + ` +) + +func (s *SQLStore) PutSenderKey(group, user string, session []byte) error { + _, err := s.db.Exec(putSenderKeyQuery, s.JID, group, user, session) + return err +} + +func (s *SQLStore) GetSenderKey(group, user string) (key []byte, err error) { + err = s.db.QueryRow(getSenderKeyQuery, s.JID, group, user).Scan(&key) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + return +} + +const ( + putAppStateSyncKeyQuery = ` + INSERT INTO whatsmeow_app_state_sync_keys (jid, key_id, key_data, timestamp, fingerprint) VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (jid, key_id) DO UPDATE SET key_data=$3, timestamp=$4, fingerprint=$5 + ` + getAppStateSyncKeyQuery = `SELECT key_data, timestamp, fingerprint FROM whatsmeow_app_state_sync_keys WHERE jid=$1 AND key_id=$2` +) + +func (s *SQLStore) PutAppStateSyncKey(id []byte, key store.AppStateSyncKey) error { + _, err := s.db.Exec(putAppStateSyncKeyQuery, s.JID, id, key.Data, key.Timestamp, key.Fingerprint) + return err +} + +func (s *SQLStore) GetAppStateSyncKey(id []byte) (*store.AppStateSyncKey, error) { + var key store.AppStateSyncKey + err := s.db.QueryRow(getAppStateSyncKeyQuery, s.JID, id).Scan(&key.Data, &key.Timestamp, &key.Fingerprint) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return &key, err +} + +const ( + putAppStateVersionQuery = ` + INSERT INTO whatsmeow_app_state_version (jid, name, version, hash) VALUES ($1, $2, $3, $4) + ON CONFLICT (jid, name) DO UPDATE SET version=$3, hash=$4 + ` + getAppStateVersionQuery = `SELECT version, hash FROM whatsmeow_app_state_version WHERE jid=$1 AND name=$2` + deleteAppStateVersionQuery = `DELETE FROM whatsmeow_app_state_version WHERE jid=$1 AND name=$2` + putAppStateMutationMACsQuery = `INSERT INTO whatsmeow_app_state_mutation_macs (jid, name, version, index_mac, value_mac) VALUES ` + deleteAppStateMutationMACsQueryPostgres = `DELETE FROM whatsmeow_app_state_mutation_macs WHERE jid=$1 AND name=$2 AND index_mac=ANY($3::bytea[])` + deleteAppStateMutationMACsQueryGeneric = `DELETE FROM whatsmeow_app_state_mutation_macs WHERE jid=$1 AND name=$2 AND index_mac IN ` + getAppStateMutationMACQuery = `SELECT value_mac FROM whatsmeow_app_state_mutation_macs WHERE jid=$1 AND name=$2 AND index_mac=$3 ORDER BY version DESC LIMIT 1` +) + +func (s *SQLStore) PutAppStateVersion(name string, version uint64, hash [128]byte) error { + _, err := s.db.Exec(putAppStateVersionQuery, s.JID, name, version, hash[:]) + return err +} + +func (s *SQLStore) GetAppStateVersion(name string) (version uint64, hash [128]byte, err error) { + var uncheckedHash []byte + err = s.db.QueryRow(getAppStateVersionQuery, s.JID, name).Scan(&version, &uncheckedHash) + if errors.Is(err, sql.ErrNoRows) { + // version will be 0 and hash will be an empty array, which is the correct initial state + err = nil + } else if err != nil { + // There's an error, just return it + } else if len(uncheckedHash) != 128 { + // This shouldn't happen + err = ErrInvalidLength + } else { + // No errors, convert hash slice to array + hash = *(*[128]byte)(uncheckedHash) + } + return +} + +func (s *SQLStore) DeleteAppStateVersion(name string) error { + _, err := s.db.Exec(deleteAppStateVersionQuery, s.JID, name) + return err +} + +type execable interface { + Exec(query string, args ...interface{}) (sql.Result, error) +} + +func (s *SQLStore) putAppStateMutationMACs(tx execable, name string, version uint64, mutations []store.AppStateMutationMAC) error { + values := make([]interface{}, 3+len(mutations)*2) + queryParts := make([]string, len(mutations)) + values[0] = s.JID + values[1] = name + values[2] = version + for i, mutation := range mutations { + baseIndex := 3 + i*2 + values[baseIndex] = mutation.IndexMAC + values[baseIndex+1] = mutation.ValueMAC + if s.dialect == "sqlite3" { + queryParts[i] = fmt.Sprintf("(?1, ?2, ?3, ?%d, ?%d)", baseIndex+1, baseIndex+2) + } else { + queryParts[i] = fmt.Sprintf("($1, $2, $3, $%d, $%d)", baseIndex+1, baseIndex+2) + } + } + _, err := tx.Exec(putAppStateMutationMACsQuery+strings.Join(queryParts, ","), values...) + return err +} + +const mutationBatchSize = 400 + +func (s *SQLStore) PutAppStateMutationMACs(name string, version uint64, mutations []store.AppStateMutationMAC) error { + if len(mutations) > mutationBatchSize { + tx, err := s.db.Begin() + if err != nil { + return fmt.Errorf("failed to start transaction: %w", err) + } + for i := 0; i < len(mutations); i += mutationBatchSize { + var mutationSlice []store.AppStateMutationMAC + if len(mutations) > i+mutationBatchSize { + mutationSlice = mutations[i : i+mutationBatchSize] + } else { + mutationSlice = mutations[i:] + } + err = s.putAppStateMutationMACs(tx, name, version, mutationSlice) + if err != nil { + _ = tx.Rollback() + return err + } + } + err = tx.Commit() + if err != nil { + return fmt.Errorf("failed to commit transaction: %w", err) + } + return nil + } else if len(mutations) > 0 { + return s.putAppStateMutationMACs(s.db, name, version, mutations) + } + return nil +} + +func (s *SQLStore) DeleteAppStateMutationMACs(name string, indexMACs [][]byte) (err error) { + if len(indexMACs) == 0 { + return + } + if s.dialect == "postgres" && PostgresArrayWrapper != nil { + _, err = s.db.Exec(deleteAppStateMutationMACsQueryPostgres, s.JID, name, PostgresArrayWrapper(indexMACs)) + } else { + args := make([]interface{}, 2+len(indexMACs)) + args[0] = s.JID + args[1] = name + queryParts := make([]string, len(indexMACs)) + for i, item := range indexMACs { + args[2+i] = item + queryParts[i] = fmt.Sprintf("$%d", i+3) + } + _, err = s.db.Exec(deleteAppStateMutationMACsQueryGeneric+"("+strings.Join(queryParts, ",")+")", args...) + } + return +} + +func (s *SQLStore) GetAppStateMutationMAC(name string, indexMAC []byte) (valueMAC []byte, err error) { + err = s.db.QueryRow(getAppStateMutationMACQuery, s.JID, name, indexMAC).Scan(&valueMAC) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + return +} + +const ( + putContactNameQuery = ` + INSERT INTO whatsmeow_contacts (our_jid, their_jid, first_name, full_name) VALUES ($1, $2, $3, $4) + ON CONFLICT (our_jid, their_jid) DO UPDATE SET first_name=$3, full_name=$4 + ` + putPushNameQuery = ` + INSERT INTO whatsmeow_contacts (our_jid, their_jid, push_name) VALUES ($1, $2, $3) + ON CONFLICT (our_jid, their_jid) DO UPDATE SET push_name=$3 + ` + putBusinessNameQuery = ` + INSERT INTO whatsmeow_contacts (our_jid, their_jid, business_name) VALUES ($1, $2, $3) + ON CONFLICT (our_jid, their_jid) DO UPDATE SET business_name=$3 + ` + getContactQuery = ` + SELECT first_name, full_name, push_name, business_name FROM whatsmeow_contacts WHERE our_jid=$1 AND their_jid=$2 + ` + getAllContactsQuery = ` + SELECT their_jid, first_name, full_name, push_name, business_name FROM whatsmeow_contacts WHERE our_jid=$1 + ` +) + +func (s *SQLStore) PutPushName(user types.JID, pushName string) (bool, string, error) { + s.contactCacheLock.Lock() + defer s.contactCacheLock.Unlock() + + cached, err := s.getContact(user) + if err != nil { + return false, "", err + } + if cached.PushName != pushName { + _, err = s.db.Exec(putPushNameQuery, s.JID, user, pushName) + if err != nil { + return false, "", err + } + previousName := cached.PushName + cached.PushName = pushName + cached.Found = true + return true, previousName, nil + } + return false, "", nil +} + +func (s *SQLStore) PutBusinessName(user types.JID, businessName string) error { + s.contactCacheLock.Lock() + defer s.contactCacheLock.Unlock() + + cached, err := s.getContact(user) + if err != nil { + return err + } + if cached.BusinessName != businessName { + _, err = s.db.Exec(putBusinessNameQuery, s.JID, user, businessName) + if err != nil { + return err + } + cached.BusinessName = businessName + cached.Found = true + } + return nil +} + +func (s *SQLStore) PutContactName(user types.JID, firstName, fullName string) error { + s.contactCacheLock.Lock() + defer s.contactCacheLock.Unlock() + + cached, err := s.getContact(user) + if err != nil { + return err + } + if cached.FirstName != firstName || cached.FullName != fullName { + _, err = s.db.Exec(putContactNameQuery, s.JID, user, firstName, fullName) + if err != nil { + return err + } + cached.FirstName = firstName + cached.FullName = fullName + cached.Found = true + } + return nil +} + +func (s *SQLStore) getContact(user types.JID) (*types.ContactInfo, error) { + cached, ok := s.contactCache[user] + if ok { + return cached, nil + } + + var first, full, push, business sql.NullString + err := s.db.QueryRow(getContactQuery, s.JID, user).Scan(&first, &full, &push, &business) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, err + } + info := &types.ContactInfo{ + Found: err == nil, + FirstName: first.String, + FullName: full.String, + PushName: push.String, + BusinessName: business.String, + } + s.contactCache[user] = info + return info, nil +} + +func (s *SQLStore) GetContact(user types.JID) (types.ContactInfo, error) { + s.contactCacheLock.Lock() + info, err := s.getContact(user) + s.contactCacheLock.Unlock() + if err != nil { + return types.ContactInfo{}, err + } + return *info, nil +} + +func (s *SQLStore) GetAllContacts() (map[types.JID]types.ContactInfo, error) { + s.contactCacheLock.Lock() + defer s.contactCacheLock.Unlock() + rows, err := s.db.Query(getAllContactsQuery, s.JID) + if err != nil { + return nil, err + } + output := make(map[types.JID]types.ContactInfo, len(s.contactCache)) + for rows.Next() { + var jid types.JID + var first, full, push, business sql.NullString + err = rows.Scan(&jid, &first, &full, &push, &business) + if err != nil { + return nil, fmt.Errorf("error scanning row: %w", err) + } + info := types.ContactInfo{ + Found: true, + FirstName: first.String, + FullName: full.String, + PushName: push.String, + BusinessName: business.String, + } + output[jid] = info + s.contactCache[jid] = &info + } + return output, nil +} + +const ( + putChatSettingQuery = ` + INSERT INTO whatsmeow_chat_settings (our_jid, chat_jid, %[1]s) VALUES ($1, $2, $3) + ON CONFLICT (our_jid, chat_jid) DO UPDATE SET %[1]s=$3 + ` + getChatSettingsQuery = ` + SELECT muted_until, pinned, archived FROM whatsmeow_chat_settings WHERE our_jid=$1 AND chat_jid=$2 + ` +) + +func (s *SQLStore) PutMutedUntil(chat types.JID, mutedUntil time.Time) error { + var val int64 + if !mutedUntil.IsZero() { + val = mutedUntil.Unix() + } + _, err := s.db.Exec(fmt.Sprintf(putChatSettingQuery, "muted_until"), s.JID, chat, val) + return err +} + +func (s *SQLStore) PutPinned(chat types.JID, pinned bool) error { + _, err := s.db.Exec(fmt.Sprintf(putChatSettingQuery, "pinned"), s.JID, chat, pinned) + return err +} + +func (s *SQLStore) PutArchived(chat types.JID, archived bool) error { + _, err := s.db.Exec(fmt.Sprintf(putChatSettingQuery, "archived"), s.JID, chat, archived) + return err +} + +func (s *SQLStore) GetChatSettings(chat types.JID) (settings types.LocalChatSettings, err error) { + var mutedUntil int64 + err = s.db.QueryRow(getChatSettingsQuery, s.JID, chat).Scan(&mutedUntil, &settings.Pinned, &settings.Archived) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } else if err != nil { + return + } else { + settings.Found = true + } + if mutedUntil != 0 { + settings.MutedUntil = time.Unix(mutedUntil, 0) + } + return +} diff --git a/vendor/go.mau.fi/whatsmeow/store/sqlstore/upgrade.go b/vendor/go.mau.fi/whatsmeow/store/sqlstore/upgrade.go new file mode 100644 index 00000000..b98f2d61 --- /dev/null +++ b/vendor/go.mau.fi/whatsmeow/store/sqlstore/upgrade.go @@ -0,0 +1,214 @@ +// Copyright (c) 2021 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package sqlstore + +import ( + "database/sql" +) + +type upgradeFunc func(*sql.Tx, *Container) error + +// Upgrades is a list of functions that will upgrade a database to the latest version. +// +// This may be of use if you want to manage the database fully manually, but in most cases you +// should just call Container.Upgrade to let the library handle everything. +var Upgrades = [...]upgradeFunc{upgradeV1} + +func (c *Container) getVersion() (int, error) { + _, err := c.db.Exec("CREATE TABLE IF NOT EXISTS whatsmeow_version (version INTEGER)") + if err != nil { + return -1, err + } + + version := 0 + row := c.db.QueryRow("SELECT version FROM whatsmeow_version LIMIT 1") + if row != nil { + _ = row.Scan(&version) + } + return version, nil +} + +func (c *Container) setVersion(tx *sql.Tx, version int) error { + _, err := tx.Exec("DELETE FROM whatsmeow_version") + if err != nil { + return err + } + _, err = tx.Exec("INSERT INTO whatsmeow_version (version) VALUES ($1)", version) + return err +} + +// Upgrade upgrades the database from the current to the latest version available. +func (c *Container) Upgrade() error { + version, err := c.getVersion() + if err != nil { + return err + } + + for ; version < len(Upgrades); version++ { + var tx *sql.Tx + tx, err = c.db.Begin() + if err != nil { + return err + } + + migrateFunc := Upgrades[version] + err = migrateFunc(tx, c) + if err != nil { + _ = tx.Rollback() + return err + } + + if err = c.setVersion(tx, version+1); err != nil { + return err + } + + if err = tx.Commit(); err != nil { + return err + } + } + + return nil +} + +func upgradeV1(tx *sql.Tx, _ *Container) error { + _, err := tx.Exec(`CREATE TABLE whatsmeow_device ( + jid TEXT PRIMARY KEY, + + registration_id BIGINT NOT NULL CHECK ( registration_id >= 0 AND registration_id < 4294967296 ), + + noise_key bytea NOT NULL CHECK ( length(noise_key) = 32 ), + identity_key bytea NOT NULL CHECK ( length(identity_key) = 32 ), + + signed_pre_key bytea NOT NULL CHECK ( length(signed_pre_key) = 32 ), + signed_pre_key_id INTEGER NOT NULL CHECK ( signed_pre_key_id >= 0 AND signed_pre_key_id < 16777216 ), + signed_pre_key_sig bytea NOT NULL CHECK ( length(signed_pre_key_sig) = 64 ), + + adv_key bytea NOT NULL, + adv_details bytea NOT NULL, + adv_account_sig bytea NOT NULL CHECK ( length(adv_account_sig) = 64 ), + adv_device_sig bytea NOT NULL CHECK ( length(adv_device_sig) = 64 ), + + platform TEXT NOT NULL DEFAULT '', + business_name TEXT NOT NULL DEFAULT '', + push_name TEXT NOT NULL DEFAULT '' + )`) + if err != nil { + return err + } + _, err = tx.Exec(`CREATE TABLE whatsmeow_identity_keys ( + our_jid TEXT, + their_id TEXT, + identity bytea NOT NULL CHECK ( length(identity) = 32 ), + + PRIMARY KEY (our_jid, their_id), + FOREIGN KEY (our_jid) REFERENCES whatsmeow_device(jid) ON DELETE CASCADE ON UPDATE CASCADE + )`) + if err != nil { + return err + } + _, err = tx.Exec(`CREATE TABLE whatsmeow_pre_keys ( + jid TEXT, + key_id INTEGER CHECK ( key_id >= 0 AND key_id < 16777216 ), + key bytea NOT NULL CHECK ( length(key) = 32 ), + uploaded BOOLEAN NOT NULL, + + PRIMARY KEY (jid, key_id), + FOREIGN KEY (jid) REFERENCES whatsmeow_device(jid) ON DELETE CASCADE ON UPDATE CASCADE + )`) + if err != nil { + return err + } + _, err = tx.Exec(`CREATE TABLE whatsmeow_sessions ( + our_jid TEXT, + their_id TEXT, + session bytea, + + PRIMARY KEY (our_jid, their_id), + FOREIGN KEY (our_jid) REFERENCES whatsmeow_device(jid) ON DELETE CASCADE ON UPDATE CASCADE + )`) + if err != nil { + return err + } + _, err = tx.Exec(`CREATE TABLE whatsmeow_sender_keys ( + our_jid TEXT, + chat_id TEXT, + sender_id TEXT, + sender_key bytea NOT NULL, + + PRIMARY KEY (our_jid, chat_id, sender_id), + FOREIGN KEY (our_jid) REFERENCES whatsmeow_device(jid) ON DELETE CASCADE ON UPDATE CASCADE + )`) + if err != nil { + return err + } + _, err = tx.Exec(`CREATE TABLE whatsmeow_app_state_sync_keys ( + jid TEXT, + key_id bytea, + key_data bytea NOT NULL, + timestamp BIGINT NOT NULL, + fingerprint bytea NOT NULL, + + PRIMARY KEY (jid, key_id), + FOREIGN KEY (jid) REFERENCES whatsmeow_device(jid) ON DELETE CASCADE ON UPDATE CASCADE + )`) + if err != nil { + return err + } + _, err = tx.Exec(`CREATE TABLE whatsmeow_app_state_version ( + jid TEXT, + name TEXT, + version BIGINT NOT NULL, + hash bytea NOT NULL CHECK ( length(hash) = 128 ), + + PRIMARY KEY (jid, name), + FOREIGN KEY (jid) REFERENCES whatsmeow_device(jid) ON DELETE CASCADE ON UPDATE CASCADE + )`) + if err != nil { + return err + } + _, err = tx.Exec(`CREATE TABLE whatsmeow_app_state_mutation_macs ( + jid TEXT, + name TEXT, + version BIGINT, + index_mac bytea CHECK ( length(index_mac) = 32 ), + value_mac bytea NOT NULL CHECK ( length(value_mac) = 32 ), + + PRIMARY KEY (jid, name, version, index_mac), + FOREIGN KEY (jid, name) REFERENCES whatsmeow_app_state_version(jid, name) ON DELETE CASCADE ON UPDATE CASCADE + )`) + if err != nil { + return err + } + _, err = tx.Exec(`CREATE TABLE whatsmeow_contacts ( + our_jid TEXT, + their_jid TEXT, + first_name TEXT, + full_name TEXT, + push_name TEXT, + business_name TEXT, + + PRIMARY KEY (our_jid, their_jid), + FOREIGN KEY (our_jid) REFERENCES whatsmeow_device(jid) ON DELETE CASCADE ON UPDATE CASCADE + )`) + if err != nil { + return err + } + _, err = tx.Exec(`CREATE TABLE whatsmeow_chat_settings ( + our_jid TEXT, + chat_jid TEXT, + muted_until BIGINT NOT NULL DEFAULT 0, + pinned BOOLEAN NOT NULL DEFAULT false, + archived BOOLEAN NOT NULL DEFAULT false, + + PRIMARY KEY (our_jid, chat_jid), + FOREIGN KEY (our_jid) REFERENCES whatsmeow_device(jid) ON DELETE CASCADE ON UPDATE CASCADE + )`) + if err != nil { + return err + } + return nil +} |