a fork of iceshrimp.net but a tweaked frontend to my personal liking. waow
fediverse social-media social iceshrimp fedi
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[backend/federation] Implement quote stamp infrastructure

Kopper edf73a73 32a3c5b8

+194 -5
+17
Iceshrimp.Backend/Controllers/Federation/ActivityPubController.cs
··· 29 29 QueueService queues, 30 30 ActivityPub.NoteRenderer noteRenderer, 31 31 ActivityPub.UserRenderer userRenderer, 32 + ActivityPub.StampRenderer stampRenderer, 32 33 IOptions<Config.InstanceSection> config, 33 34 IOptionsSnapshot<Config.SecuritySection> security 34 35 ) : ControllerBase, IScopedService ··· 337 338 Image = new ASImage { Url = new ASLink(emoji.RawPublicUrl), MediaType = emoji.Type } 338 339 }; 339 340 341 + return LdHelpers.Compact(rendered); 342 + } 343 + 344 + [HttpGet("/stamp/{id}")] 345 + [AuthorizedFetch] 346 + [OutputCache(PolicyName = "federation")] 347 + [MediaTypeRouteFilter("application/activity+json", "application/ld+json")] 348 + [OverrideResultType<ASQuoteAuthorization>] 349 + [ProducesResults(HttpStatusCode.OK)] 350 + [ProducesErrors(HttpStatusCode.NotFound)] 351 + public async Task<ActionResult<JObject>> GetStamp(string id) 352 + { 353 + var stamp = await db.InteractionStamps.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id); 354 + if (stamp == null) throw GracefulException.NotFound("Stamp not found"); 355 + 356 + var rendered = stampRenderer.RenderStamp(stamp); 340 357 return LdHelpers.Compact(rendered); 341 358 } 342 359 }
+1
Iceshrimp.Backend/Core/Configuration/Constants.cs
··· 25 25 public const string MisskeyNs = "https://misskey-hub.net/ns"; 26 26 public const string FedibirdNs = "http://fedibird.com/ns"; 27 27 public const string GoToSocialNs = "https://gotosocial.org/ns"; 28 + public const string FepNs = "https://w3id.org/fep"; 28 29 public static readonly string[] SystemUsers = ["instance.actor", "relay.actor"]; 29 30 30 31 public const string APMime = "application/activity+json";
+3
Iceshrimp.Backend/Core/Database/DatabaseContext.cs
··· 37 37 public virtual DbSet<GalleryPost> GalleryPosts { get; init; } = null!; 38 38 public virtual DbSet<Hashtag> Hashtags { get; init; } = null!; 39 39 public virtual DbSet<Instance> Instances { get; init; } = null!; 40 + public virtual DbSet<InteractionStamp> InteractionStamps { get; init; } = null!; 40 41 public virtual DbSet<Marker> Markers { get; init; } = null!; 41 42 public virtual DbSet<MessagingMessage> MessagingMessages { get; init; } = null!; 42 43 public virtual DbSet<Meta> Meta { get; init; } = null!; ··· 157 158 options.MapEnum<Job.JobStatus>("job_status"); 158 159 options.MapEnum<Filter.FilterContext>("filter_context_enum"); 159 160 options.MapEnum<Filter.FilterAction>("filter_action_enum"); 161 + options.MapEnum<InteractionStamp.InteractionStampType>("interaction_stamp_type"); 160 162 }); 161 163 162 164 optionsBuilder.UseProjectables(options => { options.CompatibilityMode(CompatibilityMode.Full); }); ··· 180 182 .HasPostgresEnum<Job.JobStatus>() 181 183 .HasPostgresEnum<Filter.FilterContext>() 182 184 .HasPostgresEnum<Filter.FilterAction>() 185 + .HasPostgresEnum<InteractionStamp.InteractionStampType>() 183 186 .HasPostgresExtension("pg_trgm"); 184 187 185 188 modelBuilder
+55
Iceshrimp.Backend/Core/Database/Tables/InteractionStamp.cs
··· 1 + using System.ComponentModel.DataAnnotations; 2 + using System.ComponentModel.DataAnnotations.Schema; 3 + using Microsoft.EntityFrameworkCore; 4 + using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 + using NpgsqlTypes; 6 + 7 + namespace Iceshrimp.Backend.Core.Database.Tables; 8 + 9 + [Table("interaction_stamp")] 10 + [Index(nameof(NoteId), IsUnique = true)] 11 + public class InteractionStamp 12 + { 13 + [PgName("interaction_stamp_type")] 14 + public enum InteractionStampType 15 + { 16 + [PgName("quote")] Quote, 17 + } 18 + 19 + [Key] 20 + [Column("id")] 21 + [StringLength(32)] 22 + public string Id { get; set; } = null!; 23 + 24 + [Column("type")] 25 + public InteractionStampType Type { get; set; } 26 + 27 + /// <summary> 28 + /// The note being interacted with 29 + /// </summary> 30 + [Column("targetNoteId")] 31 + [StringLength(32)] 32 + public string TargetNoteId { get; set; } = null!; 33 + 34 + /// <summary> 35 + /// The note doing the interaction (quote, reply, whatever) 36 + /// </summary> 37 + [Column("noteId")] 38 + [StringLength(32)] 39 + public string NoteId { get; set; } = null!; 40 + 41 + [ForeignKey(nameof(TargetNoteId))] 42 + public Note TargetNote { get; set; } = null!; 43 + 44 + [ForeignKey(nameof(NoteId))] 45 + public Note Note { get; set; } = null!; 46 + 47 + private class EntityTypeConfiguration : IEntityTypeConfiguration<InteractionStamp> 48 + { 49 + public void Configure(EntityTypeBuilder<InteractionStamp> entity) 50 + { 51 + entity.Property(e => e.TargetNoteId).HasComment("The note being interacted with"); 52 + entity.Property(e => e.NoteId).HasComment("The note doing the interaction (quote, reply, whatever)"); 53 + } 54 + } 55 + }
+6
Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs
··· 666 666 .Include(p => p.Notes) 667 667 .ThenInclude(p => p.User.UserProfile); 668 668 } 669 + 670 + public static IQueryable<InteractionStamp> IncludeCommonProperties(this IQueryable<InteractionStamp> query) 671 + { 672 + return query.Include(p => p.Note.User) 673 + .Include(p => p.TargetNote.User); 674 + } 669 675 670 676 #pragma warning restore CS8602 // Dereference of a possibly null reference. 671 677 }
+18 -2
Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityRenderer.cs
··· 11 11 public class ActivityRenderer( 12 12 IOptions<Config.InstanceSection> config, 13 13 UserRenderer userRenderer, 14 - NoteRenderer noteRenderer 14 + NoteRenderer noteRenderer, 15 + StampRenderer stampRenderer 15 16 ) : IScopedService 16 17 { 17 18 private string GenerateActivityId() => ··· 56 57 Actor = userRenderer.RenderLite(followee), 57 58 Object = RenderFollow(userRenderer.RenderLite(follower), userRenderer.RenderLite(followee), requestId) 58 59 }; 59 - 60 + 61 + public ASAccept RenderAcceptStamp(InteractionStamp stamp, ASObject request) => new() 62 + { 63 + Id = GenerateActivityId(), 64 + Actor = userRenderer.RenderLite(stamp.TargetNote.User), 65 + Object = request, 66 + Result = new ASObjectBase(stampRenderer.StampId(stamp)), 67 + }; 68 + 69 + public ASReject RenderRejectStamp(InteractionStamp stamp, ASObject request) => new() 70 + { 71 + Id = GenerateActivityId(), 72 + Actor = userRenderer.RenderLite(stamp.TargetNote.User), 73 + Object = request 74 + }; 75 + 60 76 public ASLike RenderLike(NoteLike like) 61 77 { 62 78 if (like.Note.UserHost == null)
+23
Iceshrimp.Backend/Core/Federation/ActivityPub/StampRenderer.cs
··· 1 + using Iceshrimp.Backend.Core.Configuration; 2 + using Iceshrimp.Backend.Core.Database.Tables; 3 + using Iceshrimp.Backend.Core.Extensions; 4 + using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; 5 + using Microsoft.Extensions.Options; 6 + 7 + namespace Iceshrimp.Backend.Core.Federation.ActivityPub; 8 + 9 + public class StampRenderer( 10 + IOptions<Config.InstanceSection> config, 11 + UserRenderer userRenderer, 12 + NoteRenderer noteRenderer) : IScopedService 13 + { 14 + public string StampId(InteractionStamp stamp) => $"https://{config.Value.WebDomain}/stamp/{stamp.Id}"; 15 + 16 + public ASQuoteAuthorization RenderStamp(InteractionStamp stamp) => new() 17 + { 18 + Id = StampId(stamp), 19 + AttributedTo = userRenderer.RenderLite(stamp.TargetNote.User), 20 + InteractingObject = noteRenderer.RenderLite(stamp.Note), 21 + InteractionTarget = noteRenderer.RenderLite(stamp.TargetNote), 22 + }; 23 + }
+17 -2
Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActivity.cs
··· 38 38 public const string Flag = $"{Ns}#Flag"; 39 39 40 40 // Extensions 41 - public const string Bite = "https://ns.mia.jetzt/as#Bite"; 42 - public const string EmojiReact = "http://litepub.social/ns#EmojiReact"; 41 + public const string Bite = "https://ns.mia.jetzt/as#Bite"; 42 + public const string EmojiReact = "http://litepub.social/ns#EmojiReact"; 43 + public const string QuoteRequest = $"{Constants.FepNs}/044f#QuoteRequest"; 43 44 } 44 45 } 45 46 ··· 129 130 public class ASAccept : ASActivity 130 131 { 131 132 public ASAccept() => Type = Types.Accept; 133 + 134 + [J($"{Constants.ActivityStreamsNs}#result")] 135 + [JC(typeof(ASObjectConverter))] 136 + public ASObjectBase? Result { get; set; } 132 137 } 133 138 134 139 public class ASReject : ASActivity ··· 238 243 [JC(typeof(ASLinkConverter))] 239 244 public required ASLink Target { get; set; } 240 245 } 246 + 247 + public class ASQuoteRequest : ASActivity 248 + { 249 + public ASQuoteRequest() => Type = Types.QuoteRequest; 250 + 251 + [JR] 252 + [J($"{Constants.ActivityStreamsNs}#instrument")] 253 + [JC(typeof(ASLinkConverter))] 254 + public required ASNote Instrument { get; set; } 255 + }
+1 -1
Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs
··· 62 62 [JC(typeof(VC))] 63 63 public DateTime? UpdatedAt { get; set; } 64 64 65 - [J($"https://w3id.org/fep/c16b#htmlMfm")] 65 + [J($"{Constants.FepNs}/c16b#htmlMfm")] 66 66 [JC(typeof(VC))] 67 67 public bool? HtmlMfm { get; set; } 68 68
+24
Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASQuoteAuthorization.cs
··· 1 + using Iceshrimp.Backend.Core.Configuration; 2 + using J = Newtonsoft.Json.JsonPropertyAttribute; 3 + using JC = Newtonsoft.Json.JsonConverterAttribute; 4 + 5 + namespace Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; 6 + 7 + public class ASQuoteAuthorization : ASObjectWithId 8 + { 9 + public ASQuoteAuthorization() => Type = $"{Constants.FepNs}/044f#QuoteAuthorization"; 10 + 11 + [J($"{Constants.ActivityStreamsNs}#attributedTo")] 12 + [JC(typeof(ASObjectBaseConverter))] 13 + public ASObjectBase? AttributedTo { get; set; } 14 + 15 + [J($"{Constants.GoToSocialNs}#interactingObject")] 16 + [JC(typeof(ASObjectBaseConverter))] 17 + public ASObjectBase? InteractingObject { get; set; } 18 + 19 + [J($"{Constants.GoToSocialNs}#interactionTarget")] 20 + [JC(typeof(ASObjectBaseConverter))] 21 + public ASObjectBase? InteractionTarget { get; set; } 22 + } 23 + 24 + public class ASQuoteAuthorizationConverter : ASSerializer.ListSingleObjectConverter<ASQuoteAuthorization>;
+29
Iceshrimp.Backend/Core/Services/StampService.cs
··· 1 + using Iceshrimp.Backend.Core.Database; 2 + using Iceshrimp.Backend.Core.Database.Tables; 3 + using Iceshrimp.Backend.Core.Extensions; 4 + using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; 5 + using Iceshrimp.Backend.Core.Helpers; 6 + 7 + namespace Iceshrimp.Backend.Core.Services; 8 + 9 + public class StampService(DatabaseContext db, ActivityPub.ActivityRenderer activityRenderer, ActivityPub.ActivityDeliverService activityDeliver) : IScopedService 10 + { 11 + public async Task AcceptQuoteAsync(Note targetNote, Note quote, ASQuoteRequest request) 12 + { 13 + var stamp = new InteractionStamp 14 + { 15 + Id = IdHelpers.GenerateSnowflakeId(), 16 + Type = InteractionStamp.InteractionStampType.Quote, 17 + TargetNote = targetNote, 18 + Note = quote 19 + }; 20 + 21 + // before database save so if rendering fails we don't save the stamp 22 + var accept = activityRenderer.RenderAcceptStamp(stamp, request); 23 + 24 + db.Add(stamp); 25 + await db.SaveChangesAsync(); 26 + 27 + await activityDeliver.DeliverToAsync(accept, targetNote.User, quote.User); 28 + } 29 + }