[backend/masto-client] Implement /reblog and /unreblog endpoints
This commit is contained in:
parent
a8b02aa6f8
commit
5d7035e63c
4 changed files with 107 additions and 7 deletions
|
@ -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))]
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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()
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Add table
Reference in a new issue