[backend/federation] Handle incoming ASFlag activities (ISH-116)

This commit is contained in:
Laura Hausmann 2025-03-11 00:15:37 +01:00
parent 14147a5924
commit 8bbb5c1f98
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
10 changed files with 520 additions and 114 deletions

View file

@ -16,7 +16,7 @@ namespace Iceshrimp.Backend.Core.Database;
public class DatabaseContext(DbContextOptions<DatabaseContext> options) public class DatabaseContext(DbContextOptions<DatabaseContext> options)
: DbContext(options), IDataProtectionKeyContext : DbContext(options), IDataProtectionKeyContext
{ {
public virtual DbSet<AbuseUserReport> AbuseUserReports { get; init; } = null!; public virtual DbSet<Report> Reports { get; init; } = null!;
public virtual DbSet<Announcement> Announcements { get; init; } = null!; public virtual DbSet<Announcement> Announcements { get; init; } = null!;
public virtual DbSet<AnnouncementRead> AnnouncementReads { get; init; } = null!; public virtual DbSet<AnnouncementRead> AnnouncementReads { get; init; } = null!;
public virtual DbSet<Antenna> Antennas { get; init; } = null!; public virtual DbSet<Antenna> Antennas { get; init; } = null!;

View file

@ -36,84 +36,6 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "pg_trgm"); NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "pg_trgm");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AbuseUserReport", b =>
{
b.Property<string>("Id")
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("id");
b.Property<string>("AssigneeId")
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("assigneeId");
b.Property<string>("Comment")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("comment");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("createdAt")
.HasComment("The created date of the AbuseUserReport.");
b.Property<bool>("Forwarded")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("forwarded");
b.Property<string>("ReporterHost")
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasColumnName("reporterHost")
.HasComment("[Denormalized]");
b.Property<string>("ReporterId")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("reporterId");
b.Property<bool>("Resolved")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("resolved");
b.Property<string>("TargetUserHost")
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasColumnName("targetUserHost")
.HasComment("[Denormalized]");
b.Property<string>("TargetUserId")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("targetUserId");
b.HasKey("Id");
b.HasIndex("AssigneeId");
b.HasIndex("CreatedAt");
b.HasIndex("ReporterHost");
b.HasIndex("ReporterId");
b.HasIndex("Resolved");
b.HasIndex("TargetUserHost");
b.HasIndex("TargetUserId");
b.ToTable("abuse_user_report");
});
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AllowedInstance", b => modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AllowedInstance", b =>
{ {
b.Property<string>("Host") b.Property<string>("Host")
@ -3870,6 +3792,84 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
b.ToTable("renote_muting"); b.ToTable("renote_muting");
}); });
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Report", b =>
{
b.Property<string>("Id")
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("id");
b.Property<string>("AssigneeId")
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("assigneeId");
b.Property<string>("Comment")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("comment");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("createdAt")
.HasComment("The created date of the Report.");
b.Property<bool>("Forwarded")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("forwarded");
b.Property<string>("ReporterHost")
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasColumnName("reporterHost")
.HasComment("[Denormalized]");
b.Property<string>("ReporterId")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("reporterId");
b.Property<bool>("Resolved")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("resolved");
b.Property<string>("TargetUserHost")
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasColumnName("targetUserHost")
.HasComment("[Denormalized]");
b.Property<string>("TargetUserId")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("targetUserId");
b.HasKey("Id");
b.HasIndex("AssigneeId");
b.HasIndex("CreatedAt");
b.HasIndex("ReporterHost");
b.HasIndex("ReporterId");
b.HasIndex("Resolved");
b.HasIndex("TargetUserHost");
b.HasIndex("TargetUserId");
b.ToTable("report");
});
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Rule", b => modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Rule", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
@ -4922,30 +4922,19 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
b.ToTable("data_protection_keys", (string)null); b.ToTable("data_protection_keys", (string)null);
}); });
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AbuseUserReport", b => modelBuilder.Entity("reported_note", b =>
{ {
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Assignee") b.Property<string>("note_id")
.WithMany("AbuseUserReportAssignees") .HasColumnType("character varying(32)");
.HasForeignKey("AssigneeId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Reporter") b.Property<string>("report_id")
.WithMany("AbuseUserReportReporters") .HasColumnType("character varying(32)");
.HasForeignKey("ReporterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "TargetUser") b.HasKey("note_id", "report_id");
.WithMany("AbuseUserReportTargetUsers")
.HasForeignKey("TargetUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Assignee"); b.HasIndex("report_id");
b.Navigation("Reporter"); b.ToTable("reported_note");
b.Navigation("TargetUser");
}); });
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AnnouncementRead", b => modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AnnouncementRead", b =>
@ -5720,6 +5709,32 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
b.Navigation("Muter"); b.Navigation("Muter");
}); });
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Report", b =>
{
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Assignee")
.WithMany("AbuseUserReportAssignees")
.HasForeignKey("AssigneeId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Reporter")
.WithMany("AbuseUserReportReporters")
.HasForeignKey("ReporterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "TargetUser")
.WithMany("AbuseUserReportTargetUsers")
.HasForeignKey("TargetUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Assignee");
b.Navigation("Reporter");
b.Navigation("TargetUser");
});
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Session", b => modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Session", b =>
{ {
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User")
@ -5930,6 +5945,21 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
b.Navigation("User"); b.Navigation("User");
}); });
modelBuilder.Entity("reported_note", b =>
{
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Note", null)
.WithMany()
.HasForeignKey("note_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Report", null)
.WithMany()
.HasForeignKey("report_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Announcement", b => modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Announcement", b =>
{ {
b.Navigation("AnnouncementReads"); b.Navigation("AnnouncementReads");

View file

@ -0,0 +1,242 @@
using System;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Iceshrimp.Backend.Core.Database.Migrations
{
/// <inheritdoc />
[DbContext(typeof(DatabaseContext))]
[Migration("20250310231413_RefactorReportsSchema")]
public partial class RefactorReportsSchema : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_abuse_user_report_user_assigneeId",
table: "abuse_user_report");
migrationBuilder.DropForeignKey(
name: "FK_abuse_user_report_user_reporterId",
table: "abuse_user_report");
migrationBuilder.DropForeignKey(
name: "FK_abuse_user_report_user_targetUserId",
table: "abuse_user_report");
migrationBuilder.DropPrimaryKey(
name: "PK_abuse_user_report",
table: "abuse_user_report");
migrationBuilder.RenameTable(
name: "abuse_user_report",
newName: "report");
migrationBuilder.RenameIndex(
name: "IX_abuse_user_report_targetUserId",
table: "report",
newName: "IX_report_targetUserId");
migrationBuilder.RenameIndex(
name: "IX_abuse_user_report_targetUserHost",
table: "report",
newName: "IX_report_targetUserHost");
migrationBuilder.RenameIndex(
name: "IX_abuse_user_report_resolved",
table: "report",
newName: "IX_report_resolved");
migrationBuilder.RenameIndex(
name: "IX_abuse_user_report_reporterId",
table: "report",
newName: "IX_report_reporterId");
migrationBuilder.RenameIndex(
name: "IX_abuse_user_report_reporterHost",
table: "report",
newName: "IX_report_reporterHost");
migrationBuilder.RenameIndex(
name: "IX_abuse_user_report_createdAt",
table: "report",
newName: "IX_report_createdAt");
migrationBuilder.RenameIndex(
name: "IX_abuse_user_report_assigneeId",
table: "report",
newName: "IX_report_assigneeId");
migrationBuilder.AlterColumn<DateTime>(
name: "createdAt",
table: "report",
type: "timestamp with time zone",
nullable: false,
comment: "The created date of the Report.",
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldComment: "The created date of the AbuseUserReport.");
migrationBuilder.AddPrimaryKey(
name: "PK_report",
table: "report",
column: "id");
migrationBuilder.CreateTable(
name: "reported_note",
columns: table => new
{
note_id = table.Column<string>(type: "character varying(32)", nullable: false),
report_id = table.Column<string>(type: "character varying(32)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_reported_note", x => new { x.note_id, x.report_id });
table.ForeignKey(
name: "FK_reported_note_note_note_id",
column: x => x.note_id,
principalTable: "note",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_reported_note_report_report_id",
column: x => x.report_id,
principalTable: "report",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_reported_note_report_id",
table: "reported_note",
column: "report_id");
migrationBuilder.AddForeignKey(
name: "FK_report_user_assigneeId",
table: "report",
column: "assigneeId",
principalTable: "user",
principalColumn: "id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_report_user_reporterId",
table: "report",
column: "reporterId",
principalTable: "user",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_report_user_targetUserId",
table: "report",
column: "targetUserId",
principalTable: "user",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_report_user_assigneeId",
table: "report");
migrationBuilder.DropForeignKey(
name: "FK_report_user_reporterId",
table: "report");
migrationBuilder.DropForeignKey(
name: "FK_report_user_targetUserId",
table: "report");
migrationBuilder.DropTable(
name: "reported_note");
migrationBuilder.DropPrimaryKey(
name: "PK_report",
table: "report");
migrationBuilder.RenameTable(
name: "report",
newName: "abuse_user_report");
migrationBuilder.RenameIndex(
name: "IX_report_targetUserId",
table: "abuse_user_report",
newName: "IX_abuse_user_report_targetUserId");
migrationBuilder.RenameIndex(
name: "IX_report_targetUserHost",
table: "abuse_user_report",
newName: "IX_abuse_user_report_targetUserHost");
migrationBuilder.RenameIndex(
name: "IX_report_resolved",
table: "abuse_user_report",
newName: "IX_abuse_user_report_resolved");
migrationBuilder.RenameIndex(
name: "IX_report_reporterId",
table: "abuse_user_report",
newName: "IX_abuse_user_report_reporterId");
migrationBuilder.RenameIndex(
name: "IX_report_reporterHost",
table: "abuse_user_report",
newName: "IX_abuse_user_report_reporterHost");
migrationBuilder.RenameIndex(
name: "IX_report_createdAt",
table: "abuse_user_report",
newName: "IX_abuse_user_report_createdAt");
migrationBuilder.RenameIndex(
name: "IX_report_assigneeId",
table: "abuse_user_report",
newName: "IX_abuse_user_report_assigneeId");
migrationBuilder.AlterColumn<DateTime>(
name: "createdAt",
table: "abuse_user_report",
type: "timestamp with time zone",
nullable: false,
comment: "The created date of the AbuseUserReport.",
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldComment: "The created date of the Report.");
migrationBuilder.AddPrimaryKey(
name: "PK_abuse_user_report",
table: "abuse_user_report",
column: "id");
migrationBuilder.AddForeignKey(
name: "FK_abuse_user_report_user_assigneeId",
table: "abuse_user_report",
column: "assigneeId",
principalTable: "user",
principalColumn: "id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_abuse_user_report_user_reporterId",
table: "abuse_user_report",
column: "reporterId",
principalTable: "user",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_abuse_user_report_user_targetUserId",
table: "abuse_user_report",
column: "targetUserId",
principalTable: "user",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
}
}
}

View file

@ -1,18 +1,19 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using Iceshrimp.EntityFrameworkCore.Extensions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Iceshrimp.Backend.Core.Database.Tables; namespace Iceshrimp.Backend.Core.Database.Tables;
[Table("abuse_user_report")] [Table("report")]
[Index(nameof(ReporterId))] [Index(nameof(ReporterId))]
[Index(nameof(Resolved))] [Index(nameof(Resolved))]
[Index(nameof(TargetUserHost))] [Index(nameof(TargetUserHost))]
[Index(nameof(TargetUserId))] [Index(nameof(TargetUserId))]
[Index(nameof(CreatedAt))] [Index(nameof(CreatedAt))]
[Index(nameof(ReporterHost))] [Index(nameof(ReporterHost))]
public class AbuseUserReport public class Report
{ {
[Key] [Key]
[Column("id")] [Column("id")]
@ -20,7 +21,7 @@ public class AbuseUserReport
public string Id { get; set; } = null!; public string Id { get; set; } = null!;
/// <summary> /// <summary>
/// The created date of the AbuseUserReport. /// The created date of the Report.
/// </summary> /// </summary>
[Column("createdAt")] [Column("createdAt")]
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
@ -71,11 +72,13 @@ public class AbuseUserReport
[InverseProperty(nameof(User.AbuseUserReportTargetUsers))] [InverseProperty(nameof(User.AbuseUserReportTargetUsers))]
public virtual User TargetUser { get; set; } = null!; public virtual User TargetUser { get; set; } = null!;
private class EntityTypeConfiguration : IEntityTypeConfiguration<AbuseUserReport> public virtual ICollection<Note> Notes { get; set; } = new List<Note>();
private class EntityTypeConfiguration : IEntityTypeConfiguration<Report>
{ {
public void Configure(EntityTypeBuilder<AbuseUserReport> entity) public void Configure(EntityTypeBuilder<Report> entity)
{ {
entity.Property(e => e.CreatedAt).HasComment("The created date of the AbuseUserReport."); entity.Property(e => e.CreatedAt).HasComment("The created date of the Report.");
entity.Property(e => e.Forwarded).HasDefaultValue(false); entity.Property(e => e.Forwarded).HasDefaultValue(false);
entity.Property(e => e.ReporterHost).HasComment("[Denormalized]"); entity.Property(e => e.ReporterHost).HasComment("[Denormalized]");
entity.Property(e => e.Resolved).HasDefaultValue(false); entity.Property(e => e.Resolved).HasDefaultValue(false);
@ -92,6 +95,10 @@ public class AbuseUserReport
entity.HasOne(d => d.TargetUser) entity.HasOne(d => d.TargetUser)
.WithMany(p => p.AbuseUserReportTargetUsers) .WithMany(p => p.AbuseUserReportTargetUsers)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
entity.HasMany(p => p.Notes)
.WithMany()
.UsingEntity("reported_note", "report_id", "note_id", DeleteBehavior.Cascade);
} }
} }
} }

View file

@ -260,14 +260,14 @@ public class User : IIdentifiable
[Column("splitDomainResolved")] public bool SplitDomainResolved { get; set; } [Column("splitDomainResolved")] public bool SplitDomainResolved { get; set; }
[InverseProperty(nameof(AbuseUserReport.Assignee))] [InverseProperty(nameof(Report.Assignee))]
public virtual ICollection<AbuseUserReport> AbuseUserReportAssignees { get; set; } = new List<AbuseUserReport>(); public virtual ICollection<Report> AbuseUserReportAssignees { get; set; } = new List<Report>();
[InverseProperty(nameof(AbuseUserReport.Reporter))] [InverseProperty(nameof(Report.Reporter))]
public virtual ICollection<AbuseUserReport> AbuseUserReportReporters { get; set; } = new List<AbuseUserReport>(); public virtual ICollection<Report> AbuseUserReportReporters { get; set; } = new List<Report>();
[InverseProperty(nameof(AbuseUserReport.TargetUser))] [InverseProperty(nameof(Report.TargetUser))]
public virtual ICollection<AbuseUserReport> AbuseUserReportTargetUsers { get; set; } = new List<AbuseUserReport>(); public virtual ICollection<Report> AbuseUserReportTargetUsers { get; set; } = new List<Report>();
[InverseProperty(nameof(AnnouncementRead.User))] [InverseProperty(nameof(AnnouncementRead.User))]
public virtual ICollection<AnnouncementRead> AnnouncementReads { get; set; } = new List<AnnouncementRead>(); public virtual ICollection<AnnouncementRead> AnnouncementReads { get; set; } = new List<AnnouncementRead>();

View file

@ -86,6 +86,7 @@ public class ActivityHandlerService(
ASUndo undo => HandleUndoAsync(undo, resolvedActor), ASUndo undo => HandleUndoAsync(undo, resolvedActor),
ASUnfollow unfollow => HandleUnfollowAsync(unfollow, resolvedActor), ASUnfollow unfollow => HandleUnfollowAsync(unfollow, resolvedActor),
ASUpdate update => HandleUpdateAsync(update, resolvedActor), ASUpdate update => HandleUpdateAsync(update, resolvedActor),
ASFlag flag => HandleFlagAsync(flag, resolvedActor),
// Separated for readability // Separated for readability
_ => throw GracefulException.UnprocessableEntity($"Activity type {activity.Type} is unknown") _ => throw GracefulException.UnprocessableEntity($"Activity type {activity.Type} is unknown")
@ -527,6 +528,50 @@ public class ActivityHandlerService(
await userSvc.MoveRelationshipsAsync(source, target, sourceUri, targetUri); await userSvc.MoveRelationshipsAsync(source, target, sourceUri, targetUri);
} }
private async Task HandleFlagAsync(ASFlag flag, User resolvedActor)
{
if (resolvedActor.IsLocalUser)
throw GracefulException.UnprocessableEntity("Refusing to process locally originating report via AP");
if (flag.Object is not { Length: > 0 })
throw GracefulException.UnprocessableEntity("ASFlag activity does not reference any objects");
var candidates = flag.Object.Take(25)
.Select(p => p.Id)
.NotNull()
.Where(p => p.StartsWith($"https://{config.Value.WebDomain}/users/")
|| p.StartsWith($"https://{config.Value.WebDomain}/notes/"))
.Select(p => p[$"https://{config.Value.WebDomain}/notes/".Length..])
.ToArray();
var userMatch = await db.Users.FirstOrDefaultAsync(p => p.IsLocalUser && candidates.Contains(p.Id));
var noteMatches = await db.Notes.Where(p => p.UserHost == null && candidates.Contains(p.Id))
.Include(note => note.User)
.ToListAsync();
if (userMatch == null && noteMatches.Count == 0)
throw GracefulException.UnprocessableEntity("ASFlag activity object resolution yielded zero results");
userMatch ??= noteMatches[0].User;
if (noteMatches.Count != 0 && noteMatches.Any(p => p.User != userMatch))
throw GracefulException.UnprocessableEntity("Refusing to process ASFlag: note author mismatch");
var report = new Report
{
Id = IdHelpers.GenerateSnowflakeId(),
CreatedAt = DateTime.UtcNow,
TargetUser = userMatch,
TargetUserHost = userMatch.Host,
Reporter = resolvedActor,
ReporterHost = resolvedActor.Host,
Notes = noteMatches,
Comment = flag.Content ?? ""
};
db.Add(report);
await db.SaveChangesAsync();
}
private async Task UnfollowAsync(ASActor followeeActor, User follower) private async Task UnfollowAsync(ASActor followeeActor, User follower)
{ {
//TODO: send reject? or do we not want to copy that part of the old ap core //TODO: send reject? or do we not want to copy that part of the old ap core

View file

@ -239,4 +239,20 @@ public class ActivityRenderer(
PublishedAt = bite.CreatedAt, PublishedAt = bite.CreatedAt,
To = userRenderer.RenderLite(fallbackTo) To = userRenderer.RenderLite(fallbackTo)
}; };
public ASFlag RenderFlag(User actor, IEnumerable<Note> notes, string comment) => new()
{
Id = GenerateActivityId(),
Actor = userRenderer.RenderLite(actor),
Object = notes.Select(noteRenderer.RenderLite).ToArray<ASObject>(),
Content = comment
};
public ASFlag RenderFlag(User actor, User user, string comment) => new()
{
Id = GenerateActivityId(),
Actor = userRenderer.RenderLite(actor),
Object = [userRenderer.RenderLite(user)],
Content = comment
};
} }

View file

@ -35,6 +35,7 @@ public class ASActivity : ASObjectWithId
public const string Like = $"{Ns}#Like"; public const string Like = $"{Ns}#Like";
public const string Block = $"{Ns}#Block"; public const string Block = $"{Ns}#Block";
public const string Move = $"{Ns}#Move"; public const string Move = $"{Ns}#Move";
public const string Flag = $"{Ns}#Flag";
// Extensions // Extensions
public const string Bite = "https://ns.mia.jetzt/as#Bite"; public const string Bite = "https://ns.mia.jetzt/as#Bite";
@ -42,6 +43,23 @@ public class ASActivity : ASObjectWithId
} }
} }
public class ASActivityWithObjectArray : ASActivity
{
private ASObject[]? _object;
[J($"{Constants.ActivityStreamsNs}#object")]
[JC(typeof(ASObjectArrayConverter))]
public new ASObject[]? Object
{
get => _object;
set
{
_object = value;
base.Object = value?.FirstOrDefault();
}
}
}
public class ASCreate : ASActivity public class ASCreate : ASActivity
{ {
public ASCreate() => Type = Types.Create; public ASCreate() => Type = Types.Create;
@ -128,6 +146,15 @@ public class ASBlock : ASActivity
public ASBlock() => Type = Types.Block; public ASBlock() => Type = Types.Block;
} }
public class ASFlag : ASActivityWithObjectArray
{
public ASFlag() => Type = Types.Flag;
[J($"{Constants.ActivityStreamsNs}#content")]
[JC(typeof(VC))]
public string? Content { get; set; }
}
public class ASLike : ASActivity public class ASLike : ASActivity
{ {
public ASLike() => Type = Types.Like; public ASLike() => Type = Types.Like;

View file

@ -1,4 +1,5 @@
using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Extensions;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using J = Newtonsoft.Json.JsonPropertyAttribute; using J = Newtonsoft.Json.JsonPropertyAttribute;
@ -54,6 +55,7 @@ public class ASObject : ASObjectBase
ASActivity.Types.EmojiReact => token.ToObject<ASEmojiReact>(), ASActivity.Types.EmojiReact => token.ToObject<ASEmojiReact>(),
ASActivity.Types.Block => token.ToObject<ASBlock>(), ASActivity.Types.Block => token.ToObject<ASBlock>(),
ASActivity.Types.Move => token.ToObject<ASMove>(), ASActivity.Types.Move => token.ToObject<ASMove>(),
ASActivity.Types.Flag => token.ToObject<ASFlag>(),
_ => token.ToObject<ASObject>() _ => token.ToObject<ASObject>()
}; };
case JTokenType.Array: case JTokenType.Array:
@ -131,3 +133,40 @@ internal sealed class ASObjectConverter : JsonConverter
throw new NotImplementedException(); throw new NotImplementedException();
} }
} }
internal sealed class ASObjectArrayConverter : JsonConverter
{
public override bool CanWrite => false;
public override bool CanConvert(Type objectType)
{
return true;
}
public override object? ReadJson(
JsonReader reader, Type objectType, object? existingValue,
JsonSerializer serializer
)
{
if (reader.TokenType == JsonToken.StartArray)
{
var obj = JArray.Load(reader);
var arr = obj.Select(ASObject.Deserialize).NotNull().ToArray();
return arr.Length > 0 ? arr : null;
}
if (reader.TokenType == JsonToken.StartObject)
{
var obj = JObject.Load(reader);
var deserialized = ASObject.Deserialize(obj);
return deserialized != null ? new[]{ deserialized } : null;
}
throw new Exception("this shouldn't happen");
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}

View file

@ -28,7 +28,7 @@
<PackageReference Include="EntityFrameworkCore.Projectables" Version="4.0.0" /> <PackageReference Include="EntityFrameworkCore.Projectables" Version="4.0.0" />
<PackageReference Include="FlexLabs.EntityFrameworkCore.Upsert" Version="8.1.2" /> <PackageReference Include="FlexLabs.EntityFrameworkCore.Upsert" Version="8.1.2" />
<PackageReference Include="Iceshrimp.Assets.Fonts" Version="1.0.0" /> <PackageReference Include="Iceshrimp.Assets.Fonts" Version="1.0.0" />
<PackageReference Include="Iceshrimp.EntityFrameworkCore.Extensions" Version="1.0.1" /> <PackageReference Include="Iceshrimp.EntityFrameworkCore.Extensions" Version="1.0.2" />
<PackageReference Include="Iceshrimp.ObjectStorage.S3" Version="0.34.3" /> <PackageReference Include="Iceshrimp.ObjectStorage.S3" Version="0.34.3" />
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" /> <PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0" /> <PackageReference Include="JetBrains.Annotations" Version="2024.3.0" />