feat: add im home and message action flows

This commit is contained in:
richjohnrambo
2026-08-12 10:36:41 +08:00
parent d239e37f56
commit 5bd35f97a6
21 changed files with 3087 additions and 61 deletions
+251 -11
View File
@@ -41,7 +41,7 @@ type adapter struct {
}
const (
adpVersion = 118
adpVersion = 122
adapterName = "mysql"
defaultDSN = "root:@tcp(localhost:3306)/tinode?parseTime=true"
@@ -56,6 +56,8 @@ const (
txTimeoutMultiplier = 1.5
)
const defaultTenantID = t.TenantID(1)
type configType struct {
// DB connection settings.
// See https://pkg.go.dev/github.com/go-sql-driver/mysql#Config
@@ -381,6 +383,7 @@ func (a *adapter) CreateDb(reset bool) error {
tenant_id BIGINT NOT NULL,
challenge_id VARCHAR(64) NOT NULL,
scene VARCHAR(32) NOT NULL,
action VARCHAR(64) NOT NULL DEFAULT '',
phone_hash CHAR(64) NOT NULL,
phone_mask VARCHAR(32) NOT NULL,
target_user BIGINT,
@@ -412,6 +415,7 @@ func (a *adapter) CreateDb(reset bool) error {
if _, err = tx.Exec(
`CREATE TABLE users(
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 1,
createdat DATETIME(3) NOT NULL,
updatedat DATETIME(3) NOT NULL,
state SMALLINT NOT NULL DEFAULT 0,
@@ -423,6 +427,7 @@ func (a *adapter) CreateDb(reset bool) error {
trusted JSON,
tags JSON,
PRIMARY KEY(id),
INDEX users_tenant_id_id(tenant_id, id),
INDEX users_state_stateat(state, stateat),
INDEX users_lastseen_updatedat(lastseen, updatedat)
)`); err != nil {
@@ -443,15 +448,24 @@ func (a *adapter) CreateDb(reset bool) error {
return err
}
if err = createIMSecurityTables(tx); err != nil {
return err
}
if err = createIMNotificationTables(tx); err != nil {
return err
}
// Indexed user tags.
if _, err = tx.Exec(
`CREATE TABLE usertags(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL DEFAULT 1,
userid BIGINT NOT NULL,
tag VARCHAR(96) NOT NULL,
PRIMARY KEY(id),
FOREIGN KEY(userid) REFERENCES users(id),
INDEX usertags_tag(tag),
INDEX usertags_tenant_tag(tenant_id, tag),
UNIQUE INDEX usertags_userid_tag(userid, tag)
)`); err != nil {
return err
@@ -461,6 +475,7 @@ func (a *adapter) CreateDb(reset bool) error {
if _, err = tx.Exec(
`CREATE TABLE devices(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL DEFAULT 1,
userid BIGINT NOT NULL,
hash CHAR(16) NOT NULL,
deviceid TEXT NOT NULL,
@@ -469,6 +484,7 @@ func (a *adapter) CreateDb(reset bool) error {
lang VARCHAR(8),
PRIMARY KEY(id),
FOREIGN KEY(userid) REFERENCES users(id),
INDEX devices_tenant_userid(tenant_id, userid),
UNIQUE INDEX devices_hash(hash)
)`); err != nil {
return err
@@ -478,6 +494,7 @@ func (a *adapter) CreateDb(reset bool) error {
if _, err = tx.Exec(
`CREATE TABLE auth(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL DEFAULT 1,
uname VARCHAR(32) NOT NULL,
userid BIGINT NOT NULL,
scheme VARCHAR(16) NOT NULL,
@@ -486,8 +503,8 @@ func (a *adapter) CreateDb(reset bool) error {
expires DATETIME,
PRIMARY KEY(id),
FOREIGN KEY(userid) REFERENCES users(id),
UNIQUE INDEX auth_userid_scheme(userid, scheme),
UNIQUE INDEX auth_uname(uname)
UNIQUE INDEX auth_tenant_uname(tenant_id, uname),
UNIQUE INDEX auth_userid_scheme(userid, scheme)
)`); err != nil {
return err
}
@@ -496,6 +513,7 @@ func (a *adapter) CreateDb(reset bool) error {
if _, err = tx.Exec(
`CREATE TABLE topics(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL DEFAULT 1,
createdat DATETIME(3) NOT NULL,
updatedat DATETIME(3) NOT NULL,
state SMALLINT NOT NULL DEFAULT 0,
@@ -513,8 +531,10 @@ func (a *adapter) CreateDb(reset bool) error {
tags JSON,
aux JSON,
PRIMARY KEY(id),
UNIQUE INDEX topics_tenant_name(tenant_id, name),
UNIQUE INDEX topics_name(name),
INDEX topics_owner(owner),
INDEX topics_tenant_owner(tenant_id, owner),
INDEX topics_state_stateat(state, stateat),
INDEX topics_name_state_seqid(name, state, seqid)
)`); err != nil {
@@ -530,11 +550,13 @@ func (a *adapter) CreateDb(reset bool) error {
if _, err = tx.Exec(
`CREATE TABLE topictags(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL DEFAULT 1,
topic CHAR(25) NOT NULL,
tag VARCHAR(96) NOT NULL,
PRIMARY KEY(id),
FOREIGN KEY(topic) REFERENCES topics(name),
INDEX topictags_tag(tag),
INDEX topictags_tenant_tag(tenant_id, tag),
UNIQUE INDEX topictags_topic_tag(topic, tag)
)`); err != nil {
return err
@@ -544,6 +566,7 @@ func (a *adapter) CreateDb(reset bool) error {
if _, err = tx.Exec(
`CREATE TABLE subscriptions(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL DEFAULT 1,
createdat DATETIME(3) NOT NULL,
updatedat DATETIME(3) NOT NULL,
deletedat DATETIME(3),
@@ -557,6 +580,8 @@ func (a *adapter) CreateDb(reset bool) error {
private JSON,
PRIMARY KEY(id),
FOREIGN KEY(userid) REFERENCES users(id),
INDEX subscriptions_tenant_userid(tenant_id, userid),
INDEX subscriptions_tenant_topic(tenant_id, topic),
UNIQUE INDEX subscriptions_topic_userid(topic, userid),
INDEX subscriptions_topic(topic),
INDEX subscriptions_deletedat(deletedat),
@@ -569,6 +594,7 @@ func (a *adapter) CreateDb(reset bool) error {
if _, err = tx.Exec(
`CREATE TABLE messages(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL DEFAULT 1,
createdat DATETIME(3) NOT NULL,
updatedat DATETIME(3) NOT NULL,
deletedat DATETIME(3),
@@ -576,19 +602,46 @@ func (a *adapter) CreateDb(reset bool) error {
seqid INT NOT NULL,
topic CHAR(25) NOT NULL,` +
"`from` BIGINT NOT NULL," +
`head JSON,
`client_msg_id VARCHAR(64),
version INT NOT NULL DEFAULT 1,
recalled_at DATETIME(3),
recalled_by BIGINT,
head JSON,
content JSON,
PRIMARY KEY(id),
FOREIGN KEY(topic) REFERENCES topics(name),
INDEX messages_tenant_topic(tenant_id, topic),
UNIQUE INDEX messages_sender_client_mid(tenant_id, ` + "`from`" + `, client_msg_id),
UNIQUE INDEX messages_topic_seqid(topic, seqid)
)`); err != nil {
return err
}
if _, err = tx.Exec(
`CREATE TABLE im_message_revisions(
id BIGINT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
createdat DATETIME(3) NOT NULL,
updatedat DATETIME(3) NOT NULL,
topic CHAR(25) NOT NULL,
seqid INT NOT NULL,
version INT NOT NULL,
actor BIGINT NOT NULL,
head JSON,
content JSON,
reason VARCHAR(256) NOT NULL DEFAULT '',
PRIMARY KEY(id),
UNIQUE INDEX uk_im_message_revisions_version(tenant_id, topic, seqid, version),
CONSTRAINT fk_im_message_revisions_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
)`); err != nil {
return err
}
// Deletion log
if _, err = tx.Exec(
`CREATE TABLE dellog(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL DEFAULT 1,
topic CHAR(25) NOT NULL,
deletedfor BIGINT NOT NULL DEFAULT 0,
delid INT NOT NULL,
@@ -596,6 +649,7 @@ func (a *adapter) CreateDb(reset bool) error {
hi INT NOT NULL,
PRIMARY KEY(id),
FOREIGN KEY(topic) REFERENCES topics(name),
INDEX dellog_tenant_topic(tenant_id, topic),
INDEX dellog_topic_delid_deletedfor(topic,delid,deletedfor),
INDEX dellog_topic_deletedfor_low_hi(topic,deletedfor,low,hi),
INDEX dellog_deletedfor(deletedfor)
@@ -607,6 +661,7 @@ func (a *adapter) CreateDb(reset bool) error {
if _, err = tx.Exec(
`CREATE TABLE credentials(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL DEFAULT 1,
createdat DATETIME(3) NOT NULL,
updatedat DATETIME(3) NOT NULL,
deletedat DATETIME(3),
@@ -619,7 +674,7 @@ func (a *adapter) CreateDb(reset bool) error {
retries INT NOT NULL DEFAULT 0,
PRIMARY KEY(id),
FOREIGN KEY(userid) REFERENCES users(id),
UNIQUE credentials_uniqueness(synthetic)
UNIQUE credentials_tenant_uniqueness(tenant_id, synthetic)
)`); err != nil {
return err
}
@@ -629,6 +684,7 @@ func (a *adapter) CreateDb(reset bool) error {
if _, err = tx.Exec(
`CREATE TABLE fileuploads(
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 1,
createdat DATETIME(3) NOT NULL,
updatedat DATETIME(3) NOT NULL,
userid BIGINT,
@@ -638,6 +694,7 @@ func (a *adapter) CreateDb(reset bool) error {
etag VARCHAR(128),
location VARCHAR(2048) NOT NULL,
PRIMARY KEY(id),
INDEX fileuploads_tenant_status(tenant_id, status),
INDEX fileuploads_status(status)
)`); err != nil {
return err
@@ -647,12 +704,14 @@ func (a *adapter) CreateDb(reset bool) error {
if _, err = tx.Exec(
`CREATE TABLE filemsglinks(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL DEFAULT 1,
createdat DATETIME(3) NOT NULL,
fileid BIGINT NOT NULL,
msgid INT,
topic CHAR(25),
userid BIGINT,
PRIMARY KEY(id),
INDEX filemsglinks_tenant_fileid(tenant_id, fileid),
FOREIGN KEY(fileid) REFERENCES fileuploads(id) ON DELETE CASCADE,
FOREIGN KEY(msgid) REFERENCES messages(id) ON DELETE CASCADE,
FOREIGN KEY(topic) REFERENCES topics(name) ON DELETE CASCADE,
@@ -984,6 +1043,7 @@ func (a *adapter) UpgradeDb() error {
tenant_id BIGINT NOT NULL,
challenge_id VARCHAR(64) NOT NULL,
scene VARCHAR(32) NOT NULL,
action VARCHAR(64) NOT NULL DEFAULT '',
phone_hash CHAR(64) NOT NULL,
phone_mask VARCHAR(32) NOT NULL,
target_user BIGINT,
@@ -1031,6 +1091,64 @@ func (a *adapter) UpgradeDb() error {
}
}
if a.version == 118 {
if _, err := a.db.Exec("ALTER TABLE im_verification_codes ADD COLUMN action VARCHAR(64) NOT NULL DEFAULT '' AFTER scene"); err != nil {
return err
}
tx, err := a.db.Begin()
if err != nil {
return err
}
if err = createIMSecurityTables(tx); err != nil {
tx.Rollback()
return err
}
if err = tx.Commit(); err != nil {
return err
}
if err := bumpVersion(a, 119); err != nil {
return err
}
}
if a.version == 119 {
tx, err := a.db.Begin()
if err != nil {
return err
}
if err = createIMNotificationTables(tx); err != nil {
tx.Rollback()
return err
}
if err = tx.Commit(); err != nil {
return err
}
if err := bumpVersion(a, 120); err != nil {
return err
}
}
if a.version == 120 {
if _, err := a.db.Exec("ALTER TABLE messages ADD COLUMN client_msg_id VARCHAR(64) AFTER `from`"); err != nil {
return err
}
if _, err := a.db.Exec("CREATE UNIQUE INDEX messages_sender_client_mid ON messages(tenant_id, `from`, client_msg_id)"); err != nil {
return err
}
if err := bumpVersion(a, 121); err != nil {
return err
}
}
if a.version == 121 {
if err := upgradeIMMessageActions(a); err != nil {
return err
}
if err := bumpVersion(a, 122); err != nil {
return err
}
}
if a.version != adpVersion {
return errors.New("Failed to perform database upgrade to version " + strconv.Itoa(adpVersion) +
". DB is still at " + strconv.Itoa(a.version))
@@ -1038,6 +1156,121 @@ func (a *adapter) UpgradeDb() error {
return nil
}
func createIMNotificationTables(tx *sql.Tx) error {
_, err := tx.Exec(
`CREATE TABLE im_system_notifications(
id BIGINT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
event_id VARCHAR(64) NOT NULL,
event_type VARCHAR(64) NOT NULL,
title VARCHAR(128) NOT NULL,
summary VARCHAR(512) NOT NULL,
action JSON,
read_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_system_notifications_event(tenant_id, user_id, event_id),
INDEX idx_im_system_notifications_inbox(tenant_id, user_id, id DESC),
CONSTRAINT fk_im_system_notifications_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
)`)
return err
}
func upgradeIMMessageActions(a *adapter) error {
statements := []string{
"ALTER TABLE messages ADD COLUMN version INT NOT NULL DEFAULT 1 AFTER client_msg_id",
"ALTER TABLE messages ADD COLUMN recalled_at DATETIME(3) AFTER version",
"ALTER TABLE messages ADD COLUMN recalled_by BIGINT AFTER recalled_at",
`CREATE TABLE im_message_revisions(
id BIGINT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
createdat DATETIME(3) NOT NULL,
updatedat DATETIME(3) NOT NULL,
topic CHAR(25) NOT NULL,
seqid INT NOT NULL,
version INT NOT NULL,
actor BIGINT NOT NULL,
head JSON,
content JSON,
reason VARCHAR(256) NOT NULL DEFAULT '',
PRIMARY KEY(id),
UNIQUE INDEX uk_im_message_revisions_version(tenant_id, topic, seqid, version),
CONSTRAINT fk_im_message_revisions_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
)`,
}
for _, stmt := range statements {
if _, err := a.db.Exec(stmt); err != nil {
return err
}
}
return nil
}
func createIMSecurityTables(tx *sql.Tx) error {
if _, err := tx.Exec(
`CREATE TABLE im_realname_verifications(
id BIGINT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
submission_id VARCHAR(64) NOT NULL,
status VARCHAR(16) NOT NULL,
name_cipher TEXT NOT NULL,
id_type VARCHAR(32) NOT NULL,
id_number_cipher TEXT NOT NULL,
id_number_hash CHAR(64) NOT NULL,
masked_name VARCHAR(64) NOT NULL,
masked_id_number VARCHAR(64) NOT NULL,
review_reason VARCHAR(256),
verified_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_realname_submission(tenant_id, submission_id),
INDEX idx_im_realname_user_latest(tenant_id, user_id, created_at),
INDEX idx_im_realname_id_hash(tenant_id, id_number_hash),
CONSTRAINT fk_im_realname_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
)`); err != nil {
return err
}
if _, err := tx.Exec(
`CREATE TABLE im_action_tokens(
id BIGINT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
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(tenant_id, token_id),
INDEX idx_im_action_tokens_user_action(tenant_id, user_id, action, target, state, expires_at),
CONSTRAINT fk_im_action_tokens_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
)`); err != nil {
return err
}
_, err := tx.Exec(
`CREATE TABLE im_youth_mode_settings(
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
purpose VARCHAR(64) NOT NULL,
password_hash VARBINARY(255) NOT NULL,
failed_attempts INT NOT NULL DEFAULT 0,
locked_until 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(tenant_id, user_id, purpose),
CONSTRAINT fk_im_youth_settings_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
)`)
return err
}
// Create system topic 'sys'.
func createSystemTopic(tx *sql.Tx) error {
now := t.TimeNow()
@@ -1117,9 +1350,12 @@ func removeTags(tx *sqlx.Tx, table, keyName string, keyVal any, tags []string) e
// UserCreate creates a new user. Returns error and true if error is due to duplicate user name,
// false for any other error
func (a *adapter) UserCreate(user *t.User) error {
if user == nil || user.TenantID.IsZero() {
if user == nil {
return t.ErrMalformed
}
if user.TenantID.IsZero() {
user.TenantID = defaultTenantID
}
ctx, cancel := a.getContextForTx()
if cancel != nil {
defer cancel()
@@ -1955,9 +2191,13 @@ func (a *adapter) TopicCreate(topic *t.Topic) error {
// If undelete = true - update subscription on duplicate key, otherwise ignore the duplicate.
func createSubscription(tx *sqlx.Tx, sub *t.Subscription, undelete bool) error {
if sub == nil || sub.TenantID.IsZero() {
if sub == nil {
return t.ErrMalformed
}
tenantID := sub.TenantID
if tenantID.IsZero() {
tenantID = defaultTenantID
}
isOwner := (sub.ModeGiven & sub.ModeWant).IsOwner()
@@ -1966,26 +2206,26 @@ func createSubscription(tx *sqlx.Tx, sub *t.Subscription, undelete bool) error {
_, err := tx.Exec(
"INSERT INTO subscriptions(tenant_id,createdat,updatedat,deletedat,userid,topic,modeWant,modeGiven,private) "+
"VALUES(?,?,?,NULL,?,?,?,?,?)",
sub.TenantID, sub.CreatedAt, sub.UpdatedAt, decoded_uid, sub.Topic, sub.ModeWant.String(), sub.ModeGiven.String(), jpriv)
tenantID, sub.CreatedAt, sub.UpdatedAt, decoded_uid, sub.Topic, sub.ModeWant.String(), sub.ModeGiven.String(), jpriv)
if err != nil && isDupe(err) {
if undelete {
_, err = tx.Exec("UPDATE 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(), sub.TenantID,
sub.CreatedAt, sub.UpdatedAt, sub.ModeWant.String(), sub.ModeGiven.String(), tenantID,
sub.Topic, decoded_uid)
} else {
_, err = tx.Exec("UPDATE 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,
sub.TenantID, sub.Topic, decoded_uid)
tenantID, sub.Topic, decoded_uid)
}
}
if err == nil && isOwner {
// Update topic owner if the subscription is with owner rights.
// Don't increment subscriber count here - it's done in TopicShare in bulk.
_, err = tx.Exec("UPDATE topics SET owner=? WHERE tenant_id=? AND name=?", decoded_uid, sub.TenantID, sub.Topic)
_, err = tx.Exec("UPDATE topics SET owner=? WHERE tenant_id=? AND name=?", decoded_uid, tenantID, sub.Topic)
}
return err
}