all: add support for topics and refactor other things

This commit is contained in:
Tulir Asokan
2025-12-06 14:52:39 +02:00
parent 14b3b1fed7
commit d5f87d2ec1
26 changed files with 797 additions and 390 deletions
+7
View File
@@ -20,6 +20,7 @@ import (
"context"
"go.mau.fi/util/dbutil"
"go.mau.fi/util/exsync"
"go.mau.fi/mautrix-telegram/pkg/connector/store/upgrades"
)
@@ -28,6 +29,9 @@ type Container struct {
*dbutil.Database
TelegramFile *TelegramFileQuery
Username *UsernameQuery
PhoneNumber *PhoneNumberQuery
Topic *TopicQuery
}
func NewStore(db *dbutil.Database, log dbutil.DatabaseLogger) *Container {
@@ -35,6 +39,9 @@ func NewStore(db *dbutil.Database, log dbutil.DatabaseLogger) *Container {
Database: db.Child("telegram_version", upgrades.Table, log),
TelegramFile: &TelegramFileQuery{dbutil.MakeQueryHelper(db, newTelegramFile)},
Username: &UsernameQuery{db},
PhoneNumber: &PhoneNumberQuery{db},
Topic: &TopicQuery{db: db, existingTopics: exsync.NewSet[topicKey]()},
}
}
+56
View File
@@ -0,0 +1,56 @@
// mautrix-telegram - A Matrix-Telegram puppeting bridge.
// Copyright (C) 2025 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 store
import (
"context"
"database/sql"
"errors"
"go.mau.fi/util/dbutil"
)
type PhoneNumberQuery struct {
db *dbutil.Database
}
const (
getEntityIDForPhoneNumber = "SELECT entity_id FROM telegram_phone_number WHERE phone_number=$1"
setPhoneNumberQuery = `
INSERT INTO telegram_phone_number (phone_number, entity_id)
VALUES ($1, $2)
ON CONFLICT (phone_number) DO UPDATE SET entity_id=excluded.entity_id
`
clearPhoneNumberQuery = "DELETE FROM telegram_phone_number WHERE entity_id=$1"
)
func (s *PhoneNumberQuery) GetUserID(ctx context.Context, phoneNumber string) (userID int64, err error) {
err = s.db.QueryRow(ctx, getEntityIDForPhoneNumber, phoneNumber).Scan(&userID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
return
}
func (s *PhoneNumberQuery) Set(ctx context.Context, userID int64, phoneNumber string) (err error) {
if phoneNumber == "" {
_, err = s.db.Exec(ctx, clearPhoneNumberQuery, userID)
} else {
_, err = s.db.Exec(ctx, setPhoneNumberQuery, phoneNumber, userID)
}
return
}
-64
View File
@@ -21,7 +21,6 @@ import (
"database/sql"
"errors"
"fmt"
"strings"
"go.mau.fi/util/dbutil"
@@ -71,27 +70,6 @@ const (
ON CONFLICT (user_id, entity_type, entity_id) DO UPDATE SET access_hash=excluded.access_hash
`
deleteAccessHashesForUserQuery = "DELETE FROM telegram_access_hash WHERE user_id=$1"
// User Username Queries
getUsernameQuery = "SELECT username FROM telegram_username WHERE entity_type=$1 AND entity_id=$2"
setUsernameQuery = `
INSERT INTO telegram_username (username, entity_type, entity_id)
VALUES ($1, $2, $3)
ON CONFLICT (username) DO UPDATE SET
entity_type=excluded.entity_type,
entity_id=excluded.entity_id
`
getByUsernameQuery = "SELECT entity_type, entity_id FROM telegram_username WHERE LOWER(username)=$1"
clearUsernameQuery = `DELETE FROM telegram_username WHERE entity_type=$1 AND entity_id=$2`
// User Phone Number Queries
getEntityIDForPhoneNumber = "SELECT entity_id FROM telegram_phone_number WHERE phone_number=$1"
setPhoneNumberQuery = `
INSERT INTO telegram_phone_number (phone_number, entity_id)
VALUES ($1, $2)
ON CONFLICT (phone_number) DO UPDATE SET entity_id=excluded.entity_id
`
clearPhoneNumberQuery = "DELETE FROM telegram_phone_number WHERE entity_id=$1"
)
var _ updates.StateStorage = (*ScopedStore)(nil)
@@ -242,48 +220,6 @@ func (s *ScopedStore) DeleteAccessHashesForUser(ctx context.Context) (err error)
return
}
func (s *ScopedStore) GetUsername(ctx context.Context, entityType ids.PeerType, userID int64) (username string, err error) {
err = s.db.QueryRow(ctx, getUsernameQuery, entityType, userID).Scan(&username)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
return
}
func (s *ScopedStore) SetUsername(ctx context.Context, entityType ids.PeerType, entityID int64, username string) (err error) {
if username == "" {
_, err = s.db.Exec(ctx, clearUsernameQuery, entityType, entityID)
} else {
_, err = s.db.Exec(ctx, setUsernameQuery, username, entityType, entityID)
}
return
}
func (s *ScopedStore) GetEntityIDByUsername(ctx context.Context, username string) (entityType ids.PeerType, entityID int64, err error) {
err = s.db.QueryRow(ctx, getByUsernameQuery, strings.ToLower(username)).Scan(&entityType, &entityID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
return
}
func (s *ScopedStore) GetUserIDByPhoneNumber(ctx context.Context, phoneNumber string) (userID int64, err error) {
err = s.db.QueryRow(ctx, getEntityIDForPhoneNumber, phoneNumber).Scan(&userID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
return
}
func (s *ScopedStore) SetPhoneNumber(ctx context.Context, userID int64, phoneNumber string) (err error) {
if phoneNumber == "" {
_, err = s.db.Exec(ctx, clearPhoneNumberQuery, userID)
} else {
_, err = s.db.Exec(ctx, setPhoneNumberQuery, phoneNumber, userID)
}
return
}
// Helper Functions
func (s *ScopedStore) assertUserIDMatches(userID int64) {
+62
View File
@@ -0,0 +1,62 @@
// mautrix-telegram - A Matrix-Telegram puppeting bridge.
// Copyright (C) 2025 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 store
import (
"context"
"go.mau.fi/util/dbutil"
"go.mau.fi/util/exsync"
)
type topicKey struct {
ChannelID int64
TopicID int
}
type TopicQuery struct {
db *dbutil.Database
existingTopics *exsync.Set[topicKey]
}
const (
getAllTopicsQuery = `SELECT topic_id FROM telegram_topic WHERE channel_id=$1`
addTopicQuery = `INSERT INTO telegram_topic (channel_id, topic_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`
deleteTopicQuery = `DELETE FROM telegram_topic WHERE channel_id=$1 AND topic_id=$2`
)
func (s *TopicQuery) Add(ctx context.Context, channelID int64, topicID int) (err error) {
if channelID == 0 {
return nil
}
if s.existingTopics.Add(topicKey{ChannelID: channelID, TopicID: topicID}) {
_, err = s.db.Exec(ctx, addTopicQuery, channelID, topicID)
}
return
}
func (s *TopicQuery) Delete(ctx context.Context, channelID int64, topicID int) (err error) {
s.existingTopics.Remove(topicKey{ChannelID: channelID, TopicID: topicID})
_, err = s.db.Exec(ctx, deleteTopicQuery, channelID, topicID)
return
}
var intScanner = dbutil.ConvertRowFn[int](dbutil.ScanSingleColumn[int])
func (s *TopicQuery) GetAll(ctx context.Context, channelID int64) (topics []int, err error) {
return intScanner.NewRowIter(s.db.Query(ctx, getAllTopicsQuery, channelID)).AsList()
}
+8 -1
View File
@@ -1,4 +1,4 @@
-- v0 -> v5 (compatible with v2+): Latest revision
-- v0 -> v6 (compatible with v2+): Latest revision
CREATE TABLE telegram_user_state (
user_id BIGINT NOT NULL PRIMARY KEY,
@@ -51,3 +51,10 @@ CREATE TABLE telegram_file (
mime_type TEXT,
size BIGINT
);
CREATE TABLE telegram_topic (
channel_id BIGINT NOT NULL,
topic_id BIGINT NOT NULL,
PRIMARY KEY (channel_id, topic_id)
);
@@ -0,0 +1,8 @@
-- v6 (compatible with v2+): Add table for topics
CREATE TABLE telegram_topic (
channel_id BIGINT NOT NULL,
topic_id BIGINT NOT NULL,
PRIMARY KEY (channel_id, topic_id)
);
+70
View File
@@ -0,0 +1,70 @@
// mautrix-telegram - A Matrix-Telegram puppeting bridge.
// Copyright (C) 2025 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 store
import (
"context"
"database/sql"
"errors"
"strings"
"go.mau.fi/util/dbutil"
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
)
type UsernameQuery struct {
db *dbutil.Database
}
const (
getUsernameQuery = "SELECT username FROM telegram_username WHERE entity_type=$1 AND entity_id=$2"
setUsernameQuery = `
INSERT INTO telegram_username (username, entity_type, entity_id)
VALUES ($1, $2, $3)
ON CONFLICT (username) DO UPDATE SET
entity_type=excluded.entity_type,
entity_id=excluded.entity_id
`
getByUsernameQuery = "SELECT entity_type, entity_id FROM telegram_username WHERE LOWER(username)=$1"
clearUsernameQuery = `DELETE FROM telegram_username WHERE entity_type=$1 AND entity_id=$2`
)
func (s *UsernameQuery) Get(ctx context.Context, entityType ids.PeerType, userID int64) (username string, err error) {
err = s.db.QueryRow(ctx, getUsernameQuery, entityType, userID).Scan(&username)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
return
}
func (s *UsernameQuery) Set(ctx context.Context, entityType ids.PeerType, entityID int64, username string) (err error) {
if username == "" {
_, err = s.db.Exec(ctx, clearUsernameQuery, entityType, entityID)
} else {
_, err = s.db.Exec(ctx, setUsernameQuery, username, entityType, entityID)
}
return
}
func (s *UsernameQuery) GetEntityID(ctx context.Context, username string) (entityType ids.PeerType, entityID int64, err error) {
err = s.db.QueryRow(ctx, getByUsernameQuery, strings.ToLower(username)).Scan(&entityType, &entityID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
return
}