feat: enforce tenant content moderation
This commit is contained in:
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/tinode/chat/server/auth"
|
||||
"github.com/tinode/chat/server/logs"
|
||||
"github.com/tinode/chat/server/media"
|
||||
"github.com/tinode/chat/server/moderation"
|
||||
"github.com/tinode/chat/server/store"
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -57,8 +58,36 @@ const (
|
||||
imReasonResourceNotFound = "RESOURCE_NOT_FOUND"
|
||||
imReasonGroupDissolved = "GROUP_DISSOLVED"
|
||||
imReasonValidationFailed = "VALIDATION_FAILED"
|
||||
imReasonContentRejected = "CONTENT_REJECTED"
|
||||
imReasonReviewUnavailable = "CONTENT_REVIEW_UNAVAILABLE"
|
||||
)
|
||||
|
||||
func reviewIMHTTPText(wrt http.ResponseWriter, req *http.Request, principal *imPrincipal, scene, topic, text string) bool {
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
return true
|
||||
}
|
||||
tenant, err := store.Tenants.GetByID(principal.TenantID)
|
||||
if err != nil || tenant == nil {
|
||||
logs.Warn.Printf("content moderation skipped because tenant code is unavailable: tenant=%d scene=%s err=%v", principal.TenantID, scene, err)
|
||||
return true
|
||||
}
|
||||
result, err := moderation.Review(req.Context(), moderation.Request{TenantID: principal.TenantID, TenantCode: tenant.Code, Scene: scene, ActorID: principal.Uid, Topic: topic, Text: text, TraceID: req.Header.Get("X-Request-ID")})
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
reason, message := imReasonContentRejected, "内容不符合平台规范,请修改后重试"
|
||||
status := http.StatusUnprocessableEntity
|
||||
if errors.Is(err, moderation.ErrUnavailable) {
|
||||
reason, message, status = imReasonReviewUnavailable, "内容审核暂不可用,请稍后重试", http.StatusServiceUnavailable
|
||||
}
|
||||
if result != nil && strings.TrimSpace(result.BlockMessage) != "" {
|
||||
message = result.BlockMessage
|
||||
}
|
||||
writeIMError(wrt, req, status, 30060, reason, message, nil)
|
||||
return false
|
||||
}
|
||||
|
||||
const (
|
||||
imVerificationCodeLength = 6
|
||||
imVerificationExpiresInSeconds = 300
|
||||
@@ -3183,6 +3212,12 @@ func serveIMMeProfile(wrt http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if body.Nickname != nil && !reviewIMHTTPText(wrt, req, principal, moderation.SceneUserNickname, "", *body.Nickname) {
|
||||
return
|
||||
}
|
||||
if body.Signature != nil && !reviewIMHTTPText(wrt, req, principal, moderation.SceneUserSignature, "", *body.Signature) {
|
||||
return
|
||||
}
|
||||
if body.AvatarFileID != nil {
|
||||
if avatar := strings.TrimSpace(*body.AvatarFileID); avatar != "" {
|
||||
if err := store.Files.LinkAttachments(principal.TenantID, principal.Uid.UserId(), types.ZeroUid, []string{avatar}); err != nil {
|
||||
@@ -4585,6 +4620,13 @@ func serveIMMessageRevision(wrt http.ResponseWriter, req *http.Request) {
|
||||
if body.ClientMid != "" {
|
||||
head["x-im-client-mid"] = body.ClientMid
|
||||
}
|
||||
scene := moderation.ScenePrivateMessage
|
||||
if strings.HasPrefix(topic, "grp") {
|
||||
scene = moderation.SceneGroupMessage
|
||||
}
|
||||
if !reviewIMHTTPText(wrt, req, principal, scene, topic, moderation.ExtractText(body.Content)) {
|
||||
return
|
||||
}
|
||||
editedAt := time.Now().UTC().Round(time.Millisecond)
|
||||
updated, err := store.Messages.Edit(principal.TenantID, topic, seq, principal.Uid, body.Version, head, body.Content, body.Reason, editedAt)
|
||||
if err != nil {
|
||||
@@ -4996,6 +5038,9 @@ func serveIMGroupCreate(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "description"})
|
||||
return
|
||||
}
|
||||
if !reviewIMHTTPText(wrt, req, principal, moderation.SceneGroupName, "", name) {
|
||||
return
|
||||
}
|
||||
topicName := genTopicName()
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
public := map[string]any{"name": name, "fn": name, "version": 1}
|
||||
@@ -5307,6 +5352,9 @@ func serveIMGroupDetail(wrt http.ResponseWriter, req *http.Request, topic string
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "name"})
|
||||
return
|
||||
}
|
||||
if !reviewIMHTTPText(wrt, req, principal, moderation.SceneGroupName, topic, name) {
|
||||
return
|
||||
}
|
||||
public["name"] = name
|
||||
public["fn"] = name
|
||||
}
|
||||
@@ -5493,6 +5541,9 @@ func serveIMGroupMyProfile(wrt http.ResponseWriter, req *http.Request, topic str
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "nickname"})
|
||||
return
|
||||
}
|
||||
if !reviewIMHTTPText(wrt, req, principal, moderation.SceneGroupNickname, topic, nickname) {
|
||||
return
|
||||
}
|
||||
if body.Version > 0 && body.Version != currentVersion {
|
||||
writeIMError(wrt, req, http.StatusConflict, 30006, "VERSION_CONFLICT", "version conflict", map[string]any{"current_version": currentVersion})
|
||||
return
|
||||
@@ -6115,6 +6166,9 @@ func serveIMGroupAnnouncements(wrt http.ResponseWriter, req *http.Request, topic
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "content"})
|
||||
return
|
||||
}
|
||||
if !reviewIMHTTPText(wrt, req, principal, moderation.SceneGroupAnnouncement, topic, content) {
|
||||
return
|
||||
}
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
announcement, err := store.GroupBusiness.PutAnnouncement(principal.TenantID, &types.GroupAnnouncement{
|
||||
AnnouncementID: "ann_" + store.Store.GetUidString(),
|
||||
|
||||
Reference in New Issue
Block a user