owpengram-server/internal/app/moderation/evidence.go
2026-07-24 11:56:59 +08:00

739 lines
30 KiB
Go

package moderation
import (
"bytes"
"context"
"encoding/json"
"fmt"
"sort"
"strconv"
"time"
"telesrv/internal/domain"
)
type privateMessageReader interface {
GetMessages(ctx context.Context, userID int64, ids []int) (domain.MessageList, error)
GetMessageReactions(ctx context.Context, userID int64, req domain.PrivateMessageReactionsRequest) (domain.PrivateMessageReactionsResult, error)
}
type channelMessageReader interface {
GetMessages(ctx context.Context, userID, channelID int64, ids []int) (domain.ChannelHistory, error)
FindMessageReaction(ctx context.Context, userID int64, req domain.ChannelMessageReactionLookupRequest) (domain.ChannelMessageReactionLookup, bool, error)
}
type storyReader interface {
GetStoriesByID(ctx context.Context, viewerUserID int64, peer domain.Peer, ids []int, now int) (domain.StoryList, error)
}
type userReader interface {
ByID(ctx context.Context, viewerUserID, userID int64) (domain.User, bool, error)
}
type channelPeerReader interface {
ResolveChannel(ctx context.Context, viewerUserID, channelID int64) (domain.ChannelView, error)
}
type profilePhotoReader interface {
GetProfilePhotos(ctx context.Context, ownerType domain.PeerType, ownerID int64, offset, limit int, maxID int64) ([]domain.Photo, int, error)
}
func (s *Service) ReportMessages(ctx context.Context, req domain.ModerationMessageReportRequest) (domain.ModerationReport, bool, error) {
ids, err := canonicalPositiveIDs(req.MessageIDs, domain.MaxMessageBoxID)
if err != nil || req.ReporterUserID <= 0 || req.Target.ID <= 0 {
return domain.ModerationReport{}, false, domain.ErrModerationReportInvalid
}
items := make([]domain.ModerationReportItem, 0, len(ids))
holds := make([]domain.ModerationMediaHold, 0)
switch req.Target.Type {
case domain.PeerTypeUser:
if s == nil || s.privateMessages == nil {
return domain.ModerationReport{}, false, fmt.Errorf("moderation private message reader is not configured")
}
list, err := s.privateMessages.GetMessages(ctx, req.ReporterUserID, ids)
if err != nil {
return domain.ModerationReport{}, false, err
}
byID := make(map[int]domain.Message, len(list.Messages))
for _, message := range list.Messages {
if message.Peer == req.Target {
byID[message.ID] = message
}
}
for _, id := range ids {
message, found := byID[id]
if !found {
return domain.ModerationReport{}, false, domain.ErrModerationEvidenceNotFound
}
evidence, err := json.Marshal(privateMessageEvidence(message))
if err != nil {
return domain.ModerationReport{}, false, fmt.Errorf("marshal private message evidence: %w", err)
}
items = append(items, domain.ModerationReportItem{
Kind: domain.ModerationItemMessage, Peer: req.Target,
ItemID: int64(message.ID), AuthorUserID: message.From.ID,
EvidenceSchemaVersion: 1, Evidence: evidence,
})
holds = append(holds, mediaHolds(len(items)-1, message.Media)...)
}
case domain.PeerTypeChannel:
if s == nil || s.channelMessages == nil {
return domain.ModerationReport{}, false, fmt.Errorf("moderation channel message reader is not configured")
}
history, err := s.channelMessages.GetMessages(ctx, req.ReporterUserID, req.Target.ID, ids)
if err != nil {
return domain.ModerationReport{}, false, err
}
byID := make(map[int]domain.ChannelMessage, len(history.Messages))
for _, message := range history.Messages {
if message.ChannelID == req.Target.ID && !message.Deleted {
byID[message.ID] = message
}
}
for _, id := range ids {
message, found := byID[id]
if !found {
return domain.ModerationReport{}, false, domain.ErrModerationEvidenceNotFound
}
evidence, err := marshalChannelMessageEvidence(message)
if err != nil {
return domain.ModerationReport{}, false, fmt.Errorf("marshal channel message evidence: %w", err)
}
items = append(items, domain.ModerationReportItem{
Kind: domain.ModerationItemMessage, Peer: req.Target,
ItemID: int64(message.ID), AuthorUserID: message.SenderUserID,
EvidenceSchemaVersion: 1, Evidence: evidence,
})
holds = append(holds, mediaHolds(len(items)-1, message.Media)...)
}
default:
return domain.ModerationReport{}, false, domain.ErrModerationReportInvalid
}
return s.AcceptReport(ctx, domain.ModerationReportDraft{
ReporterUserID: req.ReporterUserID,
Source: domain.ModerationSourceMessages,
Target: req.Target,
Reason: req.Reason,
Option: req.Option,
Comment: req.Comment,
Items: items,
MediaHolds: dedupeMediaHolds(holds),
CreatedAt: req.CreatedAt,
})
}
func (s *Service) ReportPeer(ctx context.Context, reporterUserID int64, source domain.ModerationReportSource, target domain.Peer, reason domain.ModerationReason, option, comment string, createdAt time.Time) (domain.ModerationReport, bool, error) {
if source != domain.ModerationSourceAccountPeer && source != domain.ModerationSourceMessagesSpam {
return domain.ModerationReport{}, false, domain.ErrModerationReportInvalid
}
snapshot := peerEvidenceV1{SchemaVersion: 1, Target: target}
switch target.Type {
case domain.PeerTypeUser:
if s == nil || s.users == nil {
return domain.ModerationReport{}, false, fmt.Errorf("moderation user reader is not configured")
}
user, found, err := s.users.ByID(ctx, reporterUserID, target.ID)
if err != nil {
return domain.ModerationReport{}, false, err
}
if !found || user.Deleted {
return domain.ModerationReport{}, false, domain.ErrModerationEvidenceNotFound
}
snapshot.User = &peerUserEvidenceV1{
ID: user.ID, FirstName: user.FirstName, LastName: user.LastName,
Username: user.Username, About: user.About, Bot: user.Bot,
Verified: user.Verified, Scam: user.Scam, Fake: user.Fake,
PhotoID: user.PhotoID,
}
case domain.PeerTypeChannel:
if s == nil || s.channels == nil {
return domain.ModerationReport{}, false, fmt.Errorf("moderation channel reader is not configured")
}
view, err := s.channels.ResolveChannel(ctx, reporterUserID, target.ID)
if err != nil {
return domain.ModerationReport{}, false, err
}
channel := view.Channel
snapshot.Channel = &peerChannelEvidenceV1{
ID: channel.ID, Title: channel.Title, About: channel.About,
Username: channel.Username, Broadcast: channel.Broadcast,
Megagroup: channel.Megagroup, Verified: channel.Verified,
Scam: channel.Scam, Fake: channel.Fake, PhotoID: channel.PhotoID,
}
default:
return domain.ModerationReport{}, false, domain.ErrModerationReportInvalid
}
evidence, err := json.Marshal(snapshot)
if err != nil {
return domain.ModerationReport{}, false, fmt.Errorf("marshal peer evidence: %w", err)
}
authorUserID := int64(0)
if target.Type == domain.PeerTypeUser {
authorUserID = target.ID
}
return s.AcceptReport(ctx, domain.ModerationReportDraft{
ReporterUserID: reporterUserID, Source: source, Target: target,
Reason: reason, Option: option, Comment: comment,
Items: []domain.ModerationReportItem{{
Kind: domain.ModerationItemPeer, Peer: target, ItemID: target.ID,
AuthorUserID: authorUserID, EvidenceSchemaVersion: 1,
Evidence: evidence,
}},
CreatedAt: createdAt,
})
}
func (s *Service) ReportProfilePhoto(ctx context.Context, req domain.ModerationProfilePhotoReportRequest) (domain.ModerationReport, bool, error) {
if req.ReporterUserID <= 0 || req.Target.ID <= 0 || req.PhotoID <= 0 ||
!req.Reason.Valid() || s == nil || s.photos == nil {
return domain.ModerationReport{}, false, domain.ErrModerationReportInvalid
}
photos, _, err := s.photos.GetProfilePhotos(ctx, req.Target.Type, req.Target.ID, -1, 1, req.PhotoID)
if err != nil {
return domain.ModerationReport{}, false, err
}
if len(photos) != 1 || photos[0].ID != req.PhotoID ||
photos[0].AccessHash != req.AccessHash {
return domain.ModerationReport{}, false, domain.ErrModerationEvidenceNotFound
}
photo := photos[0]
if len(req.FileReference) > 0 && len(photo.FileReference) > 0 &&
!bytes.Equal(req.FileReference, photo.FileReference) {
return domain.ModerationReport{}, false, domain.ErrModerationEvidenceNotFound
}
evidence, err := json.Marshal(profilePhotoEvidenceV1{
SchemaVersion: 1, Owner: req.Target, Photo: photo,
})
if err != nil {
return domain.ModerationReport{}, false, fmt.Errorf("marshal profile photo evidence: %w", err)
}
authorUserID := int64(0)
if req.Target.Type == domain.PeerTypeUser {
authorUserID = req.Target.ID
}
return s.AcceptReport(ctx, domain.ModerationReportDraft{
ReporterUserID: req.ReporterUserID, Source: domain.ModerationSourceProfilePhoto,
Target: req.Target, Reason: req.Reason, Option: string(req.Reason),
Comment: req.Comment, CreatedAt: req.CreatedAt,
Items: []domain.ModerationReportItem{{
Kind: domain.ModerationItemProfilePhoto, Peer: req.Target,
ItemID: req.PhotoID, AuthorUserID: authorUserID,
EvidenceSchemaVersion: 1, Evidence: evidence,
}},
MediaHolds: photoHolds(0, photo),
})
}
func (s *Service) ReportChannelSpam(ctx context.Context, req domain.ModerationChannelSpamReportRequest) (domain.ModerationReport, bool, error) {
if req.ReporterUserID <= 0 || req.ChannelID <= 0 || req.ParticipantUserID <= 0 ||
s == nil || s.channelMessages == nil {
return domain.ModerationReport{}, false, domain.ErrModerationReportInvalid
}
ids, err := canonicalPositiveIDs(req.MessageIDs, domain.MaxMessageBoxID)
if err != nil {
return domain.ModerationReport{}, false, err
}
history, err := s.channelMessages.GetMessages(ctx, req.ReporterUserID, req.ChannelID, ids)
if err != nil {
return domain.ModerationReport{}, false, err
}
byID := make(map[int]domain.ChannelMessage, len(history.Messages))
for _, message := range history.Messages {
if message.ChannelID == req.ChannelID && !message.Deleted {
byID[message.ID] = message
}
}
items := make([]domain.ModerationReportItem, 0, len(ids))
holds := make([]domain.ModerationMediaHold, 0)
target := domain.Peer{Type: domain.PeerTypeChannel, ID: req.ChannelID}
for _, id := range ids {
message, found := byID[id]
if !found || message.SenderUserID != req.ParticipantUserID {
return domain.ModerationReport{}, false, domain.ErrModerationEvidenceNotFound
}
evidence, err := marshalChannelMessageEvidence(message)
if err != nil {
return domain.ModerationReport{}, false, err
}
items = append(items, domain.ModerationReportItem{
Kind: domain.ModerationItemMessage, Peer: target,
ItemID: int64(message.ID), AuthorUserID: req.ParticipantUserID,
EvidenceSchemaVersion: 1, Evidence: evidence,
})
holds = append(holds, mediaHolds(len(items)-1, message.Media)...)
}
return s.AcceptReport(ctx, domain.ModerationReportDraft{
ReporterUserID: req.ReporterUserID, Source: domain.ModerationSourceChannelSpam,
Target: target, Reason: domain.ModerationReasonSpam, Option: "spam",
Items: items, MediaHolds: dedupeMediaHolds(holds), CreatedAt: req.CreatedAt,
})
}
func (s *Service) ReportReaction(ctx context.Context, req domain.ModerationReactionReportRequest) (domain.ModerationReport, bool, error) {
if req.ReporterUserID <= 0 || req.Target.ID <= 0 || req.MessageID <= 0 ||
req.MessageID > domain.MaxMessageBoxID || req.ReactorUserID <= 0 {
return domain.ModerationReport{}, false, domain.ErrModerationReportInvalid
}
var evidence []byte
var err error
switch req.Target.Type {
case domain.PeerTypeUser:
if s == nil || s.privateMessages == nil {
return domain.ModerationReport{}, false, domain.ErrModerationReportInvalid
}
result, err := s.privateMessages.GetMessageReactions(ctx, req.ReporterUserID, domain.PrivateMessageReactionsRequest{
OwnerUserID: req.ReporterUserID, Peer: req.Target, IDs: []int{req.MessageID},
})
if err != nil {
return domain.ModerationReport{}, false, err
}
if len(result.Messages) != 1 || result.Messages[0].ID != req.MessageID ||
result.Messages[0].Peer != req.Target {
return domain.ModerationReport{}, false, domain.ErrModerationEvidenceNotFound
}
reactions := reactionRowsForUser(result.Messages[0].Reactions, req.ReactorUserID)
if len(reactions) == 0 {
return domain.ModerationReport{}, false, domain.ErrModerationEvidenceNotFound
}
evidence, err = json.Marshal(privateReactionEvidenceV1{
SchemaVersion: 1, Message: privateMessageEvidence(result.Messages[0]),
ReactorUserID: req.ReactorUserID, Reactions: reactionEvidenceRows(reactions),
})
case domain.PeerTypeChannel:
if s == nil || s.channelMessages == nil {
return domain.ModerationReport{}, false, domain.ErrModerationReportInvalid
}
lookup, found, lookupErr := s.channelMessages.FindMessageReaction(ctx, req.ReporterUserID, domain.ChannelMessageReactionLookupRequest{
ViewerUserID: req.ReporterUserID, ChannelID: req.Target.ID,
MessageID: req.MessageID, ReactorUserID: req.ReactorUserID,
})
if lookupErr != nil {
return domain.ModerationReport{}, false, lookupErr
}
if !found {
return domain.ModerationReport{}, false, domain.ErrModerationEvidenceNotFound
}
evidence, err = json.Marshal(channelReactionEvidenceV1{
SchemaVersion: 1, Message: channelMessageEvidence(lookup.Message),
ReactorUserID: req.ReactorUserID, Reactions: reactionEvidenceRows(lookup.Reactions),
})
default:
return domain.ModerationReport{}, false, domain.ErrModerationReportInvalid
}
if err != nil {
return domain.ModerationReport{}, false, fmt.Errorf("marshal reaction evidence: %w", err)
}
return s.AcceptReport(ctx, domain.ModerationReportDraft{
ReporterUserID: req.ReporterUserID, Source: domain.ModerationSourceReaction,
Target: req.Target, Reason: domain.ModerationReasonOther,
Option: "reaction", CreatedAt: req.CreatedAt,
Items: []domain.ModerationReportItem{{
Kind: domain.ModerationItemReaction, Peer: req.Target,
ItemID: int64(req.MessageID), SecondaryID: req.ReactorUserID,
AuthorUserID: req.ReactorUserID, EvidenceSchemaVersion: 1,
Evidence: evidence,
}},
})
}
func (s *Service) ReportEncryptedSpam(ctx context.Context, reporterUserID int64, chat domain.SecretChat, createdAt time.Time) (domain.ModerationReport, bool, error) {
if reporterUserID <= 0 || !chat.HasParticipant(reporterUserID) || chat.ID <= 0 {
return domain.ModerationReport{}, false, domain.ErrModerationPermissionDenied
}
offenderUserID := chat.PeerOf(reporterUserID)
if offenderUserID <= 0 {
return domain.ModerationReport{}, false, domain.ErrModerationEvidenceNotFound
}
target := domain.Peer{Type: domain.PeerTypeUser, ID: offenderUserID}
evidence, err := json.Marshal(encryptedChatEvidenceV1{
SchemaVersion: 1, ChatID: chat.ID, State: chat.State,
AdminUserID: chat.AdminUserID, ParticipantUserID: chat.ParticipantUserID,
Date: chat.Date,
})
if err != nil {
return domain.ModerationReport{}, false, fmt.Errorf("marshal encrypted chat evidence: %w", err)
}
return s.AcceptReport(ctx, domain.ModerationReportDraft{
ReporterUserID: reporterUserID, Source: domain.ModerationSourceEncryptedSpam,
Target: target, Reason: domain.ModerationReasonSpam, Option: "spam",
CreatedAt: createdAt,
Items: []domain.ModerationReportItem{{
Kind: domain.ModerationItemEncryptedChat, Peer: target,
ItemID: int64(chat.ID), AuthorUserID: offenderUserID,
EvidenceSchemaVersion: 1, Evidence: evidence,
}},
})
}
func (s *Service) ReportStories(ctx context.Context, req domain.ModerationStoryReportRequest) (domain.ModerationReport, bool, error) {
ids, err := canonicalPositiveIDs(req.StoryIDs, domain.MaxStoryID)
if err != nil || req.ReporterUserID <= 0 || req.Target.ID <= 0 {
return domain.ModerationReport{}, false, domain.ErrModerationReportInvalid
}
if s == nil || s.stories == nil {
return domain.ModerationReport{}, false, fmt.Errorf("moderation story reader is not configured")
}
list, err := s.stories.GetStoriesByID(ctx, req.ReporterUserID, req.Target, ids, int(req.CreatedAt.Unix()))
if err != nil {
return domain.ModerationReport{}, false, err
}
byID := make(map[int]domain.Story, len(list.Stories))
for _, story := range list.Stories {
if story.Owner == req.Target && !story.Deleted {
byID[story.ID] = story
}
}
items := make([]domain.ModerationReportItem, 0, len(ids))
holds := make([]domain.ModerationMediaHold, 0)
for _, id := range ids {
story, found := byID[id]
if !found {
return domain.ModerationReport{}, false, domain.ErrModerationEvidenceNotFound
}
evidence, err := json.Marshal(storyEvidenceV1{
SchemaVersion: 1, Owner: story.Owner, StoryID: story.ID,
Date: story.Date, ExpireDate: story.ExpireDate, Pinned: story.Pinned,
Public: story.Public, CloseFriends: story.CloseFriends,
Contacts: story.Contacts, SelectedContacts: story.SelectedContacts,
NoForwards: story.NoForwards, Edited: story.Edited,
Caption: story.Caption, Entities: story.Entities, Media: story.Media,
MediaAreas: story.MediaAreas, Forward: story.Forward,
})
if err != nil {
return domain.ModerationReport{}, false, fmt.Errorf("marshal story evidence: %w", err)
}
authorUserID := int64(0)
if story.Owner.Type == domain.PeerTypeUser {
authorUserID = story.Owner.ID
}
items = append(items, domain.ModerationReportItem{
Kind: domain.ModerationItemStory, Peer: story.Owner,
ItemID: int64(story.ID), AuthorUserID: authorUserID,
EvidenceSchemaVersion: 1, Evidence: evidence,
})
holds = append(holds, mediaHolds(len(items)-1, story.Media)...)
}
return s.AcceptReport(ctx, domain.ModerationReportDraft{
ReporterUserID: req.ReporterUserID, Source: domain.ModerationSourceStory,
Target: req.Target, Reason: req.Reason, Option: req.Option,
Comment: req.Comment, Items: items,
MediaHolds: dedupeMediaHolds(holds), CreatedAt: req.CreatedAt,
})
}
func (s *Service) ReportEphemeral(ctx context.Context, reporterUserID int64, target domain.EphemeralMessage, reason domain.ModerationReason, option, comment string, createdAt time.Time) (domain.ModerationReport, bool, error) {
legacy := domain.NewEphemeralAbuseReport(reporterUserID, option, comment, target, createdAt)
if err := legacy.Validate(); err != nil {
return domain.ModerationReport{}, false, err
}
evidence, err := json.Marshal(struct {
SchemaVersion int `json:"schema_version"`
Evidence domain.EphemeralReportEvidence `json:"evidence"`
}{SchemaVersion: 1, Evidence: legacy.Evidence})
if err != nil {
return domain.ModerationReport{}, false, fmt.Errorf("marshal ephemeral report evidence: %w", err)
}
holds := mediaHolds(0, target.Content.Media)
return s.AcceptReport(ctx, domain.ModerationReportDraft{
ReporterUserID: reporterUserID, Source: domain.ModerationSourceEphemeral,
Target: target.Peer, Reason: reason, Option: option, Comment: comment,
Items: []domain.ModerationReportItem{{
Kind: domain.ModerationItemEphemeral, Peer: target.Peer,
ItemID: int64(target.ID), AuthorUserID: target.SenderUserID,
EvidenceSchemaVersion: 1, Evidence: evidence,
}},
MediaHolds: holds, CreatedAt: createdAt,
})
}
type peerEvidenceV1 struct {
SchemaVersion int `json:"schema_version"`
Target domain.Peer `json:"target"`
User *peerUserEvidenceV1 `json:"user,omitempty"`
Channel *peerChannelEvidenceV1 `json:"channel,omitempty"`
}
type peerUserEvidenceV1 struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Username string `json:"username"`
About string `json:"about"`
Bot bool `json:"bot,omitempty"`
Verified bool `json:"verified,omitempty"`
Scam bool `json:"scam,omitempty"`
Fake bool `json:"fake,omitempty"`
PhotoID int64 `json:"photo_id,omitempty"`
}
type peerChannelEvidenceV1 struct {
ID int64 `json:"id"`
Title string `json:"title"`
About string `json:"about"`
Username string `json:"username"`
Broadcast bool `json:"broadcast,omitempty"`
Megagroup bool `json:"megagroup,omitempty"`
Verified bool `json:"verified,omitempty"`
Scam bool `json:"scam,omitempty"`
Fake bool `json:"fake,omitempty"`
PhotoID int64 `json:"photo_id,omitempty"`
}
type profilePhotoEvidenceV1 struct {
SchemaVersion int `json:"schema_version"`
Owner domain.Peer `json:"owner"`
Photo domain.Photo `json:"photo"`
}
type privateReactionEvidenceV1 struct {
SchemaVersion int `json:"schema_version"`
Message privateMessageEvidenceV1 `json:"message"`
ReactorUserID int64 `json:"reactor_user_id"`
Reactions []messageReactionEvidenceV1 `json:"reactions"`
}
type channelReactionEvidenceV1 struct {
SchemaVersion int `json:"schema_version"`
Message channelMessageEvidenceV1 `json:"message"`
ReactorUserID int64 `json:"reactor_user_id"`
Reactions []messageReactionEvidenceV1 `json:"reactions"`
}
type messageReactionEvidenceV1 struct {
UserID int64 `json:"user_id"`
Type domain.MessageReactionType `json:"type"`
Value string `json:"value"`
Big bool `json:"big,omitempty"`
Unread bool `json:"unread,omitempty"`
ChosenOrder int `json:"chosen_order,omitempty"`
Date int `json:"date"`
}
type encryptedChatEvidenceV1 struct {
SchemaVersion int `json:"schema_version"`
ChatID int `json:"chat_id"`
State domain.SecretChatState `json:"state"`
AdminUserID int64 `json:"admin_user_id"`
ParticipantUserID int64 `json:"participant_user_id"`
Date int `json:"date"`
}
type privateMessageEvidenceV1 struct {
SchemaVersion int `json:"schema_version"`
MessageID int `json:"message_id"`
UID int64 `json:"uid"`
Peer domain.Peer `json:"peer"`
From domain.Peer `json:"from"`
Date int `json:"date"`
EditDate int `json:"edit_date,omitempty"`
Body string `json:"body"`
Entities []domain.MessageEntity `json:"entities,omitempty"`
ReplyTo *domain.MessageReply `json:"reply_to,omitempty"`
Forward *domain.MessageForward `json:"forward,omitempty"`
Reactions *domain.ChannelMessageReactions `json:"reactions,omitempty"`
Media *domain.MessageMedia `json:"media,omitempty"`
RichMessage *domain.MessageRichMessage `json:"rich_message,omitempty"`
GroupedID int64 `json:"grouped_id,omitempty"`
}
type channelMessageEvidenceV1 struct {
SchemaVersion int `json:"schema_version"`
ChannelID int64 `json:"channel_id"`
MessageID int `json:"message_id"`
SenderUserID int64 `json:"sender_user_id"`
From domain.Peer `json:"from"`
SendAs *domain.Peer `json:"send_as,omitempty"`
Date int `json:"date"`
EditDate int `json:"edit_date,omitempty"`
Post bool `json:"post,omitempty"`
Body string `json:"body"`
Entities []domain.MessageEntity `json:"entities,omitempty"`
ReplyTo *domain.MessageReply `json:"reply_to,omitempty"`
Forward *domain.MessageForward `json:"forward,omitempty"`
Reactions *domain.ChannelMessageReactions `json:"reactions,omitempty"`
Action *domain.ChannelMessageAction `json:"action,omitempty"`
Media *domain.MessageMedia `json:"media,omitempty"`
RichMessage *domain.MessageRichMessage `json:"rich_message,omitempty"`
GroupedID int64 `json:"grouped_id,omitempty"`
}
type storyEvidenceV1 struct {
SchemaVersion int `json:"schema_version"`
Owner domain.Peer `json:"owner"`
StoryID int `json:"story_id"`
Date int `json:"date"`
ExpireDate int `json:"expire_date"`
Pinned bool `json:"pinned,omitempty"`
Public bool `json:"public,omitempty"`
CloseFriends bool `json:"close_friends,omitempty"`
Contacts bool `json:"contacts,omitempty"`
SelectedContacts bool `json:"selected_contacts,omitempty"`
NoForwards bool `json:"no_forwards,omitempty"`
Edited bool `json:"edited,omitempty"`
Caption string `json:"caption"`
Entities []domain.MessageEntity `json:"entities,omitempty"`
Media *domain.MessageMedia `json:"media,omitempty"`
MediaAreas []domain.StoryMediaArea `json:"media_areas,omitempty"`
Forward *domain.StoryForward `json:"forward,omitempty"`
}
func privateMessageEvidence(message domain.Message) privateMessageEvidenceV1 {
return privateMessageEvidenceV1{
SchemaVersion: 1, MessageID: message.ID, UID: message.UID,
Peer: message.Peer, From: message.From, Date: message.Date,
EditDate: message.EditDate, Body: message.Body,
Entities: message.Entities, ReplyTo: message.ReplyTo,
Forward: message.Forward, Reactions: message.Reactions,
Media: message.Media, RichMessage: message.RichMessage,
GroupedID: message.GroupedID,
}
}
func channelMessageEvidence(message domain.ChannelMessage) channelMessageEvidenceV1 {
return channelMessageEvidenceV1{
SchemaVersion: 1, ChannelID: message.ChannelID,
MessageID: message.ID, SenderUserID: message.SenderUserID,
From: message.From, SendAs: message.SendAs, Date: message.Date,
EditDate: message.EditDate, Post: message.Post, Body: message.Body,
Entities: message.Entities, ReplyTo: message.ReplyTo,
Forward: message.Forward, Reactions: message.Reactions,
Action: message.Action, Media: message.Media,
RichMessage: message.RichMessage, GroupedID: message.GroupedID,
}
}
func marshalChannelMessageEvidence(message domain.ChannelMessage) ([]byte, error) {
evidence, err := json.Marshal(channelMessageEvidence(message))
if err != nil {
return nil, fmt.Errorf("marshal channel message evidence: %w", err)
}
return evidence, nil
}
func reactionRowsForUser(reactions *domain.ChannelMessageReactions, userID int64) []domain.ChannelMessagePeerReaction {
if reactions == nil || userID <= 0 {
return nil
}
rows := make([]domain.ChannelMessagePeerReaction, 0, len(reactions.Recent))
for _, reaction := range reactions.Recent {
if reaction.UserID == userID {
rows = append(rows, reaction)
}
}
return rows
}
func reactionEvidenceRows(rows []domain.ChannelMessagePeerReaction) []messageReactionEvidenceV1 {
out := make([]messageReactionEvidenceV1, 0, len(rows))
for _, row := range rows {
out = append(out, messageReactionEvidenceV1{
UserID: row.UserID, Type: row.Reaction.Type,
Value: row.Reaction.Value(), Big: row.Big, Unread: row.Unread,
ChosenOrder: row.ChosenOrder, Date: row.Date,
})
}
sort.Slice(out, func(i, j int) bool {
if out[i].ChosenOrder != out[j].ChosenOrder {
return out[i].ChosenOrder < out[j].ChosenOrder
}
if out[i].Type != out[j].Type {
return out[i].Type < out[j].Type
}
return out[i].Value < out[j].Value
})
return out
}
func canonicalPositiveIDs(ids []int, max int) ([]int, error) {
if len(ids) == 0 || len(ids) > domain.MaxModerationReportItems {
return nil, domain.ErrModerationReportInvalid
}
seen := make(map[int]struct{}, len(ids))
out := make([]int, 0, len(ids))
for _, id := range ids {
if id <= 0 || id > max {
return nil, domain.ErrModerationReportInvalid
}
if _, duplicate := seen[id]; !duplicate {
seen[id] = struct{}{}
out = append(out, id)
}
}
sort.Ints(out)
return out, nil
}
func mediaHolds(itemIndex int, media *domain.MessageMedia) []domain.ModerationMediaHold {
if media == nil {
return nil
}
holds := make([]domain.ModerationMediaHold, 0, 8)
addPhoto := func(photo *domain.Photo) {
if photo == nil || photo.ID <= 0 {
return
}
for _, size := range photo.Sizes {
if size.Type != "" {
holds = append(holds, domain.ModerationMediaHold{
ItemIndex: itemIndex, Kind: domain.ModerationMediaPhoto,
StorageKey: "photo:" + strconv.FormatInt(photo.ID, 10) + ":" + size.Type,
})
}
}
}
addDocument := func(document *domain.Document) {
if document == nil || document.ID <= 0 {
return
}
prefix := "doc:" + strconv.FormatInt(document.ID, 10)
holds = append(holds, domain.ModerationMediaHold{
ItemIndex: itemIndex, Kind: domain.ModerationMediaDocument,
StorageKey: prefix,
})
for _, thumb := range document.Thumbs {
if thumb.Type != "" {
holds = append(holds, domain.ModerationMediaHold{
ItemIndex: itemIndex, Kind: domain.ModerationMediaDocument,
StorageKey: prefix + ":" + thumb.Type,
})
}
}
}
addPhoto(media.Photo)
addDocument(media.Document)
addDocument(media.LivePhotoVideo)
return dedupeMediaHolds(holds)
}
func photoHolds(itemIndex int, photo domain.Photo) []domain.ModerationMediaHold {
if photo.ID <= 0 {
return nil
}
holds := make([]domain.ModerationMediaHold, 0, len(photo.Sizes))
for _, size := range photo.Sizes {
if size.Type == "" {
continue
}
holds = append(holds, domain.ModerationMediaHold{
ItemIndex: itemIndex, Kind: domain.ModerationMediaPhoto,
StorageKey: "photo:" + strconv.FormatInt(photo.ID, 10) + ":" + size.Type,
})
}
return dedupeMediaHolds(holds)
}
func dedupeMediaHolds(holds []domain.ModerationMediaHold) []domain.ModerationMediaHold {
if len(holds) == 0 {
return nil
}
seen := make(map[domain.ModerationMediaHold]struct{}, len(holds))
out := make([]domain.ModerationMediaHold, 0, len(holds))
for _, hold := range holds {
if _, duplicate := seen[hold]; duplicate {
continue
}
seen[hold] = struct{}{}
out = append(out, hold)
}
return out
}