[backend/masto-client] Implement /reblog and /unreblog endpoints

This commit is contained in:
Laura Hausmann 2024-02-22 02:23:21 +01:00
parent a8b02aa6f8
commit 5d7035e63c
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
4 changed files with 107 additions and 7 deletions

View file

@ -5,6 +5,7 @@ using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Services;
@ -119,6 +120,52 @@ public class StatusController(
return await GetNote(id); return await GetNote(id);
} }
[HttpPost("{id}/reblog")]
[Authorize("write:favourites")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
public async Task<IActionResult> Renote(string id)
{
var user = HttpContext.GetUserOrFail();
var note = await db.Notes.Where(p => p.Id == id)
.IncludeCommonProperties()
.EnsureVisibleFor(user)
.FirstOrDefaultAsync() ??
throw GracefulException.RecordNotFound();
await noteSvc.CreateNoteAsync(user, Note.NoteVisibility.Followers, renote: note);
return await GetNote(id);
}
[HttpPost("{id}/unreblog")]
[Authorize("write:favourites")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
public async Task<IActionResult> UndoRenote(string id)
{
var user = HttpContext.GetUserOrFail();
if (!await db.Notes.Where(p => p.Id == id).EnsureVisibleFor(user).AnyAsync())
throw GracefulException.RecordNotFound();
var renotes = await db.Notes.Where(p => p.RenoteId == id && p.IsPureRenote && p.User == user)
.IncludeCommonProperties()
.ToListAsync();
if (renotes.Count > 0)
{
renotes[0].Renote!.RenoteCount--;
await db.SaveChangesAsync();
}
foreach (var renote in renotes) await noteSvc.DeleteNoteAsync(renote);
return await GetNote(id);
}
[HttpPost] [HttpPost]
[Authorize("write:statuses")] [Authorize("write:statuses")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]

View file

@ -122,4 +122,36 @@ public class ActivityRenderer(
[SuppressMessage("ReSharper", "SuggestBaseTypeForParameter", Justification = "This only makes sense for users")] [SuppressMessage("ReSharper", "SuggestBaseTypeForParameter", Justification = "This only makes sense for users")]
private string RenderFollowId(User follower, User followee) => private string RenderFollowId(User follower, User followee) =>
$"https://{config.Value.WebDomain}/follows/{follower.Id}/{followee.Id}"; $"https://{config.Value.WebDomain}/follows/{follower.Id}/{followee.Id}";
public static ASAnnounce RenderAnnounce(
ASNote note, ASActor actor, List<ASObjectBase> to, List<ASObjectBase> cc, string uri
) => new()
{
Id = uri,
Actor = actor.Compact(),
Object = note,
To = to,
Cc = cc
};
public static ASAnnounce RenderAnnounce(
ASNote note, string renoteUri, ASActor actor, Note.NoteVisibility visibility, string followersUri
)
{
List<ASObjectBase> to = visibility switch
{
Note.NoteVisibility.Public => [new ASLink($"{Constants.ActivityStreamsNs}#Public")],
Note.NoteVisibility.Followers => [new ASLink(followersUri)],
Note.NoteVisibility.Specified => throw new Exception("Announce cannot be specified"),
_ => []
};
List<ASObjectBase> cc = visibility switch
{
Note.NoteVisibility.Home => [new ASLink($"{Constants.ActivityStreamsNs}#Public")],
_ => []
};
return RenderAnnounce(note, actor, to, cc, renoteUri);
}
} }

View file

@ -4,6 +4,7 @@ using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion; using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
using Iceshrimp.Backend.Core.Middleware;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -29,6 +30,9 @@ public class NoteRenderer(IOptions<Config.InstanceSection> config, MfmConverter
? new ASObjectBase(note.Reply.Uri ?? note.Reply.GetPublicUri(config.Value)) ? new ASObjectBase(note.Reply.Uri ?? note.Reply.GetPublicUri(config.Value))
: null; : null;
if (note.IsPureRenote)
throw GracefulException.BadRequest("Refusing to render pure renote as ASNote");
mentions ??= await db.Users mentions ??= await db.Users
.Where(p => note.Mentions.Contains(p.Id)) .Where(p => note.Mentions.Contains(p.Id))
.IncludeCommonProperties() .IncludeCommonProperties()

View file

@ -104,12 +104,20 @@ public class NoteService(
if (user.Host != null) return note; if (user.Host != null) return note;
var actor = userRenderer.RenderLite(user); var actor = userRenderer.RenderLite(user);
var obj = await noteRenderer.RenderAsync(note, mentions); ASActivity activity = note is { IsPureRenote: true, Renote: not null }
var activity = ActivityPub.ActivityRenderer.RenderCreate(obj, actor); ? ActivityPub.ActivityRenderer.RenderAnnounce(noteRenderer.RenderLite(note.Renote),
note.GetPublicUri(config.Value), actor, note.Visibility,
user.GetPublicUri(config.Value) + "/followers")
: ActivityPub.ActivityRenderer.RenderCreate(await noteRenderer.RenderAsync(note, mentions), actor);
List<string> additionalUserIds =
note is { IsPureRenote: true, Renote: not null, Visibility: < Note.NoteVisibility.Followers }
? [note.Renote.User.Id]
: [];
var recipients = await db.Users var recipients = await db.Users
.Where(p => mentionedUserIds.Contains(p.Id)) .Where(p => mentionedUserIds.Concat(additionalUserIds).Contains(p.Id))
.Select(p => new User { Host = p.Host, Inbox = p.Inbox }) .Select(p => new User { Host = p.Host, Inbox = p.Inbox })
.ToListAsync(); .ToListAsync();
@ -221,8 +229,16 @@ public class NoteService(
.Select(p => new User { Host = p.Host, Inbox = p.Inbox }) .Select(p => new User { Host = p.Host, Inbox = p.Inbox })
.ToListAsync(); .ToListAsync();
var actor = userRenderer.RenderLite(note.User); var actor = userRenderer.RenderLite(note.User);
var activity = activityRenderer.RenderDelete(actor, new ASTombstone { Id = note.GetPublicUri(config.Value) }); ASActivity activity = note.IsPureRenote
? activityRenderer.RenderUndo(actor,
ActivityPub.ActivityRenderer
.RenderAnnounce(noteRenderer.RenderLite(note.Renote ?? throw new Exception("Refusing to undo renote without renote")),
note.GetPublicUri(config.Value), actor,
note.Visibility,
note.User.GetPublicUri(config.Value) +
"/followers"))
: activityRenderer.RenderDelete(actor, new ASTombstone { Id = note.GetPublicUri(config.Value) });
if (note.Visibility == Note.NoteVisibility.Specified) if (note.Visibility == Note.NoteVisibility.Specified)
await deliverSvc.DeliverToAsync(activity, note.User, recipients.ToArray()); await deliverSvc.DeliverToAsync(activity, note.User, recipients.ToArray());
@ -250,6 +266,7 @@ public class NoteService(
logger.LogDebug("Deleting note '{id}' owned by {userId}", note.Id, actor.Id); logger.LogDebug("Deleting note '{id}' owned by {userId}", note.Id, actor.Id);
actor.NotesCount--; actor.NotesCount--;
if (dbNote.IsPureRenote) dbNote.RenoteCount--;
db.Remove(dbNote); db.Remove(dbNote);
eventSvc.RaiseNoteDeleted(this, dbNote); eventSvc.RaiseNoteDeleted(this, dbNote);
await db.SaveChangesAsync(); await db.SaveChangesAsync();