[backend/masto-client] Add follow/unfollow/relationship endpoints
This commit is contained in:
parent
e31a0719f4
commit
8c9e6ef56c
12 changed files with 351 additions and 21 deletions
|
@ -1,13 +1,16 @@
|
|||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Extensions;
|
||||
using Iceshrimp.Backend.Core.Federation.ActivityPub;
|
||||
using Iceshrimp.Backend.Core.Helpers;
|
||||
using Iceshrimp.Backend.Core.Middleware;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MastodonUserRenderer = Iceshrimp.Backend.Controllers.Mastodon.Renderers.UserRenderer;
|
||||
|
||||
namespace Iceshrimp.Backend.Controllers.Mastodon;
|
||||
|
||||
|
@ -16,10 +19,15 @@ namespace Iceshrimp.Backend.Controllers.Mastodon;
|
|||
[Authenticate]
|
||||
[EnableRateLimiting("sliding")]
|
||||
[Produces("application/json")]
|
||||
public class MastodonAccountController(DatabaseContext db, UserRenderer userRenderer) : Controller {
|
||||
[Authorize("read:accounts")]
|
||||
public class MastodonAccountController(
|
||||
DatabaseContext db,
|
||||
MastodonUserRenderer userRenderer,
|
||||
ActivityRenderer activityRenderer,
|
||||
UserRenderer apUserRenderer,
|
||||
ActivityDeliverService deliverSvc
|
||||
) : Controller {
|
||||
[HttpGet("verify_credentials")]
|
||||
[Produces("application/json")]
|
||||
[Authorize("read:accounts")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Account))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||
|
@ -30,7 +38,6 @@ public class MastodonAccountController(DatabaseContext db, UserRenderer userRend
|
|||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[Produces("application/json")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Account))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetUser(string id) {
|
||||
|
@ -39,4 +46,177 @@ public class MastodonAccountController(DatabaseContext db, UserRenderer userRend
|
|||
var res = await userRenderer.RenderAsync(user);
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/follow")]
|
||||
[Authorize("write:follows")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Relationship))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||
//TODO: [FromHybrid] request (bool reblogs, bool notify, bool languages)
|
||||
public async Task<IActionResult> FollowUser(string id) {
|
||||
var user = HttpContext.GetUser() ?? throw new GracefulException("Failed to get user from HttpContext");
|
||||
if (user.Id == id)
|
||||
throw GracefulException.BadRequest("You cannot follow yourself");
|
||||
|
||||
var followee = await db.Users.IncludeCommonProperties()
|
||||
.Where(p => p.Id == id)
|
||||
.PrecomputeRelationshipData(user)
|
||||
.FirstOrDefaultAsync()
|
||||
?? throw GracefulException.RecordNotFound();
|
||||
|
||||
if ((followee.PrecomputedIsBlockedBy ?? true) || (followee.PrecomputedIsBlocking ?? true))
|
||||
throw GracefulException.Forbidden("This action is not allowed");
|
||||
|
||||
if (!(followee.PrecomputedIsFollowedBy ?? false) && !(followee.PrecomputedIsRequestedBy ?? false)) {
|
||||
if (followee.Host != null) {
|
||||
var followerActor = await apUserRenderer.RenderAsync(user);
|
||||
var followeeActor = await apUserRenderer.RenderAsync(followee);
|
||||
var followId = activityRenderer.RenderFollowId(user, followee);
|
||||
var activity = ActivityRenderer.RenderFollow(followerActor, followeeActor, followId);
|
||||
await deliverSvc.DeliverToAsync(activity, user, followee);
|
||||
}
|
||||
|
||||
if (followee.IsLocked || followee.Host != null) {
|
||||
var request = new FollowRequest {
|
||||
Id = IdHelpers.GenerateSlowflakeId(),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Followee = followee,
|
||||
Follower = user,
|
||||
FolloweeHost = followee.Host,
|
||||
FollowerHost = user.Host,
|
||||
FolloweeInbox = followee.Inbox,
|
||||
FollowerInbox = user.Inbox,
|
||||
FolloweeSharedInbox = followee.SharedInbox,
|
||||
FollowerSharedInbox = user.SharedInbox
|
||||
};
|
||||
|
||||
await db.AddAsync(request);
|
||||
}
|
||||
else {
|
||||
var following = new Following {
|
||||
Id = IdHelpers.GenerateSlowflakeId(),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Followee = followee,
|
||||
Follower = user,
|
||||
FolloweeHost = followee.Host,
|
||||
FollowerHost = user.Host,
|
||||
FolloweeInbox = followee.Inbox,
|
||||
FollowerInbox = user.Inbox,
|
||||
FolloweeSharedInbox = followee.SharedInbox,
|
||||
FollowerSharedInbox = user.SharedInbox
|
||||
};
|
||||
|
||||
await db.AddAsync(following);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
if (followee.IsLocked)
|
||||
followee.PrecomputedIsRequestedBy = true;
|
||||
else
|
||||
followee.PrecomputedIsFollowedBy = true;
|
||||
}
|
||||
|
||||
var res = new Relationship {
|
||||
Id = followee.Id,
|
||||
Following = followee.PrecomputedIsFollowedBy ?? false,
|
||||
FollowedBy = followee.PrecomputedIsFollowing ?? false,
|
||||
Blocking = followee.PrecomputedIsBlockedBy ?? false,
|
||||
BlockedBy = followee.PrecomputedIsBlocking ?? false,
|
||||
Requested = followee.PrecomputedIsRequestedBy ?? false,
|
||||
RequestedBy = followee.PrecomputedIsRequested ?? false,
|
||||
Muting = followee.PrecomputedIsMutedBy ?? false,
|
||||
Endorsed = false, //FIXME
|
||||
Note = "", //FIXME
|
||||
Notifying = false, //FIXME
|
||||
DomainBlocking = false, //FIXME
|
||||
MutingNotifications = false, //FIXME
|
||||
ShowingReblogs = true, //FIXME
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpGet("relationships")]
|
||||
[Authorize("read:follows")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Relationship[]))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetRelationships([FromQuery(Name = "id")] List<string> ids) {
|
||||
var user = HttpContext.GetUser() ?? throw new GracefulException("Failed to get user from HttpContext");
|
||||
|
||||
if (ids.Contains(user.Id))
|
||||
throw GracefulException.BadRequest("You cannot request relationship status with yourself");
|
||||
|
||||
var users = await db.Users.IncludeCommonProperties()
|
||||
.Where(p => ids.Contains(p.Id))
|
||||
.PrecomputeRelationshipData(user)
|
||||
.ToListAsync();
|
||||
|
||||
var res = users.Select(u => new Relationship {
|
||||
Id = u.Id,
|
||||
Following = u.PrecomputedIsFollowedBy ?? false,
|
||||
FollowedBy = u.PrecomputedIsFollowing ?? false,
|
||||
Blocking = u.PrecomputedIsBlockedBy ?? false,
|
||||
BlockedBy = u.PrecomputedIsBlocking ?? false,
|
||||
Requested = u.PrecomputedIsRequestedBy ?? false,
|
||||
RequestedBy = u.PrecomputedIsRequested ?? false,
|
||||
Muting = u.PrecomputedIsMutedBy ?? false,
|
||||
Endorsed = false, //FIXME
|
||||
Note = "", //FIXME
|
||||
Notifying = false, //FIXME
|
||||
DomainBlocking = false, //FIXME
|
||||
MutingNotifications = false, //FIXME
|
||||
ShowingReblogs = true, //FIXME
|
||||
});
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/unfollow")]
|
||||
[Authorize("write:follows")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Relationship))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> UnfollowUser(string id) {
|
||||
var user = HttpContext.GetUser() ?? throw new GracefulException("Failed to get user from HttpContext");
|
||||
if (user.Id == id)
|
||||
throw GracefulException.BadRequest("You cannot unfollow yourself");
|
||||
|
||||
var followee = await db.Users.IncludeCommonProperties()
|
||||
.Where(p => p.Id == id)
|
||||
.PrecomputeRelationshipData(user)
|
||||
.FirstOrDefaultAsync()
|
||||
?? throw GracefulException.RecordNotFound();
|
||||
|
||||
// TODO: send federation events for remote users
|
||||
if (!(followee.PrecomputedIsFollowedBy ?? false)) {
|
||||
await db.Followings.Where(p => p.Follower == user && p.Followee == followee).ExecuteDeleteAsync();
|
||||
followee.PrecomputedIsFollowedBy = false;
|
||||
}
|
||||
|
||||
if (followee.PrecomputedIsRequestedBy ?? false) {
|
||||
await db.FollowRequests.Where(p => p.Follower == user && p.Followee == followee).ExecuteDeleteAsync();
|
||||
followee.PrecomputedIsRequestedBy = false;
|
||||
}
|
||||
|
||||
var res = new Relationship {
|
||||
Id = followee.Id,
|
||||
Following = followee.PrecomputedIsFollowedBy ?? false,
|
||||
FollowedBy = followee.PrecomputedIsFollowing ?? false,
|
||||
Blocking = followee.PrecomputedIsBlockedBy ?? false,
|
||||
BlockedBy = followee.PrecomputedIsBlocking ?? false,
|
||||
Requested = followee.PrecomputedIsRequestedBy ?? false,
|
||||
RequestedBy = followee.PrecomputedIsRequested ?? false,
|
||||
Muting = followee.PrecomputedIsMutedBy ?? false,
|
||||
Endorsed = false, //FIXME
|
||||
Note = "", //FIXME
|
||||
Notifying = false, //FIXME
|
||||
DomainBlocking = false, //FIXME
|
||||
MutingNotifications = false, //FIXME
|
||||
ShowingReblogs = true, //FIXME
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||
using JI = System.Text.Json.Serialization.JsonIgnoreAttribute;
|
||||
|
||||
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||
|
||||
public class Relationship : IEntity {
|
||||
[J("following")] public required bool Following { get; set; }
|
||||
[J("followed_by")] public required bool FollowedBy { get; set; }
|
||||
[J("blocking")] public required bool Blocking { get; set; }
|
||||
[J("blocked_by")] public required bool BlockedBy { get; set; }
|
||||
[J("requested")] public required bool Requested { get; set; }
|
||||
[J("requested_by")] public required bool RequestedBy { get; set; }
|
||||
[J("muting")] public required bool Muting { get; set; }
|
||||
[J("muting_notifications")] public required bool MutingNotifications { get; set; }
|
||||
[J("domain_blocking")] public required bool DomainBlocking { get; set; }
|
||||
[J("endorsed")] public required bool Endorsed { get; set; }
|
||||
[J("showing_reblogs")] public required bool ShowingReblogs { get; set; }
|
||||
[J("notifying")] public required bool Notifying { get; set; }
|
||||
[J("note")] public required string Note { get; set; }
|
||||
|
||||
//TODO: implement this
|
||||
[J("languages")]
|
||||
[JI(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public List<string>? Languages { get; set; }
|
||||
|
||||
[J("id")] public required string Id { get; set; }
|
||||
}
|
|
@ -319,6 +319,11 @@ public class User : IEntity {
|
|||
[InverseProperty(nameof(FollowRequest.Follower))]
|
||||
public virtual ICollection<FollowRequest> OutgoingFollowRequests { get; set; } = new List<FollowRequest>();
|
||||
|
||||
[Projectable]
|
||||
public virtual IEnumerable<User> ReceivedFollowRequests => IncomingFollowRequests.Select(p => p.Follower);
|
||||
|
||||
[Projectable] public virtual IEnumerable<User> SentFollowRequests => OutgoingFollowRequests.Select(p => p.Followee);
|
||||
|
||||
[InverseProperty(nameof(Tables.Following.Followee))]
|
||||
public virtual ICollection<Following> IncomingFollowRelationships { get; set; } = new List<Following>();
|
||||
|
||||
|
@ -451,6 +456,18 @@ public class User : IEntity {
|
|||
[InverseProperty(nameof(Webhook.User))]
|
||||
public virtual ICollection<Webhook> Webhooks { get; set; } = new List<Webhook>();
|
||||
|
||||
[NotMapped] public bool? PrecomputedIsBlocking { get; set; }
|
||||
[NotMapped] public bool? PrecomputedIsBlockedBy { get; set; }
|
||||
|
||||
[NotMapped] public bool? PrecomputedIsMuting { get; set; }
|
||||
[NotMapped] public bool? PrecomputedIsMutedBy { get; set; }
|
||||
|
||||
[NotMapped] public bool? PrecomputedIsFollowing { get; set; }
|
||||
[NotMapped] public bool? PrecomputedIsFollowedBy { get; set; }
|
||||
|
||||
[NotMapped] public bool? PrecomputedIsRequested { get; set; }
|
||||
[NotMapped] public bool? PrecomputedIsRequestedBy { get; set; }
|
||||
|
||||
[Key]
|
||||
[Column("id")]
|
||||
[StringLength(32)]
|
||||
|
@ -468,9 +485,38 @@ public class User : IEntity {
|
|||
[Projectable]
|
||||
public bool IsFollowing(User user) => Following.Contains(user);
|
||||
|
||||
[Projectable]
|
||||
public bool IsRequestedBy(User user) => ReceivedFollowRequests.Contains(user);
|
||||
|
||||
[Projectable]
|
||||
public bool IsRequested(User user) => SentFollowRequests.Contains(user);
|
||||
|
||||
[Projectable]
|
||||
public bool IsMutedBy(User user) => MutedBy.Contains(user);
|
||||
|
||||
[Projectable]
|
||||
public bool IsMuting(User user) => Muting.Contains(user);
|
||||
|
||||
public User WithPrecomputedBlockStatus(bool blocking, bool blockedBy) {
|
||||
PrecomputedIsBlocking = blocking;
|
||||
PrecomputedIsBlockedBy = blockedBy;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public User WithPrecomputedMuteStatus(bool muting, bool mutedBy) {
|
||||
PrecomputedIsMuting = muting;
|
||||
PrecomputedIsMutedBy = mutedBy;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public User WithPrecomputedFollowStatus(bool following, bool followedBy, bool requested, bool requestedBy) {
|
||||
PrecomputedIsFollowing = following;
|
||||
PrecomputedIsFollowedBy = followedBy;
|
||||
PrecomputedIsRequested = requested;
|
||||
PrecomputedIsRequestedBy = requestedBy;
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
|
||||
|
||||
|
@ -11,12 +12,19 @@ public static class ModelBinderProviderExtensions {
|
|||
ComplexObjectModelBinderProvider complexProvider)
|
||||
throw new Exception("Failed to set up hybrid model binding provider");
|
||||
|
||||
if (providers.Single(provider => provider.GetType() == typeof(CollectionModelBinderProvider)) is not
|
||||
CollectionModelBinderProvider collectionProvider)
|
||||
throw new Exception("Failed to set up query collection model binding provider");
|
||||
|
||||
var hybridProvider = new HybridModelBinderProvider(bodyProvider, complexProvider);
|
||||
var queryProvider = new QueryCollectionModelBinderProvider(collectionProvider);
|
||||
|
||||
providers.Insert(0, hybridProvider);
|
||||
providers.Insert(1, queryProvider);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: this doesn't work with QueryCollectionModelBinderProvider yet
|
||||
public class HybridModelBinderProvider(
|
||||
IModelBinderProvider bodyProvider,
|
||||
IModelBinderProvider complexProvider) : IModelBinderProvider {
|
||||
|
@ -33,10 +41,17 @@ public class HybridModelBinderProvider(
|
|||
}
|
||||
}
|
||||
|
||||
public class HybridModelBinder(
|
||||
IModelBinder? bodyBinder,
|
||||
IModelBinder? complexBinder
|
||||
) : IModelBinder {
|
||||
public class QueryCollectionModelBinderProvider(IModelBinderProvider provider) : IModelBinderProvider {
|
||||
public IModelBinder? GetBinder(ModelBinderProviderContext context) {
|
||||
if (context.BindingInfo.BindingSource == null) return null;
|
||||
if (!context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Query)) return null;
|
||||
|
||||
var binder = provider.GetBinder(context);
|
||||
return new QueryCollectionModelBinder(binder);
|
||||
}
|
||||
}
|
||||
|
||||
public class HybridModelBinder(IModelBinder? bodyBinder, IModelBinder? complexBinder) : IModelBinder {
|
||||
public async Task BindModelAsync(ModelBindingContext bindingContext) {
|
||||
if (bodyBinder != null && bindingContext is
|
||||
{ IsTopLevelObject: true, HttpContext.Request: { HasFormContentType: false, ContentLength: > 0 } }) {
|
||||
|
@ -53,6 +68,27 @@ public class HybridModelBinder(
|
|||
}
|
||||
}
|
||||
|
||||
public class QueryCollectionModelBinder(IModelBinder? binder) : IModelBinder {
|
||||
public async Task BindModelAsync(ModelBindingContext bindingContext) {
|
||||
if (binder != null && !bindingContext.Result.IsModelSet) {
|
||||
await binder.BindModelAsync(bindingContext);
|
||||
|
||||
if (!bindingContext.Result.IsModelSet || (bindingContext.Result.Model as IList) is not { Count: > 0 }) {
|
||||
bindingContext.ModelName = bindingContext.ModelName.EndsWith("[]")
|
||||
? bindingContext.ModelName[..^2]
|
||||
: bindingContext.ModelName + "[]";
|
||||
|
||||
await binder.BindModelAsync(bindingContext);
|
||||
}
|
||||
}
|
||||
|
||||
if (bindingContext.Result.IsModelSet) {
|
||||
bindingContext.Model = bindingContext.Result.Model;
|
||||
bindingContext.BindingSource = BindingSource.ModelBinding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)]
|
||||
public class FromHybridAttribute : Attribute, IBindingSourceMetadata {
|
||||
public BindingSource BindingSource => HybridBindingSource.Hybrid;
|
||||
|
|
|
@ -90,6 +90,13 @@ public static class NoteQueryableExtensions {
|
|||
p.Renote.IsVisibleFor(user)));
|
||||
}
|
||||
|
||||
public static IQueryable<User> PrecomputeRelationshipData(this IQueryable<User> query, User user) {
|
||||
return query.Select(p => p.WithPrecomputedBlockStatus(p.IsBlocking(user), p.IsBlockedBy(user))
|
||||
.WithPrecomputedMuteStatus(p.IsMuting(user), p.IsMutedBy(user))
|
||||
.WithPrecomputedFollowStatus(p.IsFollowing(user), p.IsFollowedBy(user),
|
||||
p.IsRequested(user), p.IsRequestedBy(user)));
|
||||
}
|
||||
|
||||
public static IQueryable<Note> FilterBlocked(this IQueryable<Note> query, User user) {
|
||||
return query.Where(note => !note.User.IsBlocking(user) && !note.User.IsBlockedBy(user))
|
||||
.Where(note => note.Renote == null ||
|
||||
|
|
|
@ -2,6 +2,7 @@ using Iceshrimp.Backend.Core.Database;
|
|||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Federation.ActivityStreams;
|
||||
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
||||
using Iceshrimp.Backend.Core.Middleware;
|
||||
using Iceshrimp.Backend.Core.Queues;
|
||||
using Iceshrimp.Backend.Core.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -37,4 +38,23 @@ public class ActivityDeliverService(
|
|||
UserId = actor.Id
|
||||
});
|
||||
}
|
||||
|
||||
public async Task DeliverToAsync(ASActivity activity, User actor, User recipient) {
|
||||
var inboxUrl = recipient.Inbox ?? recipient.SharedInbox;
|
||||
if (recipient.Host == null || inboxUrl == null)
|
||||
throw new GracefulException("Refusing to deliver to local user");
|
||||
|
||||
logger.LogDebug("Delivering activity {id} to {recipient}", activity.Id, inboxUrl);
|
||||
if (activity.Actor == null) throw new Exception("Actor must not be null");
|
||||
|
||||
var keypair = await db.UserKeypairs.FirstAsync(p => p.User == actor);
|
||||
var payload = await activity.SignAndCompactAsync(keypair);
|
||||
|
||||
await queueService.DeliverQueue.EnqueueAsync(new DeliverJob {
|
||||
InboxUrl = inboxUrl,
|
||||
Payload = payload,
|
||||
ContentType = "application/activity+json",
|
||||
UserId = actor.Id
|
||||
});
|
||||
}
|
||||
}
|
|
@ -78,7 +78,7 @@ public class ActivityHandlerService(
|
|||
}
|
||||
|
||||
var acceptActivity = activityRenderer.RenderAccept(followeeActor,
|
||||
activityRenderer.RenderFollow(followerActor,
|
||||
ActivityRenderer.RenderFollow(followerActor,
|
||||
followeeActor, requestId));
|
||||
var keypair = await db.UserKeypairs.FirstAsync(p => p.User == followee);
|
||||
var payload = await acceptActivity.SignAndCompactAsync(keypair);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
|
@ -25,14 +26,17 @@ public class ActivityRenderer(IOptions<Config.InstanceSection> config) {
|
|||
};
|
||||
}
|
||||
|
||||
public ASActivity RenderFollow(ASObject followerActor, ASObject followeeActor, string requestId) {
|
||||
return new ASActivity {
|
||||
Id = requestId,
|
||||
Type = ASActivity.Types.Follow,
|
||||
public static ASFollow RenderFollow(ASObject followerActor, ASObject followeeActor, string requestId) {
|
||||
return new ASFollow {
|
||||
Id = requestId,
|
||||
Actor = new ASActor {
|
||||
Id = followerActor.Id
|
||||
},
|
||||
Object = followeeActor
|
||||
};
|
||||
}
|
||||
|
||||
public string RenderFollowId(User follower, User followee) {
|
||||
return $"https://{config.Value.WebDomain}/follows/{follower.Id}/{followee.Id}";
|
||||
}
|
||||
}
|
|
@ -12,8 +12,11 @@ namespace Iceshrimp.Backend.Core.Federation.ActivityPub;
|
|||
|
||||
public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseContext db) {
|
||||
public async Task<ASActor> RenderAsync(User user) {
|
||||
if (user.Host != null)
|
||||
throw new GracefulException("Refusing to render remote user");
|
||||
if (user.Host != null) {
|
||||
return new ASActor {
|
||||
Id = user.Uri ?? throw new GracefulException("Remote user must have an URI")
|
||||
};
|
||||
}
|
||||
|
||||
var profile = await db.UserProfiles.FirstOrDefaultAsync(p => p.User == user);
|
||||
var keypair = await db.UserKeypairs.FirstOrDefaultAsync(p => p.User == user);
|
||||
|
|
|
@ -26,4 +26,10 @@ public class ASActivity : ASObject {
|
|||
}
|
||||
}
|
||||
|
||||
public class ASFollow : ASActivity {
|
||||
public ASFollow() => Type = Types.Follow;
|
||||
}
|
||||
|
||||
//TODO: add the rest
|
||||
|
||||
public sealed class ASActivityConverter : ASSerializer.ListSingleObjectConverter<ASActivity>;
|
|
@ -51,16 +51,15 @@ public static class MastodonOauthHelpers {
|
|||
|
||||
public static IEnumerable<string> ExpandScopes(IEnumerable<string> scopes) {
|
||||
var res = new List<string>();
|
||||
foreach (var scope in scopes) {
|
||||
foreach (var scope in scopes)
|
||||
if (scope == "read")
|
||||
res.AddRange(ReadScopes);
|
||||
if (scope == "write")
|
||||
else if (scope == "write")
|
||||
res.AddRange(WriteScopes);
|
||||
if (scope == "follow")
|
||||
else if (scope == "follow")
|
||||
res.AddRange(FollowScopes);
|
||||
else
|
||||
res.Add(scope);
|
||||
}
|
||||
|
||||
return res.Distinct();
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ public class AuthenticationMiddleware(DatabaseContext db) : IMiddleware {
|
|||
.Include(p => p.User)
|
||||
.ThenInclude(p => p.UserProfile)
|
||||
.Include(p => p.App)
|
||||
.FirstOrDefaultAsync(p => p.Token == header && p.Active);
|
||||
.FirstOrDefaultAsync(p => p.Token == token && p.Active);
|
||||
|
||||
if (oauthToken == null) {
|
||||
await next(ctx);
|
||||
|
|
Loading…
Add table
Reference in a new issue