[backend/federation] Add relay support (ISH-266)
This commit is contained in:
parent
a5fa4e4be9
commit
df26db0585
17 changed files with 307 additions and 27 deletions
|
@ -33,7 +33,8 @@ public class AdminController(
|
||||||
ActivityPub.NoteRenderer noteRenderer,
|
ActivityPub.NoteRenderer noteRenderer,
|
||||||
ActivityPub.UserRenderer userRenderer,
|
ActivityPub.UserRenderer userRenderer,
|
||||||
IOptions<Config.InstanceSection> config,
|
IOptions<Config.InstanceSection> config,
|
||||||
QueueService queueSvc
|
QueueService queueSvc,
|
||||||
|
RelayService relaySvc
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpPost("invites/generate")]
|
[HttpPost("invites/generate")]
|
||||||
|
@ -144,6 +145,38 @@ public class AdminController(
|
||||||
await queueSvc.AbandonJobAsync(job);
|
await queueSvc.AbandonJobAsync(job);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("relays")]
|
||||||
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
|
public async Task<List<RelaySchemas.RelayResponse>> GetRelays()
|
||||||
|
{
|
||||||
|
return await db.Relays
|
||||||
|
.ToArrayAsync()
|
||||||
|
.ContinueWithResult(res => res.Select(p => new RelaySchemas.RelayResponse
|
||||||
|
{
|
||||||
|
Id = p.Id,
|
||||||
|
Inbox = p.Inbox,
|
||||||
|
Status = (RelaySchemas.RelayStatus)p.Status
|
||||||
|
})
|
||||||
|
.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("relays")]
|
||||||
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
|
public async Task SubscribeToRelay(RelaySchemas.RelayRequest rq)
|
||||||
|
{
|
||||||
|
await relaySvc.SubscribeToRelay(rq.Inbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("relays/{id}")]
|
||||||
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||||
|
public async Task UnsubscribeFromRelay(string id)
|
||||||
|
{
|
||||||
|
var relay = await db.Relays.FirstOrDefaultAsync(p => p.Id == id) ??
|
||||||
|
throw GracefulException.NotFound("Relay not found");
|
||||||
|
await relaySvc.UnsubscribeFromRelay(relay);
|
||||||
|
}
|
||||||
|
|
||||||
[UseNewtonsoftJson]
|
[UseNewtonsoftJson]
|
||||||
[HttpGet("activities/notes/{id}")]
|
[HttpGet("activities/notes/{id}")]
|
||||||
[OverrideResultType<ASNote>]
|
[OverrideResultType<ASNote>]
|
||||||
|
|
|
@ -4053,6 +4053,10 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
.HasColumnName("isModerator")
|
.HasColumnName("isModerator")
|
||||||
.HasComment("Whether the User is a moderator.");
|
.HasComment("Whether the User is a moderator.");
|
||||||
|
|
||||||
|
b.Property<bool>("IsRelayActor")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("isRelayActor");
|
||||||
|
|
||||||
b.Property<bool>("IsSilenced")
|
b.Property<bool>("IsSilenced")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("boolean")
|
.HasColumnType("boolean")
|
||||||
|
@ -4067,6 +4071,10 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
.HasColumnName("isSuspended")
|
.HasColumnName("isSuspended")
|
||||||
.HasComment("Whether the User is suspended.");
|
.HasComment("Whether the User is suspended.");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSystem")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("isSystem");
|
||||||
|
|
||||||
b.Property<DateTime?>("LastActiveDate")
|
b.Property<DateTime?>("LastActiveDate")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("lastActiveDate");
|
.HasColumnName("lastActiveDate");
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
[DbContext(typeof(DatabaseContext))]
|
||||||
|
[Migration("20240927214613_AddRelayAndSystemUserColumns")]
|
||||||
|
public partial class AddRelayAndSystemUserColumns : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "isRelayActor",
|
||||||
|
table: "user",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "isSystem",
|
||||||
|
table: "user",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
UPDATE "user" SET "isSystem" = TRUE WHERE "host" IS NULL AND ("usernameLower" = 'instance.actor' OR "usernameLower" = 'relay.actor');
|
||||||
|
""");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
UPDATE "user" SET "isRelayActor" = TRUE WHERE "inbox" IN (SELECT "inbox" FROM "relay");
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "isRelayActor",
|
||||||
|
table: "user");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "isSystem",
|
||||||
|
table: "user");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,9 +12,9 @@ public class Relay
|
||||||
[PgName("relay_status_enum")]
|
[PgName("relay_status_enum")]
|
||||||
public enum RelayStatus
|
public enum RelayStatus
|
||||||
{
|
{
|
||||||
[PgName("requesting")] Requesting,
|
[PgName("requesting")] Requesting = 0,
|
||||||
[PgName("accepted")] Accepted,
|
[PgName("accepted")] Accepted = 1,
|
||||||
[PgName("rejected")] Rejected
|
[PgName("rejected")] Rejected = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
[Key]
|
[Key]
|
||||||
|
|
|
@ -124,6 +124,10 @@ public class User : IEntity
|
||||||
[Column("isBot")]
|
[Column("isBot")]
|
||||||
public bool IsBot { get; set; }
|
public bool IsBot { get; set; }
|
||||||
|
|
||||||
|
[Column("isSystem")] public bool IsSystem { get; set; }
|
||||||
|
|
||||||
|
[Column("isRelayActor")] public bool IsRelayActor { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the User is a cat.
|
/// Whether the User is a cat.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -82,7 +82,8 @@ public static class ServiceExtensions
|
||||||
.AddScoped<UserProfileRenderer>()
|
.AddScoped<UserProfileRenderer>()
|
||||||
.AddScoped<CacheService>()
|
.AddScoped<CacheService>()
|
||||||
.AddScoped<MetaService>()
|
.AddScoped<MetaService>()
|
||||||
.AddScoped<StorageMaintenanceService>();
|
.AddScoped<StorageMaintenanceService>()
|
||||||
|
.AddScoped<RelayService>();
|
||||||
|
|
||||||
// Singleton = instantiated once across application lifetime
|
// Singleton = instantiated once across application lifetime
|
||||||
services
|
services
|
||||||
|
|
|
@ -64,4 +64,19 @@ public class ActivityDeliverService(
|
||||||
|
|
||||||
await DeliverToAsync(activity, actor, recipients);
|
await DeliverToAsync(activity, actor, recipients);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task DeliverToAsync(ASActivity activity, User actor, string recipientInbox)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Queuing deliver-to-inbox job for activity {id}", activity.Id);
|
||||||
|
if (activity.Actor == null) throw new Exception("Actor must not be null");
|
||||||
|
|
||||||
|
await queueService.DeliverQueue.EnqueueAsync(new DeliverJobData
|
||||||
|
{
|
||||||
|
RecipientHost = new Uri(recipientInbox).Host,
|
||||||
|
InboxUrl = recipientInbox,
|
||||||
|
Payload = activity.CompactToPayload(),
|
||||||
|
ContentType = "application/activity+json",
|
||||||
|
UserId = actor.Id
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -26,7 +26,8 @@ public class ActivityHandlerService(
|
||||||
ObjectResolver objectResolver,
|
ObjectResolver objectResolver,
|
||||||
FollowupTaskService followupTaskSvc,
|
FollowupTaskService followupTaskSvc,
|
||||||
EmojiService emojiSvc,
|
EmojiService emojiSvc,
|
||||||
EventService eventSvc
|
EventService eventSvc,
|
||||||
|
RelayService relaySvc
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
public async Task PerformActivityAsync(ASActivity activity, string? inboxUserId, string? authenticatedUserId)
|
public async Task PerformActivityAsync(ASActivity activity, string? inboxUserId, string? authenticatedUserId)
|
||||||
|
@ -187,6 +188,13 @@ public class ActivityHandlerService(
|
||||||
if (activity.Object is not ASFollow obj)
|
if (activity.Object is not ASFollow obj)
|
||||||
throw GracefulException.UnprocessableEntity("Accept activity object is invalid");
|
throw GracefulException.UnprocessableEntity("Accept activity object is invalid");
|
||||||
|
|
||||||
|
var relayPrefix = $"https://{config.Value.WebDomain}/activities/follow-relay/";
|
||||||
|
if (obj.Id.StartsWith(relayPrefix))
|
||||||
|
{
|
||||||
|
await relaySvc.HandleAccept(actor, obj.Id[relayPrefix.Length..]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var prefix = $"https://{config.Value.WebDomain}/follows/";
|
var prefix = $"https://{config.Value.WebDomain}/follows/";
|
||||||
if (!obj.Id.StartsWith(prefix))
|
if (!obj.Id.StartsWith(prefix))
|
||||||
throw GracefulException.UnprocessableEntity($"Object id '{obj.Id}' not a valid follow request id");
|
throw GracefulException.UnprocessableEntity($"Object id '{obj.Id}' not a valid follow request id");
|
||||||
|
@ -227,6 +235,13 @@ public class ActivityHandlerService(
|
||||||
if (activity.Object is not ASFollow follow)
|
if (activity.Object is not ASFollow follow)
|
||||||
throw GracefulException.UnprocessableEntity("Reject activity object is invalid");
|
throw GracefulException.UnprocessableEntity("Reject activity object is invalid");
|
||||||
|
|
||||||
|
var relayPrefix = $"https://{config.Value.WebDomain}/activities/follow-relay/";
|
||||||
|
if (follow.Id.StartsWith(relayPrefix))
|
||||||
|
{
|
||||||
|
await relaySvc.HandleReject(resolvedActor, follow.Id[relayPrefix.Length..]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (follow is not { Actor: not null })
|
if (follow is not { Actor: not null })
|
||||||
throw GracefulException.UnprocessableEntity("Refusing to reject object with invalid follow object");
|
throw GracefulException.UnprocessableEntity("Refusing to reject object with invalid follow object");
|
||||||
|
|
||||||
|
@ -436,6 +451,9 @@ public class ActivityHandlerService(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resolvedActor.IsRelayActor)
|
||||||
|
return;
|
||||||
|
|
||||||
if (await db.Notes.AnyAsync(p => p.Uri == activity.Id))
|
if (await db.Notes.AnyAsync(p => p.Uri == activity.Id))
|
||||||
{
|
{
|
||||||
logger.LogDebug("Renote '{id}' already exists, skipping", activity.Id);
|
logger.LogDebug("Renote '{id}' already exists, skipping", activity.Id);
|
||||||
|
|
|
@ -112,6 +112,16 @@ public class ActivityRenderer(
|
||||||
RenderFollowId(follower, followee, relationshipId));
|
RenderFollowId(follower, followee, relationshipId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ASFollow RenderFollow(User actor, Relay relay)
|
||||||
|
{
|
||||||
|
return new ASFollow
|
||||||
|
{
|
||||||
|
Id = $"https://{config.Value.WebDomain}/activities/follow-relay/{relay.Id}",
|
||||||
|
Actor = userRenderer.RenderLite(actor),
|
||||||
|
Object = new ASObject { Id = "https://www.w3.org/ns/activitystreams#Public" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public ASActivity RenderUnfollow(User follower, User followee, Guid? relationshipId)
|
public ASActivity RenderUnfollow(User follower, User followee, Guid? relationshipId)
|
||||||
{
|
{
|
||||||
if (follower.IsLocalUser && followee.IsLocalUser)
|
if (follower.IsLocalUser && followee.IsLocalUser)
|
||||||
|
|
|
@ -174,9 +174,21 @@ public class UserResolver(
|
||||||
return (finalAcct, finalUri);
|
return (finalAcct, finalUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? GetAcctUri(WebFingerResponse fingerRes) => (fingerRes.Aliases ?? [])
|
private static string? GetAcctUri(WebFingerResponse fingerRes)
|
||||||
|
{
|
||||||
|
var acct = (fingerRes.Aliases ?? [])
|
||||||
.Prepend(fingerRes.Subject)
|
.Prepend(fingerRes.Subject)
|
||||||
.FirstOrDefault(p => p.StartsWith("acct:"));
|
.FirstOrDefault(p => p.StartsWith("acct:"));
|
||||||
|
if (acct != null) return acct;
|
||||||
|
|
||||||
|
// AodeRelay doesn't prefix its actor's subject with acct, so we have to fall back to guessing here
|
||||||
|
acct = (fingerRes.Aliases ?? [])
|
||||||
|
.Prepend(fingerRes.Subject)
|
||||||
|
.FirstOrDefault(p => !p.Contains(':') &&
|
||||||
|
!p.Contains(' ') &&
|
||||||
|
p.Split("@").Length == 2);
|
||||||
|
return acct is not null ? $"acct:{acct}" : acct;
|
||||||
|
}
|
||||||
|
|
||||||
public static string NormalizeQuery(string query)
|
public static string NormalizeQuery(string query)
|
||||||
{
|
{
|
||||||
|
|
|
@ -91,6 +91,9 @@ public class ASAnnounce : ASActivity
|
||||||
public class ASDelete : ASActivity
|
public class ASDelete : ASActivity
|
||||||
{
|
{
|
||||||
public ASDelete() => Type = Types.Delete;
|
public ASDelete() => Type = Types.Delete;
|
||||||
|
|
||||||
|
[J($"{Constants.ActivityStreamsNs}#to")]
|
||||||
|
public List<ASObjectBase>? To { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ASFollow : ASActivity
|
public class ASFollow : ASActivity
|
||||||
|
|
|
@ -161,8 +161,10 @@ public class InboxValidationMiddleware(
|
||||||
logger.LogDebug("Error validating HTTP signature: {error}", e.Message);
|
logger.LogDebug("Error validating HTTP signature: {error}", e.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((!verified || (key?.User.Uri != null && activity.Actor?.Id != key.User.Uri)) &&
|
if (
|
||||||
(activity is ASDelete || config.Value.AcceptLdSignatures))
|
(!verified || (key?.User.Uri != null && activity.Actor?.Id != key.User.Uri)) &&
|
||||||
|
(activity is ASDelete || config.Value.AcceptLdSignatures)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (activity is ASDelete)
|
if (activity is ASDelete)
|
||||||
logger.LogDebug("Activity is ASDelete & actor uri is not matching, trying LD signature next...");
|
logger.LogDebug("Activity is ASDelete & actor uri is not matching, trying LD signature next...");
|
||||||
|
|
|
@ -80,19 +80,43 @@ public class PreDeliverQueue(int parallelism)
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var inboxQueryResult in inboxQueryResults)
|
foreach (var inboxQueryResult in inboxQueryResults)
|
||||||
|
{
|
||||||
|
// @formatter:off
|
||||||
await queueSvc.DeliverQueue.EnqueueAsync(new DeliverJobData
|
await queueSvc.DeliverQueue.EnqueueAsync(new DeliverJobData
|
||||||
{
|
{
|
||||||
RecipientHost =
|
RecipientHost = inboxQueryResult.Host ?? throw new Exception("Recipient host must not be null"),
|
||||||
inboxQueryResult.Host ??
|
InboxUrl = inboxQueryResult.InboxUrl ?? throw new Exception("Recipient inboxUrl must not be null"),
|
||||||
throw new Exception("Recipient host must not be null"),
|
|
||||||
InboxUrl =
|
|
||||||
inboxQueryResult.InboxUrl ??
|
|
||||||
throw new
|
|
||||||
Exception("Recipient inboxUrl must not be null"),
|
|
||||||
Payload = payload,
|
Payload = payload,
|
||||||
ContentType = "application/activity+json",
|
ContentType = "application/activity+json",
|
||||||
UserId = jobData.ActorId
|
UserId = jobData.ActorId
|
||||||
});
|
});
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activity is ASCreate or ASDelete { Object: ASNote })
|
||||||
|
{
|
||||||
|
var relays = await db.Relays.ToArrayAsync(token);
|
||||||
|
if (relays is []) return;
|
||||||
|
|
||||||
|
if (!config.Value.AttachLdSignatures || activity is ASDelete)
|
||||||
|
{
|
||||||
|
if (activity is ASDelete del) del.To = [new ASObjectBase($"{Constants.ActivityStreamsNs}#Public")];
|
||||||
|
var keypair = await db.UserKeypairs.FirstAsync(p => p.UserId == jobData.ActorId, token);
|
||||||
|
payload = await activity.SignAndCompactAsync(keypair);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var relay in relays)
|
||||||
|
{
|
||||||
|
await queueSvc.DeliverQueue.EnqueueAsync(new DeliverJobData
|
||||||
|
{
|
||||||
|
RecipientHost = new Uri(relay.Inbox).Host,
|
||||||
|
InboxUrl = relay.Inbox,
|
||||||
|
Payload = payload,
|
||||||
|
ContentType = "application/activity+json",
|
||||||
|
UserId = jobData.ActorId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
75
Iceshrimp.Backend/Core/Services/RelayService.cs
Normal file
75
Iceshrimp.Backend/Core/Services/RelayService.cs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
using Iceshrimp.Backend.Core.Database;
|
||||||
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
|
using Iceshrimp.Backend.Core.Helpers;
|
||||||
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Core.Services;
|
||||||
|
|
||||||
|
public class RelayService(
|
||||||
|
DatabaseContext db,
|
||||||
|
SystemUserService systemUserSvc,
|
||||||
|
ActivityPub.ActivityRenderer activityRenderer,
|
||||||
|
ActivityPub.ActivityDeliverService deliverSvc,
|
||||||
|
ActivityPub.UserRenderer userRenderer
|
||||||
|
)
|
||||||
|
{
|
||||||
|
public async Task SubscribeToRelay(string uri)
|
||||||
|
{
|
||||||
|
uri = new Uri(uri).AbsoluteUri;
|
||||||
|
if (await db.Relays.AnyAsync(p => p.Inbox == uri)) return;
|
||||||
|
|
||||||
|
var relay = new Relay
|
||||||
|
{
|
||||||
|
Id = IdHelpers.GenerateSlowflakeId(),
|
||||||
|
Inbox = uri,
|
||||||
|
Status = Relay.RelayStatus.Requesting
|
||||||
|
};
|
||||||
|
|
||||||
|
db.Add(relay);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var actor = await systemUserSvc.GetRelayActorAsync();
|
||||||
|
var activity = activityRenderer.RenderFollow(actor, relay);
|
||||||
|
await deliverSvc.DeliverToAsync(activity, actor, uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UnsubscribeFromRelay(Relay relay)
|
||||||
|
{
|
||||||
|
var actor = await systemUserSvc.GetRelayActorAsync();
|
||||||
|
var follow = activityRenderer.RenderFollow(actor, relay);
|
||||||
|
var activity = activityRenderer.RenderUndo(userRenderer.RenderLite(actor), follow);
|
||||||
|
await deliverSvc.DeliverToAsync(activity, actor, relay.Inbox);
|
||||||
|
|
||||||
|
db.Remove(relay);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HandleAccept(User actor, string id)
|
||||||
|
{
|
||||||
|
// @formatter:off
|
||||||
|
if (await db.Relays.FirstOrDefaultAsync(p => p.Id == id) is not { } relay)
|
||||||
|
throw GracefulException.UnprocessableEntity($"Relay with id {id} was not found");
|
||||||
|
if (relay.Inbox != new Uri(actor.Inbox ?? throw new Exception("Relay actor must have an inbox")).AbsoluteUri)
|
||||||
|
throw GracefulException.UnprocessableEntity($"Relay inbox ({relay.Inbox}) does not match relay actor inbox ({actor.Inbox})");
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
relay.Status = Relay.RelayStatus.Accepted;
|
||||||
|
actor.IsRelayActor = true;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HandleReject(User actor, string id)
|
||||||
|
{
|
||||||
|
// @formatter:off
|
||||||
|
if (db.Relays.FirstOrDefault(p => p.Id == id) is not { } relay)
|
||||||
|
throw GracefulException.UnprocessableEntity($"Relay with id {id} was not found");
|
||||||
|
if (relay.Inbox != new Uri(actor.Inbox ?? throw new Exception("Relay actor must have an inbox")).AbsoluteUri)
|
||||||
|
throw GracefulException.UnprocessableEntity($"Relay inbox ({relay.Inbox}) does not match relay actor inbox ({actor.Inbox})");
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
relay.Status = Relay.RelayStatus.Rejected;
|
||||||
|
actor.IsRelayActor = true;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
|
@ -79,7 +79,8 @@ public class SystemUserService(ILogger<SystemUserService> logger, DatabaseContex
|
||||||
IsAdmin = false,
|
IsAdmin = false,
|
||||||
IsLocked = true,
|
IsLocked = true,
|
||||||
IsExplorable = false,
|
IsExplorable = false,
|
||||||
IsBot = true
|
IsBot = true,
|
||||||
|
IsSystem = true
|
||||||
};
|
};
|
||||||
|
|
||||||
var userKeypair = new UserKeypair
|
var userKeypair = new UserKeypair
|
||||||
|
|
|
@ -27,10 +27,10 @@ CharacterLimit = 8192
|
||||||
;; It is highly recommend you keep this enabled if you intend to use block- or allowlist federation
|
;; It is highly recommend you keep this enabled if you intend to use block- or allowlist federation
|
||||||
AuthorizedFetch = true
|
AuthorizedFetch = true
|
||||||
|
|
||||||
;; Whether to attach LD signatures to outgoing activities
|
;; Whether to attach LD signatures to outgoing activities. Outgoing relayed activities get signed regardless of this option.
|
||||||
AttachLdSignatures = false
|
AttachLdSignatures = false
|
||||||
|
|
||||||
;; Whether to accept activities signed using LD signatures
|
;; Whether to accept activities signed using LD signatures. Needs to be enabled for relayed activities to be accepted.
|
||||||
AcceptLdSignatures = false
|
AcceptLdSignatures = false
|
||||||
|
|
||||||
;; Whether to allow requests to IPv4 & IPv6 loopback addresses
|
;; Whether to allow requests to IPv4 & IPv6 loopback addresses
|
||||||
|
|
23
Iceshrimp.Shared/Schemas/Web/RelaySchemas.cs
Normal file
23
Iceshrimp.Shared/Schemas/Web/RelaySchemas.cs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
namespace Iceshrimp.Shared.Schemas.Web;
|
||||||
|
|
||||||
|
public class RelaySchemas
|
||||||
|
{
|
||||||
|
public enum RelayStatus
|
||||||
|
{
|
||||||
|
Requesting = 0,
|
||||||
|
Accepted = 1,
|
||||||
|
Rejected = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RelayResponse
|
||||||
|
{
|
||||||
|
public required string Id { get; set; }
|
||||||
|
public required string Inbox { get; set; }
|
||||||
|
public required RelayStatus Status { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RelayRequest
|
||||||
|
{
|
||||||
|
public required string Inbox { get; set; }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue