feat: support group invite privacy verification
This commit is contained in:
+3
-3
@@ -2714,7 +2714,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
|
|||||||
|
|
||||||
**状态:新增。**
|
**状态:新增。**
|
||||||
|
|
||||||
- `GET /v1/im/me/privacy` 返回 `add_me_methods,profile_visibility,last_seen_visibility,read_receipts,version` 和各字段 `editable`。
|
- `GET /v1/im/me/privacy` 返回 `add_me_methods,profile_visibility,last_seen_visibility,read_receipts,group_invite_verification,version` 和各字段 `editable`;`group_invite_verification=true` 表示别人邀请我入群时需要走入群审批。
|
||||||
- `PATCH /v1/im/me/privacy` 只提交变更字段和 `version`;成功返回完整最新配置并向其他设备推送 `privacy.updated`。
|
- `PATCH /v1/im/me/privacy` 只提交变更字段和 `version`;成功返回完整最新配置并向其他设备推送 `privacy.updated`。
|
||||||
- 黑名单入口使用 5.12。服务端在搜索、主页、回执等实际读写路径执行隐私规则,不能只保存开关。
|
- 黑名单入口使用 5.12。服务端在搜索、主页、回执等实际读写路径执行隐私规则,不能只保存开关。
|
||||||
|
|
||||||
@@ -2822,8 +2822,8 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
|
|||||||
| `GET /v1/im/me` | Query only | `{"user_id":"usr...","profile":{"public_account":"usr...","nickname":"张三","avatar_file_id":"","signature":"","version":1},"profile_version":1,"realname":{"status":"unverified"},"youth_mode":{"enabled":false}}` |
|
| `GET /v1/im/me` | Query only | `{"user_id":"usr...","profile":{"public_account":"usr...","nickname":"张三","avatar_file_id":"","signature":"","version":1},"profile_version":1,"realname":{"status":"unverified"},"youth_mode":{"enabled":false}}` |
|
||||||
| `GET /v1/im/me/profile` | Query only | `{"avatar_file_id":"","nickname":"张三","birth_date":"1990-01-01","signature":"签名","gender":"male","region":"上海","email":"a@example.com","public_account":"usr...","version":1,"field_rules":{"nickname":{"min_length":1,"max_length":32},"signature":{"max_length":120}}}` |
|
| `GET /v1/im/me/profile` | Query only | `{"avatar_file_id":"","nickname":"张三","birth_date":"1990-01-01","signature":"签名","gender":"male","region":"上海","email":"a@example.com","public_account":"usr...","version":1,"field_rules":{"nickname":{"min_length":1,"max_length":32},"signature":{"max_length":120}}}` |
|
||||||
| `PATCH /v1/im/me/profile` | `{"nickname":"新昵称","signature":"新签名","version":1}` | `{"avatar_file_id":"","nickname":"新昵称","signature":"新签名","version":2,"updated_at":"2026-08-20T09:00:00Z"}` |
|
| `PATCH /v1/im/me/profile` | `{"nickname":"新昵称","signature":"新签名","version":1}` | `{"avatar_file_id":"","nickname":"新昵称","signature":"新签名","version":2,"updated_at":"2026-08-20T09:00:00Z"}` |
|
||||||
| `GET /v1/im/me/privacy` | Query only | `{"add_me_methods":{"account":true,"phone":true,"qr":true,"group":true},"profile_visibility":"friends","last_seen_visibility":"friends","read_receipts":true,"version":1,"editable":{"add_me_methods":true,"profile_visibility":true,"last_seen_visibility":true,"read_receipts":true}}` |
|
| `GET /v1/im/me/privacy` | Query only | `{"add_me_methods":{"account":true,"phone":true,"qr":true,"group":true},"profile_visibility":"friends","last_seen_visibility":"friends","read_receipts":true,"group_invite_verification":false,"version":1,"editable":{"add_me_methods":true,"profile_visibility":true,"last_seen_visibility":true,"read_receipts":true,"group_invite_verification":true}}` |
|
||||||
| `PATCH /v1/im/me/privacy` | `{"add_me_methods":{"phone":false},"read_receipts":false,"version":1}` | `{"add_me_methods":{"account":true,"phone":false,"qr":true,"group":true},"read_receipts":false,"version":2,"updated_at":"2026-08-20T09:00:00Z"}` |
|
| `PATCH /v1/im/me/privacy` | `{"add_me_methods":{"phone":false},"read_receipts":false,"group_invite_verification":true,"version":1}` | `{"add_me_methods":{"account":true,"phone":false,"qr":true,"group":true},"read_receipts":false,"group_invite_verification":true,"version":2,"updated_at":"2026-08-20T09:00:00Z"}` |
|
||||||
| `GET /v1/im/settings/youth-mode` | Query only | `{"enabled":false,"policy_version":1,"restrictions":["night_quiet","content_filter","payment_guard"],"restriction_items":[{"code":"night_quiet","title":"深夜免打扰","description":"夜间减少消息提醒打扰,帮助用户保持健康作息。","enforced":false},{"code":"content_filter","title":"内容过滤","description":"对不适合青少年的内容进行识别和限制展示。","enforced":false},{"code":"payment_guard","title":"支付守护","description":"限制或确认支付、充值等敏感消费行为。","enforced":false}],"password_set":false,"can_enable":false,"updated_at":"2026-08-20T09:00:00Z"}` |
|
| `GET /v1/im/settings/youth-mode` | Query only | `{"enabled":false,"policy_version":1,"restrictions":["night_quiet","content_filter","payment_guard"],"restriction_items":[{"code":"night_quiet","title":"深夜免打扰","description":"夜间减少消息提醒打扰,帮助用户保持健康作息。","enforced":false},{"code":"content_filter","title":"内容过滤","description":"对不适合青少年的内容进行识别和限制展示。","enforced":false},{"code":"payment_guard","title":"支付守护","description":"限制或确认支付、充值等敏感消费行为。","enforced":false}],"password_set":false,"can_enable":false,"updated_at":"2026-08-20T09:00:00Z"}` |
|
||||||
| `POST /v1/im/settings/independent-password` | `{"password":"1234","purpose":"youth_mode","action_token":"action_token"}` | `{"status":"ok","password_set":true,"purpose":"youth_mode","enable_action_token":"enable_token","enable_action_expires_at":"2026-08-20T09:05:00Z","next_action":"enable_youth_mode"}` |
|
| `POST /v1/im/settings/independent-password` | `{"password":"1234","purpose":"youth_mode","action_token":"action_token"}` | `{"status":"ok","password_set":true,"purpose":"youth_mode","enable_action_token":"enable_token","enable_action_expires_at":"2026-08-20T09:05:00Z","next_action":"enable_youth_mode"}` |
|
||||||
| `POST /v1/im/settings/independent-password/verify` | `{"password":"1234","purpose":"close_youth_mode"}` | `{"action_token":"action_token","action":"close_youth_mode","expires_at":"2026-08-20T09:05:00Z"}` |
|
| `POST /v1/im/settings/independent-password/verify` | `{"password":"1234","purpose":"close_youth_mode"}` | `{"action_token":"action_token","action":"close_youth_mode","expires_at":"2026-08-20T09:05:00Z"}` |
|
||||||
|
|||||||
@@ -732,6 +732,7 @@ CREATE TABLE IF NOT EXISTS tinode.im_user_privacy_settings (
|
|||||||
profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
||||||
last_seen_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
last_seen_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
||||||
read_receipts TINYINT NOT NULL DEFAULT 1,
|
read_receipts TINYINT NOT NULL DEFAULT 1,
|
||||||
|
group_invite_verification TINYINT NOT NULL DEFAULT 0,
|
||||||
version INT NOT NULL DEFAULT 1,
|
version INT NOT NULL DEFAULT 1,
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(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),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- Add personal privacy switch for requiring approval when others invite the user into a group.
|
||||||
|
-- Default is off to preserve existing behavior.
|
||||||
|
|
||||||
|
SET @im_sql = (
|
||||||
|
SELECT IF(
|
||||||
|
COUNT(*) = 0,
|
||||||
|
'ALTER TABLE tinode.im_user_privacy_settings ADD COLUMN group_invite_verification TINYINT NOT NULL DEFAULT 0 AFTER read_receipts',
|
||||||
|
'SELECT 1'
|
||||||
|
)
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'tinode'
|
||||||
|
AND table_name = 'im_user_privacy_settings'
|
||||||
|
AND column_name = 'group_invite_verification'
|
||||||
|
);
|
||||||
|
PREPARE im_stmt FROM @im_sql;
|
||||||
|
EXECUTE im_stmt;
|
||||||
|
DEALLOCATE PREPARE im_stmt;
|
||||||
@@ -383,6 +383,7 @@ func main() {
|
|||||||
profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
||||||
last_seen_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
last_seen_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
||||||
read_receipts TINYINT NOT NULL DEFAULT 1,
|
read_receipts TINYINT NOT NULL DEFAULT 1,
|
||||||
|
group_invite_verification TINYINT NOT NULL DEFAULT 0,
|
||||||
version INT NOT NULL DEFAULT 1,
|
version INT NOT NULL DEFAULT 1,
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(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),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
@@ -539,6 +540,7 @@ func main() {
|
|||||||
"ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN enabled_at DATETIME(3) AFTER restrictions",
|
"ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN enabled_at DATETIME(3) AFTER restrictions",
|
||||||
"ALTER TABLE tinode.im_users ADD COLUMN phone VARCHAR(32) NULL AFTER useragent",
|
"ALTER TABLE tinode.im_users ADD COLUMN phone VARCHAR(32) NULL AFTER useragent",
|
||||||
"ALTER TABLE tinode.im_user_profiles ADD COLUMN email VARCHAR(128) NOT NULL DEFAULT '' AFTER region",
|
"ALTER TABLE tinode.im_user_profiles ADD COLUMN email VARCHAR(128) NOT NULL DEFAULT '' AFTER region",
|
||||||
|
"ALTER TABLE tinode.im_user_privacy_settings ADD COLUMN group_invite_verification TINYINT NOT NULL DEFAULT 0 AFTER read_receipts",
|
||||||
} {
|
} {
|
||||||
if _, err = db.Exec(stmt); err != nil && !strings.Contains(err.Error(), "Duplicate column name") {
|
if _, err = db.Exec(stmt); err != nil && !strings.Contains(err.Error(), "Duplicate column name") {
|
||||||
fatal(fmt.Errorf("exec failed: %s: %w", stmt, err))
|
fatal(fmt.Errorf("exec failed: %s: %w", stmt, err))
|
||||||
|
|||||||
@@ -1851,6 +1851,7 @@ func createIMUserProductTables(tx *sql.Tx) error {
|
|||||||
profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
||||||
last_seen_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
last_seen_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
||||||
read_receipts TINYINT NOT NULL DEFAULT 1,
|
read_receipts TINYINT NOT NULL DEFAULT 1,
|
||||||
|
group_invite_verification TINYINT NOT NULL DEFAULT 0,
|
||||||
version INT NOT NULL DEFAULT 1,
|
version INT NOT NULL DEFAULT 1,
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(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),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
|||||||
@@ -6201,6 +6201,10 @@ func (a *adapter) TenantUserPrivacyUpdate(id t.TenantID, uid t.Uid, patch t.User
|
|||||||
sets = append(sets, "read_receipts=?")
|
sets = append(sets, "read_receipts=?")
|
||||||
args = append(args, *patch.ReadReceipts)
|
args = append(args, *patch.ReadReceipts)
|
||||||
}
|
}
|
||||||
|
if patch.GroupInviteVerification != nil {
|
||||||
|
sets = append(sets, "group_invite_verification=?")
|
||||||
|
args = append(args, *patch.GroupInviteVerification)
|
||||||
|
}
|
||||||
args = append(args, decodeUID(uid), patch.Version)
|
args = append(args, decodeUID(uid), patch.Version)
|
||||||
res, err := a.db.ExecContext(ctx, `UPDATE im_user_privacy_settings SET `+strings.Join(sets, ",")+` WHERE user_id=? AND version=?`, args...)
|
res, err := a.db.ExecContext(ctx, `UPDATE im_user_privacy_settings SET `+strings.Join(sets, ",")+` WHERE user_id=? AND version=?`, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -6830,12 +6834,13 @@ func (a *adapter) userPrivacyByUID(ctx context.Context, uid t.Uid) (*t.UserPriva
|
|||||||
ProfileVisibility string `db:"profile_visibility"`
|
ProfileVisibility string `db:"profile_visibility"`
|
||||||
LastSeenVisibility string `db:"last_seen_visibility"`
|
LastSeenVisibility string `db:"last_seen_visibility"`
|
||||||
ReadReceipts bool `db:"read_receipts"`
|
ReadReceipts bool `db:"read_receipts"`
|
||||||
|
GroupInviteVerification bool `db:"group_invite_verification"`
|
||||||
Version int
|
Version int
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}{}
|
}{}
|
||||||
if err := sqlx.GetContext(ctx, a.db, &row, `SELECT user_id,add_me_methods,profile_visibility,last_seen_visibility,
|
if err := sqlx.GetContext(ctx, a.db, &row, `SELECT user_id,add_me_methods,profile_visibility,last_seen_visibility,
|
||||||
read_receipts,version,created_at,updated_at FROM im_user_privacy_settings WHERE user_id=?`, decodeUID(uid)); err != nil {
|
read_receipts,group_invite_verification,version,created_at,updated_at FROM im_user_privacy_settings WHERE user_id=?`, decodeUID(uid)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
settings := &t.UserPrivacySettings{
|
settings := &t.UserPrivacySettings{
|
||||||
@@ -6844,6 +6849,7 @@ func (a *adapter) userPrivacyByUID(ctx context.Context, uid t.Uid) (*t.UserPriva
|
|||||||
ProfileVisibility: row.ProfileVisibility,
|
ProfileVisibility: row.ProfileVisibility,
|
||||||
LastSeenVisibility: row.LastSeenVisibility,
|
LastSeenVisibility: row.LastSeenVisibility,
|
||||||
ReadReceipts: row.ReadReceipts,
|
ReadReceipts: row.ReadReceipts,
|
||||||
|
GroupInviteVerification: row.GroupInviteVerification,
|
||||||
Version: row.Version,
|
Version: row.Version,
|
||||||
CreatedAt: row.CreatedAt,
|
CreatedAt: row.CreatedAt,
|
||||||
UpdatedAt: row.UpdatedAt,
|
UpdatedAt: row.UpdatedAt,
|
||||||
|
|||||||
@@ -499,6 +499,12 @@ func (a *adapter) TenantGroupInvitationAccept(id t.TenantID, invitationID string
|
|||||||
if inv.Status == t.GroupInvitationStatusJoined || inv.Result == "joined" {
|
if inv.Status == t.GroupInvitationStatusJoined || inv.Result == "joined" {
|
||||||
return &inv, nil
|
return &inv, nil
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(inv.JoinRequestID) != "" {
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &inv, nil
|
||||||
|
}
|
||||||
var deletedAt sql.NullTime
|
var deletedAt sql.NullTime
|
||||||
err = tx.QueryRowxContext(ctx, "SELECT deletedat FROM im_subscriptions WHERE topic=? AND userid=?",
|
err = tx.QueryRowxContext(ctx, "SELECT deletedat FROM im_subscriptions WHERE topic=? AND userid=?",
|
||||||
inv.Topic, decodeUID(uid)).Scan(&deletedAt)
|
inv.Topic, decodeUID(uid)).Scan(&deletedAt)
|
||||||
|
|||||||
+33
-3
@@ -3181,6 +3181,7 @@ func serveIMMePrivacy(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
ProfileVisibility *string `json:"profile_visibility"`
|
ProfileVisibility *string `json:"profile_visibility"`
|
||||||
LastSeenVisibility *string `json:"last_seen_visibility"`
|
LastSeenVisibility *string `json:"last_seen_visibility"`
|
||||||
ReadReceipts *bool `json:"read_receipts"`
|
ReadReceipts *bool `json:"read_receipts"`
|
||||||
|
GroupInviteVerification *bool `json:"group_invite_verification"`
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
}
|
}
|
||||||
if !decodeIMJSONBody(wrt, req, &body) {
|
if !decodeIMJSONBody(wrt, req, &body) {
|
||||||
@@ -3191,6 +3192,7 @@ func serveIMMePrivacy(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
ProfileVisibility: body.ProfileVisibility,
|
ProfileVisibility: body.ProfileVisibility,
|
||||||
LastSeenVisibility: body.LastSeenVisibility,
|
LastSeenVisibility: body.LastSeenVisibility,
|
||||||
ReadReceipts: body.ReadReceipts,
|
ReadReceipts: body.ReadReceipts,
|
||||||
|
GroupInviteVerification: body.GroupInviteVerification,
|
||||||
Version: body.Version,
|
Version: body.Version,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -5351,8 +5353,24 @@ func serveIMGroupInvitations(wrt http.ResponseWriter, req *http.Request, topic s
|
|||||||
} else {
|
} else {
|
||||||
idempotencyKey = invitationID
|
idempotencyKey = invitationID
|
||||||
}
|
}
|
||||||
|
targetRequiresApproval := settings != nil && settings.JoinApproval
|
||||||
|
if !targetRequiresApproval && store.Profiles != nil {
|
||||||
|
privacy, privacyErr := store.Profiles.GetPrivacy(principal.TenantID, uid)
|
||||||
|
if privacyErr != nil {
|
||||||
|
if errors.Is(privacyErr, types.ErrNotFound) || errors.Is(privacyErr, types.ErrUnsupported) {
|
||||||
|
privacyErr = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if privacyErr != nil {
|
||||||
|
item["status"] = "failed"
|
||||||
|
item["reason"] = relationErrorReason(privacyErr)
|
||||||
|
results = append(results, item)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
targetRequiresApproval = privacy != nil && privacy.GroupInviteVerification
|
||||||
|
}
|
||||||
joinRequestID := ""
|
joinRequestID := ""
|
||||||
if settings != nil && settings.JoinApproval {
|
if targetRequiresApproval {
|
||||||
joinRequestID = "gjr_" + store.Store.GetUidString()
|
joinRequestID = "gjr_" + store.Store.GetUidString()
|
||||||
}
|
}
|
||||||
inv, err := store.GroupBusiness.CreateInvitation(principal.TenantID, &types.GroupInvitation{
|
inv, err := store.GroupBusiness.CreateInvitation(principal.TenantID, &types.GroupInvitation{
|
||||||
@@ -5376,7 +5394,7 @@ func serveIMGroupInvitations(wrt http.ResponseWriter, req *http.Request, topic s
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
item["invitation_id"] = inv.InvitationID
|
item["invitation_id"] = inv.InvitationID
|
||||||
item["status"] = inv.Status
|
item["status"] = groupInvitationDisplayStatus(inv)
|
||||||
item["expires_at"] = inv.ExpiresAt.Format(time.RFC3339Nano)
|
item["expires_at"] = inv.ExpiresAt.Format(time.RFC3339Nano)
|
||||||
if inv.JoinRequestID != "" {
|
if inv.JoinRequestID != "" {
|
||||||
item["join_request_id"] = inv.JoinRequestID
|
item["join_request_id"] = inv.JoinRequestID
|
||||||
@@ -7112,7 +7130,7 @@ func groupInvitationPayload(inv *types.GroupInvitation) map[string]any {
|
|||||||
"topic": inv.Topic,
|
"topic": inv.Topic,
|
||||||
"inviter_id": inv.InviterUserID.UserId(),
|
"inviter_id": inv.InviterUserID.UserId(),
|
||||||
"target_user_id": inv.TargetUserID.UserId(),
|
"target_user_id": inv.TargetUserID.UserId(),
|
||||||
"status": inv.Status,
|
"status": groupInvitationDisplayStatus(inv),
|
||||||
"result": inv.Result,
|
"result": inv.Result,
|
||||||
"expires_at": inv.ExpiresAt.Format(time.RFC3339Nano),
|
"expires_at": inv.ExpiresAt.Format(time.RFC3339Nano),
|
||||||
}
|
}
|
||||||
@@ -7125,6 +7143,16 @@ func groupInvitationPayload(inv *types.GroupInvitation) map[string]any {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func groupInvitationDisplayStatus(inv *types.GroupInvitation) string {
|
||||||
|
if inv == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if inv.Status == types.GroupInvitationStatusPending && strings.TrimSpace(inv.JoinRequestID) != "" {
|
||||||
|
return types.GroupInvitationStatusPendingApproval
|
||||||
|
}
|
||||||
|
return inv.Status
|
||||||
|
}
|
||||||
|
|
||||||
func groupFilePayloads(files []types.GroupFile) []map[string]any {
|
func groupFilePayloads(files []types.GroupFile) []map[string]any {
|
||||||
items := make([]map[string]any, 0, len(files))
|
items := make([]map[string]any, 0, len(files))
|
||||||
for i := range files {
|
for i := range files {
|
||||||
@@ -9214,6 +9242,7 @@ func userPrivacyPayload(settings *types.UserPrivacySettings) map[string]any {
|
|||||||
"profile_visibility": settings.ProfileVisibility,
|
"profile_visibility": settings.ProfileVisibility,
|
||||||
"last_seen_visibility": settings.LastSeenVisibility,
|
"last_seen_visibility": settings.LastSeenVisibility,
|
||||||
"read_receipts": settings.ReadReceipts,
|
"read_receipts": settings.ReadReceipts,
|
||||||
|
"group_invite_verification": settings.GroupInviteVerification,
|
||||||
"version": settings.Version,
|
"version": settings.Version,
|
||||||
"updated_at": settings.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
"updated_at": settings.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
||||||
"editable": map[string]any{
|
"editable": map[string]any{
|
||||||
@@ -9221,6 +9250,7 @@ func userPrivacyPayload(settings *types.UserPrivacySettings) map[string]any {
|
|||||||
"profile_visibility": true,
|
"profile_visibility": true,
|
||||||
"last_seen_visibility": true,
|
"last_seen_visibility": true,
|
||||||
"read_receipts": true,
|
"read_receipts": true,
|
||||||
|
"group_invite_verification": true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+72
-4
@@ -451,6 +451,9 @@ func (s *testGroupBusinessStore) AcceptInvitation(_ types.TenantID, invitationID
|
|||||||
for i := range s.invitations {
|
for i := range s.invitations {
|
||||||
if s.invitations[i].InvitationID == invitationID {
|
if s.invitations[i].InvitationID == invitationID {
|
||||||
s.invitations[i].TargetUserID = uid
|
s.invitations[i].TargetUserID = uid
|
||||||
|
if s.invitations[i].JoinRequestID != "" {
|
||||||
|
return &s.invitations[i], nil
|
||||||
|
}
|
||||||
s.invitations[i].Status = types.GroupInvitationStatusJoined
|
s.invitations[i].Status = types.GroupInvitationStatusJoined
|
||||||
s.invitations[i].Result = "joined"
|
s.invitations[i].Result = "joined"
|
||||||
s.invitations[i].HandledAt = &now
|
s.invitations[i].HandledAt = &now
|
||||||
@@ -1063,7 +1066,7 @@ func (s *testProfilesStore) GetPrivacy(_ types.TenantID, uid types.Uid) (*types.
|
|||||||
now := time.Now().UTC().Round(time.Millisecond)
|
now := time.Now().UTC().Round(time.Millisecond)
|
||||||
s.privacy = &types.UserPrivacySettings{
|
s.privacy = &types.UserPrivacySettings{
|
||||||
UserID: uid, AddMeMethods: map[string]bool{"account": true, "phone": true, "qr": true, "group": true},
|
UserID: uid, AddMeMethods: map[string]bool{"account": true, "phone": true, "qr": true, "group": true},
|
||||||
ProfileVisibility: "friends", LastSeenVisibility: "friends", ReadReceipts: true, Version: 1, CreatedAt: now, UpdatedAt: now,
|
ProfileVisibility: "friends", LastSeenVisibility: "friends", ReadReceipts: true, GroupInviteVerification: false, Version: 1, CreatedAt: now, UpdatedAt: now,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return s.privacy, nil
|
return s.privacy, nil
|
||||||
@@ -1083,6 +1086,9 @@ func (s *testProfilesStore) UpdatePrivacy(_ types.TenantID, uid types.Uid, patch
|
|||||||
if patch.ReadReceipts != nil {
|
if patch.ReadReceipts != nil {
|
||||||
privacy.ReadReceipts = *patch.ReadReceipts
|
privacy.ReadReceipts = *patch.ReadReceipts
|
||||||
}
|
}
|
||||||
|
if patch.GroupInviteVerification != nil {
|
||||||
|
privacy.GroupInviteVerification = *patch.GroupInviteVerification
|
||||||
|
}
|
||||||
privacy.Version = patch.Version + 1
|
privacy.Version = patch.Version + 1
|
||||||
privacy.UpdatedAt = time.Now().UTC().Round(time.Millisecond)
|
privacy.UpdatedAt = time.Now().UTC().Round(time.Millisecond)
|
||||||
return privacy, nil
|
return privacy, nil
|
||||||
@@ -3716,7 +3722,7 @@ func TestServeIMMePrivacy(t *testing.T) {
|
|||||||
store.Profiles = profiles
|
store.Profiles = profiles
|
||||||
defer func() { store.Profiles = oldProfiles }()
|
defer func() { store.Profiles = oldProfiles }()
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPatch, "/v1/im/me/privacy", strings.NewReader(`{"add_me_methods":{"account":true,"phone":false,"qr":true,"group":true},"read_receipts":false,"version":1}`))
|
req := httptest.NewRequest(http.MethodPatch, "/v1/im/me/privacy", strings.NewReader(`{"add_me_methods":{"account":true,"phone":false,"qr":true,"group":true},"read_receipts":false,"group_invite_verification":true,"version":1}`))
|
||||||
req.Header.Set("Authorization", "Bearer token-1")
|
req.Header.Set("Authorization", "Bearer token-1")
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
serveIMMePrivacy(rec, req)
|
serveIMMePrivacy(rec, req)
|
||||||
@@ -3726,7 +3732,9 @@ func TestServeIMMePrivacy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||||
methods := data["add_me_methods"].(map[string]any)
|
methods := data["add_me_methods"].(map[string]any)
|
||||||
if methods["phone"] != false || data["read_receipts"] != false || data["version"].(float64) != 2 {
|
editable := data["editable"].(map[string]any)
|
||||||
|
if methods["phone"] != false || data["read_receipts"] != false || data["group_invite_verification"] != true ||
|
||||||
|
editable["group_invite_verification"] != true || data["version"].(float64) != 2 {
|
||||||
t.Fatalf("unexpected privacy: %#v", data)
|
t.Fatalf("unexpected privacy: %#v", data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4802,7 +4810,7 @@ func TestServeIMGroupInvitationCreatesApprovalRequest(t *testing.T) {
|
|||||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||||
items := data["items"].([]any)
|
items := data["items"].([]any)
|
||||||
item := items[0].(map[string]any)
|
item := items[0].(map[string]any)
|
||||||
if item["invitation_id"] != "ginv_invite" || item["join_request_id"] != "gjr_request" || item["status"] != types.GroupInvitationStatusPending {
|
if item["invitation_id"] != "ginv_invite" || item["join_request_id"] != "gjr_request" || item["status"] != types.GroupInvitationStatusPendingApproval {
|
||||||
t.Fatalf("unexpected invitation response: %#v", data)
|
t.Fatalf("unexpected invitation response: %#v", data)
|
||||||
}
|
}
|
||||||
if len(groupStore.joinRequests) != 1 || groupStore.joinRequests[0].RequestID != "gjr_request" ||
|
if len(groupStore.joinRequests) != 1 || groupStore.joinRequests[0].RequestID != "gjr_request" ||
|
||||||
@@ -4850,6 +4858,66 @@ func TestServeIMGroupInvitationCreatesApprovalRequest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServeIMGroupInvitationRespectsTargetPrivacyApproval(t *testing.T) {
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
mockStore := expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||||
|
mockStore.EXPECT().GetUidString().Return("invite")
|
||||||
|
mockStore.EXPECT().GetUidString().Return("request")
|
||||||
|
oldSubs := store.Subs
|
||||||
|
oldGroupBusiness := store.GroupBusiness
|
||||||
|
oldProfiles := store.Profiles
|
||||||
|
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||||
|
groupStore := &testGroupBusinessStore{settings: &types.GroupSettings{
|
||||||
|
Topic: "grpAlpha", OwnerUserID: types.Uid(42), Name: "Alpha", GroupStatus: types.GroupStatusActive,
|
||||||
|
JoinApproval: false, MemberInvite: true, MaxAdminCount: groupMaxAdminCount, Version: 1,
|
||||||
|
}}
|
||||||
|
store.Subs = subs
|
||||||
|
store.GroupBusiness = groupStore
|
||||||
|
store.Profiles = &testProfilesStore{privacy: &types.UserPrivacySettings{
|
||||||
|
UserID: types.Uid(43),
|
||||||
|
AddMeMethods: map[string]bool{"account": true, "phone": true, "qr": true, "group": true},
|
||||||
|
ProfileVisibility: "friends",
|
||||||
|
LastSeenVisibility: "friends",
|
||||||
|
ReadReceipts: true,
|
||||||
|
GroupInviteVerification: true,
|
||||||
|
Version: 1,
|
||||||
|
CreatedAt: time.Now().UTC(),
|
||||||
|
UpdatedAt: time.Now().UTC(),
|
||||||
|
}}
|
||||||
|
defer func() {
|
||||||
|
store.Subs = oldSubs
|
||||||
|
store.GroupBusiness = oldGroupBusiness
|
||||||
|
store.Profiles = oldProfiles
|
||||||
|
}()
|
||||||
|
owner := types.Uid(42)
|
||||||
|
target := types.Uid(43)
|
||||||
|
subs.EXPECT().Get(types.TenantID(7), "grpAlpha", owner, false).Return(&types.Subscription{
|
||||||
|
TenantID: 7, User: owner.String(), Topic: "grpAlpha", ModeWant: types.ModeCFull, ModeGiven: types.ModeCFull,
|
||||||
|
}, nil)
|
||||||
|
subs.EXPECT().Get(types.TenantID(7), "grpAlpha", target, false).Return(nil, types.ErrNotFound)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/v1/im/groups/grpAlpha/invitations",
|
||||||
|
bytes.NewBufferString(`{"user_ids":["`+target.UserId()+`"],"message":"welcome"}`))
|
||||||
|
req.Header.Set("Authorization", "Bearer token-1")
|
||||||
|
req.Header.Set("Idempotency-Key", "invite-privacy")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
serveIMGroupResource(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status: got %d body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||||
|
items := data["items"].([]any)
|
||||||
|
item := items[0].(map[string]any)
|
||||||
|
if item["invitation_id"] != "ginv_invite" || item["join_request_id"] != "gjr_request" || item["status"] != types.GroupInvitationStatusPendingApproval {
|
||||||
|
t.Fatalf("unexpected invitation response: %#v", data)
|
||||||
|
}
|
||||||
|
if len(groupStore.joinRequests) != 1 || groupStore.joinRequests[0].ApplicantUserID != target || groupStore.joinRequests[0].Source != "invitation" {
|
||||||
|
t.Fatalf("join request was not created from privacy setting: %#v", groupStore.joinRequests)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestServeIMGroupInvitationAccept(t *testing.T) {
|
func TestServeIMGroupInvitationAccept(t *testing.T) {
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|||||||
@@ -3020,7 +3020,7 @@ func (profileMapper) UpdatePrivacy(tenantID types.TenantID, uid types.Uid, patch
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validUserPrivacyPatch(patch types.UserPrivacyPatch) bool {
|
func validUserPrivacyPatch(patch types.UserPrivacyPatch) bool {
|
||||||
if patch.AddMeMethods == nil && patch.ProfileVisibility == nil && patch.LastSeenVisibility == nil && patch.ReadReceipts == nil {
|
if patch.AddMeMethods == nil && patch.ProfileVisibility == nil && patch.LastSeenVisibility == nil && patch.ReadReceipts == nil && patch.GroupInviteVerification == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for method := range patch.AddMeMethods {
|
for method := range patch.AddMeMethods {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const (
|
|||||||
GroupMemberStatusLeft = "left"
|
GroupMemberStatusLeft = "left"
|
||||||
|
|
||||||
GroupInvitationStatusPending = "pending"
|
GroupInvitationStatusPending = "pending"
|
||||||
|
GroupInvitationStatusPendingApproval = "pending_approval"
|
||||||
GroupInvitationStatusJoined = "joined"
|
GroupInvitationStatusJoined = "joined"
|
||||||
|
|
||||||
GroupJoinRequestStatusPending = "pending"
|
GroupJoinRequestStatusPending = "pending"
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ type UserPrivacySettings struct {
|
|||||||
ProfileVisibility string
|
ProfileVisibility string
|
||||||
LastSeenVisibility string
|
LastSeenVisibility string
|
||||||
ReadReceipts bool
|
ReadReceipts bool
|
||||||
|
GroupInviteVerification bool
|
||||||
Version int
|
Version int
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
@@ -44,6 +45,7 @@ type UserPrivacyPatch struct {
|
|||||||
ProfileVisibility *string
|
ProfileVisibility *string
|
||||||
LastSeenVisibility *string
|
LastSeenVisibility *string
|
||||||
ReadReceipts *bool
|
ReadReceipts *bool
|
||||||
|
GroupInviteVerification *bool
|
||||||
Version int
|
Version int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user