owpengram-server/internal/rpc/messages_reports.go
2026-07-24 11:57:01 +08:00

394 lines
12 KiB
Go

package rpc
import (
"context"
"errors"
"sort"
"unicode/utf8"
"github.com/iamxvbaba/td/tg"
"github.com/iamxvbaba/td/tgerr"
compatandroid "telesrv/internal/compat/android"
"telesrv/internal/domain"
)
type readMetricTelemetry struct {
MessageID int `json:"message_id"`
ViewID int64 `json:"view_id"`
TimeInViewMS int `json:"time_in_view_ms"`
ActiveTimeInViewMS int `json:"active_time_in_view_ms"`
HeightToViewportRatioPermille int `json:"height_to_viewport_ratio_permille"`
SeenRangeRatioPermille int `json:"seen_range_ratio_permille"`
}
func (r *Router) onMessagesReportSpam(ctx context.Context, peer tg.InputPeerClass) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
target, err := r.checkedDomainPeerFromInputPeer(ctx, userID, peer)
if err != nil {
return false, err
}
if r.deps.Moderation == nil {
return false, internalErr()
}
if _, _, err := r.deps.Moderation.ReportPeer(
ctx, userID, domain.ModerationSourceMessagesSpam, target,
domain.ModerationReasonSpam, "spam", "", r.clock.Now(),
); err != nil {
return false, moderationReportError(err)
}
return true, nil
}
func (r *Router) onMessagesReport(ctx context.Context, req *tg.MessagesReportRequest) (tg.ReportResultClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
target, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
if len(req.ID) == 0 {
if compatandroid.OfferInitialMessageReportOptions(
string(ClientTypeFrom(ctx)),
len(req.ID),
req.Option,
req.Message,
) {
return reportResultForOption("")
}
return nil, tgerr.New(400, "MESSAGE_ID_REQUIRED")
}
if len(req.ID) > maxGetMessagesIDs || len(req.Option) > maxReportOptionLength || utf8.RuneCountInString(req.Message) > maxReportCommentLength {
return nil, limitInvalidErr()
}
for _, msgID := range req.ID {
if msgID <= 0 || msgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
}
result, err := reportResultForOption(string(req.Option))
if err != nil {
return nil, err
}
if _, final := result.(*tg.ReportResultReported); !final {
return result, nil
}
reason, ok := moderationReasonForReportOption(string(req.Option))
if !ok {
return nil, tgerr.New(400, "OPTION_INVALID")
}
if r.deps.Moderation == nil {
return nil, internalErr()
}
if _, _, err := r.deps.Moderation.ReportMessages(ctx, domain.ModerationMessageReportRequest{
ReporterUserID: userID, Target: target, MessageIDs: req.ID,
Reason: reason, Option: string(req.Option), Comment: req.Message,
CreatedAt: r.clock.Now(),
}); err != nil {
return nil, moderationReportError(err)
}
return result, nil
}
func (r *Router) onMessagesReportReaction(ctx context.Context, req *tg.MessagesReportReactionRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if req.ID <= 0 || req.ID > domain.MaxMessageBoxID {
return false, messageIDInvalidErr()
}
target, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return false, err
}
reactor, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.ReactionPeer)
if err != nil {
return false, err
}
if reactor.Type != domain.PeerTypeUser || reactor.ID <= 0 {
return false, peerIDInvalidErr()
}
if r.deps.Moderation == nil {
return false, internalErr()
}
if _, _, err := r.deps.Moderation.ReportReaction(ctx, domain.ModerationReactionReportRequest{
ReporterUserID: userID, Target: target, MessageID: req.ID,
ReactorUserID: reactor.ID, CreatedAt: r.clock.Now(),
}); err != nil {
return false, moderationReportError(err)
}
return true, nil
}
func moderationReportError(err error) error {
switch {
case errors.Is(err, domain.ErrModerationEvidenceNotFound):
return messageIDInvalidErr()
case errors.Is(err, domain.ErrModerationPermissionDenied):
return tgerr.New(403, "CHAT_ADMIN_REQUIRED")
case errors.Is(err, domain.ErrModerationRateLimited):
return floodWaitErr(60)
case errors.Is(err, domain.ErrModerationReportInvalid):
return inputRequestInvalidErr()
default:
return internalErr()
}
}
func (r *Router) onMessagesReportMessagesDelivery(ctx context.Context, req *tg.MessagesReportMessagesDeliveryRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if len(req.ID) == 0 || len(req.ID) > maxGetMessagesIDs {
return false, limitInvalidErr()
}
for _, msgID := range req.ID {
if msgID <= 0 || msgID > domain.MaxMessageBoxID {
return false, messageIDInvalidErr()
}
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return false, err
}
if err := r.validateTelemetryMessageIDs(ctx, userID, peer, req.ID); err != nil {
return false, err
}
ids := messageIDs64(req.ID)
if r.deps.ClientTelemetry == nil {
return false, internalErr()
}
if _, _, err := r.deps.ClientTelemetry.Record(
ctx, userID, domain.ClientTelemetryMessageDelivery, peer, ids,
struct {
Push bool `json:"push"`
}{Push: req.Push},
r.clock.Now(),
); err != nil {
return false, clientTelemetryError(err)
}
return true, nil
}
func (r *Router) onMessagesReportReadMetrics(ctx context.Context, req *tg.MessagesReportReadMetricsRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if len(req.Metrics) == 0 || len(req.Metrics) > maxReadMetrics {
return false, limitInvalidErr()
}
ids := make([]int, 0, len(req.Metrics))
payload := make([]readMetricTelemetry, 0, len(req.Metrics))
for _, metric := range req.Metrics {
if metric.MsgID <= 0 || metric.MsgID > domain.MaxMessageBoxID {
return false, messageIDInvalidErr()
}
if metric.ViewID == 0 || metric.TimeInViewMs < 0 ||
metric.TimeInViewMs > 24*60*60*1000 ||
metric.ActiveTimeInViewMs < 0 ||
metric.ActiveTimeInViewMs > metric.TimeInViewMs ||
metric.HeightToViewportRatioPermille < 0 ||
metric.HeightToViewportRatioPermille > 1_000_000 ||
metric.SeenRangeRatioPermille < 0 ||
metric.SeenRangeRatioPermille > 1000 {
return false, limitInvalidErr()
}
ids = append(ids, metric.MsgID)
payload = append(payload, readMetricTelemetry{
MessageID: metric.MsgID, ViewID: metric.ViewID,
TimeInViewMS: metric.TimeInViewMs,
ActiveTimeInViewMS: metric.ActiveTimeInViewMs,
HeightToViewportRatioPermille: metric.HeightToViewportRatioPermille,
SeenRangeRatioPermille: metric.SeenRangeRatioPermille,
})
}
sort.Slice(payload, func(i, j int) bool {
return payload[i].MessageID < payload[j].MessageID
})
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return false, err
}
if err := r.validateTelemetryMessageIDs(ctx, userID, peer, ids); err != nil {
return false, err
}
if r.deps.ClientTelemetry == nil {
return false, internalErr()
}
if _, _, err := r.deps.ClientTelemetry.Record(
ctx, userID, domain.ClientTelemetryReadMetrics, peer,
messageIDs64(ids),
struct {
Metrics []readMetricTelemetry `json:"metrics"`
}{Metrics: payload},
r.clock.Now(),
); err != nil {
return false, clientTelemetryError(err)
}
return true, nil
}
func (r *Router) onMessagesReportMusicListen(ctx context.Context, req *tg.MessagesReportMusicListenRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if req.ListenedDuration < 0 || req.ListenedDuration > 24*60*60 {
return false, limitInvalidErr()
}
document, err := r.musicDocumentFromInput(ctx, req.ID)
if err != nil {
return false, err
}
if r.deps.ClientTelemetry == nil {
return false, internalErr()
}
if _, _, err := r.deps.ClientTelemetry.Record(
ctx, userID, domain.ClientTelemetryMusicListen, domain.Peer{},
[]int64{document.ID},
struct {
ListenedDuration int `json:"listened_duration"`
}{ListenedDuration: req.ListenedDuration},
r.clock.Now(),
); err != nil {
return false, clientTelemetryError(err)
}
return true, nil
}
func (r *Router) onMessagesReportSponsoredMessage(ctx context.Context, req *tg.MessagesReportSponsoredMessageRequest) (tg.ChannelsSponsoredMessageReportResultClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if len(req.RandomID) == 0 || len(req.RandomID) > maxReportRandomIDLength || len(req.Option) > maxReportOptionLength {
return nil, limitInvalidErr()
}
if r.deps.Moderation == nil {
return nil, internalErr()
}
if _, err := r.deps.Moderation.SponsoredImpression(
ctx, userID, req.RandomID, r.clock.Now(),
); err != nil {
if errors.Is(err, domain.ErrModerationImpressionExpired) ||
errors.Is(err, domain.ErrModerationEvidenceNotFound) {
return nil, tgerr.New(400, "RANDOM_ID_INVALID")
}
return nil, internalErr()
}
option := string(req.Option)
if option == "" {
return &tg.ChannelsSponsoredMessageReportResultChooseOption{
Title: "Report sponsored message",
Options: []tg.SponsoredMessageReportOption{
{Text: "Scam or spam", Option: []byte("spam")},
{Text: "Violence", Option: []byte("violence")},
{Text: "Pornography", Option: []byte("pornography")},
{Text: "Child abuse", Option: []byte("child_abuse")},
{Text: "Illegal drugs", Option: []byte("illegal_drugs")},
{Text: "Personal details", Option: []byte("personal_details")},
{Text: "Fake or impersonation", Option: []byte("fake")},
{Text: "Other", Option: []byte("other")},
},
}, nil
}
reason, ok := moderationReasonForReportOption(option)
if option == "other" {
reason, ok = domain.ModerationReasonOther, true
}
if !ok {
return nil, tgerr.New(400, "OPTION_INVALID")
}
if _, _, err := r.deps.Moderation.ReportSponsored(
ctx, userID, req.RandomID, reason, option, r.clock.Now(),
); err != nil {
if errors.Is(err, domain.ErrModerationImpressionExpired) ||
errors.Is(err, domain.ErrModerationEvidenceNotFound) {
return nil, tgerr.New(400, "RANDOM_ID_INVALID")
}
return nil, moderationReportError(err)
}
return &tg.ChannelsSponsoredMessageReportResultReported{}, nil
}
func (r *Router) validateTelemetryMessageIDs(ctx context.Context, userID int64, peer domain.Peer, ids []int) error {
if len(ids) == 0 || len(ids) > domain.MaxGetMessageIDs {
return limitInvalidErr()
}
needed := make(map[int]struct{}, len(ids))
for _, id := range ids {
if id <= 0 || id > domain.MaxMessageBoxID {
return messageIDInvalidErr()
}
if _, duplicate := needed[id]; duplicate {
return messageIDInvalidErr()
}
needed[id] = struct{}{}
}
switch peer.Type {
case domain.PeerTypeUser:
if r.deps.Messages == nil {
return internalErr()
}
list, err := r.deps.Messages.GetMessages(ctx, userID, ids)
if err != nil {
return internalErr()
}
for _, message := range list.Messages {
if message.Peer == peer {
delete(needed, message.ID)
}
}
case domain.PeerTypeChannel:
if r.deps.Channels == nil {
return internalErr()
}
history, err := r.deps.Channels.GetMessages(ctx, userID, peer.ID, ids)
if err != nil {
return internalErr()
}
for _, message := range history.Messages {
delete(needed, message.ID)
}
default:
return peerIDInvalidErr()
}
if len(needed) != 0 {
return messageIDInvalidErr()
}
return nil
}
func messageIDs64(ids []int) []int64 {
out := make([]int64, len(ids))
for i, id := range ids {
out[i] = int64(id)
}
return out
}
func clientTelemetryError(err error) error {
switch {
case errors.Is(err, domain.ErrClientTelemetryRateLimited):
return floodWaitErr(60)
case errors.Is(err, domain.ErrClientTelemetryInvalid):
return inputRequestInvalidErr()
default:
return internalErr()
}
}
func (r *Router) onMessagesGetSponsoredMessages(ctx context.Context, req *tg.MessagesGetSponsoredMessagesRequest) (tg.MessagesSponsoredMessagesClass, error) {
if _, _, err := r.currentUserID(ctx); err != nil {
return nil, internalErr()
}
return &tg.MessagesSponsoredMessagesEmpty{}, nil
}