fix: resolve P1 backend consistency issues

This commit is contained in:
richjohnrambo
2026-09-08 16:23:21 +08:00
parent 06945a4050
commit 0b3c0b1c78
13 changed files with 587 additions and 57 deletions
+119 -10
View File
@@ -62,7 +62,7 @@ const (
imVerificationResendAfterSeconds = 60
imVerificationMaxAttempts = 5
imVerificationCreateRateWindow = time.Hour
imSensitiveChallengeRateLimitEnabled = false
imSensitiveChallengeRateLimitEnabled = true
imActionTokenExpiresInSeconds = 300
imIndependentPasswordMaxAttempts = 5
imIndependentPasswordLockSeconds = 300
@@ -1503,7 +1503,7 @@ func serveIMSecurityChallenges(wrt http.ResponseWriter, req *http.Request) {
return
}
action := strings.TrimSpace(body.Scene)
if action == "" || len(action) > 64 || strings.TrimSpace(body.Channel) != "sms" {
if !validIMSecurityChallengeScene(action) || strings.TrimSpace(body.Channel) != "sms" {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", nil)
return
}
@@ -2530,7 +2530,8 @@ func serveIMMeetingReminders(wrt http.ResponseWriter, req *http.Request, meeting
meetingParticipantUserIDs(participants), "会议提醒", meetingID, map[string]any{"meeting_id": meetingID, "reminded_count": len(participants)})
writeIMSuccess(wrt, req, map[string]any{
"meeting_id": meetingID,
"reminded": meetingParticipantPayloads(participants),
"reminded": meetingParticipantPayloadsWithSummaries(participants,
meetingUserSummaries(principal.TenantID, []types.Meeting{{Participants: participants}})),
})
}
@@ -3706,9 +3707,10 @@ func serveIMFavoriteForward(wrt http.ResponseWriter, req *http.Request) {
results := make([]map[string]any, 0, len(body.TargetTopics)*len(body.FavoriteIDs))
comment := strings.TrimSpace(body.Comment)
for _, target := range body.TargetTopics {
target = strings.TrimSpace(target)
if target == "" {
results = append(results, map[string]any{"target_topic": target, "status": "failed", "reason": imReasonMalformedRequest})
rawTarget := strings.TrimSpace(target)
target, targetOK := normalizeIMForwardTopic(principal.Uid, rawTarget)
if target == "" || !targetOK {
results = append(results, map[string]any{"target_topic": rawTarget, "status": "failed", "reason": imReasonMalformedRequest})
continue
}
if err := requireTopicWriter(principal, target); err != nil {
@@ -3733,6 +3735,10 @@ func serveIMFavoriteForward(wrt http.ResponseWriter, req *http.Request) {
},
Content: favoriteForwardContent(item, comment),
}
if err := prepareHTTPForwardMessage(principal.TenantID, msg); err != nil {
results = append(results, map[string]any{"target_topic": target, "favorite_id": favoriteID, "status": "failed", "reason": relationErrorReason(err)})
continue
}
saved, err := saveMessageMaybeKafka(req.Context(), principal.TenantID, msg, nil, true)
if err != nil {
results = append(results, map[string]any{"target_topic": target, "favorite_id": favoriteID, "status": "failed", "reason": relationErrorReason(err)})
@@ -4402,6 +4408,60 @@ func serveIMFriendRequestCreate(wrt http.ResponseWriter, req *http.Request) {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", nil)
return
}
relation, err := store.Relations.GetRelationshipContext(principal.TenantID, principal.Uid, target, principal.Uid.P2PName(target))
if err != nil {
writeIMRelationError(wrt, req, err)
return
}
if relation == nil || relation.Relationship != types.RelationshipNone {
reason := "FRIEND_REQUEST_CONFLICT"
if relation != nil && (relation.Relationship == types.RelationshipBlockedByMe || relation.Relationship == types.RelationshipBlockedByPeer) {
reason = imReasonPermissionDenied
}
writeIMError(wrt, req, http.StatusConflict, 30006, reason, "friend request is not allowed for current relationship", nil)
return
}
source := strings.TrimSpace(body.Source)
sourceTopic := strings.TrimSpace(body.SourceTopic)
switch source {
case "search", "qr":
if sourceTopic != "" {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "source_topic is only valid for group source", nil)
return
}
case "group":
if types.GetTopicCat(sourceTopic) != types.TopicCatGrp {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "valid group source_topic is required", nil)
return
}
requesterSub, requesterErr := store.Subs.Get(principal.TenantID, sourceTopic, principal.Uid, false)
targetSub, targetErr := store.Subs.Get(principal.TenantID, sourceTopic, target, false)
if requesterErr != nil || targetErr != nil {
if requesterErr != nil {
writeIMRelationError(wrt, req, requesterErr)
} else {
writeIMRelationError(wrt, req, targetErr)
}
return
}
if requesterSub == nil || targetSub == nil ||
!(requesterSub.ModeGiven & requesterSub.ModeWant).IsJoiner() || !(targetSub.ModeGiven & targetSub.ModeWant).IsJoiner() {
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "group membership required", nil)
return
}
settings, settingsErr := store.GroupBusiness.GetSettings(principal.TenantID, sourceTopic)
if settingsErr != nil {
writeIMRelationError(wrt, req, settingsErr)
return
}
if settings == nil || !settings.MemberPrivateChat {
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "member private chat is disabled", nil)
return
}
default:
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "invalid friend request source", nil)
return
}
privacy, err := store.Profiles.GetPrivacy(principal.TenantID, target)
if err != nil {
writeIMRelationError(wrt, req, err)
@@ -4424,8 +4484,8 @@ func serveIMFriendRequestCreate(wrt http.ResponseWriter, req *http.Request) {
ApplicantID: principal.Uid,
TargetID: target,
Message: message,
Source: strings.TrimSpace(body.Source),
SourceTopic: strings.TrimSpace(body.SourceTopic),
Source: source,
SourceTopic: sourceTopic,
Status: types.FriendRequestPending,
IdempotencyKey: idempotencyKey,
ExpiresAt: now.Add(7 * 24 * time.Hour),
@@ -5038,7 +5098,7 @@ func serveIMGroupMyProfile(wrt http.ResponseWriter, req *http.Request, topic str
writeIMRelationError(wrt, req, err)
return
}
if _, err := store.GroupBusiness.UpdateMember(principal.TenantID, topic, principal.Uid, types.GroupMemberPatch{Nickname: &nickname, Version: body.Version}); err != nil {
if _, err := store.GroupBusiness.UpdateMember(principal.TenantID, topic, principal.Uid, types.GroupMemberPatch{Nickname: &nickname}); err != nil {
if errors.Is(err, types.ErrNotFound) {
logs.Warn.Printf("group[%s]: member profile saved to subscription only; business member row missing for user %s", topic, principal.Uid.UserId())
dispatchIMGroupEvents(principal.TenantID)
@@ -5129,6 +5189,17 @@ func serveIMGroupInvitations(wrt http.ResponseWriter, req *http.Request, topic s
return
}
access := selfSub.ModeGiven & selfSub.ModeWant
if !access.IsAdmin() {
settings, err := store.GroupBusiness.GetSettings(principal.TenantID, topic)
if err != nil {
writeIMRelationError(wrt, req, err)
return
}
if settings == nil || !settings.MemberInvite {
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "member invitation is disabled", nil)
return
}
}
if !access.IsSharer() && !access.IsAdmin() {
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "permission denied", nil)
return
@@ -5232,6 +5303,17 @@ func serveIMGroupFiles(wrt http.ResponseWriter, req *http.Request, topic, tail s
"has_more": nextCursor != "",
})
case tail == "files" && req.Method == http.MethodPost:
if !access.IsAdmin() {
settings, err := store.GroupBusiness.GetSettings(principal.TenantID, topic)
if err != nil {
writeIMRelationError(wrt, req, err)
return
}
if settings == nil || !settings.MemberUploadFiles {
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "member file upload is disabled", nil)
return
}
}
idempotencyKey, ok := requireIMIdempotencyKey(wrt, req)
if !ok {
return
@@ -5344,6 +5426,18 @@ func serveIMGroupJoinRequests(wrt http.ResponseWriter, req *http.Request, topic
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "group not found", nil)
return
}
if !settings.JoinApproval {
now := time.Now().UTC().Round(time.Millisecond)
joined, joinErr := store.GroupBusiness.InviteMember(principal.TenantID, topic, principal.Uid, principal.Uid,
"", strings.TrimSpace(req.Header.Get("Idempotency-Key")), groupMemberPrivate("", false, false, 1, now), now)
if joinErr != nil {
writeIMRelationError(wrt, req, joinErr)
return
}
dispatchIMGroupEvents(principal.TenantID)
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "status": "joined", "joined": joined, "source": "open_group"})
return
}
sub, err := store.Subs.Get(principal.TenantID, topic, principal.Uid, false)
if err != nil {
writeIMRelationError(wrt, req, err)
@@ -7353,7 +7447,9 @@ func mergeGroupSettingsPayload(payload map[string]any, settings *types.GroupSett
payload["description"] = settings.Description
payload["owner"] = settings.OwnerUserID.UserId()
payload["member_count"] = settings.MemberCount
payload["version"] = settings.Version
if _, exists := payload["version"]; !exists {
payload["version"] = settings.Version
}
return payload
}
@@ -8682,6 +8778,7 @@ func userProfilePayload10(profile *types.UserProfile) map[string]any {
"field_rules": map[string]any{
"nickname": map[string]any{"min_length": 1, "max_length": 32},
"signature": map[string]any{"max_length": 120},
"region": map[string]any{"max_length": 128},
"birth_date": map[string]any{"format": "YYYY-MM-DD"},
"gender": []string{"male", "female", "other", ""},
"email": map[string]any{"max_length": 128},
@@ -8690,6 +8787,15 @@ func userProfilePayload10(profile *types.UserProfile) map[string]any {
return data
}
func validIMSecurityChallengeScene(scene string) bool {
switch strings.TrimSpace(scene) {
case "set_independent_password", "enable_youth_mode", "close_youth_mode", "reset_independent_password":
return true
default:
return false
}
}
func userPrivacyPayload(settings *types.UserPrivacySettings) map[string]any {
return map[string]any{
"add_me_methods": settings.AddMeMethods,
@@ -9437,6 +9543,9 @@ func normalizeIMUserVisibleTopic(viewer types.Uid, topic string) (string, bool)
if topic == "" {
return "", false
}
if topic == "slf" {
return viewer.SlfName(), !viewer.IsZero()
}
if len(topic) < 3 {
return "", false
}