refactor: retire legacy tinode database routing

This commit is contained in:
richjohnrambo
2026-09-23 17:04:30 +08:00
parent 204043382c
commit aa9f562e1e
19 changed files with 294 additions and 744 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ const (
adapterName = "mongodb"
defaultHost = "localhost:27017"
defaultDatabase = "tinode"
defaultDatabase = "meeting_im"
defaultMaxResults = 1024
// This is capped by the Session's send queue limit (128).
+3
View File
@@ -248,6 +248,9 @@ func (a *adapter) Open(jsonconfig json.RawMessage) error {
if a.dbName == "" {
a.dbName = defaultDatabase
}
if strings.EqualFold(strings.TrimSpace(a.dbName), "tinode") {
return errors.New("mysql database tinode is deprecated; use meeting_im")
}
config.Config.DBName = a.dbName
a.baseConfig = &config
a.tenantDBRouting = config.TenantDBRouting
+15 -3
View File
@@ -4,6 +4,8 @@
package mysql
import (
"encoding/json"
"strings"
"testing"
ms "github.com/go-sql-driver/mysql"
@@ -17,17 +19,17 @@ func TestNormalizeMySQLDSN(t *testing.T) {
}{
{
name: "go driver dsn",
dsn: "root:secret@tcp(127.0.0.1:3306)/tinode?parseTime=true",
dsn: "root:secret@tcp(127.0.0.1:3306)/meeting_im?parseTime=true",
user: "root",
},
{
name: "url scheme dsn",
dsn: "mysql://root:secret@tcp(127.0.0.1:3306)/tinode?parseTime=true",
dsn: "mysql://root:secret@tcp(127.0.0.1:3306)/meeting_im?parseTime=true",
user: "root",
},
{
name: "legacy scheme dsn",
dsn: "mysql:root:secret@tcp(127.0.0.1:3306)/tinode?parseTime=true",
dsn: "mysql:root:secret@tcp(127.0.0.1:3306)/meeting_im?parseTime=true",
user: "root",
},
}
@@ -43,3 +45,13 @@ func TestNormalizeMySQLDSN(t *testing.T) {
})
}
}
func TestOpenRejectsDeprecatedTinodeDatabase(t *testing.T) {
config := json.RawMessage(`{"dsn":"mysql://root:secret@tcp(127.0.0.1:3306)/tinode?parseTime=true"}`)
adp := &adapter{}
err := adp.Open(config)
if err == nil || !strings.Contains(err.Error(), "tinode is deprecated") {
t.Fatalf("Open() error = %v, want deprecated database error", err)
}
}
+1 -1
View File
@@ -36,7 +36,7 @@ const (
adapterName = "rethinkdb"
defaultHost = "localhost:28015"
defaultDatabase = "tinode"
defaultDatabase = "meeting_im"
defaultMaxResults = 1024
// This is capped by the Session's send queue limit (128).
+38 -1
View File
@@ -3816,7 +3816,7 @@ func serveIMFriendRequestCreate(wrt http.ResponseWriter, req *http.Request) {
source := strings.TrimSpace(body.Source)
sourceTopic := strings.TrimSpace(body.SourceTopic)
switch source {
case "search", "qr":
case "search", "qr", "profile_card":
if sourceTopic != "" {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "source_topic is only valid for group source", nil)
return
@@ -6037,6 +6037,10 @@ func serveIMUserProfile(wrt http.ResponseWriter, req *http.Request, identifier s
writeIMSuccess(wrt, req, blockedUserProfilePayload(user, ctx))
return
}
if ctx.Relationship == types.RelationshipNone {
writeIMSuccess(wrt, req, limitedUserProfilePayload(user, ctx, privacy))
return
}
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "profile is private", nil)
return
}
@@ -9030,6 +9034,39 @@ func blockedUserProfilePayload(user *types.User, ctx *types.RelationshipContext)
return data
}
func limitedUserProfilePayload(user *types.User, ctx *types.RelationshipContext, privacy *types.UserPrivacySettings) map[string]any {
data := userSummaryPayload(user, "")
data["username"] = user.Username
if original := userOriginalDisplayName(user); original != "" {
data["profile_display_name"] = original
data["original_display_name"] = original
}
data["relationship"] = ctx.Relationship
data["topic"] = ctx.Topic
data["capabilities"] = map[string]any{
"can_send": false,
"can_call": false,
"can_add_friend": ctx.Relationship == types.RelationshipNone && imPrivacyAllowsAdd(privacy, "profile_card"),
}
if ctx.RequestID != "" {
data["request_id"] = ctx.RequestID
}
if public, ok := user.Public.(map[string]any); ok {
copyLimitedProfileFields(data, public)
} else if public, ok := user.Public.(types.KVMap); ok {
copyLimitedProfileFields(data, map[string]any(public))
}
return data
}
func copyLimitedProfileFields(data map[string]any, public map[string]any) {
for _, key := range []string{"nickname", "public_account"} {
if value, ok := public[key]; ok {
data[key] = value
}
}
}
func copyOptionalProfileFields(data map[string]any, public map[string]any) {
for _, key := range []string{"nickname", "gender", "region", "public_account", "signature"} {
if value, ok := public[key]; ok {
+95
View File
@@ -7549,6 +7549,37 @@ func TestServeIMFriendRequestCreate(t *testing.T) {
}
}
func TestServeIMFriendRequestCreateAllowsProfileCardSource(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockStore := expectIMBearer(t, ctrl, "token-1", 7, 1)
mockStore.EXPECT().GetUidString().Return("card-1")
oldRelations := store.Relations
oldProfiles := store.Profiles
relations := &testRelationsStore{context: &types.RelationshipContext{Relationship: types.RelationshipNone}}
store.Relations = relations
store.Profiles = &testProfilesStore{}
defer func() {
store.Relations = oldRelations
store.Profiles = oldProfiles
}()
req := httptest.NewRequest(http.MethodPost, "/v1/im/friend-requests",
bytes.NewBufferString(`{"target_user_id":"`+types.Uid(2).UserId()+`","message":"你好","source":"profile_card"}`))
req.Header.Set("Authorization", "Bearer token-1")
req.Header.Set("Idempotency-Key", "fr-card-1")
rec := httptest.NewRecorder()
serveIMFriendRequests(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
if relations.created == nil || relations.created.Source != "profile_card" || relations.created.SourceTopic != "" {
t.Fatalf("unexpected friend request created from profile card: %#v", relations.created)
}
}
func TestServeIMFriendRequestCreateReturnsNotFoundForMissingTarget(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
@@ -8105,6 +8136,70 @@ func TestServeIMUserProfile(t *testing.T) {
}
}
func TestServeIMUserProfilePrivateNonFriendReturnsLimitedProfile(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
expectIMBearer(t, ctrl, "token-1", 7, 1)
oldUsers := store.Users
oldRelations := store.Relations
oldProfiles := store.Profiles
users := mock_store.NewMockUsersPersistenceInterface(ctrl)
peer := types.Uid(2)
users.EXPECT().Get(types.TenantID(7), peer).Return(&types.User{
ObjHeader: types.ObjHeader{Id: peer.String()},
TenantID: 7,
Username: "alice7",
Public: map[string]any{
"display_name": "Alice",
"nickname": "Alice",
"avatar_file_id": "file-1",
"signature": "hello",
"gender": "female",
"region": "Shanghai",
"public_account": "alice001",
},
}, nil)
store.Users = users
store.Relations = &testRelationsStore{context: &types.RelationshipContext{
TenantID: 7,
UserID: 1,
PeerID: peer,
Topic: types.Uid(1).P2PName(peer),
Relationship: types.RelationshipNone,
CanSend: false,
}}
store.Profiles = &testProfilesStore{}
defer func() {
store.Users = oldUsers
store.Relations = oldRelations
store.Profiles = oldProfiles
}()
req := httptest.NewRequest(http.MethodGet, "/v1/im/users/"+peer.UserId()+"/profile", nil)
req.Header.Set("Authorization", "Bearer token-1")
rec := httptest.NewRecorder()
serveIMUserResource(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
if data["relationship"] != types.RelationshipNone || data["display_name"] != "Alice" ||
data["avatar_file_id"] != "file-1" || data["public_account"] != "alice001" || data["username"] != "alice7" {
t.Fatalf("unexpected limited profile response: %#v", data)
}
capabilities := data["capabilities"].(map[string]any)
if capabilities["can_send"] != false || capabilities["can_call"] != false || capabilities["can_add_friend"] != true {
t.Fatalf("unexpected limited profile capabilities: %#v", data)
}
for _, field := range []string{"signature", "gender", "region"} {
if _, ok := data[field]; ok {
t.Fatalf("limited profile should omit private field %q: %#v", field, data)
}
}
}
func TestServeIMUserProfileBlockedByMeReturnsMinimalProfile(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
+1 -1
View File
@@ -81,7 +81,7 @@
"use_adapter": "mysql",
"adapters": {
"mysql": {
"dsn": "mysql://root:oWxz*2=sb),ouW%p@tcp(mysql.118im.com:30306)/tinode?parseTime=true&collation=utf8mb4_0900_ai_ci",
"dsn": "mysql://root:oWxz*2=sb),ouW%p@tcp(mysql.118im.com:30306)/meeting_im?parseTime=true&collation=utf8mb4_0900_ai_ci",
"tenant_registry_db": "hotgo",
"tenant_db_routing": true,
"max_open_conns": 64,
@@ -72,7 +72,7 @@ func TestLocalMySQLMessageCryptoFlow(t *testing.T) {
"max_results":1024,
"use_adapter":"mysql",
"adapters":{"mysql":{
"dsn":"mysql://root:root@tcp(127.0.0.1:3306)/tinode?parseTime=true&collation=utf8mb4_0900_ai_ci",
"dsn":"mysql://root:root@tcp(127.0.0.1:3306)/meeting_im?parseTime=true&collation=utf8mb4_0900_ai_ci",
"tenant_registry_db":"hotgo",
"tenant_db_routing":true,
"sql_timeout":10
@@ -86,7 +86,7 @@ func TestLocalMySQLMessageCryptoFlow(t *testing.T) {
store.SetAdminInternalConfigProvider(localCryptoProvider{key: base64.StdEncoding.EncodeToString([]byte("0123456789abcdef0123456789abcdef"))})
store.InvalidateMessageEncryptionKeys(1)
db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/tinode?parseTime=true&collation=utf8mb4_0900_ai_ci")
db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/meeting_im?parseTime=true&collation=utf8mb4_0900_ai_ci")
if err != nil {
t.Fatal(err)
}
+1 -1
View File
@@ -81,7 +81,7 @@
"use_adapter": "mysql",
"adapters": {
"mysql": {
"dsn": "mysql://root:oWxz*2=sb),ouW%p@tcp(mysql.118im.com:30306)/tinode?parseTime=true&collation=utf8mb4_0900_ai_ci",
"dsn": "mysql://root:oWxz*2=sb),ouW%p@tcp(mysql.118im.com:30306)/meeting_im?parseTime=true&collation=utf8mb4_0900_ai_ci",
"tenant_registry_db": "hotgo",
"tenant_db_routing": true,
"max_open_conns": 64,