739 lines
30 KiB
Go
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
|
|
}
|