feat: complete shared database tenant sharding
This commit is contained in:
@@ -261,14 +261,8 @@ type TenantRoutedAdapter interface {
|
||||
}
|
||||
|
||||
// TenantHTTPInfrastructureAdapter is the optional durable infrastructure used
|
||||
// by product HTTP middleware and online business-event delivery.
|
||||
// by Kafka dead-letter handling.
|
||||
type TenantHTTPInfrastructureAdapter interface {
|
||||
TenantHTTPIdempotencyBegin(t.TenantID, *t.HTTPIdempotencyRecord) (*t.HTTPIdempotencyRecord, bool, error)
|
||||
TenantHTTPIdempotencyComplete(t.TenantID, *t.HTTPIdempotencyRecord) error
|
||||
TenantHTTPIdempotencyAbort(t.TenantID, *t.HTTPIdempotencyRecord) error
|
||||
TenantKafkaConsumeBegin(t.TenantID, *t.KafkaConsumeRecord, time.Time) (*t.KafkaConsumeRecord, bool, error)
|
||||
TenantKafkaConsumeSucceed(t.TenantID, string, string, time.Time) error
|
||||
TenantKafkaConsumeFail(t.TenantID, string, string, string, string, time.Time) (*t.KafkaConsumeRecord, error)
|
||||
TenantKafkaConsumeDead(t.TenantID, *t.KafkaDeadLetter, time.Time) error
|
||||
TenantKafkaDeadLetterList(t.TenantID, t.KafkaDeadLetterListOptions) (*t.KafkaDeadLetterPage, error)
|
||||
TenantKafkaDeadLetterGet(t.TenantID, string) (*t.KafkaDeadLetter, error)
|
||||
@@ -476,7 +470,7 @@ type TenantBusinessAdapter interface {
|
||||
TenantSystemNotificationList(t.TenantID, t.Uid, string, int) ([]t.SystemNotification, string, error)
|
||||
TenantSystemNotificationUnreadCount(t.TenantID, t.Uid) (int, error)
|
||||
TenantSystemNotificationUnreadCountByEventPrefix(t.TenantID, t.Uid, string) (int, error)
|
||||
TenantSystemNotificationMarkRead(t.TenantID, t.Uid, string, time.Time) error
|
||||
TenantSystemNotificationMarkRead(t.TenantID, t.Uid, string, time.Time) (*t.SystemNotification, error)
|
||||
|
||||
TenantFriendRequestCreate(t.TenantID, *t.FriendRequest) (*t.FriendRequest, error)
|
||||
TenantFriendRequestList(t.TenantID, t.Uid, string, string, string, int) ([]t.FriendRequestListItem, string, error)
|
||||
|
||||
@@ -1148,25 +1148,6 @@ func createIMSecurityTables(tx *sql.Tx) error {
|
||||
)`); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(
|
||||
`CREATE TABLE im_action_tokens(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
token_id VARCHAR(64) NOT NULL,
|
||||
action VARCHAR(64) NOT NULL,
|
||||
target VARCHAR(128) NOT NULL DEFAULT '',
|
||||
token_hash CHAR(64) NOT NULL,
|
||||
state SMALLINT NOT NULL,
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
consumed_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_action_tokens_token(token_id),
|
||||
INDEX idx_im_action_tokens_user_action(user_id, action, target, state, expires_at)
|
||||
)`); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := tx.Exec(
|
||||
`CREATE TABLE im_youth_mode_settings(
|
||||
id BIGINT NOT NULL,
|
||||
|
||||
@@ -21,33 +21,6 @@ func ensureIMAdminGovernanceTables(exec sqlExecutor) error {
|
||||
INDEX idx_im_user_login_device_time(device_id(64), login_at DESC),
|
||||
INDEX idx_im_user_login_request(request_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS im_login_failure_logs (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT,
|
||||
account_type VARCHAR(24) NOT NULL DEFAULT '', account_hash CHAR(64) NOT NULL DEFAULT '',
|
||||
account_mask VARCHAR(128) NOT NULL DEFAULT '', failure_type VARCHAR(32) NOT NULL,
|
||||
reason VARCHAR(96) NOT NULL DEFAULT '', detail VARCHAR(500) NOT NULL DEFAULT '',
|
||||
ip VARCHAR(64) NOT NULL DEFAULT '', region VARCHAR(128) NOT NULL DEFAULT '',
|
||||
device_id VARCHAR(255) NOT NULL DEFAULT '', platform VARCHAR(32) NOT NULL DEFAULT 'unknown',
|
||||
app_version VARCHAR(32) NOT NULL DEFAULT '', user_agent VARCHAR(512) NOT NULL DEFAULT '',
|
||||
risk_level VARCHAR(16) NOT NULL DEFAULT 'medium', risk_action VARCHAR(24) NOT NULL DEFAULT 'audit_only',
|
||||
request_id VARCHAR(64) NOT NULL DEFAULT '', attempt_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
metadata JSON, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id), INDEX idx_im_login_failure_account_time(account_hash, attempt_at DESC, id DESC),
|
||||
INDEX idx_im_login_failure_user_time(user_id, attempt_at DESC, id DESC),
|
||||
INDEX idx_im_login_failure_ip_time(ip, attempt_at DESC, id DESC),
|
||||
INDEX idx_im_login_failure_type_time(failure_type, attempt_at DESC, id DESC),
|
||||
INDEX idx_im_login_failure_request(request_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS im_login_lockouts (
|
||||
id BIGINT NOT NULL, user_id BIGINT,
|
||||
account_hash CHAR(64) NOT NULL,
|
||||
failed_attempts INT NOT NULL DEFAULT 0,
|
||||
window_started_at DATETIME(3), locked_until DATETIME(3), last_attempt_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id), UNIQUE INDEX uk_im_login_lockouts_account(account_hash),
|
||||
INDEX idx_im_login_lockouts_user(user_id), INDEX idx_im_login_lockouts_locked(locked_until)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS im_message_admin_actions (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT, action_id VARCHAR(96) NOT NULL,
|
||||
command_id VARCHAR(96) NOT NULL, conversation_type VARCHAR(16) NOT NULL,
|
||||
|
||||
@@ -13,15 +13,20 @@ func TestEnsureIMAdminGovernanceTablesUsesSelectedTenantDatabase(t *testing.T) {
|
||||
if err := ensureIMAdminGovernanceTables(exec); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(exec.statements) != 5 {
|
||||
t.Fatalf("created %d admin governance tables, want 5", len(exec.statements))
|
||||
if len(exec.statements) != 3 {
|
||||
t.Fatalf("created %d admin governance tables, want 3", len(exec.statements))
|
||||
}
|
||||
joined := strings.ToLower(strings.Join(exec.statements, "\n"))
|
||||
for _, table := range []string{"im_user_login_logs", "im_login_failure_logs", "im_login_lockouts", "im_message_admin_actions", "im_user_ip_whitelists"} {
|
||||
for _, table := range []string{"im_user_login_logs", "im_message_admin_actions", "im_user_ip_whitelists"} {
|
||||
if !strings.Contains(joined, table) {
|
||||
t.Fatalf("missing table %s", table)
|
||||
}
|
||||
}
|
||||
for _, table := range []string{"im_login_failure_logs", "im_login_lockouts"} {
|
||||
if strings.Contains(joined, table) {
|
||||
t.Fatalf("hot login state table must not be created: %s", table)
|
||||
}
|
||||
}
|
||||
if strings.Contains(joined, "tinode.") || strings.Contains(joined, "create database") || strings.Contains(joined, "tenant_id") {
|
||||
t.Fatalf("admin governance schema must stay inside the selected tenant database: %s", joined)
|
||||
}
|
||||
|
||||
@@ -25,24 +25,6 @@ func ensureGroupMessageShardInfrastructure(exec sqlExecutor, tenantID t.TenantID
|
||||
PRIMARY KEY(tenant_id, shard_month), UNIQUE INDEX uk_im_group_message_shards_table(tenant_id, table_name),
|
||||
INDEX idx_im_group_message_shards_status(tenant_id, status, shard_month)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS im_group_message_topic_shards (
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0, topic VARCHAR(64) NOT NULL, shard_month CHAR(6) NOT NULL,
|
||||
min_seq BIGINT NOT NULL, max_seq BIGINT NOT NULL, message_count BIGINT NOT NULL DEFAULT 0,
|
||||
min_created_at DATETIME(3) NOT NULL, max_created_at DATETIME(3) NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(tenant_id, topic, shard_month), INDEX idx_im_group_message_topic_shards_seq(tenant_id, topic, max_seq),
|
||||
INDEX idx_im_group_message_topic_shards_month(tenant_id, shard_month, topic)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS im_group_message_dedup (
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0, topic VARCHAR(64) NOT NULL, from_user BIGINT NOT NULL, client_msg_id VARCHAR(64) NOT NULL,
|
||||
seqid BIGINT NOT NULL, shard_month CHAR(6) NOT NULL, message_id BIGINT,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'pending', expires_at DATETIME(3) NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(tenant_id, topic, from_user, client_msg_id), INDEX idx_im_group_message_dedup_expires(expires_at),
|
||||
INDEX idx_im_group_message_dedup_topic_seq(tenant_id, topic, seqid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
}
|
||||
for _, statement := range statements {
|
||||
if _, err := exec.Exec(statement); err != nil {
|
||||
@@ -129,22 +111,13 @@ func groupMessageShardTable(tenantID t.TenantID, month string) (string, bool) {
|
||||
|
||||
func groupMessageTablesForTopic(q sqlx.QueryerContext, ctx context.Context, tenantID t.TenantID, topic string) ([]string, error) {
|
||||
routes, cached, err := store.GetGroupMessageShardRoutes(tenantID, topic)
|
||||
redisAvailable := err == nil
|
||||
if err != nil {
|
||||
cached = false
|
||||
}
|
||||
if !cached {
|
||||
routes = nil
|
||||
if err = sqlx.SelectContext(ctx, q, &routes,
|
||||
`SELECT shard_month AS month,min_seq,max_seq,message_count AS count
|
||||
FROM im_group_message_topic_shards WHERE tenant_id=? AND topic=? ORDER BY shard_month DESC`, tenantID, topic); err != nil {
|
||||
if routes, err = groupMessageAllShardRoutes(q, ctx, tenantID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if redisAvailable {
|
||||
for _, route := range routes {
|
||||
_ = store.PutGroupMessageShardRoute(tenantID, topic, route)
|
||||
}
|
||||
}
|
||||
}
|
||||
tables := make([]string, 0, len(routes)+1)
|
||||
for _, route := range routes {
|
||||
@@ -157,23 +130,13 @@ func groupMessageTablesForTopic(q sqlx.QueryerContext, ctx context.Context, tena
|
||||
|
||||
func groupMessageTablesForSeq(q sqlx.QueryerContext, ctx context.Context, tenantID t.TenantID, topic string, seq int64) ([]string, error) {
|
||||
routes, cached, err := store.GetGroupMessageShardRoutes(tenantID, topic)
|
||||
redisAvailable := err == nil
|
||||
if err != nil {
|
||||
cached = false
|
||||
}
|
||||
if !cached {
|
||||
routes = nil
|
||||
if err = sqlx.SelectContext(ctx, q, &routes,
|
||||
`SELECT shard_month AS month,min_seq,max_seq,message_count AS count
|
||||
FROM im_group_message_topic_shards WHERE tenant_id=? AND topic=? AND min_seq<=? AND max_seq>=?
|
||||
ORDER BY shard_month DESC`, tenantID, topic, seq, seq); err != nil {
|
||||
if routes, err = groupMessageAllShardRoutes(q, ctx, tenantID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if redisAvailable {
|
||||
for _, route := range routes {
|
||||
_ = store.PutGroupMessageShardRoute(tenantID, topic, route)
|
||||
}
|
||||
}
|
||||
}
|
||||
tables := make([]string, 0, 2)
|
||||
for _, route := range routes {
|
||||
@@ -186,6 +149,22 @@ func groupMessageTablesForSeq(q sqlx.QueryerContext, ctx context.Context, tenant
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func groupMessageAllShardRoutes(q sqlx.QueryerContext, ctx context.Context, tenantID t.TenantID) ([]store.GroupMessageShardRoute, error) {
|
||||
var rows []struct {
|
||||
Month string `db:"month"`
|
||||
}
|
||||
if err := sqlx.SelectContext(ctx, q, &rows, `SELECT shard_month AS month FROM im_group_message_shards WHERE tenant_id=? ORDER BY shard_month DESC`, tenantID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
routes := make([]store.GroupMessageShardRoute, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if validShardMonth(row.Month) {
|
||||
routes = append(routes, store.GroupMessageShardRoute{Month: row.Month, MinSeq: 1, MaxSeq: 1<<62 - 1, Count: 0})
|
||||
}
|
||||
}
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
func groupMessageMonthFromTable(table string) string {
|
||||
const suffixLength = 6
|
||||
if len(table) > suffixLength && validShardMonth(table[len(table)-suffixLength:]) {
|
||||
|
||||
@@ -149,15 +149,6 @@ func ensureIMGroupBusinessTables(exec sqlExecutor) error {
|
||||
INDEX idx_im_group_admin_logs_topic_time(topic, id DESC),
|
||||
INDEX idx_im_group_admin_logs_operator(operator_id, id DESC), INDEX idx_im_group_admin_logs_object(object_type, object_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS im_group_idempotency (
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0, id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, method VARCHAR(8) NOT NULL,
|
||||
route VARCHAR(256) NOT NULL, idempotency_key VARCHAR(128) NOT NULL, body_hash CHAR(64) NOT NULL,
|
||||
result_code INT NOT NULL DEFAULT 0, result_body JSON,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id), UNIQUE INDEX uk_im_group_idempotency_request(user_id, method, route, idempotency_key),
|
||||
INDEX idx_im_group_idempotency_created(created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`UPDATE im_topics SET public=JSON_SET(public, '$.fn', JSON_UNQUOTE(JSON_EXTRACT(public, '$.name')))
|
||||
WHERE name LIKE 'grp%' AND JSON_VALID(public)
|
||||
AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(public, '$.fn')), '') = ''
|
||||
|
||||
@@ -24,8 +24,8 @@ func TestEnsureIMGroupBusinessTablesUsesSelectedTenantDatabase(t *testing.T) {
|
||||
if err := ensureIMGroupBusinessTables(exec); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(exec.statements) != 25 {
|
||||
t.Fatalf("executed %d group schema statements, want 25", len(exec.statements))
|
||||
if len(exec.statements) != 22 {
|
||||
t.Fatalf("executed %d group schema statements, want 22", len(exec.statements))
|
||||
}
|
||||
for _, statement := range exec.statements {
|
||||
if strings.Contains(strings.ToLower(statement), "tinode.") || strings.Contains(strings.ToLower(statement), "create database") {
|
||||
@@ -36,6 +36,9 @@ func TestEnsureIMGroupBusinessTablesUsesSelectedTenantDatabase(t *testing.T) {
|
||||
if strings.Contains(joined, "im_group_events_outbox") {
|
||||
t.Fatal("group event outbox table must not be created")
|
||||
}
|
||||
if strings.Contains(joined, "im_group_idempotency") {
|
||||
t.Fatal("group idempotency table must not be created")
|
||||
}
|
||||
for _, expected := range []string{
|
||||
"primary key(id), unique index uk_im_group_settings_topic(topic)",
|
||||
"primary key(id), unique index uk_im_group_announcement_reads_announcement_user(announcement_id, user_id)",
|
||||
@@ -48,11 +51,17 @@ func TestEnsureIMGroupBusinessTablesUsesSelectedTenantDatabase(t *testing.T) {
|
||||
if !strings.Contains(joined, "json_set(public, '$.fn'") {
|
||||
t.Fatal("group schema is missing historical public.fn backfill")
|
||||
}
|
||||
for _, expected := range []string{"im_group_message_shards", "im_group_message_topic_shards", "im_group_message_dedup", "primary key(message_id)"} {
|
||||
for _, expected := range []string{"im_group_message_shards", "primary key(message_id)"} {
|
||||
if !strings.Contains(joined, expected) {
|
||||
t.Fatalf("group shard schema is missing %q", expected)
|
||||
}
|
||||
}
|
||||
if strings.Contains(joined, "im_group_message_topic_shards") {
|
||||
t.Fatal("group message topic shard routes must not be created in MySQL")
|
||||
}
|
||||
if strings.Contains(joined, "im_group_message_dedup") {
|
||||
t.Fatal("group message dedup table must not be created")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupMessageShardTableRejectsUnsafeMonth(t *testing.T) {
|
||||
@@ -71,11 +80,15 @@ func TestTenantPartitionTableNames(t *testing.T) {
|
||||
tenantID types.TenantID
|
||||
friends string
|
||||
messages string
|
||||
topics string
|
||||
members string
|
||||
subs string
|
||||
notices string
|
||||
group string
|
||||
}{
|
||||
{tenantID: 1, friends: "im_001_friendships", messages: "im_001_messages", group: "im_001_group_messages_202609"},
|
||||
{tenantID: 10, friends: "im_010_friendships", messages: "im_010_messages", group: "im_010_group_messages_202609"},
|
||||
{tenantID: 1000, friends: "im_1000_friendships", messages: "im_1000_messages", group: "im_1000_group_messages_202609"},
|
||||
{tenantID: 1, friends: "im_001_friendships", messages: "im_001_messages", topics: "im_001_topics", members: "im_001_group_members", subs: "im_001_subscriptions", notices: "im_001_system_notifications", group: "im_001_group_messages_202609"},
|
||||
{tenantID: 10, friends: "im_010_friendships", messages: "im_010_messages", topics: "im_010_topics", members: "im_010_group_members", subs: "im_010_subscriptions", notices: "im_010_system_notifications", group: "im_010_group_messages_202609"},
|
||||
{tenantID: 1000, friends: "im_1000_friendships", messages: "im_1000_messages", topics: "im_1000_topics", members: "im_1000_group_members", subs: "im_1000_subscriptions", notices: "im_1000_system_notifications", group: "im_1000_group_messages_202609"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := tenantFriendshipsTable(tc.tenantID); got != tc.friends {
|
||||
@@ -84,8 +97,34 @@ func TestTenantPartitionTableNames(t *testing.T) {
|
||||
if got := tenantMessagesTable(tc.tenantID); got != tc.messages {
|
||||
t.Fatalf("tenant %d message table: got %q, want %q", tc.tenantID, got, tc.messages)
|
||||
}
|
||||
if got := tenantTopicsTable(tc.tenantID); got != tc.topics {
|
||||
t.Fatalf("tenant %d topics table: got %q, want %q", tc.tenantID, got, tc.topics)
|
||||
}
|
||||
if got := tenantGroupMembersTable(tc.tenantID); got != tc.members {
|
||||
t.Fatalf("tenant %d group members table: got %q, want %q", tc.tenantID, got, tc.members)
|
||||
}
|
||||
if got := tenantSubscriptionsTable(tc.tenantID); got != tc.subs {
|
||||
t.Fatalf("tenant %d subscriptions table: got %q, want %q", tc.tenantID, got, tc.subs)
|
||||
}
|
||||
if got := tenantSystemNotificationsTable(tc.tenantID); got != tc.notices {
|
||||
t.Fatalf("tenant %d system notifications table: got %q, want %q", tc.tenantID, got, tc.notices)
|
||||
}
|
||||
if got, ok := tenantGroupMessagesTable(tc.tenantID, "202609"); !ok || got != tc.group {
|
||||
t.Fatalf("tenant %d group message table: got %q, ok=%v, want %q", tc.tenantID, got, ok, tc.group)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTenantShardSQL(t *testing.T) {
|
||||
query := tenantShardSQL(7, "SELECT * FROM im_topics t JOIN im_group_members gm ON gm.topic=t.name JOIN im_subscriptions s ON s.topic=gm.topic; INSERT INTO im_system_notifications(event_id) VALUES(?)")
|
||||
for _, expected := range []string{"im_007_topics", "im_007_group_members", "im_007_subscriptions", "im_007_system_notifications"} {
|
||||
if !strings.Contains(query, expected) {
|
||||
t.Fatalf("tenant shard query is missing %q: %s", expected, query)
|
||||
}
|
||||
}
|
||||
for _, legacy := range []string{" im_topics", " im_group_members", " im_subscriptions", " im_system_notifications"} {
|
||||
if strings.Contains(query, legacy) {
|
||||
t.Fatalf("tenant shard query still references %q: %s", legacy, query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,33 +5,6 @@ package mysql
|
||||
|
||||
func ensureIMHTTPInfrastructureTables(exec sqlExecutor) error {
|
||||
statements := []string{
|
||||
`CREATE TABLE IF NOT EXISTS im_http_idempotency (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT, tenant_id BIGINT NOT NULL DEFAULT 0, user_id BIGINT NOT NULL,
|
||||
method VARCHAR(8) NOT NULL, route VARCHAR(512) NOT NULL,
|
||||
idempotency_key VARCHAR(128) NOT NULL, body_hash CHAR(64) NOT NULL,
|
||||
state VARCHAR(16) NOT NULL DEFAULT 'processing', response_status INT NOT NULL DEFAULT 0,
|
||||
response_body MEDIUMBLOB, expires_at DATETIME(3) NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_http_idempotency_scope(tenant_id, user_id, method, route, idempotency_key),
|
||||
INDEX idx_im_http_idempotency_expiry(expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS im_kafka_consume_log (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT, event_id VARCHAR(96) NOT NULL,
|
||||
consumer_group VARCHAR(128) NOT NULL, source_topic VARCHAR(128) NOT NULL,
|
||||
event_type VARCHAR(96) NOT NULL DEFAULT '', biz_type VARCHAR(64) NOT NULL DEFAULT '',
|
||||
biz_id VARCHAR(128) NOT NULL DEFAULT '', status VARCHAR(16) NOT NULL DEFAULT 'processing',
|
||||
retry_count INT NOT NULL DEFAULT 0, last_reason VARCHAR(64) NOT NULL DEFAULT '',
|
||||
last_error VARCHAR(1000) NOT NULL DEFAULT '', processing_deadline DATETIME(3),
|
||||
succeeded_at DATETIME(3), dead_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id), UNIQUE INDEX uk_im_kafka_consume_log_event_group(event_id, consumer_group),
|
||||
INDEX idx_im_kafka_consume_log_status(status, processing_deadline),
|
||||
INDEX idx_im_kafka_consume_log_biz(biz_type, biz_id),
|
||||
INDEX idx_im_kafka_consume_log_topic(source_topic, id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS im_kafka_dead_letters (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT, dead_id VARCHAR(96) NOT NULL,
|
||||
event_id VARCHAR(96) NOT NULL, parent_event_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
@@ -82,15 +55,5 @@ func ensureIMHTTPInfrastructureTables(exec sqlExecutor) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
migrations := []ignoreSQLError{
|
||||
{`ALTER TABLE im_http_idempotency ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 0 AFTER id`, 1060},
|
||||
{`ALTER TABLE im_http_idempotency DROP INDEX uk_im_http_idempotency_scope`, 1091},
|
||||
{`ALTER TABLE im_http_idempotency ADD UNIQUE INDEX uk_im_http_idempotency_scope(tenant_id, user_id, method, route, idempotency_key)`, 1061},
|
||||
}
|
||||
for _, migration := range migrations {
|
||||
if err := execIgnoreMySQLError(exec, migration.sql, migration.code); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,13 +12,11 @@ func TestEnsureIMHTTPInfrastructureTablesUsesSelectedTenantDatabase(t *testing.T
|
||||
if err := ensureIMHTTPInfrastructureTables(exec); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(exec.statements) != 9 {
|
||||
t.Fatalf("executed %d infrastructure schema statements, want 9", len(exec.statements))
|
||||
if len(exec.statements) != 4 {
|
||||
t.Fatalf("executed %d infrastructure schema statements, want 4", len(exec.statements))
|
||||
}
|
||||
joined := strings.ToLower(strings.Join(exec.statements, "\n"))
|
||||
for _, table := range []string{
|
||||
"im_http_idempotency",
|
||||
"im_kafka_consume_log",
|
||||
"im_kafka_dead_letters",
|
||||
"im_realname_appeals",
|
||||
"im_conversation_settings",
|
||||
@@ -31,12 +29,15 @@ func TestEnsureIMHTTPInfrastructureTablesUsesSelectedTenantDatabase(t *testing.T
|
||||
if strings.Contains(joined, "tinode.") || strings.Contains(joined, "create database") {
|
||||
t.Fatalf("infrastructure schema must stay inside the selected tenant database: %s", joined)
|
||||
}
|
||||
if !strings.Contains(joined, "tenant_id") || !strings.Contains(joined, "uk_im_http_idempotency_scope(tenant_id, user_id") {
|
||||
t.Fatalf("HTTP idempotency schema must be tenant scoped in shared DB mode: %s", joined)
|
||||
if strings.Contains(joined, "im_http_idempotency") {
|
||||
t.Fatalf("HTTP idempotency must not create MySQL tables: %s", joined)
|
||||
}
|
||||
if strings.Contains(joined, "im_kafka_outbox") {
|
||||
t.Fatalf("kafka outbox table must not be created: %s", joined)
|
||||
}
|
||||
if strings.Contains(joined, "im_kafka_consume_log") {
|
||||
t.Fatalf("kafka consume state must not create MySQL tables: %s", joined)
|
||||
}
|
||||
if strings.Contains(joined, "im_business_events_outbox") {
|
||||
t.Fatalf("business event outbox table must not be created: %s", joined)
|
||||
}
|
||||
|
||||
@@ -4,203 +4,45 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
t "github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
func (a *adapter) routedLoginLockoutAdapter(id t.TenantID) (*adapter, error) {
|
||||
if err := validTenant(id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if a.tenantScopedDB {
|
||||
return a, nil
|
||||
}
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp, nil
|
||||
}
|
||||
|
||||
type loginLockoutRow struct {
|
||||
ID int64 `db:"id"`
|
||||
UserID sql.NullInt64 `db:"user_id"`
|
||||
AccountHash string `db:"account_hash"`
|
||||
FailedAttempts int `db:"failed_attempts"`
|
||||
WindowStartAt sql.NullTime `db:"window_started_at"`
|
||||
LockedUntil sql.NullTime `db:"locked_until"`
|
||||
LastAttemptAt sql.NullTime `db:"last_attempt_at"`
|
||||
UpdatedAt sql.NullTime `db:"updated_at"`
|
||||
}
|
||||
|
||||
func loginLockoutFromRow(id t.TenantID, row loginLockoutRow) *t.LoginLockout {
|
||||
out := &t.LoginLockout{
|
||||
TenantID: id,
|
||||
ID: row.ID,
|
||||
AccountHash: row.AccountHash,
|
||||
FailedAttempts: row.FailedAttempts,
|
||||
}
|
||||
if row.UserID.Valid {
|
||||
out.UserID = row.UserID.Int64
|
||||
}
|
||||
if row.WindowStartAt.Valid {
|
||||
v := row.WindowStartAt.Time
|
||||
out.WindowStartAt = &v
|
||||
}
|
||||
if row.LockedUntil.Valid {
|
||||
v := row.LockedUntil.Time
|
||||
out.LockedUntil = &v
|
||||
}
|
||||
if row.LastAttemptAt.Valid {
|
||||
v := row.LastAttemptAt.Time
|
||||
out.LastAttemptAt = &v
|
||||
}
|
||||
if row.UpdatedAt.Valid {
|
||||
v := row.UpdatedAt.Time
|
||||
out.UpdatedAt = &v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const loginLockoutColumns = `id,user_id,account_hash,failed_attempts,window_started_at,locked_until,last_attempt_at,updated_at`
|
||||
|
||||
// TenantLoginLockoutGet reads the lockout counter for the given account hash.
|
||||
// Login lockout counters are runtime security state and are stored in Redis by
|
||||
// the store layer. The MySQL adapter keeps these methods only to satisfy older
|
||||
// adapter interfaces while preventing accidental writes to im_login_lockouts.
|
||||
func (a *adapter) TenantLoginLockoutGet(id t.TenantID, accountHash string) (*t.LoginLockout, error) {
|
||||
tenantAdp, err := a.routedLoginLockoutAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantLoginLockoutGet(id, accountHash)
|
||||
}
|
||||
if accountHash == "" {
|
||||
if err := validTenant(id); err != nil || accountHash == "" {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
var row loginLockoutRow
|
||||
err = a.db.GetContext(ctx, &row,
|
||||
`SELECT `+loginLockoutColumns+` FROM im_login_lockouts WHERE account_hash=?`, accountHash)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return loginLockoutFromRow(id, row), nil
|
||||
return nil, t.ErrUnsupported
|
||||
}
|
||||
|
||||
// TenantLoginLockoutRecordFailure atomically inserts or increments the failure
|
||||
// counter. windowCutoff is "now minus window seconds": an older window start
|
||||
// resets the counter to 1. The updated row is returned to the caller.
|
||||
func (a *adapter) TenantLoginLockoutRecordFailure(id t.TenantID, accountHash string, userID *int64, windowCutoff, now time.Time) (*t.LoginLockout, error) {
|
||||
tenantAdp, err := a.routedLoginLockoutAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantLoginLockoutRecordFailure(id, accountHash, userID, windowCutoff, now)
|
||||
}
|
||||
if accountHash == "" || now.IsZero() {
|
||||
if err := validTenant(id); err != nil || accountHash == "" || now.IsZero() {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
rowID, err := nextSnowflakeID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// MySQL evaluates ON DUPLICATE KEY UPDATE assignments left to right, so
|
||||
// failed_attempts is recomputed before window_started_at is overwritten.
|
||||
_, err = a.db.ExecContext(ctx, `INSERT INTO im_login_lockouts
|
||||
(id,user_id,account_hash,failed_attempts,window_started_at,last_attempt_at)
|
||||
VALUES(?,?,?,1,?,?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
failed_attempts = IF(window_started_at IS NULL OR window_started_at < ?, 1, failed_attempts + 1),
|
||||
window_started_at = IF(window_started_at IS NULL OR window_started_at < ?, ?, window_started_at),
|
||||
last_attempt_at = ?,
|
||||
user_id = COALESCE(user_id, ?)`,
|
||||
rowID, userID, accountHash, now, now,
|
||||
windowCutoff, windowCutoff, now, now, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var row loginLockoutRow
|
||||
err = a.db.GetContext(ctx, &row,
|
||||
`SELECT `+loginLockoutColumns+` FROM im_login_lockouts WHERE account_hash=?`, accountHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return loginLockoutFromRow(id, row), nil
|
||||
return nil, t.ErrUnsupported
|
||||
}
|
||||
|
||||
// TenantLoginLockoutLock sets locked_until unless a newer lock already covers now.
|
||||
func (a *adapter) TenantLoginLockoutLock(id t.TenantID, accountHash string, lockedUntil, now time.Time) error {
|
||||
tenantAdp, err := a.routedLoginLockoutAdapter(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantLoginLockoutLock(id, accountHash, lockedUntil, now)
|
||||
}
|
||||
if accountHash == "" || lockedUntil.IsZero() || now.IsZero() {
|
||||
if err := validTenant(id); err != nil || accountHash == "" || lockedUntil.IsZero() || now.IsZero() {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
_, err = a.db.ExecContext(ctx,
|
||||
`UPDATE im_login_lockouts SET locked_until=?, updated_at=? WHERE account_hash=? AND (locked_until IS NULL OR locked_until < ?)`,
|
||||
lockedUntil, now, accountHash, now)
|
||||
return err
|
||||
return t.ErrUnsupported
|
||||
}
|
||||
|
||||
// TenantLoginLockoutReset removes the counter after a successful login.
|
||||
func (a *adapter) TenantLoginLockoutReset(id t.TenantID, accountHash string) error {
|
||||
tenantAdp, err := a.routedLoginLockoutAdapter(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantLoginLockoutReset(id, accountHash)
|
||||
}
|
||||
if accountHash == "" {
|
||||
if err := validTenant(id); err != nil || accountHash == "" {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
_, err = a.db.ExecContext(ctx, `DELETE FROM im_login_lockouts WHERE account_hash=?`, accountHash)
|
||||
return err
|
||||
return t.ErrUnsupported
|
||||
}
|
||||
|
||||
// TenantLoginLockoutRelease clears the lock and zeroes the counter (admin unlock).
|
||||
func (a *adapter) TenantLoginLockoutRelease(id t.TenantID, accountHash string, now time.Time) error {
|
||||
tenantAdp, err := a.routedLoginLockoutAdapter(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantLoginLockoutRelease(id, accountHash, now)
|
||||
}
|
||||
if accountHash == "" || now.IsZero() {
|
||||
if err := validTenant(id); err != nil || accountHash == "" || now.IsZero() {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
_, err = a.db.ExecContext(ctx,
|
||||
`UPDATE im_login_lockouts SET failed_attempts=0, window_started_at=NULL, locked_until=NULL, updated_at=? WHERE account_hash=?`,
|
||||
now, accountHash)
|
||||
return err
|
||||
return t.ErrUnsupported
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#
|
||||
# - Shared tenant business database: meeting_im
|
||||
# - im_users, im_topics, im_subscriptions, im_messages and extension tables
|
||||
# - im_http_idempotency for HTTP write idempotency
|
||||
# - Redis short-state keys for HTTP write idempotency
|
||||
# - im_file_review_tasks for asynchronous content safety and malware checks
|
||||
# - business tables store tenant_id; high-volume tables are physically partitioned by tenant
|
||||
#
|
||||
|
||||
+119
-210
@@ -322,11 +322,11 @@ func messageHeadBool(head t.KVMap, key string) bool {
|
||||
|
||||
func (a *adapter) tenantTopicBumpMutationVersionTx(ctx context.Context, tx *sqlx.Tx, id t.TenantID, topic string, at time.Time) error {
|
||||
if a.tenantScopedDB {
|
||||
_, err := tx.ExecContext(ctx, "UPDATE im_topics SET mutation_version=mutation_version+1,updatedat=?,touchedat=? WHERE name=?",
|
||||
_, err := tx.ExecContext(ctx, tenantShardSQL(id, "UPDATE im_topics SET mutation_version=mutation_version+1,updatedat=?,touchedat=? WHERE name=?"),
|
||||
at, at, topic)
|
||||
return err
|
||||
}
|
||||
_, err := tx.ExecContext(ctx, "UPDATE im_topics SET mutation_version=mutation_version+1,updatedat=?,touchedat=? WHERE tenant_id=? AND name=?",
|
||||
_, err := tx.ExecContext(ctx, tenantShardSQL(id, "UPDATE im_topics SET mutation_version=mutation_version+1,updatedat=?,touchedat=? WHERE tenant_id=? AND name=?"),
|
||||
at, at, id, topic)
|
||||
return err
|
||||
}
|
||||
@@ -634,7 +634,7 @@ func (a *adapter) TenantUserDelete(id t.TenantID, uid t.Uid, hard bool) error {
|
||||
}()
|
||||
|
||||
var ownTopics []string
|
||||
q := "SELECT name FROM im_topics WHERE tenant_id=? AND owner=?"
|
||||
q := tenantShardSQL(id, "SELECT name FROM im_topics WHERE tenant_id=? AND owner=?")
|
||||
args := []any{id, decoded}
|
||||
if !hard {
|
||||
q += " AND state!=?"
|
||||
@@ -663,20 +663,20 @@ func (a *adapter) TenantUserDelete(id t.TenantID, uid t.Uid, hard bool) error {
|
||||
return err
|
||||
}
|
||||
if len(ownTopics) > 0 {
|
||||
if _, err = tx.Exec("DELETE dl FROM im_dellog AS dl JOIN im_topics AS tp ON tp.tenant_id=dl.tenant_id AND tp.name=dl.topic WHERE tp.tenant_id=? AND tp.owner=?", id, decoded); err != nil {
|
||||
if _, err = tx.Exec(tenantShardSQL(id, "DELETE dl FROM im_dellog AS dl JOIN im_topics AS tp ON tp.tenant_id=dl.tenant_id AND tp.name=dl.topic WHERE tp.tenant_id=? AND tp.owner=?"), id, decoded); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec(fmt.Sprintf("DELETE m FROM %s AS m JOIN im_topics AS tp ON tp.tenant_id=m.tenant_id AND tp.name=m.topic WHERE tp.tenant_id=? AND tp.owner=?", tenantMessagesTable(id)), id, decoded); err != nil {
|
||||
if _, err = tx.Exec(tenantShardSQL(id, fmt.Sprintf("DELETE m FROM %s AS m JOIN im_topics AS tp ON tp.tenant_id=m.tenant_id AND tp.name=m.topic WHERE tp.tenant_id=? AND tp.owner=?", tenantMessagesTable(id))), id, decoded); err != nil {
|
||||
return err
|
||||
}
|
||||
sqlq, params, _ := sqlx.In("DELETE FROM im_subscriptions WHERE tenant_id=? AND topic IN (?)", id, ownAll)
|
||||
sqlq, params, _ := sqlx.In(tenantShardSQL(id, "DELETE FROM im_subscriptions WHERE tenant_id=? AND topic IN (?)"), id, ownAll)
|
||||
if _, err = tx.Exec(tx.Rebind(sqlq), params...); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec("DELETE tt FROM im_topictags AS tt JOIN im_topics AS tp ON tp.tenant_id=tt.tenant_id AND tp.name=tt.topic WHERE tp.tenant_id=? AND tp.owner=?", id, decoded); err != nil {
|
||||
if _, err = tx.Exec(tenantShardSQL(id, "DELETE tt FROM im_topictags AS tt JOIN im_topics AS tp ON tp.tenant_id=tt.tenant_id AND tp.name=tt.topic WHERE tp.tenant_id=? AND tp.owner=?"), id, decoded); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec("DELETE FROM im_topics WHERE tenant_id=? AND owner=?", id, decoded); err != nil {
|
||||
if _, err = tx.Exec(tenantShardSQL(id, "DELETE FROM im_topics WHERE tenant_id=? AND owner=?"), id, decoded); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -694,23 +694,23 @@ func (a *adapter) TenantUserDelete(id t.TenantID, uid t.Uid, hard bool) error {
|
||||
return err
|
||||
}
|
||||
if len(ownAll) > 0 {
|
||||
sqlq, params, _ := sqlx.In("UPDATE im_subscriptions SET updatedat=?,deletedat=? WHERE tenant_id=? AND topic IN (?)", now, now, id, ownAll)
|
||||
sqlq, params, _ := sqlx.In(tenantShardSQL(id, "UPDATE im_subscriptions SET updatedat=?,deletedat=? WHERE tenant_id=? AND topic IN (?)"), now, now, id, ownAll)
|
||||
if _, err = tx.Exec(tx.Rebind(sqlq), params...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err = tx.Exec("UPDATE im_topics SET updatedat=?,touchedat=?,state=?,stateat=? WHERE tenant_id=? AND owner=?",
|
||||
if _, err = tx.Exec(tenantShardSQL(id, "UPDATE im_topics SET updatedat=?,touchedat=?,state=?,stateat=? WHERE tenant_id=? AND owner=?"),
|
||||
now, now, t.StateDeleted, now, id, decoded); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec("UPDATE im_topics AS tp JOIN im_subscriptions AS s ON tp.tenant_id=s.tenant_id AND tp.name=s.topic "+
|
||||
if _, err = tx.Exec(tenantShardSQL(id, "UPDATE im_topics AS tp JOIN im_subscriptions AS s ON tp.tenant_id=s.tenant_id AND tp.name=s.topic "+
|
||||
"SET tp.updatedat=?,tp.touchedat=?,tp.state=?,tp.stateat=? WHERE tp.tenant_id=? AND tp.owner=0 AND s.userid=? AND tp.name LIKE 'p2p%'",
|
||||
now, now, t.StateDeleted, now, id, decoded); err != nil {
|
||||
), now, now, t.StateDeleted, now, id, decoded); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec("UPDATE im_subscriptions AS s_one JOIN im_subscriptions AS s_two ON s_one.tenant_id=s_two.tenant_id AND s_one.topic=s_two.topic "+
|
||||
if _, err = tx.Exec(tenantShardSQL(id, "UPDATE im_subscriptions AS s_one JOIN im_subscriptions AS s_two ON s_one.tenant_id=s_two.tenant_id AND s_one.topic=s_two.topic "+
|
||||
"SET s_two.updatedat=?,s_two.deletedat=? WHERE s_one.tenant_id=? AND s_one.userid=? AND s_one.topic LIKE 'p2p%'",
|
||||
now, now, id, decoded); err != nil {
|
||||
), now, now, id, decoded); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec("UPDATE im_users SET updatedat=?,state=?,stateat=? WHERE tenant_id=? AND id=?",
|
||||
@@ -729,13 +729,13 @@ func (a *adapter) tenantTopicStateForUser(tx *sqlx.Tx, tenantID t.TenantID, deco
|
||||
if now.IsZero() {
|
||||
now = t.TimeNow()
|
||||
}
|
||||
if _, err := tx.Exec("UPDATE im_topics SET state=?,stateat=? WHERE tenant_id=? AND owner=? AND state!=?",
|
||||
if _, err := tx.Exec(tenantShardSQL(tenantID, "UPDATE im_topics SET state=?,stateat=? WHERE tenant_id=? AND owner=? AND state!=?"),
|
||||
objState, now, tenantID, decodedUID, t.StateDeleted); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := tx.Exec("UPDATE im_topics AS tp JOIN im_subscriptions AS s ON tp.tenant_id=s.tenant_id AND tp.name=s.topic "+
|
||||
_, err := tx.Exec(tenantShardSQL(tenantID, "UPDATE im_topics AS tp JOIN im_subscriptions AS s ON tp.tenant_id=s.tenant_id AND tp.name=s.topic "+
|
||||
"SET tp.state=?,tp.stateat=? WHERE tp.tenant_id=? AND tp.owner=0 AND s.userid=? AND tp.state!=?",
|
||||
objState, now, tenantID, decodedUID, t.StateDeleted)
|
||||
), objState, now, tenantID, decodedUID, t.StateDeleted)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -822,10 +822,10 @@ func (a *adapter) TenantUserUnreadCount(id t.TenantID, uids ...t.Uid) (map[t.Uid
|
||||
if len(decoded) == 0 {
|
||||
return counts, nil
|
||||
}
|
||||
q, args, _ := sqlx.In("SELECT s.userid,SUM(tp.seqid)-SUM(s.readseqid) AS unreadcount "+
|
||||
q, args, _ := sqlx.In(tenantShardSQL(id, "SELECT s.userid,SUM(tp.seqid)-SUM(s.readseqid) AS unreadcount "+
|
||||
"FROM im_topics AS tp JOIN im_subscriptions AS s ON tp.tenant_id=s.tenant_id AND tp.name=s.topic "+
|
||||
"WHERE s.tenant_id=? AND s.userid IN (?) AND s.deletedat IS NULL AND tp.state!=? "+
|
||||
"AND INSTR(s.modewant,'R')>0 AND INSTR(s.modegiven,'R')>0 GROUP BY s.userid", id, decoded, t.StateDeleted)
|
||||
"AND INSTR(s.modewant,'R')>0 AND INSTR(s.modegiven,'R')>0 GROUP BY s.userid"), id, decoded, t.StateDeleted)
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
@@ -886,9 +886,9 @@ func tenantTopicCreate(tx *sqlx.Tx, topic *t.Topic) error {
|
||||
if topic == nil || topic.TenantID.IsZero() {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
_, err := tx.Exec("INSERT INTO im_topics(tenant_id,createdat,updatedat,touchedat,state,name,usebt,owner,access,public,trusted,tags,aux) "+
|
||||
_, err := tx.Exec(tenantShardSQL(topic.TenantID, "INSERT INTO im_topics(tenant_id,createdat,updatedat,touchedat,state,name,usebt,owner,access,public,trusted,tags,aux) "+
|
||||
"VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
topic.TenantID, topic.CreatedAt, topic.UpdatedAt, topic.TouchedAt, topic.State, topic.Id, topic.UseBt,
|
||||
), topic.TenantID, topic.CreatedAt, topic.UpdatedAt, topic.TouchedAt, topic.State, topic.Id, topic.UseBt,
|
||||
decodeUID(t.ParseUid(topic.Owner)), topic.Access, common.ToJSON(topic.Public), common.ToJSON(topic.Trusted),
|
||||
topic.Tags, common.ToJSON(topic.Aux))
|
||||
if err != nil {
|
||||
@@ -913,25 +913,25 @@ func createTenantSubscription(tx *sqlx.Tx, tenantID t.TenantID, sub *t.Subscript
|
||||
DeletedAt sql.NullTime `db:"deletedat"`
|
||||
}{}
|
||||
err := tx.Get(&existing,
|
||||
"SELECT id,deletedat FROM im_subscriptions WHERE tenant_id=? AND topic=? AND userid=? ORDER BY deletedat IS NULL DESC,id ASC LIMIT 1",
|
||||
tenantShardSQL(tenantID, "SELECT id,deletedat FROM im_subscriptions WHERE tenant_id=? AND topic=? AND userid=? ORDER BY deletedat IS NULL DESC,id ASC LIMIT 1"),
|
||||
tenantID, sub.Topic, decodedUID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
if err == nil {
|
||||
if existing.DeletedAt.Valid {
|
||||
_, err = tx.Exec("UPDATE im_subscriptions SET createdat=?,updatedat=?,deletedat=NULL,modeWant=?,modeGiven=?,"+
|
||||
_, err = tx.Exec(tenantShardSQL(tenantID, "UPDATE im_subscriptions SET createdat=?,updatedat=?,deletedat=NULL,modeWant=?,modeGiven=?,"+
|
||||
"delid=0,recvseqid=0,readseqid=0,private=? WHERE tenant_id=? AND id=?",
|
||||
sub.CreatedAt, sub.UpdatedAt, sub.ModeWant.String(), sub.ModeGiven.String(), jpriv, tenantID, existing.ID)
|
||||
), sub.CreatedAt, sub.UpdatedAt, sub.ModeWant.String(), sub.ModeGiven.String(), jpriv, tenantID, existing.ID)
|
||||
} else {
|
||||
_, err = tx.Exec("UPDATE im_subscriptions SET updatedat=?,modeWant=?,modeGiven=?,private=? WHERE tenant_id=? AND id=?",
|
||||
_, err = tx.Exec(tenantShardSQL(tenantID, "UPDATE im_subscriptions SET updatedat=?,modeWant=?,modeGiven=?,private=? WHERE tenant_id=? AND id=?"),
|
||||
sub.UpdatedAt, sub.ModeWant.String(), sub.ModeGiven.String(), jpriv, tenantID, existing.ID)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isOwner {
|
||||
_, err = tx.Exec("UPDATE im_topics SET owner=? WHERE tenant_id=? AND name=?", decodedUID, tenantID, sub.Topic)
|
||||
_, err = tx.Exec(tenantShardSQL(tenantID, "UPDATE im_topics SET owner=? WHERE tenant_id=? AND name=?"), decodedUID, tenantID, sub.Topic)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -941,30 +941,30 @@ func createTenantSubscription(tx *sqlx.Tx, tenantID t.TenantID, sub *t.Subscript
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(
|
||||
"INSERT INTO im_subscriptions(id,tenant_id,createdat,updatedat,deletedat,userid,topic,modeWant,modeGiven,private) "+
|
||||
"VALUES(?,?,?,?,NULL,?,?,?,?,?)",
|
||||
tenantShardSQL(tenantID, "INSERT INTO im_subscriptions(id,tenant_id,createdat,updatedat,deletedat,userid,topic,modeWant,modeGiven,private) "+
|
||||
"VALUES(?,?,?,?,NULL,?,?,?,?,?)"),
|
||||
subID, tenantID, sub.CreatedAt, sub.UpdatedAt, decodedUID, sub.Topic, sub.ModeWant.String(), sub.ModeGiven.String(), jpriv)
|
||||
if isIDOutOfRange(err) {
|
||||
_, err = tx.Exec(
|
||||
"INSERT INTO im_subscriptions(tenant_id,createdat,updatedat,deletedat,userid,topic,modeWant,modeGiven,private) "+
|
||||
"VALUES(?,?,?,NULL,?,?,?,?,?)",
|
||||
tenantShardSQL(tenantID, "INSERT INTO im_subscriptions(tenant_id,createdat,updatedat,deletedat,userid,topic,modeWant,modeGiven,private) "+
|
||||
"VALUES(?,?,?,NULL,?,?,?,?,?)"),
|
||||
tenantID, sub.CreatedAt, sub.UpdatedAt, decodedUID, sub.Topic, sub.ModeWant.String(), sub.ModeGiven.String(), jpriv)
|
||||
}
|
||||
|
||||
if err != nil && isDupe(err) {
|
||||
if undelete {
|
||||
_, err = tx.Exec("UPDATE im_subscriptions SET createdat=?,updatedat=?,deletedat=NULL,modeWant=?,modeGiven=?,"+
|
||||
_, err = tx.Exec(tenantShardSQL(tenantID, "UPDATE im_subscriptions SET createdat=?,updatedat=?,deletedat=NULL,modeWant=?,modeGiven=?,"+
|
||||
"delid=0,recvseqid=0,readseqid=0 WHERE tenant_id=? AND topic=? AND userid=?",
|
||||
sub.CreatedAt, sub.UpdatedAt, sub.ModeWant.String(), sub.ModeGiven.String(), tenantID, sub.Topic, decodedUID)
|
||||
), sub.CreatedAt, sub.UpdatedAt, sub.ModeWant.String(), sub.ModeGiven.String(), tenantID, sub.Topic, decodedUID)
|
||||
} else {
|
||||
_, err = tx.Exec("UPDATE im_subscriptions SET createdat=?,updatedat=?,deletedat=NULL,modeWant=?,modeGiven=?,"+
|
||||
_, err = tx.Exec(tenantShardSQL(tenantID, "UPDATE im_subscriptions SET createdat=?,updatedat=?,deletedat=NULL,modeWant=?,modeGiven=?,"+
|
||||
"delid=0,recvseqid=0,readseqid=0,private=? WHERE tenant_id=? AND topic=? AND userid=?",
|
||||
sub.CreatedAt, sub.UpdatedAt, sub.ModeWant.String(), sub.ModeGiven.String(), jpriv, tenantID, sub.Topic, decodedUID)
|
||||
), sub.CreatedAt, sub.UpdatedAt, sub.ModeWant.String(), sub.ModeGiven.String(), jpriv, tenantID, sub.Topic, decodedUID)
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil && isOwner {
|
||||
_, err = tx.Exec("UPDATE im_topics SET owner=? WHERE tenant_id=? AND name=?", decodedUID, tenantID, sub.Topic)
|
||||
_, err = tx.Exec(tenantShardSQL(tenantID, "UPDATE im_topics SET owner=? WHERE tenant_id=? AND name=?"), decodedUID, tenantID, sub.Topic)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -1033,7 +1033,7 @@ func (a *adapter) TenantTopicCreateP2P(id t.TenantID, one, two *t.Subscription)
|
||||
topic.ObjHeader.MergeTimes(&one.ObjHeader)
|
||||
topic.TouchedAt = one.GetTouchedAt()
|
||||
var existingTopicID int64
|
||||
err = tx.Get(&existingTopicID, "SELECT id FROM im_topics WHERE tenant_id=? AND name=? ORDER BY id ASC LIMIT 1", id, topic.Id)
|
||||
err = tx.Get(&existingTopicID, tenantShardSQL(id, "SELECT id FROM im_topics WHERE tenant_id=? AND name=? ORDER BY id ASC LIMIT 1"), id, topic.Id)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
@@ -1041,7 +1041,7 @@ func (a *adapter) TenantTopicCreateP2P(id t.TenantID, one, two *t.Subscription)
|
||||
if err = tenantTopicCreate(tx, topic); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if _, err = tx.ExecContext(ctx, "UPDATE im_topics SET updatedat=?,touchedat=?,state=? WHERE tenant_id=? AND id=?",
|
||||
} else if _, err = tx.ExecContext(ctx, tenantShardSQL(id, "UPDATE im_topics SET updatedat=?,touchedat=?,state=? WHERE tenant_id=? AND id=?"),
|
||||
topic.UpdatedAt, topic.TouchedAt, t.StateOK, id, existingTopicID); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1067,7 +1067,7 @@ func (a *adapter) TenantTopicGet(id t.TenantID, topic string) (*t.Topic, error)
|
||||
var top t.Topic
|
||||
err = a.db.GetContext(ctx, &top,
|
||||
"SELECT tenant_id,createdat,updatedat,state,stateat,touchedat,name AS id,usebt,access,owner,seqid,delid,subcnt,IFNULL(mutation_version,0) AS mutation_version,public,trusted,tags,aux "+
|
||||
"FROM im_topics WHERE tenant_id=? AND name=?", id, topic)
|
||||
tenantShardSQL(id, "FROM im_topics WHERE tenant_id=? AND name=?"), id, topic)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -1077,13 +1077,13 @@ func (a *adapter) TenantTopicGet(id t.TenantID, topic string) (*t.Topic, error)
|
||||
if t.GetTopicCat(topic) == t.TopicCatGrp {
|
||||
var subCnt int
|
||||
if err = a.db.GetContext(ctx, &subCnt,
|
||||
"SELECT COUNT(*) FROM im_subscriptions WHERE tenant_id=? AND topic IN (?,?) AND deletedat IS NULL",
|
||||
tenantShardSQL(id, "SELECT COUNT(*) FROM im_subscriptions WHERE tenant_id=? AND topic IN (?,?) AND deletedat IS NULL"),
|
||||
id, topic, t.GrpToChn(topic)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if subCnt != top.SubCnt {
|
||||
top.SubCnt = subCnt
|
||||
if _, err = a.db.ExecContext(ctx, "UPDATE im_topics SET subcnt=? WHERE tenant_id=? AND name=?", subCnt, id, topic); err != nil {
|
||||
if _, err = a.db.ExecContext(ctx, tenantShardSQL(id, "UPDATE im_topics SET subcnt=? WHERE tenant_id=? AND name=?"), subCnt, id, topic); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -1106,6 +1106,7 @@ func (a *adapter) TenantTopicsForUser(id t.TenantID, uid t.Uid, keepDeleted bool
|
||||
}
|
||||
q := `SELECT tenant_id,createdat,updatedat,deletedat,topic,delid,recvseqid,
|
||||
readseqid,modewant,modegiven,private FROM im_subscriptions WHERE tenant_id=? AND userid=?`
|
||||
q = tenantShardSQL(id, q)
|
||||
args := []any{id, decodeUID(uid)}
|
||||
if !keepDeleted {
|
||||
q += " AND deletedat IS NULL"
|
||||
@@ -1190,7 +1191,7 @@ func (a *adapter) TenantTopicsForUser(id t.TenantID, uid t.Uid, keepDeleted bool
|
||||
return nil, err
|
||||
}
|
||||
if len(topq) > 0 {
|
||||
q = "SELECT tenant_id,updatedat,state,touchedat,name AS id,usebt,access,seqid,delid,subcnt,IFNULL(mutation_version,0) AS mutation_version,public,trusted FROM im_topics WHERE tenant_id=? AND name IN (?)"
|
||||
q = tenantShardSQL(id, "SELECT tenant_id,updatedat,state,touchedat,name AS id,usebt,access,seqid,delid,subcnt,IFNULL(mutation_version,0) AS mutation_version,public,trusted FROM im_topics WHERE tenant_id=? AND name IN (?)")
|
||||
q, args, _ = sqlx.In(q, id, topq)
|
||||
if !keepDeleted {
|
||||
q += " AND state!=?"
|
||||
@@ -1306,6 +1307,7 @@ func (a *adapter) TenantUsersForTopic(id t.TenantID, topic string, keepDeleted b
|
||||
FROM im_subscriptions AS s JOIN im_users AS u ON s.tenant_id=u.tenant_id AND s.userid=u.id
|
||||
LEFT JOIN im_user_profiles AS p ON p.tenant_id=s.tenant_id AND p.user_id=s.userid
|
||||
WHERE s.tenant_id=? AND s.topic=?`
|
||||
q = tenantShardSQL(id, q)
|
||||
args := []any{id, topic}
|
||||
if !keepDeleted {
|
||||
q += " AND u.state!=?"
|
||||
@@ -1426,7 +1428,7 @@ func (a *adapter) TenantOwnTopics(id t.TenantID, uid t.Uid) ([]string, error) {
|
||||
if a.tenantScopedDB {
|
||||
return a.OwnTopics(uid)
|
||||
}
|
||||
return a.tenantTopicNames("SELECT name FROM im_topics WHERE tenant_id=? AND owner=? AND state!=?",
|
||||
return a.tenantTopicNames(tenantShardSQL(id, "SELECT name FROM im_topics WHERE tenant_id=? AND owner=? AND state!=?"),
|
||||
id, decodeUID(uid), t.StateDeleted)
|
||||
}
|
||||
|
||||
@@ -1441,8 +1443,8 @@ func (a *adapter) TenantChannelsForUser(id t.TenantID, uid t.Uid) ([]string, err
|
||||
if a.tenantScopedDB {
|
||||
return a.ChannelsForUser(uid)
|
||||
}
|
||||
return a.tenantTopicNames("SELECT topic FROM im_subscriptions WHERE tenant_id=? AND userid=? AND topic LIKE 'chn%' "+
|
||||
"AND INSTR(modewant,'P')>0 AND INSTR(modegiven,'P')>0 AND deletedat IS NULL", id, decodeUID(uid))
|
||||
return a.tenantTopicNames(tenantShardSQL(id, "SELECT topic FROM im_subscriptions WHERE tenant_id=? AND userid=? AND topic LIKE 'chn%' "+
|
||||
"AND INSTR(modewant,'P')>0 AND INSTR(modegiven,'P')>0 AND deletedat IS NULL"), id, decodeUID(uid))
|
||||
}
|
||||
|
||||
func (a *adapter) TenantTopicShare(id t.TenantID, topic string, subs []*t.Subscription) error {
|
||||
@@ -1480,7 +1482,7 @@ func (a *adapter) TenantTopicShare(id t.TenantID, topic string, subs []*t.Subscr
|
||||
}
|
||||
}
|
||||
if topic != "" {
|
||||
if _, err = tx.Exec("UPDATE im_topics SET subcnt=subcnt+? WHERE tenant_id=? AND name=?", len(subs), id, topic); err != nil {
|
||||
if _, err = tx.Exec(tenantShardSQL(id, "UPDATE im_topics SET subcnt=subcnt+? WHERE tenant_id=? AND name=?"), len(subs), id, topic); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1563,11 +1565,7 @@ func groupMessageDeleteList(tx *sqlx.Tx, tenantID t.TenantID, topic string, toDe
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err = tx.Exec("DELETE FROM im_group_message_topic_shards WHERE tenant_id=? AND topic=?", tenantID, topic); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec("DELETE FROM im_group_message_dedup WHERE tenant_id=? AND topic=?", tenantID, topic)
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
delRanges := toDel.SeqIdRanges
|
||||
if toDel.DeletedFor == "" {
|
||||
@@ -1659,7 +1657,7 @@ func (a *adapter) TenantTopicDelete(id t.TenantID, topic string, isChan, hard bo
|
||||
}
|
||||
}()
|
||||
if hard {
|
||||
q, args, _ := sqlx.In("DELETE FROM im_subscriptions WHERE tenant_id=? AND topic IN (?)", id, topics)
|
||||
q, args, _ := sqlx.In(tenantShardSQL(id, "DELETE FROM im_subscriptions WHERE tenant_id=? AND topic IN (?)"), id, topics)
|
||||
if _, err = tx.Exec(tx.Rebind(q), args...); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1674,16 +1672,16 @@ func (a *adapter) TenantTopicDelete(id t.TenantID, topic string, isChan, hard bo
|
||||
if _, err = tx.Exec("DELETE FROM im_topictags WHERE tenant_id=? AND topic=?", id, topic); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec("DELETE FROM im_topics WHERE tenant_id=? AND name=?", id, topic); err != nil {
|
||||
if _, err = tx.Exec(tenantShardSQL(id, "DELETE FROM im_topics WHERE tenant_id=? AND name=?"), id, topic); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
now := t.TimeNow()
|
||||
q, args, _ := sqlx.In("UPDATE im_subscriptions SET updatedat=?,deletedat=? WHERE tenant_id=? AND topic IN (?)", now, now, id, topics)
|
||||
q, args, _ := sqlx.In(tenantShardSQL(id, "UPDATE im_subscriptions SET updatedat=?,deletedat=? WHERE tenant_id=? AND topic IN (?)"), now, now, id, topics)
|
||||
if _, err = tx.Exec(tx.Rebind(q), args...); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec("UPDATE im_topics SET updatedat=?,touchedat=?,state=?,stateat=? WHERE tenant_id=? AND name=?",
|
||||
if _, err = tx.Exec(tenantShardSQL(id, "UPDATE im_topics SET updatedat=?,touchedat=?,state=?,stateat=? WHERE tenant_id=? AND name=?"),
|
||||
now, now, t.StateDeleted, now, id, topic); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1706,7 +1704,7 @@ func (a *adapter) TenantTopicUpdateOnMessage(id t.TenantID, topic string, msg *t
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
_, err = a.db.ExecContext(ctx, "UPDATE im_topics SET seqid=?,touchedat=? WHERE tenant_id=? AND name=?",
|
||||
_, err = a.db.ExecContext(ctx, tenantShardSQL(id, "UPDATE im_topics SET seqid=?,touchedat=? WHERE tenant_id=? AND name=?"),
|
||||
msg.SeqId, msg.CreatedAt, id, topic)
|
||||
return err
|
||||
}
|
||||
@@ -1727,7 +1725,7 @@ func (a *adapter) TenantTopicUpdateSubCnt(id t.TenantID, topic string) error {
|
||||
defer cancel()
|
||||
}
|
||||
_, err = a.db.ExecContext(ctx,
|
||||
"UPDATE im_topics SET subcnt=(SELECT COUNT(*) FROM im_subscriptions WHERE tenant_id=? AND topic IN (?,?) AND deletedat IS NULL) WHERE tenant_id=? AND name=?",
|
||||
tenantShardSQL(id, "UPDATE im_topics SET subcnt=(SELECT COUNT(*) FROM im_subscriptions WHERE tenant_id=? AND topic IN (?,?) AND deletedat IS NULL) WHERE tenant_id=? AND name=?"),
|
||||
id, topic, t.GrpToChn(topic), id, topic)
|
||||
return err
|
||||
}
|
||||
@@ -1761,7 +1759,7 @@ func (a *adapter) TenantTopicUpdate(id t.TenantID, topic string, update map[stri
|
||||
}
|
||||
cols, args := common.UpdateByMap(update)
|
||||
args = append(args, id, topic)
|
||||
res, err := tx.Exec("UPDATE im_topics SET "+strings.Join(cols, ",")+" WHERE tenant_id=? AND name=?", args...)
|
||||
res, err := tx.Exec(tenantShardSQL(id, "UPDATE im_topics SET "+strings.Join(cols, ",")+" WHERE tenant_id=? AND name=?"), args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1813,6 +1811,7 @@ func (a *adapter) TenantTopicBumpMutationVersion(id t.TenantID, topic string, at
|
||||
query = "SELECT mutation_version FROM im_topics WHERE tenant_id=? AND name=?"
|
||||
args = []any{id, topic}
|
||||
}
|
||||
query = tenantShardSQL(id, query)
|
||||
if err = tx.GetContext(ctx, &version, query, args...); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -1837,7 +1836,7 @@ func (a *adapter) TenantTopicOwnerChange(id t.TenantID, topic string, owner t.Ui
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
_, err = a.db.ExecContext(ctx, "UPDATE im_topics SET owner=? WHERE tenant_id=? AND name=?", decodeUID(owner), id, topic)
|
||||
_, err = a.db.ExecContext(ctx, tenantShardSQL(id, "UPDATE im_topics SET owner=? WHERE tenant_id=? AND name=?"), decodeUID(owner), id, topic)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1858,6 +1857,7 @@ func (a *adapter) TenantSubscriptionGet(id t.TenantID, topic string, uid t.Uid,
|
||||
}
|
||||
query := `SELECT tenant_id,createdat,updatedat,deletedat,userid AS user,topic,delid,recvseqid,
|
||||
readseqid,modewant,modegiven,private FROM im_subscriptions WHERE tenant_id=? AND topic=? AND userid=?`
|
||||
query = tenantShardSQL(id, query)
|
||||
if !keepDeleted {
|
||||
query += " AND deletedat IS NULL"
|
||||
}
|
||||
@@ -1890,8 +1890,8 @@ func (a *adapter) TenantSubsForUser(id t.TenantID, uid t.Uid) ([]t.Subscription,
|
||||
subs, err := a.SubsForUser(uid)
|
||||
return stampTenantSubs(id, subs), err
|
||||
}
|
||||
rows, err := a.db.Queryx(`SELECT tenant_id,createdat,updatedat,deletedat,userid AS user,topic,delid,recvseqid,
|
||||
readseqid,modewant,modegiven FROM im_subscriptions WHERE tenant_id=? AND userid=? AND deletedat IS NULL`,
|
||||
rows, err := a.db.Queryx(tenantShardSQL(id, `SELECT tenant_id,createdat,updatedat,deletedat,userid AS user,topic,delid,recvseqid,
|
||||
readseqid,modewant,modegiven FROM im_subscriptions WHERE tenant_id=? AND userid=? AND deletedat IS NULL`),
|
||||
id, decodeUID(uid))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1923,6 +1923,7 @@ func (a *adapter) TenantSubsForTopic(id t.TenantID, topic string, keepDeleted bo
|
||||
}
|
||||
q := `SELECT tenant_id,createdat,updatedat,deletedat,userid AS user,topic,delid,recvseqid,
|
||||
readseqid,modewant,modegiven,private FROM im_subscriptions WHERE tenant_id=? AND topic=?`
|
||||
q = tenantShardSQL(id, q)
|
||||
args := []any{id, topic}
|
||||
if !keepDeleted {
|
||||
q += " AND deletedat IS NULL"
|
||||
@@ -1976,7 +1977,7 @@ func (a *adapter) TenantSubsUpdate(id t.TenantID, topic string, uid t.Uid, updat
|
||||
return a.SubsUpdate(topic, uid, update)
|
||||
}
|
||||
cols, args := common.UpdateByMap(update)
|
||||
q := "UPDATE im_subscriptions SET " + strings.Join(cols, ",") + " WHERE tenant_id=? AND topic=?"
|
||||
q := tenantShardSQL(id, "UPDATE im_subscriptions SET ") + strings.Join(cols, ",") + " WHERE tenant_id=? AND topic=?"
|
||||
args = append(args, id, topic)
|
||||
if !uid.IsZero() {
|
||||
q += " AND userid=?"
|
||||
@@ -2016,7 +2017,7 @@ func (a *adapter) TenantSubsDelete(id t.TenantID, topic string, uid t.Uid) error
|
||||
}()
|
||||
now := t.TimeNow()
|
||||
decoded := decodeUID(uid)
|
||||
res, err := tx.Exec("UPDATE im_subscriptions SET updatedat=?,deletedat=? WHERE tenant_id=? AND topic=? AND userid=? AND deletedat IS NULL",
|
||||
res, err := tx.Exec(tenantShardSQL(id, "UPDATE im_subscriptions SET updatedat=?,deletedat=? WHERE tenant_id=? AND topic=? AND userid=? AND deletedat IS NULL"),
|
||||
now, now, id, topic, decoded)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -2030,7 +2031,7 @@ func (a *adapter) TenantSubsDelete(id t.TenantID, topic string, uid t.Uid) error
|
||||
}
|
||||
}
|
||||
if t.GetTopicCat(topic) == t.TopicCatGrp {
|
||||
if _, err = tx.Exec("UPDATE im_topics SET subcnt=subcnt-1 WHERE tenant_id=? AND name=?", id, t.ChnToGrp(topic)); err != nil {
|
||||
if _, err = tx.Exec(tenantShardSQL(id, "UPDATE im_topics SET subcnt=subcnt-1 WHERE tenant_id=? AND name=?"), id, t.ChnToGrp(topic)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -2038,7 +2039,7 @@ func (a *adapter) TenantSubsDelete(id t.TenantID, topic string, uid t.Uid) error
|
||||
}
|
||||
|
||||
func tenantSubsDelForUser(tx *sqlx.Tx, tenantID t.TenantID, decodedUID int64, hard bool) error {
|
||||
rows, err := tx.Query("SELECT topic FROM im_subscriptions WHERE tenant_id=? AND userid=? AND deletedat IS NULL", tenantID, decodedUID)
|
||||
rows, err := tx.Query(tenantShardSQL(tenantID, "SELECT topic FROM im_subscriptions WHERE tenant_id=? AND userid=? AND deletedat IS NULL"), tenantID, decodedUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2060,16 +2061,16 @@ func tenantSubsDelForUser(tx *sqlx.Tx, tenantID t.TenantID, decodedUID int64, ha
|
||||
return err
|
||||
}
|
||||
if len(topics) > 0 {
|
||||
q, args, _ := sqlx.In("UPDATE im_topics SET subcnt=subcnt-1 WHERE tenant_id=? AND name IN (?)", tenantID, topics)
|
||||
q, args, _ := sqlx.In(tenantShardSQL(tenantID, "UPDATE im_topics SET subcnt=subcnt-1 WHERE tenant_id=? AND name IN (?)"), tenantID, topics)
|
||||
if _, err = tx.Exec(tx.Rebind(q), args...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if hard {
|
||||
_, err = tx.Exec("DELETE FROM im_subscriptions WHERE tenant_id=? AND userid=?", tenantID, decodedUID)
|
||||
_, err = tx.Exec(tenantShardSQL(tenantID, "DELETE FROM im_subscriptions WHERE tenant_id=? AND userid=?"), tenantID, decodedUID)
|
||||
} else {
|
||||
now := t.TimeNow()
|
||||
_, err = tx.Exec("UPDATE im_subscriptions SET updatedat=?,deletedat=? WHERE tenant_id=? AND userid=? AND deletedat IS NULL",
|
||||
_, err = tx.Exec(tenantShardSQL(tenantID, "UPDATE im_subscriptions SET updatedat=?,deletedat=? WHERE tenant_id=? AND userid=? AND deletedat IS NULL"),
|
||||
now, now, tenantID, decodedUID)
|
||||
}
|
||||
return err
|
||||
@@ -2111,6 +2112,7 @@ func (a *adapter) TenantFind(id t.TenantID, caller, promoPrefix string, req [][]
|
||||
"FROM im_topics AS t JOIN im_topictags AS tg ON t.tenant_id=tg.tenant_id AND t.name=tg.topic " +
|
||||
"WHERE t.tenant_id=? AND " + stateConstraint + "tg.tag IN (?" + strings.Repeat(",?", len(allTags)-1) + ") " +
|
||||
"GROUP BY t.name,t.createdat,t.updatedat,t.usebt,t.access,t.subcnt,t.public,t.trusted,t.tags "
|
||||
query = tenantShardSQL(id, query)
|
||||
if len(allReq) > 0 {
|
||||
q, addArgs := common.DisjunctionSql(req, "tg.tag")
|
||||
query += q
|
||||
@@ -2177,8 +2179,8 @@ func (a *adapter) TenantFindOne(id t.TenantID, tag string) (string, error) {
|
||||
defer cancel()
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx,
|
||||
"SELECT tp.name AS topic FROM im_topics AS tp LEFT JOIN im_topictags AS tt ON tp.tenant_id=tt.tenant_id AND tp.name=tt.topic "+
|
||||
"WHERE tp.tenant_id=? AND tt.tag=? LIMIT 1", id, tag)
|
||||
tenantShardSQL(id, "SELECT tp.name AS topic FROM im_topics AS tp LEFT JOIN im_topictags AS tt ON tp.tenant_id=tt.tenant_id AND tp.name=tt.topic "+
|
||||
"WHERE tp.tenant_id=? AND tt.tag=? LIMIT 1"), id, tag)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -2273,17 +2275,6 @@ func (a *adapter) TenantGroupMessageSave(id t.TenantID, msg *t.Message) error {
|
||||
}
|
||||
defer tx.Rollback()
|
||||
messageID := store.DecodeUid(msg.Uid())
|
||||
if clientMsgID.Valid {
|
||||
if _, err = tx.ExecContext(ctx, `INSERT INTO im_group_message_dedup
|
||||
(tenant_id,topic,from_user,client_msg_id,seqid,shard_month,message_id,status,expires_at)
|
||||
VALUES(?,?,?,?,?,?,?,'done',?)`, id, msg.Topic, decodeUID(t.ParseUid(msg.From)), clientMsgID,
|
||||
msg.SeqId, msg.ShardMonth, messageID, time.Now().UTC().Add(30*24*time.Hour)); err != nil {
|
||||
if isDupe(err) {
|
||||
return t.ErrDuplicate
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
query := fmt.Sprintf(`INSERT INTO %s
|
||||
(tenant_id,message_id,topic,group_id,seqid,from_user,client_msg_id,msg_type,status,content,head,mentioned_user_ids,mention_all,
|
||||
file_count,content_size,version,created_at,updated_at,content_nonce,key_version)
|
||||
@@ -2298,21 +2289,6 @@ func (a *adapter) TenantGroupMessageSave(id t.TenantID, msg *t.Message) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.ExecContext(ctx, `INSERT INTO im_group_message_topic_shards
|
||||
(tenant_id,topic,shard_month,min_seq,max_seq,message_count,min_created_at,max_created_at)
|
||||
VALUES(?,?,?,?,?,1,?,?) ON DUPLICATE KEY UPDATE
|
||||
min_seq=LEAST(min_seq,VALUES(min_seq)),max_seq=GREATEST(max_seq,VALUES(max_seq)),
|
||||
message_count=message_count+1,min_created_at=LEAST(min_created_at,VALUES(min_created_at)),
|
||||
max_created_at=GREATEST(max_created_at,VALUES(max_created_at))`,
|
||||
id, msg.Topic, msg.ShardMonth, msg.SeqId, msg.SeqId, msg.CreatedAt, msg.CreatedAt); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.ExecContext(ctx, `UPDATE im_group_message_shards SET row_count=row_count+1,
|
||||
min_created_at=IF(min_created_at IS NULL,?,LEAST(min_created_at,?)),
|
||||
max_created_at=IF(max_created_at IS NULL,?,GREATEST(max_created_at,?)) WHERE tenant_id=? AND shard_month=?`,
|
||||
msg.CreatedAt, msg.CreatedAt, msg.CreatedAt, msg.CreatedAt, id, msg.ShardMonth); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
@@ -2366,19 +2342,6 @@ func (a *adapter) TenantGroupMessageGetByClientID(id t.TenantID, topic string, f
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
var seq int64
|
||||
if err = a.db.QueryRowContext(ctx, `SELECT seqid FROM im_group_message_dedup
|
||||
WHERE tenant_id=? AND topic=? AND from_user=? AND client_msg_id=?`, id, topic, decodeUID(from), clientMsgID).Scan(&seq); err == nil {
|
||||
msg, getErr := tenantGroupMessageGetOne(a.db, ctx, id, topic, seq, "")
|
||||
if getErr == nil {
|
||||
return msg, nil
|
||||
}
|
||||
if getErr != sql.ErrNoRows {
|
||||
return nil, getErr
|
||||
}
|
||||
} else if err != sql.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
tables, err := groupMessageTablesForTopic(a.db, ctx, id, topic)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -3111,14 +3074,14 @@ func (a *adapter) TenantMessageMutations(id t.TenantID, topic string, uid t.Uid,
|
||||
}
|
||||
var currentVersion int64
|
||||
if a.tenantScopedDB {
|
||||
if err := a.db.GetContext(ctx, ¤tVersion, "SELECT IFNULL(mutation_version,0) FROM im_topics WHERE name=?", topic); err != nil {
|
||||
if err := a.db.GetContext(ctx, ¤tVersion, tenantShardSQL(id, "SELECT IFNULL(mutation_version,0) FROM im_topics WHERE name=?"), topic); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, "", false, 0, t.ErrNotFound
|
||||
}
|
||||
return nil, "", false, 0, err
|
||||
}
|
||||
} else {
|
||||
if err := a.db.GetContext(ctx, ¤tVersion, "SELECT IFNULL(mutation_version,0) FROM im_topics WHERE tenant_id=? AND name=?", id, topic); err != nil {
|
||||
if err := a.db.GetContext(ctx, ¤tVersion, tenantShardSQL(id, "SELECT IFNULL(mutation_version,0) FROM im_topics WHERE tenant_id=? AND name=?"), id, topic); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, "", false, 0, t.ErrNotFound
|
||||
}
|
||||
@@ -5261,7 +5224,7 @@ func (a *adapter) TenantFileUserHasAccess(id t.TenantID, fid string, uid t.Uid)
|
||||
defer cancel()
|
||||
}
|
||||
var allowed int
|
||||
err = a.db.GetContext(ctx, &allowed, fmt.Sprintf(`
|
||||
err = a.db.GetContext(ctx, &allowed, tenantShardSQL(id, fmt.Sprintf(`
|
||||
SELECT COUNT(*) FROM im_fileuploads AS fu
|
||||
WHERE fu.tenant_id=? AND (fu.id=? OR fu.location=? OR fu.location LIKE ?) AND fu.status=? AND (
|
||||
fu.userid=?
|
||||
@@ -5310,7 +5273,7 @@ func (a *adapter) TenantFileUserHasAccess(id t.TenantID, fid string, uid t.Uid)
|
||||
WHERE (gf.file_id=? OR gf.file_id LIKE CONCAT(?,'.%%')) AND gf.status='active'
|
||||
AND s.deletedat IS NULL AND INSTR(s.modewant,'R')>0 AND INSTR(s.modegiven,'R')>0
|
||||
)
|
||||
) LIMIT 1`, tenantFriendshipsTable(id), tenantMessagesTable(id)),
|
||||
) LIMIT 1`, tenantFriendshipsTable(id), tenantMessagesTable(id))),
|
||||
id, decodeUID(fileID), fileID.String32(), "%/"+fileID.String32(), t.UploadCompleted, decodeUID(uid), decodeUID(uid), decodeUID(uid), t.RelationshipFriend,
|
||||
decodeUID(uid), decodeUID(uid), t.GroupMemberStatusActive, decodeUID(uid), decodeUID(uid),
|
||||
decodeUID(uid), fid, fid)
|
||||
@@ -5379,33 +5342,14 @@ func (a *adapter) TenantVerificationCreate(id t.TenantID, vc *t.VerificationCode
|
||||
return t.ErrMalformed
|
||||
}
|
||||
|
||||
ctx, cancel := a.getContextForTx()
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
tx, err := a.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
now := t.TimeNow()
|
||||
_, err = tx.ExecContext(ctx,
|
||||
`UPDATE im_verification_codes
|
||||
SET state=?,updated_at=?
|
||||
WHERE tenant_id=? AND scene=? AND phone_hash=? AND state=?`,
|
||||
t.VerificationStateExpired, now, id, vc.Scene, vc.PhoneHash, t.VerificationStatePending)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vc.CreatedAt = now
|
||||
vc.UpdatedAt = now
|
||||
_, err = tx.ExecContext(ctx,
|
||||
_, err := a.db.ExecContext(ctx,
|
||||
`INSERT INTO im_verification_codes
|
||||
(tenant_id,challenge_id,scene,action,phone_hash,phone_mask,target_user,code_hash,state,attempts,max_attempts,
|
||||
expires_at,resend_after,created_at,updated_at)
|
||||
@@ -5418,7 +5362,7 @@ func (a *adapter) TenantVerificationCreate(id t.TenantID, vc *t.VerificationCode
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantVerificationGet(id t.TenantID, challengeID string) (*t.VerificationCode, error) {
|
||||
@@ -5854,23 +5798,10 @@ func (a *adapter) TenantActionTokenCreate(id t.TenantID, token *t.ActionToken) e
|
||||
token.TokenID == "" || token.Action == "" || token.TokenHash == "" || token.ExpiresAt.IsZero() {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
now := t.TimeNow()
|
||||
token.CreatedAt = now
|
||||
token.UpdatedAt = now
|
||||
_, err := a.db.ExecContext(ctx,
|
||||
`INSERT INTO im_action_tokens
|
||||
(tenant_id,user_id,token_id,action,target,token_hash,state,expires_at,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?)`,
|
||||
id, decodeUID(token.UserID), token.TokenID, token.Action, token.Target, token.TokenHash, token.State,
|
||||
token.ExpiresAt, token.CreatedAt, token.UpdatedAt)
|
||||
if isDupe(err) {
|
||||
return t.ErrDuplicate
|
||||
}
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantActionTokenConsume(id t.TenantID, uid t.Uid, token, action, target string, now time.Time) (*t.ActionToken, error) {
|
||||
@@ -5893,50 +5824,7 @@ func (a *adapter) TenantActionTokenConsume(id t.TenantID, uid t.Uid, token, acti
|
||||
if !ok || tokenID == "" || secret == "" {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
tokenHash := securityTokenHash(id, uid, action, target, tokenID, secret)
|
||||
ctx, cancel := a.getContextForTx()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
tx, err := a.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
var at t.ActionToken
|
||||
err = tx.GetContext(ctx, &at,
|
||||
`SELECT id,user_id,token_id,action,target,token_hash,state,expires_at,consumed_at,created_at,updated_at
|
||||
FROM im_action_tokens
|
||||
WHERE tenant_id=? AND user_id=? AND token_id=? AND action=? AND target=? LIMIT 1`,
|
||||
id, decodeUID(uid), tokenID, action, target)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if at.State != 0 || at.TokenHash != tokenHash || !now.Before(at.ExpiresAt) {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
res, err := tx.ExecContext(ctx,
|
||||
`UPDATE im_action_tokens SET state=1,consumed_at=?,updated_at=?
|
||||
WHERE tenant_id=? AND user_id=? AND token_id=? AND action=? AND target=? AND state=0 AND expires_at>?`,
|
||||
now, now, id, decodeUID(uid), tokenID, action, target, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rows, _ := res.RowsAffected(); rows == 0 {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
at.TenantID = id
|
||||
return &at, nil
|
||||
return nil, t.ErrUnsupported
|
||||
}
|
||||
|
||||
func (a *adapter) TenantIndependentPasswordGet(id t.TenantID, uid t.Uid, purpose string) (*t.IndependentPassword, error) {
|
||||
@@ -6166,7 +6054,7 @@ func (a *adapter) TenantSystemNotificationCreate(id t.TenantID, notification *t.
|
||||
notification.CreatedAt = now
|
||||
notification.UpdatedAt = now
|
||||
_, err := a.db.ExecContext(ctx,
|
||||
`INSERT INTO im_system_notifications
|
||||
tenantShardSQL(id, `INSERT INTO im_system_notifications
|
||||
(tenant_id,user_id,event_id,event_type,title,summary,action,read_at,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
@@ -6174,7 +6062,7 @@ func (a *adapter) TenantSystemNotificationCreate(id t.TenantID, notification *t.
|
||||
title=VALUES(title),
|
||||
summary=VALUES(summary),
|
||||
action=VALUES(action),
|
||||
updated_at=VALUES(updated_at)`,
|
||||
updated_at=VALUES(updated_at)`),
|
||||
id, decodeUID(notification.UserID), notification.EventID, notification.EventType, notification.Title,
|
||||
notification.Summary, common.ToJSON(notification.Action), notification.ReadAt, notification.CreatedAt, notification.UpdatedAt)
|
||||
return err
|
||||
@@ -6207,6 +6095,7 @@ func (a *adapter) TenantSystemNotificationList(id t.TenantID, uid t.Uid, cursor
|
||||
query := `SELECT id,user_id,event_id,event_type,title,summary,action,read_at,created_at,updated_at
|
||||
FROM im_system_notifications
|
||||
WHERE tenant_id=? AND user_id=?`
|
||||
query = tenantShardSQL(id, query)
|
||||
args := []any{id, decodeUID(uid)}
|
||||
if lastID > 0 {
|
||||
query += " AND id<?"
|
||||
@@ -6265,7 +6154,7 @@ func (a *adapter) TenantSystemNotificationUnreadCount(id t.TenantID, uid t.Uid)
|
||||
}
|
||||
var count int
|
||||
if err := a.db.GetContext(ctx, &count,
|
||||
`SELECT COUNT(*) FROM im_system_notifications WHERE tenant_id=? AND user_id=? AND read_at IS NULL`,
|
||||
tenantShardSQL(id, `SELECT COUNT(*) FROM im_system_notifications WHERE tenant_id=? AND user_id=? AND read_at IS NULL`),
|
||||
id, decodeUID(uid)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -6292,25 +6181,25 @@ func (a *adapter) TenantSystemNotificationUnreadCountByEventPrefix(id t.TenantID
|
||||
}
|
||||
var count int
|
||||
if err := a.db.GetContext(ctx, &count,
|
||||
`SELECT COUNT(*) FROM im_system_notifications WHERE tenant_id=? AND user_id=? AND read_at IS NULL AND event_type LIKE ?`,
|
||||
tenantShardSQL(id, `SELECT COUNT(*) FROM im_system_notifications WHERE tenant_id=? AND user_id=? AND read_at IS NULL AND event_type LIKE ?`),
|
||||
id, decodeUID(uid), eventPrefix+"%"); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantSystemNotificationMarkRead(id t.TenantID, uid t.Uid, eventID string, readAt time.Time) error {
|
||||
func (a *adapter) TenantSystemNotificationMarkRead(id t.TenantID, uid t.Uid, eventID string, readAt time.Time) (*t.SystemNotification, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantSystemNotificationMarkRead(id, uid, eventID, readAt)
|
||||
}
|
||||
}
|
||||
if err := validTenant(id); err != nil || uid.IsZero() || eventID == "" {
|
||||
return t.ErrMalformed
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
if readAt.IsZero() {
|
||||
readAt = t.TimeNow()
|
||||
@@ -6319,18 +6208,38 @@ func (a *adapter) TenantSystemNotificationMarkRead(id t.TenantID, uid t.Uid, eve
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
res, err := a.db.ExecContext(ctx,
|
||||
`UPDATE im_system_notifications
|
||||
SET read_at=?,updated_at=?
|
||||
WHERE tenant_id=? AND user_id=? AND event_id=?`,
|
||||
_, err := a.db.ExecContext(ctx,
|
||||
tenantShardSQL(id, `UPDATE im_system_notifications
|
||||
SET read_at=COALESCE(read_at,?),updated_at=?
|
||||
WHERE tenant_id=? AND user_id=? AND event_id=?`),
|
||||
readAt, readAt, id, decodeUID(uid), eventID)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if rows, _ := res.RowsAffected(); rows == 0 {
|
||||
return t.ErrNotFound
|
||||
notification, err := a.tenantSystemNotificationGet(ctx, id, uid, eventID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil
|
||||
return notification, nil
|
||||
}
|
||||
|
||||
func (a *adapter) tenantSystemNotificationGet(ctx context.Context, id t.TenantID, uid t.Uid, eventID string) (*t.SystemNotification, error) {
|
||||
var notification t.SystemNotification
|
||||
err := a.db.QueryRowxContext(ctx, tenantShardSQL(id, `SELECT id,user_id,event_id,event_type,title,summary,action,read_at,created_at,updated_at
|
||||
FROM im_system_notifications
|
||||
WHERE tenant_id=? AND user_id=? AND event_id=?`),
|
||||
id, decodeUID(uid), eventID).StructScan(¬ification)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if action, ok := common.FromJSON(notification.Action).(map[string]any); ok {
|
||||
notification.Action = t.KVMap(action)
|
||||
}
|
||||
notification.TenantID = id
|
||||
return ¬ification, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantFriendRequestCreate(id t.TenantID, req *t.FriendRequest) (*t.FriendRequest, error) {
|
||||
@@ -6705,10 +6614,10 @@ func (a *adapter) TenantFriendRequestAccept(id t.TenantID, requestID string, act
|
||||
targetSub.SetTouchedAt(handledAt)
|
||||
targetSub.SetPublic(applicantUser.Public)
|
||||
targetSub.SetTrusted(applicantUser.Trusted)
|
||||
if err = createSubscription(tx, applicantSub, true); err != nil {
|
||||
if err = createTenantSubscription(tx, id, applicantSub, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = createSubscription(tx, targetSub, true); err != nil {
|
||||
if err = createTenantSubscription(tx, id, targetSub, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
topicRow := &t.Topic{TenantID: id, ObjHeader: t.ObjHeader{Id: topic, CreatedAt: handledAt, UpdatedAt: handledAt}, TouchedAt: handledAt}
|
||||
@@ -6721,7 +6630,7 @@ func (a *adapter) TenantFriendRequestAccept(id t.TenantID, requestID string, act
|
||||
if !isDupe(err) {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = tx.ExecContext(ctx, "UPDATE im_topics SET updatedat=?,touchedat=?,state=? WHERE tenant_id=? AND name=?",
|
||||
if _, err = tx.ExecContext(ctx, tenantShardSQL(id, "UPDATE im_topics SET updatedat=?,touchedat=?,state=? WHERE tenant_id=? AND name=?"),
|
||||
handledAt, handledAt, t.StateOK, id, topic); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -20,12 +20,16 @@ const (
|
||||
groupSettingsSelectColumns = `topic,owner_user_id,name,avatar_file_id,description,group_status,join_approval,mute_all,
|
||||
member_upload_files,member_private_chat,member_invite,max_member_count,max_admin_count,member_count,version,
|
||||
created_by,dissolved_by,dissolved_at,created_at,updated_at`
|
||||
groupAnnouncementSelectColumns = `id,announcement_id,topic,author_user_id,content,status,version,published_at,idempotency_key,created_at,updated_at,
|
||||
groupAnnouncementSelectColumnsBase = `id,announcement_id,topic,author_user_id,content,status,version,published_at,idempotency_key,created_at,updated_at,
|
||||
(SELECT COUNT(*) FROM im_group_announcement_reads AS ar WHERE ar.topic=im_group_announcements.topic AND ar.announcement_id=im_group_announcements.announcement_id) AS read_count,
|
||||
(SELECT COUNT(*) FROM im_group_members AS gm WHERE gm.topic=im_group_announcements.topic AND gm.member_status='active') AS member_count`
|
||||
groupMuteSelectColumns = `topic,user_id,muted_by,muted_at,expires_at,reason,status,revoked_by,revoked_at,version,created_at,updated_at`
|
||||
)
|
||||
|
||||
func groupAnnouncementSelectColumns(tenantID t.TenantID) string {
|
||||
return tenantShardSQL(tenantID, groupAnnouncementSelectColumnsBase)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantGroupSettingsCreate(id t.TenantID, settings *t.GroupSettings) (*t.GroupSettings, error) {
|
||||
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
||||
return nil, err
|
||||
@@ -174,7 +178,7 @@ func (a *adapter) TenantGroupSettingsUpdate(id t.TenantID, topic string, patch t
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
if patch.MuteAll != nil {
|
||||
if err = syncGroupMuteAllTx(tx, topic, *patch.MuteAll, time.Now().UTC().Round(time.Millisecond)); err != nil {
|
||||
if err = syncGroupMuteAllTx(tx, id, topic, *patch.MuteAll, time.Now().UTC().Round(time.Millisecond)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -214,19 +218,19 @@ func (a *adapter) TenantGroupDissolve(id t.TenantID, topic string, actor t.Uid,
|
||||
if affected, _ := res.RowsAffected(); affected == 0 {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
if _, err = tx.ExecContext(ctx, `UPDATE im_subscriptions
|
||||
if _, err = tx.ExecContext(ctx, tenantShardSQL(id, `UPDATE im_subscriptions
|
||||
SET updatedat=?,deletedat=?
|
||||
WHERE topic IN (?,?) AND deletedat IS NULL`, now, now, strings.TrimSpace(topic), t.GrpToChn(strings.TrimSpace(topic))); err != nil {
|
||||
WHERE tenant_id=? AND topic IN (?,?) AND deletedat IS NULL`), now, now, id, strings.TrimSpace(topic), t.GrpToChn(strings.TrimSpace(topic))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = tx.ExecContext(ctx, `UPDATE im_group_members
|
||||
if _, err = tx.ExecContext(ctx, tenantShardSQL(id, `UPDATE im_group_members
|
||||
SET member_status=?,left_at=?,removed_by=?,version=version+1,updated_at=?
|
||||
WHERE topic=? AND member_status=?`, t.GroupMemberStatusLeft, now, decodeUID(actor), now, strings.TrimSpace(topic), t.GroupMemberStatusActive); err != nil {
|
||||
WHERE tenant_id=? AND topic=? AND member_status=?`), t.GroupMemberStatusLeft, now, decodeUID(actor), now, id, strings.TrimSpace(topic), t.GroupMemberStatusActive); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = tx.ExecContext(ctx, `UPDATE im_topics
|
||||
if _, err = tx.ExecContext(ctx, tenantShardSQL(id, `UPDATE im_topics
|
||||
SET updatedat=?,touchedat=?,state=?,stateat=?,subcnt=0
|
||||
WHERE name=?`, now, now, t.StateDeleted, now, strings.TrimSpace(topic)); err != nil {
|
||||
WHERE tenant_id=? AND name=?`), now, now, t.StateDeleted, now, id, strings.TrimSpace(topic)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
@@ -270,16 +274,16 @@ func (a *adapter) TenantGroupMemberUpsert(id t.TenantID, member *t.GroupMember)
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
_, err := a.db.ExecContext(ctx, `INSERT INTO im_group_members
|
||||
(topic,user_id,role,member_status,nickname,pinned,mute_notifications,join_source,joined_by,joined_at,version,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
_, err := a.db.ExecContext(ctx, tenantShardSQL(id, `INSERT INTO im_group_members
|
||||
(tenant_id,topic,user_id,role,member_status,nickname,pinned,mute_notifications,join_source,joined_by,joined_at,version,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
ON DUPLICATE KEY UPDATE role=VALUES(role),member_status=VALUES(member_status),nickname=VALUES(nickname),pinned=VALUES(pinned),mute_notifications=VALUES(mute_notifications),join_source=VALUES(join_source),joined_by=VALUES(joined_by),joined_at=IF(member_status='active',joined_at,VALUES(joined_at)),left_at=NULL,removed_by=NULL,version=version+1,updated_at=VALUES(updated_at)`,
|
||||
member.Topic, decodeUID(member.UserID), member.Role, member.MemberStatus, member.Nickname, member.Pinned, member.MuteNotifications,
|
||||
), id, member.Topic, decodeUID(member.UserID), member.Role, member.MemberStatus, member.Nickname, member.Pinned, member.MuteNotifications,
|
||||
member.JoinSource, nullableUID(member.JoinedBy), member.JoinedAt, member.Version, member.CreatedAt, member.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = a.recountGroupMembers(ctx, member.Topic)
|
||||
_ = a.recountGroupMembers(ctx, id, member.Topic)
|
||||
return a.TenantGroupMemberGet(id, member.Topic, member.UserID)
|
||||
}
|
||||
|
||||
@@ -296,7 +300,7 @@ func (a *adapter) TenantGroupMemberGet(id t.TenantID, topic string, uid t.Uid) (
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
return a.groupMemberByQuery(ctx, "SELECT * FROM im_group_members WHERE topic=? AND user_id=?", topic, decodeUID(uid))
|
||||
return a.groupMemberByQuery(ctx, tenantShardSQL(id, "SELECT * FROM im_group_members WHERE tenant_id=? AND topic=? AND user_id=?"), id, topic, decodeUID(uid))
|
||||
}
|
||||
|
||||
func (a *adapter) TenantGroupMemberList(id t.TenantID, topic string, opts t.GroupMemberListOptions) ([]t.GroupMember, string, error) {
|
||||
@@ -312,8 +316,8 @@ func (a *adapter) TenantGroupMemberList(id t.TenantID, topic string, opts t.Grou
|
||||
opts.Limit = 30
|
||||
}
|
||||
memberIndex := "idx_im_group_members_topic_status"
|
||||
where := []string{"gm.topic=?", "gm.member_status=?"}
|
||||
args := []any{topic, t.GroupMemberStatusActive}
|
||||
where := []string{"gm.tenant_id=?", "gm.topic=?", "gm.member_status=?"}
|
||||
args := []any{id, topic, t.GroupMemberStatusActive}
|
||||
if opts.Role != "" {
|
||||
memberIndex = "idx_im_group_members_topic_role"
|
||||
where = append(where, "gm.role=?")
|
||||
@@ -343,14 +347,14 @@ func (a *adapter) TenantGroupMemberList(id t.TenantID, topic string, opts t.Grou
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, `SELECT STRAIGHT_JOIN gm.*,s.modewant,s.modegiven,u.trusted,
|
||||
rows, err := a.db.QueryxContext(ctx, tenantShardSQL(id, `SELECT STRAIGHT_JOIN gm.*,s.modewant,s.modegiven,u.trusted,
|
||||
p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature,
|
||||
p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account
|
||||
FROM im_group_members AS gm FORCE INDEX (`+memberIndex+`)
|
||||
JOIN im_subscriptions AS s ON s.topic=gm.topic AND s.userid=gm.user_id AND s.deletedat IS NULL
|
||||
JOIN im_users AS u ON u.id=gm.user_id AND u.state!=?
|
||||
LEFT JOIN im_user_profiles AS p ON p.user_id=gm.user_id
|
||||
WHERE `+strings.Join(where, " AND ")+` ORDER BY gm.id LIMIT ?`, append([]any{t.StateDeleted}, args...)...)
|
||||
JOIN im_subscriptions AS s ON s.tenant_id=gm.tenant_id AND s.topic=gm.topic AND s.userid=gm.user_id AND s.deletedat IS NULL
|
||||
JOIN im_users AS u ON u.tenant_id=gm.tenant_id AND u.id=gm.user_id AND u.state!=?
|
||||
LEFT JOIN im_user_profiles AS p ON p.tenant_id=gm.tenant_id AND p.user_id=gm.user_id
|
||||
WHERE `+strings.Join(where, " AND ")+` ORDER BY gm.id LIMIT ?`), append([]any{t.StateDeleted}, args...)...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
@@ -422,8 +426,8 @@ func (a *adapter) TenantGroupMemberUpdate(id t.TenantID, topic string, uid t.Uid
|
||||
set = append(set, "removed_by=?")
|
||||
args = append(args, nullableUID(*patch.RemovedBy))
|
||||
}
|
||||
args = append(args, topic, decodeUID(uid))
|
||||
where := "topic=? AND user_id=?"
|
||||
args = append(args, id, topic, decodeUID(uid))
|
||||
where := "tenant_id=? AND topic=? AND user_id=?"
|
||||
if patch.Version > 0 {
|
||||
where += " AND version=?"
|
||||
args = append(args, patch.Version)
|
||||
@@ -437,7 +441,7 @@ func (a *adapter) TenantGroupMemberUpdate(id t.TenantID, topic string, uid t.Uid
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
res, err := tx.ExecContext(ctx, "UPDATE im_group_members SET "+strings.Join(set, ",")+" WHERE "+where, args...)
|
||||
res, err := tx.ExecContext(ctx, tenantShardSQL(id, "UPDATE im_group_members SET "+strings.Join(set, ",")+" WHERE "+where), args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -447,13 +451,13 @@ func (a *adapter) TenantGroupMemberUpdate(id t.TenantID, topic string, uid t.Uid
|
||||
}
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
if err = recountGroupMembersTx(tx, topic); err != nil {
|
||||
if err = recountGroupMembersTx(tx, id, topic); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.groupMemberByQuery(ctx, "SELECT * FROM im_group_members WHERE topic=? AND user_id=?", topic, decodeUID(uid))
|
||||
return a.groupMemberByQuery(ctx, tenantShardSQL(id, "SELECT * FROM im_group_members WHERE tenant_id=? AND topic=? AND user_id=?"), id, topic, decodeUID(uid))
|
||||
}
|
||||
|
||||
func (a *adapter) TenantGroupInvitationCreate(id t.TenantID, inv *t.GroupInvitation) (*t.GroupInvitation, error) {
|
||||
@@ -572,8 +576,8 @@ func (a *adapter) TenantGroupInvitationAccept(id t.TenantID, invitationID string
|
||||
return &inv, nil
|
||||
}
|
||||
var deletedAt sql.NullTime
|
||||
err = tx.QueryRowxContext(ctx, "SELECT deletedat FROM im_subscriptions WHERE topic=? AND userid=?",
|
||||
inv.Topic, decodeUID(uid)).Scan(&deletedAt)
|
||||
err = tx.QueryRowxContext(ctx, tenantShardSQL(id, "SELECT deletedat FROM im_subscriptions WHERE tenant_id=? AND topic=? AND userid=?"),
|
||||
id, inv.Topic, decodeUID(uid)).Scan(&deletedAt)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
@@ -581,20 +585,20 @@ func (a *adapter) TenantGroupInvitationAccept(id t.TenantID, invitationID string
|
||||
sub := &t.Subscription{TenantID: id, User: uid.String(), Topic: inv.Topic,
|
||||
ModeWant: t.ModeCPublic, ModeGiven: t.ModeCPublic, Private: memberPrivate}
|
||||
sub.InitTimes()
|
||||
if err = createSubscription(tx, sub, deletedAt.Valid); err != nil {
|
||||
if err = createTenantSubscription(tx, id, sub, deletedAt.Valid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
member := &t.GroupMember{Topic: inv.Topic, UserID: uid, Role: t.GroupMemberRoleMember,
|
||||
MemberStatus: t.GroupMemberStatusActive, JoinSource: "invitation", JoinedBy: inv.InviterUserID, JoinedAt: now,
|
||||
Version: 1, CreatedAt: now, UpdatedAt: now}
|
||||
if err = upsertGroupMemberTx(tx, member); err != nil {
|
||||
if err = upsertGroupMemberTx(tx, id, member); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = recountGroupMembersAndBumpVersionTx(tx, inv.Topic); err != nil {
|
||||
if err = recountGroupMembersAndBumpVersionTx(tx, id, inv.Topic); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = updateGroupTopicSubCountTx(tx, inv.Topic); err != nil {
|
||||
if err = updateGroupTopicSubCountTx(tx, id, inv.Topic); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = tx.ExecContext(ctx, `UPDATE im_group_invitations
|
||||
@@ -761,13 +765,13 @@ func (a *adapter) TenantGroupJoinRequestResolve(id t.TenantID, topic, requestID
|
||||
sub := &t.Subscription{TenantID: id, User: current.ApplicantUserID.String(), Topic: topic,
|
||||
ModeWant: t.ModeCPublic, ModeGiven: t.ModeCPublic, Private: memberPrivate}
|
||||
sub.InitTimes()
|
||||
if err = createSubscription(tx, sub, false); err != nil {
|
||||
if err = createTenantSubscription(tx, id, sub, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
member := &t.GroupMember{Topic: topic, UserID: current.ApplicantUserID, Role: t.GroupMemberRoleMember,
|
||||
MemberStatus: t.GroupMemberStatusActive, JoinSource: "approval", JoinedBy: actor, JoinedAt: now,
|
||||
Version: 1, CreatedAt: now, UpdatedAt: now}
|
||||
if err = upsertGroupMemberTx(tx, member); err != nil {
|
||||
if err = upsertGroupMemberTx(tx, id, member); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -776,10 +780,10 @@ func (a *adapter) TenantGroupJoinRequestResolve(id t.TenantID, topic, requestID
|
||||
return nil, err
|
||||
}
|
||||
if action == "accept" {
|
||||
if err = recountGroupMembersAndBumpVersionTx(tx, topic); err != nil {
|
||||
if err = recountGroupMembersAndBumpVersionTx(tx, id, topic); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = updateGroupTopicSubCountTx(tx, topic); err != nil {
|
||||
if err = updateGroupTopicSubCountTx(tx, id, topic); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -799,7 +803,7 @@ func (a *adapter) TenantGroupAnnouncementLatest(id t.TenantID, topic string) (*t
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
return a.groupAnnouncementByQuery(ctx, "SELECT "+groupAnnouncementSelectColumns+" FROM im_group_announcements WHERE topic=? AND status=? ORDER BY version DESC,id DESC LIMIT 1", topic, t.GroupAnnouncementStatusCurrent)
|
||||
return a.groupAnnouncementByQuery(ctx, "SELECT "+groupAnnouncementSelectColumns(id)+" FROM im_group_announcements WHERE topic=? AND status=? ORDER BY version DESC,id DESC LIMIT 1", topic, t.GroupAnnouncementStatusCurrent)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantGroupAnnouncementGet(id t.TenantID, topic, announcementID string) (*t.GroupAnnouncement, error) {
|
||||
@@ -812,7 +816,7 @@ func (a *adapter) TenantGroupAnnouncementGet(id t.TenantID, topic, announcementI
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
return a.groupAnnouncementByQuery(ctx, "SELECT "+groupAnnouncementSelectColumns+" FROM im_group_announcements WHERE topic=? AND announcement_id=? LIMIT 1", topic, announcementID)
|
||||
return a.groupAnnouncementByQuery(ctx, "SELECT "+groupAnnouncementSelectColumns(id)+" FROM im_group_announcements WHERE topic=? AND announcement_id=? LIMIT 1", topic, announcementID)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantGroupAnnouncementPut(id t.TenantID, ann *t.GroupAnnouncement, expectedVersion int) (*t.GroupAnnouncement, error) {
|
||||
@@ -1233,7 +1237,7 @@ func (a *adapter) TenantGroupCreate(id t.TenantID, topic *t.Topic, owner t.Uid,
|
||||
ownerSub := &t.Subscription{TenantID: id, ObjHeader: t.ObjHeader{CreatedAt: topic.CreatedAt}, User: owner.String(),
|
||||
Topic: topic.Id, ModeGiven: t.ModeCFull, ModeWant: topic.GetAccess(owner), Private: ownerPrivate}
|
||||
ownerSub.InitTimes()
|
||||
if err = createSubscription(tx, ownerSub, false); err != nil {
|
||||
if err = createTenantSubscription(tx, id, ownerSub, false); err != nil {
|
||||
return err
|
||||
}
|
||||
settings.MemberCount = 1
|
||||
@@ -1243,7 +1247,7 @@ func (a *adapter) TenantGroupCreate(id t.TenantID, topic *t.Topic, owner t.Uid,
|
||||
ownerMember := &t.GroupMember{Topic: topic.Id, UserID: owner, Role: t.GroupMemberRoleOwner,
|
||||
MemberStatus: t.GroupMemberStatusActive, JoinSource: "create", JoinedBy: owner, JoinedAt: now,
|
||||
Version: 1, CreatedAt: now, UpdatedAt: now}
|
||||
if err = upsertGroupMemberTx(tx, ownerMember); err != nil {
|
||||
if err = upsertGroupMemberTx(tx, id, ownerMember); err != nil {
|
||||
return err
|
||||
}
|
||||
seen := map[t.Uid]bool{owner: true}
|
||||
@@ -1255,20 +1259,20 @@ func (a *adapter) TenantGroupCreate(id t.TenantID, topic *t.Topic, owner t.Uid,
|
||||
sub := &t.Subscription{TenantID: id, User: uid.String(), Topic: topic.Id,
|
||||
ModeWant: t.ModeCPublic, ModeGiven: t.ModeCPublic, Private: memberPrivate}
|
||||
sub.InitTimes()
|
||||
if err = createSubscription(tx, sub, false); err != nil {
|
||||
if err = createTenantSubscription(tx, id, sub, false); err != nil {
|
||||
return err
|
||||
}
|
||||
member := &t.GroupMember{Topic: topic.Id, UserID: uid, Role: t.GroupMemberRoleMember,
|
||||
MemberStatus: t.GroupMemberStatusActive, JoinSource: "create", JoinedBy: owner, JoinedAt: now,
|
||||
Version: 1, CreatedAt: now, UpdatedAt: now}
|
||||
if err = upsertGroupMemberTx(tx, member); err != nil {
|
||||
if err = upsertGroupMemberTx(tx, id, member); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = recountGroupMembersTx(tx, topic.Id); err != nil {
|
||||
if err = recountGroupMembersTx(tx, id, topic.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = updateGroupTopicSubCountTx(tx, topic.Id); err != nil {
|
||||
if err = updateGroupTopicSubCountTx(tx, id, topic.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
@@ -1291,7 +1295,7 @@ func (a *adapter) TenantGroupInviteMember(id t.TenantID, topic string, inviter,
|
||||
}
|
||||
defer tx.Rollback()
|
||||
var deletedAt sql.NullTime
|
||||
err = tx.QueryRowContext(ctx, "SELECT deletedat FROM im_subscriptions WHERE topic=? AND userid=? FOR UPDATE", topic, decodeUID(target)).Scan(&deletedAt)
|
||||
err = tx.QueryRowContext(ctx, tenantShardSQL(id, "SELECT deletedat FROM im_subscriptions WHERE tenant_id=? AND topic=? AND userid=? FOR UPDATE"), id, topic, decodeUID(target)).Scan(&deletedAt)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return false, err
|
||||
}
|
||||
@@ -1301,7 +1305,7 @@ func (a *adapter) TenantGroupInviteMember(id t.TenantID, topic string, inviter,
|
||||
sub := &t.Subscription{TenantID: id, User: target.String(), Topic: topic,
|
||||
ModeWant: t.ModeCPublic, ModeGiven: t.ModeCPublic, Private: private}
|
||||
sub.InitTimes()
|
||||
if err = createSubscription(tx, sub, false); err != nil {
|
||||
if err = createTenantSubscription(tx, id, sub, false); err != nil {
|
||||
return false, err
|
||||
}
|
||||
invitationID := "ginv_" + store.Store.GetUidString()
|
||||
@@ -1319,13 +1323,13 @@ func (a *adapter) TenantGroupInviteMember(id t.TenantID, topic string, inviter,
|
||||
member := &t.GroupMember{Topic: topic, UserID: target, Role: t.GroupMemberRoleMember,
|
||||
MemberStatus: t.GroupMemberStatusActive, JoinSource: "invitation", JoinedBy: inviter, JoinedAt: now,
|
||||
Version: 1, CreatedAt: now, UpdatedAt: now}
|
||||
if err = upsertGroupMemberTx(tx, member); err != nil {
|
||||
if err = upsertGroupMemberTx(tx, id, member); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err = recountGroupMembersAndBumpVersionTx(tx, topic); err != nil {
|
||||
if err = recountGroupMembersAndBumpVersionTx(tx, id, topic); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err = updateGroupTopicSubCountTx(tx, topic); err != nil {
|
||||
if err = updateGroupTopicSubCountTx(tx, id, topic); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
@@ -1349,21 +1353,21 @@ func (a *adapter) TenantGroupLeave(id t.TenantID, topic string, uid t.Uid, now t
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
res, err := tx.ExecContext(ctx, "UPDATE im_subscriptions SET updatedat=?,deletedat=? WHERE topic=? AND userid=? AND deletedat IS NULL", now, now, topic, decodeUID(uid))
|
||||
res, err := tx.ExecContext(ctx, tenantShardSQL(id, "UPDATE im_subscriptions SET updatedat=?,deletedat=? WHERE tenant_id=? AND topic=? AND userid=? AND deletedat IS NULL"), now, now, id, topic, decodeUID(uid))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if affected, _ := res.RowsAffected(); affected == 0 {
|
||||
return t.ErrNotFound
|
||||
}
|
||||
if _, err = tx.ExecContext(ctx, `UPDATE im_group_members SET member_status=?,left_at=?,version=version+1,updated_at=?
|
||||
WHERE topic=? AND user_id=? AND member_status=?`, t.GroupMemberStatusLeft, now, now, topic, decodeUID(uid), t.GroupMemberStatusActive); err != nil {
|
||||
if _, err = tx.ExecContext(ctx, tenantShardSQL(id, `UPDATE im_group_members SET member_status=?,left_at=?,version=version+1,updated_at=?
|
||||
WHERE tenant_id=? AND topic=? AND user_id=? AND member_status=?`), t.GroupMemberStatusLeft, now, now, id, topic, decodeUID(uid), t.GroupMemberStatusActive); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = recountGroupMembersAndBumpVersionTx(tx, topic); err != nil {
|
||||
if err = recountGroupMembersAndBumpVersionTx(tx, id, topic); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = updateGroupTopicSubCountTx(tx, topic); err != nil {
|
||||
if err = updateGroupTopicSubCountTx(tx, id, topic); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
@@ -1385,16 +1389,16 @@ func (a *adapter) TenantGroupRoleUpdate(id t.TenantID, topic string, actor, targ
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
res, err := tx.ExecContext(ctx, "UPDATE im_subscriptions SET modegiven=?,modewant=?,updatedat=? WHERE topic=? AND userid=? AND deletedat IS NULL",
|
||||
modeGiven.String(), modeGiven.String(), now, topic, decodeUID(target))
|
||||
res, err := tx.ExecContext(ctx, tenantShardSQL(id, "UPDATE im_subscriptions SET modegiven=?,modewant=?,updatedat=? WHERE tenant_id=? AND topic=? AND userid=? AND deletedat IS NULL"),
|
||||
modeGiven.String(), modeGiven.String(), now, id, topic, decodeUID(target))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if affected, _ := res.RowsAffected(); affected == 0 {
|
||||
return t.ErrNotFound
|
||||
}
|
||||
res, err = tx.ExecContext(ctx, "UPDATE im_group_members SET role=?,version=version+1,updated_at=? WHERE topic=? AND user_id=? AND member_status=?",
|
||||
role, now, topic, decodeUID(target), t.GroupMemberStatusActive)
|
||||
res, err = tx.ExecContext(ctx, tenantShardSQL(id, "UPDATE im_group_members SET role=?,version=version+1,updated_at=? WHERE tenant_id=? AND topic=? AND user_id=? AND member_status=?"),
|
||||
role, now, id, topic, decodeUID(target), t.GroupMemberStatusActive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1430,16 +1434,16 @@ func (a *adapter) TenantGroupOwnerTransfer(id t.TenantID, topic string, previous
|
||||
role string
|
||||
}{{previousOwner, groupAdminAccessMode(), t.GroupMemberRoleAdmin}, {newOwner, t.ModeCFull, t.GroupMemberRoleOwner}}
|
||||
for _, update := range updates {
|
||||
res, execErr := tx.ExecContext(ctx, "UPDATE im_subscriptions SET modegiven=?,modewant=?,updatedat=? WHERE topic=? AND userid=? AND deletedat IS NULL",
|
||||
update.mode.String(), update.mode.String(), now, topic, decodeUID(update.uid))
|
||||
res, execErr := tx.ExecContext(ctx, tenantShardSQL(id, "UPDATE im_subscriptions SET modegiven=?,modewant=?,updatedat=? WHERE tenant_id=? AND topic=? AND userid=? AND deletedat IS NULL"),
|
||||
update.mode.String(), update.mode.String(), now, id, topic, decodeUID(update.uid))
|
||||
if execErr != nil {
|
||||
return execErr
|
||||
}
|
||||
if affected, _ := res.RowsAffected(); affected == 0 {
|
||||
return t.ErrNotFound
|
||||
}
|
||||
res, execErr = tx.ExecContext(ctx, "UPDATE im_group_members SET role=?,version=version+1,updated_at=? WHERE topic=? AND user_id=? AND member_status=?",
|
||||
update.role, now, topic, decodeUID(update.uid), t.GroupMemberStatusActive)
|
||||
res, execErr = tx.ExecContext(ctx, tenantShardSQL(id, "UPDATE im_group_members SET role=?,version=version+1,updated_at=? WHERE tenant_id=? AND topic=? AND user_id=? AND member_status=?"),
|
||||
update.role, now, id, topic, decodeUID(update.uid), t.GroupMemberStatusActive)
|
||||
if execErr != nil {
|
||||
return execErr
|
||||
}
|
||||
@@ -1447,7 +1451,7 @@ func (a *adapter) TenantGroupOwnerTransfer(id t.TenantID, topic string, previous
|
||||
return t.ErrNotFound
|
||||
}
|
||||
}
|
||||
if _, err = tx.ExecContext(ctx, "UPDATE im_topics SET owner=?,updatedat=? WHERE name=?", decodeUID(newOwner), now, topic); err != nil {
|
||||
if _, err = tx.ExecContext(ctx, tenantShardSQL(id, "UPDATE im_topics SET owner=?,updatedat=? WHERE tenant_id=? AND name=?"), decodeUID(newOwner), now, id, topic); err != nil {
|
||||
return err
|
||||
}
|
||||
args := []any{decodeUID(newOwner), now, topic}
|
||||
@@ -1504,48 +1508,48 @@ func insertGroupSettingsTx(tx *sqlx.Tx, settings *t.GroupSettings) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func upsertGroupMemberTx(tx *sqlx.Tx, member *t.GroupMember) error {
|
||||
_, err := tx.Exec(`INSERT INTO im_group_members
|
||||
(topic,user_id,role,member_status,nickname,pinned,mute_notifications,join_source,joined_by,joined_at,version,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
func upsertGroupMemberTx(tx *sqlx.Tx, tenantID t.TenantID, member *t.GroupMember) error {
|
||||
_, err := tx.Exec(tenantShardSQL(tenantID, `INSERT INTO im_group_members
|
||||
(tenant_id,topic,user_id,role,member_status,nickname,pinned,mute_notifications,join_source,joined_by,joined_at,version,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
ON DUPLICATE KEY UPDATE role=VALUES(role),member_status=VALUES(member_status),nickname=VALUES(nickname),
|
||||
pinned=VALUES(pinned),mute_notifications=VALUES(mute_notifications),join_source=VALUES(join_source),
|
||||
joined_by=VALUES(joined_by),joined_at=VALUES(joined_at),left_at=NULL,removed_by=NULL,version=version+1,updated_at=VALUES(updated_at)`,
|
||||
member.Topic, decodeUID(member.UserID), member.Role, member.MemberStatus, member.Nickname, member.Pinned,
|
||||
joined_by=VALUES(joined_by),joined_at=VALUES(joined_at),left_at=NULL,removed_by=NULL,version=version+1,updated_at=VALUES(updated_at)`),
|
||||
tenantID, member.Topic, decodeUID(member.UserID), member.Role, member.MemberStatus, member.Nickname, member.Pinned,
|
||||
member.MuteNotifications, member.JoinSource, nullableUID(member.JoinedBy), member.JoinedAt, member.Version,
|
||||
member.CreatedAt, member.UpdatedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func recountGroupMembersTx(tx *sqlx.Tx, topic string) error {
|
||||
_, err := tx.Exec(`UPDATE im_group_settings SET member_count=(SELECT COUNT(*) FROM im_group_members
|
||||
WHERE topic=? AND member_status=?),updated_at=CURRENT_TIMESTAMP(3) WHERE topic=?`, topic, t.GroupMemberStatusActive, topic)
|
||||
func recountGroupMembersTx(tx *sqlx.Tx, tenantID t.TenantID, topic string) error {
|
||||
_, err := tx.Exec(tenantShardSQL(tenantID, `UPDATE im_group_settings SET member_count=(SELECT COUNT(*) FROM im_group_members
|
||||
WHERE tenant_id=? AND topic=? AND member_status=?),updated_at=CURRENT_TIMESTAMP(3) WHERE tenant_id=? AND topic=?`), tenantID, topic, t.GroupMemberStatusActive, tenantID, topic)
|
||||
return err
|
||||
}
|
||||
|
||||
func recountGroupMembersAndBumpVersionTx(tx *sqlx.Tx, topic string) error {
|
||||
_, err := tx.Exec(`UPDATE im_group_settings SET member_count=(SELECT COUNT(*) FROM im_group_members
|
||||
WHERE topic=? AND member_status=?),version=version+1,updated_at=CURRENT_TIMESTAMP(3) WHERE topic=?`, topic, t.GroupMemberStatusActive, topic)
|
||||
func recountGroupMembersAndBumpVersionTx(tx *sqlx.Tx, tenantID t.TenantID, topic string) error {
|
||||
_, err := tx.Exec(tenantShardSQL(tenantID, `UPDATE im_group_settings SET member_count=(SELECT COUNT(*) FROM im_group_members
|
||||
WHERE tenant_id=? AND topic=? AND member_status=?),version=version+1,updated_at=CURRENT_TIMESTAMP(3) WHERE tenant_id=? AND topic=?`), tenantID, topic, t.GroupMemberStatusActive, tenantID, topic)
|
||||
return err
|
||||
}
|
||||
|
||||
func updateGroupTopicSubCountTx(tx *sqlx.Tx, topic string) error {
|
||||
_, err := tx.Exec(`UPDATE im_topics SET subcnt=(SELECT COUNT(*) FROM im_subscriptions
|
||||
WHERE topic IN (?,?) AND deletedat IS NULL) WHERE name=?`, topic, t.GrpToChn(topic), topic)
|
||||
func updateGroupTopicSubCountTx(tx *sqlx.Tx, tenantID t.TenantID, topic string) error {
|
||||
_, err := tx.Exec(tenantShardSQL(tenantID, `UPDATE im_topics SET subcnt=(SELECT COUNT(*) FROM im_subscriptions
|
||||
WHERE tenant_id=? AND topic IN (?,?) AND deletedat IS NULL) WHERE tenant_id=? AND name=?`), tenantID, topic, t.GrpToChn(topic), tenantID, topic)
|
||||
return err
|
||||
}
|
||||
|
||||
func syncGroupMuteAllTx(tx *sqlx.Tx, topic string, muteAll bool, now time.Time) error {
|
||||
func syncGroupMuteAllTx(tx *sqlx.Tx, tenantID t.TenantID, topic string, muteAll bool, now time.Time) error {
|
||||
mode := t.ModeCPublic
|
||||
if muteAll {
|
||||
mode = t.ModeCPublic &^ t.ModeWrite
|
||||
}
|
||||
_, err := tx.Exec(`UPDATE im_subscriptions AS s
|
||||
_, err := tx.Exec(tenantShardSQL(tenantID, `UPDATE im_subscriptions AS s
|
||||
JOIN im_group_members AS gm ON gm.topic=s.topic AND gm.user_id=s.userid
|
||||
SET s.modewant=?,s.modegiven=?,s.updatedat=?,
|
||||
gm.version=gm.version+1,gm.updated_at=?
|
||||
WHERE s.topic=? AND s.deletedat IS NULL
|
||||
AND gm.member_status=? AND gm.role=?`,
|
||||
AND gm.member_status=? AND gm.role=?`),
|
||||
mode.String(), mode.String(), now, now, topic, t.GroupMemberStatusActive, t.GroupMemberRoleMember)
|
||||
return err
|
||||
}
|
||||
@@ -1656,8 +1660,8 @@ func (a *adapter) groupFileByQuery(ctx context.Context, query string, args ...an
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
|
||||
func (a *adapter) recountGroupMembers(ctx context.Context, topic string) error {
|
||||
_, err := a.db.ExecContext(ctx, "UPDATE im_group_settings SET member_count=(SELECT COUNT(*) FROM im_group_members WHERE topic=? AND member_status=?),updated_at=CURRENT_TIMESTAMP(3) WHERE topic=?", topic, t.GroupMemberStatusActive, topic)
|
||||
func (a *adapter) recountGroupMembers(ctx context.Context, tenantID t.TenantID, topic string) error {
|
||||
_, err := a.db.ExecContext(ctx, tenantShardSQL(tenantID, "UPDATE im_group_settings SET member_count=(SELECT COUNT(*) FROM im_group_members WHERE tenant_id=? AND topic=? AND member_status=?),updated_at=CURRENT_TIMESTAMP(3) WHERE tenant_id=? AND topic=?"), tenantID, topic, t.GroupMemberStatusActive, tenantID, topic)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -4,15 +4,12 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/tinode/chat/server/store"
|
||||
t "github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
@@ -26,214 +23,6 @@ func (a *adapter) infrastructureAdapter(id t.TenantID) (*adapter, error) {
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantHTTPIdempotencyBegin(id t.TenantID, record *t.HTTPIdempotencyRecord) (*t.HTTPIdempotencyRecord, bool, error) {
|
||||
tenantAdp, err := a.infrastructureAdapter(id)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if record == nil || record.UserID.IsZero() || record.Method == "" || record.Route == "" || record.Key == "" || record.BodyHash == "" {
|
||||
return nil, false, t.ErrMalformed
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantHTTPIdempotencyBegin(id, record)
|
||||
}
|
||||
now := t.TimeNow()
|
||||
if record.ExpiresAt.IsZero() {
|
||||
record.ExpiresAt = now.Add(24 * time.Hour)
|
||||
}
|
||||
_, err = a.db.Exec(`INSERT INTO im_http_idempotency
|
||||
(tenant_id,user_id,method,route,idempotency_key,body_hash,state,response_status,response_body,expires_at,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`, id, decodeUID(record.UserID), record.Method, record.Route, record.Key,
|
||||
record.BodyHash, t.HTTPIdempotencyProcessing, 0, nil, record.ExpiresAt, now, now)
|
||||
if err == nil {
|
||||
record.State = t.HTTPIdempotencyProcessing
|
||||
record.CreatedAt = now
|
||||
record.UpdatedAt = now
|
||||
return record, true, nil
|
||||
}
|
||||
if !isDupe(err) {
|
||||
return nil, false, err
|
||||
}
|
||||
existing, err := a.httpIdempotencyGet(id, record.UserID, record.Method, record.Route, record.Key)
|
||||
if err == nil && !existing.ExpiresAt.After(now) {
|
||||
res, deleteErr := a.db.Exec(`DELETE FROM im_http_idempotency
|
||||
WHERE tenant_id=? AND user_id=? AND method=? AND route=? AND idempotency_key=? AND expires_at<=?`,
|
||||
id, decodeUID(record.UserID), record.Method, record.Route, record.Key, now)
|
||||
if deleteErr != nil {
|
||||
return nil, false, deleteErr
|
||||
}
|
||||
if deleted, _ := res.RowsAffected(); deleted > 0 {
|
||||
return a.TenantHTTPIdempotencyBegin(id, record)
|
||||
}
|
||||
current, getErr := a.httpIdempotencyGet(id, record.UserID, record.Method, record.Route, record.Key)
|
||||
return current, false, getErr
|
||||
}
|
||||
return existing, false, err
|
||||
}
|
||||
|
||||
func (a *adapter) TenantHTTPIdempotencyComplete(id t.TenantID, record *t.HTTPIdempotencyRecord) error {
|
||||
tenantAdp, err := a.infrastructureAdapter(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if record == nil || record.UserID.IsZero() {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantHTTPIdempotencyComplete(id, record)
|
||||
}
|
||||
now := t.TimeNow()
|
||||
res, err := a.db.Exec(`UPDATE im_http_idempotency SET state=?,response_status=?,response_body=?,updated_at=?
|
||||
WHERE tenant_id=? AND user_id=? AND method=? AND route=? AND idempotency_key=? AND body_hash=? AND state=?`,
|
||||
t.HTTPIdempotencyCompleted, record.ResponseStatus, record.ResponseBody, now, id, decodeUID(record.UserID),
|
||||
record.Method, record.Route, record.Key, record.BodyHash, t.HTTPIdempotencyProcessing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count, _ := res.RowsAffected(); count == 0 {
|
||||
return t.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantHTTPIdempotencyAbort(id t.TenantID, record *t.HTTPIdempotencyRecord) error {
|
||||
tenantAdp, err := a.infrastructureAdapter(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if record == nil || record.UserID.IsZero() {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantHTTPIdempotencyAbort(id, record)
|
||||
}
|
||||
_, err = a.db.Exec(`DELETE FROM im_http_idempotency
|
||||
WHERE tenant_id=? AND user_id=? AND method=? AND route=? AND idempotency_key=? AND body_hash=? AND state=?`,
|
||||
id, decodeUID(record.UserID), record.Method, record.Route, record.Key, record.BodyHash, t.HTTPIdempotencyProcessing)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *adapter) httpIdempotencyGet(tenantID t.TenantID, uid t.Uid, method, route, key string) (*t.HTTPIdempotencyRecord, error) {
|
||||
row := struct {
|
||||
UserID int64 `db:"user_id"`
|
||||
Method string
|
||||
Route string
|
||||
Key string `db:"idempotency_key"`
|
||||
BodyHash string `db:"body_hash"`
|
||||
State string
|
||||
ResponseStatus int `db:"response_status"`
|
||||
ResponseBody []byte `db:"response_body"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
ExpiresAt time.Time `db:"expires_at"`
|
||||
}{}
|
||||
err := a.db.Get(&row, `SELECT user_id,method,route,idempotency_key,body_hash,state,response_status,response_body,created_at,updated_at,expires_at
|
||||
FROM im_http_idempotency WHERE tenant_id=? AND user_id=? AND method=? AND route=? AND idempotency_key=?`,
|
||||
tenantID, decodeUID(uid), method, route, key)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &t.HTTPIdempotencyRecord{UserID: store.EncodeUid(row.UserID), Method: row.Method, Route: row.Route,
|
||||
Key: row.Key, BodyHash: row.BodyHash, State: row.State, ResponseStatus: row.ResponseStatus,
|
||||
ResponseBody: row.ResponseBody, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt, ExpiresAt: row.ExpiresAt}, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantKafkaConsumeBegin(id t.TenantID, record *t.KafkaConsumeRecord, deadline time.Time) (*t.KafkaConsumeRecord, bool, error) {
|
||||
tenantAdp, err := a.infrastructureAdapter(id)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if record == nil || record.EventID == "" || record.ConsumerGroup == "" || record.SourceTopic == "" {
|
||||
return nil, false, t.ErrMalformed
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantKafkaConsumeBegin(id, record, deadline)
|
||||
}
|
||||
now := t.TimeNow()
|
||||
_, err = a.db.Exec(`INSERT INTO im_kafka_consume_log
|
||||
(event_id,consumer_group,source_topic,event_type,biz_type,biz_id,status,processing_deadline,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?)`, record.EventID, record.ConsumerGroup, record.SourceTopic, record.EventType,
|
||||
record.BizType, record.BizID, t.KafkaConsumeProcessing, deadline, now, now)
|
||||
if err == nil {
|
||||
record.Status = t.KafkaConsumeProcessing
|
||||
record.ProcessingDeadline = &deadline
|
||||
record.CreatedAt = now
|
||||
record.UpdatedAt = now
|
||||
return record, true, nil
|
||||
}
|
||||
if !isDupe(err) {
|
||||
return nil, false, err
|
||||
}
|
||||
existing, err := a.kafkaConsumeGet(record.EventID, record.ConsumerGroup)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if existing.Status == t.KafkaConsumeSucceeded || existing.Status == t.KafkaConsumeDead {
|
||||
return existing, false, nil
|
||||
}
|
||||
if existing.Status == t.KafkaConsumeProcessing && existing.ProcessingDeadline != nil && existing.ProcessingDeadline.After(now) {
|
||||
return existing, false, nil
|
||||
}
|
||||
res, err := a.db.Exec(`UPDATE im_kafka_consume_log SET status=?,source_topic=?,event_type=?,biz_type=?,biz_id=?,
|
||||
processing_deadline=?,updated_at=? WHERE event_id=? AND consumer_group=? AND status<>?`,
|
||||
t.KafkaConsumeProcessing, record.SourceTopic, record.EventType, record.BizType, record.BizID,
|
||||
deadline, now, record.EventID, record.ConsumerGroup, t.KafkaConsumeSucceeded)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if affected, _ := res.RowsAffected(); affected == 0 {
|
||||
current, getErr := a.kafkaConsumeGet(record.EventID, record.ConsumerGroup)
|
||||
return current, false, getErr
|
||||
}
|
||||
current, err := a.kafkaConsumeGet(record.EventID, record.ConsumerGroup)
|
||||
return current, err == nil, err
|
||||
}
|
||||
|
||||
func (a *adapter) TenantKafkaConsumeSucceed(id t.TenantID, eventID, consumerGroup string, now time.Time) error {
|
||||
tenantAdp, err := a.infrastructureAdapter(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if eventID == "" || consumerGroup == "" {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantKafkaConsumeSucceed(id, eventID, consumerGroup, now)
|
||||
}
|
||||
res, err := a.db.Exec(`UPDATE im_kafka_consume_log SET status=?,succeeded_at=?,processing_deadline=NULL,updated_at=?
|
||||
WHERE event_id=? AND consumer_group=?`, t.KafkaConsumeSucceeded, now, now, eventID, consumerGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if affected, _ := res.RowsAffected(); affected == 0 {
|
||||
return t.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantKafkaConsumeFail(id t.TenantID, eventID, consumerGroup, reason, message string, now time.Time) (*t.KafkaConsumeRecord, error) {
|
||||
tenantAdp, err := a.infrastructureAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if eventID == "" || consumerGroup == "" {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantKafkaConsumeFail(id, eventID, consumerGroup, reason, message, now)
|
||||
}
|
||||
_, err = a.db.Exec(`UPDATE im_kafka_consume_log SET status=?,retry_count=retry_count+1,last_reason=?,
|
||||
last_error=?,processing_deadline=NULL,updated_at=? WHERE event_id=? AND consumer_group=?`,
|
||||
t.KafkaConsumeFailed, truncateKafkaString(reason, 64), truncateKafkaString(message, 1000), now, eventID, consumerGroup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.kafkaConsumeGet(eventID, consumerGroup)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantKafkaConsumeDead(id t.TenantID, dead *t.KafkaDeadLetter, now time.Time) error {
|
||||
tenantAdp, err := a.infrastructureAdapter(id)
|
||||
if err != nil {
|
||||
@@ -245,12 +34,7 @@ func (a *adapter) TenantKafkaConsumeDead(id t.TenantID, dead *t.KafkaDeadLetter,
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantKafkaConsumeDead(id, dead, now)
|
||||
}
|
||||
tx, err := a.db.BeginTxx(context.Background(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if _, err = tx.Exec(`INSERT INTO im_kafka_dead_letters
|
||||
if _, err = a.db.Exec(`INSERT INTO im_kafka_dead_letters
|
||||
(dead_id,event_id,parent_event_id,source_topic,consumer_group,event_type,biz_type,biz_id,envelope,
|
||||
retry_count,last_reason,last_error,status,replay_count,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
@@ -260,13 +44,7 @@ func (a *adapter) TenantKafkaConsumeDead(id t.TenantID, dead *t.KafkaDeadLetter,
|
||||
truncateKafkaString(dead.LastError, 1000), t.KafkaDeadStatusDead, dead.ReplayCount, now, now); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec(`UPDATE im_kafka_consume_log SET status=?,dead_at=?,processing_deadline=NULL,last_reason=?,
|
||||
last_error=?,updated_at=? WHERE event_id=? AND consumer_group=?`,
|
||||
t.KafkaConsumeDead, now, truncateKafkaString(dead.LastReason, 64), truncateKafkaString(dead.LastError, 1000),
|
||||
now, dead.EventID, dead.ConsumerGroup); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantKafkaDeadLetterList(id t.TenantID, opts t.KafkaDeadLetterListOptions) (*t.KafkaDeadLetterPage, error) {
|
||||
@@ -390,22 +168,7 @@ func (a *adapter) TenantKafkaDeadLetterMarkReplay(id t.TenantID, deadID, resolve
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantKafkaDeadLetterMarkReplay(id, deadID, resolvedBy, note, now)
|
||||
}
|
||||
tx, err := a.db.BeginTxx(context.Background(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
var row struct {
|
||||
EventID string `db:"event_id"`
|
||||
ConsumerGroup string `db:"consumer_group"`
|
||||
}
|
||||
if err = tx.Get(&row, `SELECT event_id,consumer_group FROM im_kafka_dead_letters WHERE dead_id=? FOR UPDATE`, deadID); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
res, err := tx.Exec(`UPDATE im_kafka_dead_letters SET status=?,replay_count=replay_count+1,resolved_by=?,
|
||||
res, err := a.db.Exec(`UPDATE im_kafka_dead_letters SET status=?,replay_count=replay_count+1,resolved_by=?,
|
||||
resolved_at=?,resolve_note=?,updated_at=? WHERE dead_id=? AND status IN (?,?)`,
|
||||
t.KafkaDeadStatusPendingReplay, truncateKafkaString(resolvedBy, 64), now, truncateKafkaString(note, 1000),
|
||||
now, deadID, t.KafkaDeadStatusDead, t.KafkaDeadStatusPendingReplay)
|
||||
@@ -415,13 +178,6 @@ func (a *adapter) TenantKafkaDeadLetterMarkReplay(id t.TenantID, deadID, resolve
|
||||
if affected, _ := res.RowsAffected(); affected == 0 {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
if _, err = tx.Exec(`DELETE FROM im_kafka_consume_log WHERE event_id=? AND consumer_group=?`,
|
||||
row.EventID, row.ConsumerGroup); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.TenantKafkaDeadLetterGet(id, deadID)
|
||||
}
|
||||
|
||||
@@ -469,39 +225,6 @@ func scanKafkaDeadLetter(rows *sqlx.Rows) (*t.KafkaDeadLetter, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *adapter) kafkaConsumeGet(eventID, consumerGroup string) (*t.KafkaConsumeRecord, error) {
|
||||
row := struct {
|
||||
EventID string `db:"event_id"`
|
||||
ConsumerGroup string `db:"consumer_group"`
|
||||
SourceTopic string `db:"source_topic"`
|
||||
EventType string `db:"event_type"`
|
||||
BizType string `db:"biz_type"`
|
||||
BizID string `db:"biz_id"`
|
||||
Status string `db:"status"`
|
||||
RetryCount int `db:"retry_count"`
|
||||
LastReason string `db:"last_reason"`
|
||||
LastError string `db:"last_error"`
|
||||
ProcessingDeadline *time.Time `db:"processing_deadline"`
|
||||
SucceededAt *time.Time `db:"succeeded_at"`
|
||||
DeadAt *time.Time `db:"dead_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}{}
|
||||
err := a.db.Get(&row, `SELECT event_id,consumer_group,source_topic,event_type,biz_type,biz_id,status,retry_count,
|
||||
last_reason,last_error,processing_deadline,succeeded_at,dead_at,created_at,updated_at
|
||||
FROM im_kafka_consume_log WHERE event_id=? AND consumer_group=?`, eventID, consumerGroup)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &t.KafkaConsumeRecord{EventID: row.EventID, ConsumerGroup: row.ConsumerGroup, SourceTopic: row.SourceTopic,
|
||||
EventType: row.EventType, BizType: row.BizType, BizID: row.BizID, Status: row.Status, RetryCount: row.RetryCount,
|
||||
LastReason: row.LastReason, LastError: row.LastError, ProcessingDeadline: row.ProcessingDeadline,
|
||||
SucceededAt: row.SucceededAt, DeadAt: row.DeadAt, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}, nil
|
||||
}
|
||||
|
||||
func truncateKafkaString(value string, limit int) string {
|
||||
if limit <= 0 {
|
||||
return ""
|
||||
@@ -519,10 +242,10 @@ func insertSystemNotificationTx(tx *sqlx.Tx, tenantID t.TenantID, uid t.Uid, eve
|
||||
return err
|
||||
}
|
||||
now := t.TimeNow()
|
||||
_, err = tx.Exec(`INSERT INTO im_system_notifications
|
||||
_, err = tx.Exec(tenantShardSQL(tenantID, `INSERT INTO im_system_notifications
|
||||
(tenant_id,user_id,event_id,event_type,title,summary,action,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE event_type=VALUES(event_type),title=VALUES(title),
|
||||
summary=VALUES(summary),action=VALUES(action),updated_at=VALUES(updated_at)`, tenantID, decodeUID(uid), eventID,
|
||||
summary=VALUES(summary),action=VALUES(action),updated_at=VALUES(updated_at)`), tenantID, decodeUID(uid), eventID,
|
||||
eventType, title, summary, encoded, now, now)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -68,21 +68,7 @@ func (a *adapter) TenantLoginFailureCreate(id t.TenantID, event *t.LoginFailureE
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantLoginFailureCreate(id, event)
|
||||
}
|
||||
metadata, err := json.Marshal(event.Metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var userID any
|
||||
if !event.UserID.IsZero() {
|
||||
userID = decodeUID(event.UserID)
|
||||
}
|
||||
_, err = a.db.Exec(`INSERT INTO im_login_failure_logs
|
||||
(user_id,account_type,account_hash,account_mask,failure_type,reason,detail,ip,region,device_id,platform,app_version,user_agent,risk_level,risk_action,request_id,attempt_at,metadata)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
userID, event.AccountType, event.AccountHash, event.AccountMask, event.FailureType, event.Reason, event.Detail,
|
||||
event.IP, event.Region, event.DeviceID, event.Platform, event.AppVersion, event.UserAgent,
|
||||
event.RiskLevel, event.RiskAction, event.RequestID, event.AttemptAt, nullableJSON(metadata))
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantLoginAuditClose(id t.TenantID, sessionID string, at time.Time) error {
|
||||
|
||||
@@ -5,10 +5,18 @@ package mysql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
t "github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
func tenantShardSQL(tenantID t.TenantID, query string) string {
|
||||
query = strings.ReplaceAll(query, "im_topics", tenantTopicsTable(tenantID))
|
||||
query = strings.ReplaceAll(query, "im_group_members", tenantGroupMembersTable(tenantID))
|
||||
query = strings.ReplaceAll(query, "im_subscriptions", tenantSubscriptionsTable(tenantID))
|
||||
return strings.ReplaceAll(query, "im_system_notifications", tenantSystemNotificationsTable(tenantID))
|
||||
}
|
||||
|
||||
func tenantTablePrefix(tenantID t.TenantID) string {
|
||||
return fmt.Sprintf("im_%03d_", tenantID)
|
||||
}
|
||||
@@ -21,6 +29,22 @@ func tenantMessagesTable(tenantID t.TenantID) string {
|
||||
return tenantTablePrefix(tenantID) + "messages"
|
||||
}
|
||||
|
||||
func tenantTopicsTable(tenantID t.TenantID) string {
|
||||
return tenantTablePrefix(tenantID) + "topics"
|
||||
}
|
||||
|
||||
func tenantGroupMembersTable(tenantID t.TenantID) string {
|
||||
return tenantTablePrefix(tenantID) + "group_members"
|
||||
}
|
||||
|
||||
func tenantSubscriptionsTable(tenantID t.TenantID) string {
|
||||
return tenantTablePrefix(tenantID) + "subscriptions"
|
||||
}
|
||||
|
||||
func tenantSystemNotificationsTable(tenantID t.TenantID) string {
|
||||
return tenantTablePrefix(tenantID) + "system_notifications"
|
||||
}
|
||||
|
||||
func tenantGroupMessagesTable(tenantID t.TenantID, month string) (string, bool) {
|
||||
if !validShardMonth(month) {
|
||||
return "", false
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestGroupBusinessTablesCreated(t *testing.T) {
|
||||
tables := []string{
|
||||
"im_group_settings", "im_group_members", "im_group_invitations", "im_group_join_requests",
|
||||
"im_group_announcements", "im_group_announcement_reads", "im_group_mutes", "im_group_qrcodes",
|
||||
"im_group_files", "im_group_admin_logs", "im_group_idempotency",
|
||||
"im_group_files", "im_group_admin_logs",
|
||||
}
|
||||
args := make([]any, len(tables))
|
||||
for i, table := range tables {
|
||||
@@ -103,8 +103,8 @@ func TestHTTPInfrastructureTablesCreated(t *testing.T) {
|
||||
if err := db.Get(&count, query, args...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != len(tables) {
|
||||
t.Fatalf("HTTP infrastructure table count: got %d, want %d", count, len(tables))
|
||||
if count != 0 {
|
||||
t.Fatalf("HTTP idempotency must not create MySQL table: got %d", count)
|
||||
}
|
||||
|
||||
query = "SELECT COUNT(*) FROM information_schema.columns " +
|
||||
@@ -112,8 +112,8 @@ func TestHTTPInfrastructureTablesCreated(t *testing.T) {
|
||||
if err := db.Get(&count, query, args...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != len(tables) {
|
||||
t.Fatalf("HTTP infrastructure tenant_id columns: got %d, want %d", count, len(tables))
|
||||
if count != 0 {
|
||||
t.Fatalf("HTTP idempotency tenant_id columns should not exist: got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user