From 7e65b21c5ec19487a53e6312d8fb99afba71ab7e Mon Sep 17 00:00:00 2001 From: hawklin2017 <32898629+hawklin2017@users.noreply.github.com> Date: Sat, 11 Apr 2026 21:38:30 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=BE=E6=8A=A5=E5=9E=83=E5=9C=BE=E6=B6=88?= =?UTF-8?q?=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/api/msg.go | 12 ++ internal/api/router.go | 3 + internal/rpc/msg/report.go | 143 ++++++++++++++++++ internal/rpc/msg/server.go | 7 + .../storage/database/mgo/spam_report.go | 110 ++++++++++++++ pkg/common/storage/database/name.go | 1 + pkg/common/storage/database/spam_report.go | 35 +++++ pkg/common/storage/model/spam_report.go | 53 +++++++ 8 files changed, 364 insertions(+) create mode 100644 internal/rpc/msg/report.go create mode 100644 pkg/common/storage/database/mgo/spam_report.go create mode 100644 pkg/common/storage/database/spam_report.go create mode 100644 pkg/common/storage/model/spam_report.go diff --git a/internal/api/msg.go b/internal/api/msg.go index 4fe950ffa..ee07a47c4 100644 --- a/internal/api/msg.go +++ b/internal/api/msg.go @@ -379,3 +379,15 @@ func (m *MessageApi) GetStreamMsg(c *gin.Context) { func (m *MessageApi) AppendStreamMsg(c *gin.Context) { a2r.Call(c, msg.MsgClient.GetServerTime, m.Client) } + +func (m *MessageApi) ReportSpam(c *gin.Context) { + a2r.Call(c, msg.MsgClient.ReportSpam, m.Client) +} + +func (m *MessageApi) GetSpamReports(c *gin.Context) { + a2r.Call(c, msg.MsgClient.GetSpamReports, m.Client) +} + +func (m *MessageApi) HandleSpamReport(c *gin.Context) { + a2r.Call(c, msg.MsgClient.HandleSpamReport, m.Client) +} diff --git a/internal/api/router.go b/internal/api/router.go index 46437a020..06e08dde3 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -288,6 +288,9 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co msgGroup.POST("/batch_send_msg", m.BatchSendMsg) msgGroup.POST("/check_msg_is_send_success", m.CheckMsgIsSendSuccess) msgGroup.POST("/get_server_time", m.GetServerTime) + msgGroup.POST("/report_spam", m.ReportSpam) + msgGroup.POST("/get_spam_reports", m.GetSpamReports) + msgGroup.POST("/handle_spam_report", m.HandleSpamReport) } // Conversation { diff --git a/internal/rpc/msg/report.go b/internal/rpc/msg/report.go new file mode 100644 index 000000000..07bf5cf99 --- /dev/null +++ b/internal/rpc/msg/report.go @@ -0,0 +1,143 @@ +// Copyright © 2024 OpenIM. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package msg + +import ( + "context" + "crypto/rand" + "time" + + "github.com/openimsdk/open-im-server/v3/pkg/authverify" + "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" + "github.com/openimsdk/protocol/msg" + "github.com/openimsdk/tools/errs" + "github.com/openimsdk/tools/mcontext" + "github.com/openimsdk/tools/utils/datautil" +) + +func genReportID() string { + const dataLen = 12 + data := make([]byte, dataLen) + rand.Read(data) + chars := []byte("0123456789abcdefghijklmnopqrstuvwxyz") + for i := 0; i < len(data); i++ { + data[i] = chars[data[i]%byte(len(chars))] + } + return string(data) +} + +func (m *msgServer) ReportSpam(ctx context.Context, req *msg.ReportSpamReq) (*msg.ReportSpamResp, error) { + if req.ReportedUserID == "" { + return nil, errs.ErrArgs.WrapMsg("reportedUserID is required") + } + if req.ReasonType <= 0 { + return nil, errs.ErrArgs.WrapMsg("reasonType must be positive") + } + + reporterUserID := mcontext.GetOpUserID(ctx) + + report := &model.SpamReport{ + ReporterUserID: reporterUserID, + ReportedUserID: req.ReportedUserID, + ConversationID: req.ConversationID, + ClientMsgID: req.ClientMsgID, + Seq: req.Seq, + ReasonType: req.ReasonType, + Reason: req.Reason, + Status: model.SpamReportStatusPending, + CreateTime: time.Now(), + Ex: req.Ex, + } + + // Generate a unique reportID. + for i := 0; i < 20; i++ { + id := genReportID() + existing, err := m.spamReportDB.Get(ctx, id) + if err == nil && existing != nil { + continue + } + report.ReportID = id + break + } + if report.ReportID == "" { + return nil, errs.ErrInternalServer.WrapMsg("failed to generate report ID") + } + + if err := m.spamReportDB.Create(ctx, report); err != nil { + return nil, err + } + return &msg.ReportSpamResp{ReportID: report.ReportID}, nil +} + +func (m *msgServer) GetSpamReports(ctx context.Context, req *msg.GetSpamReportsReq) (*msg.GetSpamReportsResp, error) { + if err := authverify.CheckAdmin(ctx, m.config.Share.IMAdminUserID); err != nil { + return nil, err + } + + var start, end time.Time + if req.StartTime > 0 { + start = time.UnixMilli(req.StartTime) + } + if req.EndTime > 0 { + end = time.UnixMilli(req.EndTime) + } + + total, reports, err := m.spamReportDB.Find(ctx, req.Status, req.ReportedUserID, req.ReporterUserID, + start, end, req.Pagination) + if err != nil { + return nil, err + } + + pbReports := datautil.Slice(reports, func(r *model.SpamReport) *msg.SpamReportInfo { + return &msg.SpamReportInfo{ + ReportID: r.ReportID, + ReporterUserID: r.ReporterUserID, + ReportedUserID: r.ReportedUserID, + ConversationID: r.ConversationID, + ClientMsgID: r.ClientMsgID, + Seq: r.Seq, + ReasonType: r.ReasonType, + Reason: r.Reason, + Status: r.Status, + CreateTime: r.CreateTime.UnixMilli(), + HandleTime: r.HandleTime.UnixMilli(), + HandlerUserID: r.HandlerUserID, + Ex: r.Ex, + } + }) + + return &msg.GetSpamReportsResp{ + Reports: pbReports, + Total: uint32(total), + }, nil +} + +func (m *msgServer) HandleSpamReport(ctx context.Context, req *msg.HandleSpamReportReq) (*msg.HandleSpamReportResp, error) { + if err := authverify.CheckAdmin(ctx, m.config.Share.IMAdminUserID); err != nil { + return nil, err + } + if req.ReportID == "" { + return nil, errs.ErrArgs.WrapMsg("reportID is required") + } + if req.Status != model.SpamReportStatusHandled && req.Status != model.SpamReportStatusIgnored { + return nil, errs.ErrArgs.WrapMsg("status must be 1 (handled) or 2 (ignored)") + } + + handlerUserID := mcontext.GetOpUserID(ctx) + if err := m.spamReportDB.UpdateStatus(ctx, req.ReportID, req.Status, handlerUserID, time.Now()); err != nil { + return nil, err + } + return &msg.HandleSpamReportResp{}, nil +} diff --git a/internal/rpc/msg/server.go b/internal/rpc/msg/server.go index df5a72075..2b91c4405 100644 --- a/internal/rpc/msg/server.go +++ b/internal/rpc/msg/server.go @@ -22,6 +22,7 @@ import ( "github.com/openimsdk/open-im-server/v3/pkg/common/config" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis" + "github.com/openimsdk/open-im-server/v3/pkg/common/storage/database" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo" "github.com/openimsdk/open-im-server/v3/pkg/common/webhook" "github.com/openimsdk/protocol/sdkws" @@ -69,6 +70,7 @@ type msgServer struct { config *Config // Global configuration settings. webhookClient *webhook.Client conversationClient *rpcli.ConversationClient + spamReportDB database.SpamReport } func (m *msgServer) addInterceptorHandler(interceptorFunc ...MessageInterceptorFunc) { @@ -121,6 +123,10 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg return err } conversationClient := rpcli.NewConversationClient(conversationConn) + spamReportDB, err := mgo.NewSpamReportMongo(mgocli.GetDB()) + if err != nil { + return err + } s := &msgServer{ MsgDatabase: msgDatabase, RegisterCenter: client, @@ -131,6 +137,7 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg config: config, webhookClient: webhook.NewWebhookClient(config.WebhooksConfig.URL), conversationClient: conversationClient, + spamReportDB: spamReportDB, } s.notificationSender = notification.NewNotificationSender(&config.NotificationConfig, notification.WithLocalSendMsg(s.SendMsg)) diff --git a/pkg/common/storage/database/mgo/spam_report.go b/pkg/common/storage/database/mgo/spam_report.go new file mode 100644 index 000000000..5c8802e48 --- /dev/null +++ b/pkg/common/storage/database/mgo/spam_report.go @@ -0,0 +1,110 @@ +// Copyright © 2024 OpenIM. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mgo + +import ( + "context" + "time" + + "github.com/openimsdk/open-im-server/v3/pkg/common/storage/database" + "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" + "github.com/openimsdk/tools/db/mongoutil" + "github.com/openimsdk/tools/db/pagination" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func NewSpamReportMongo(db *mongo.Database) (database.SpamReport, error) { + coll := db.Collection(database.SpamReportName) + _, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{ + { + Keys: bson.D{{Key: "report_id", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{ + {Key: "reporter_user_id", Value: 1}, + {Key: "create_time", Value: -1}, + }, + }, + { + Keys: bson.D{ + {Key: "reported_user_id", Value: 1}, + {Key: "create_time", Value: -1}, + }, + }, + { + Keys: bson.D{ + {Key: "status", Value: 1}, + {Key: "create_time", Value: -1}, + }, + }, + }) + if err != nil { + return nil, err + } + return &SpamReportMgo{coll: coll}, nil +} + +type SpamReportMgo struct { + coll *mongo.Collection +} + +func (s *SpamReportMgo) Create(ctx context.Context, report *model.SpamReport) error { + return mongoutil.InsertOne(ctx, s.coll, report) +} + +func (s *SpamReportMgo) Find(ctx context.Context, status int32, reportedUserID, reporterUserID string, + start, end time.Time, pagination pagination.Pagination) (int64, []*model.SpamReport, error) { + filter := bson.M{} + if status >= 0 { + filter["status"] = status + } + if reportedUserID != "" { + filter["reported_user_id"] = reportedUserID + } + if reporterUserID != "" { + filter["reporter_user_id"] = reporterUserID + } + if !start.IsZero() || !end.IsZero() { + timeFilter := bson.M{} + if !start.IsZero() { + timeFilter["$gte"] = start + } + if !end.IsZero() { + timeFilter["$lte"] = end + } + filter["create_time"] = timeFilter + } + return mongoutil.FindPage[*model.SpamReport](ctx, s.coll, filter, pagination, + options.Find().SetSort(bson.D{{Key: "create_time", Value: -1}})) +} + +func (s *SpamReportMgo) UpdateStatus(ctx context.Context, reportID string, status int32, handlerUserID string, handleTime time.Time) error { + return mongoutil.UpdateOne(ctx, s.coll, + bson.M{"report_id": reportID}, + bson.M{"$set": bson.M{ + "status": status, + "handler_user_id": handlerUserID, + "handle_time": handleTime, + }}, + false, + ) +} + +func (s *SpamReportMgo) Get(ctx context.Context, reportID string) (*model.SpamReport, error) { + return mongoutil.FindOne[*model.SpamReport](ctx, s.coll, bson.M{"report_id": reportID}) +} diff --git a/pkg/common/storage/database/name.go b/pkg/common/storage/database/name.go index 8f6241e49..100e6d112 100644 --- a/pkg/common/storage/database/name.go +++ b/pkg/common/storage/database/name.go @@ -21,4 +21,5 @@ const ( PhoneSNInfoName = "phone_sn_info" SignalInvitationName = "signal_invitation" SignalRecordName = "signal_record" + SpamReportName = "spam_report" ) diff --git a/pkg/common/storage/database/spam_report.go b/pkg/common/storage/database/spam_report.go new file mode 100644 index 000000000..ccaec7798 --- /dev/null +++ b/pkg/common/storage/database/spam_report.go @@ -0,0 +1,35 @@ +// Copyright © 2024 OpenIM. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package database + +import ( + "context" + "time" + + "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" + "github.com/openimsdk/tools/db/pagination" +) + +type SpamReport interface { + // Create inserts a new spam report record. + Create(ctx context.Context, report *model.SpamReport) error + // Find queries spam reports with optional filters, returns total count and records. + Find(ctx context.Context, status int32, reportedUserID, reporterUserID string, + start, end time.Time, pagination pagination.Pagination) (int64, []*model.SpamReport, error) + // UpdateStatus updates the handling status of a spam report. + UpdateStatus(ctx context.Context, reportID string, status int32, handlerUserID string, handleTime time.Time) error + // Get retrieves a single spam report by its reportID. + Get(ctx context.Context, reportID string) (*model.SpamReport, error) +} diff --git a/pkg/common/storage/model/spam_report.go b/pkg/common/storage/model/spam_report.go new file mode 100644 index 000000000..644798c01 --- /dev/null +++ b/pkg/common/storage/model/spam_report.go @@ -0,0 +1,53 @@ +// Copyright © 2024 OpenIM. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// SpamReport status constants. +const ( + SpamReportStatusPending int32 = 0 // 待处理 + SpamReportStatusHandled int32 = 1 // 已处理 + SpamReportStatusIgnored int32 = 2 // 已忽略 +) + +// SpamReport reason type constants. +const ( + SpamReasonTypeSpam int32 = 1 // 垃圾消息 + SpamReasonTypePorn int32 = 2 // 色情内容 + SpamReasonTypeIllegal int32 = 3 // 违法内容 + SpamReasonTypeOther int32 = 4 // 其他 +) + +type SpamReport struct { + ID primitive.ObjectID `bson:"_id"` + ReportID string `bson:"report_id"` + ReporterUserID string `bson:"reporter_user_id"` + ReportedUserID string `bson:"reported_user_id"` + ConversationID string `bson:"conversation_id"` // 举报具体消息时填写 + ClientMsgID string `bson:"client_msg_id"` // 举报具体消息时填写 + Seq int64 `bson:"seq"` + ReasonType int32 `bson:"reason_type"` // 1垃圾 2色情 3违法 4其他 + Reason string `bson:"reason"` + Status int32 `bson:"status"` // 0待处理 1已处理 2已忽略 + CreateTime time.Time `bson:"create_time"` + HandleTime time.Time `bson:"handle_time"` + HandlerUserID string `bson:"handler_user_id"` + Ex string `bson:"ex"` +}