diff --git a/Iceshrimp.Backend/Controllers/Mastodon/MastodonAccountController.cs b/Iceshrimp.Backend/Controllers/Mastodon/MastodonAccountController.cs index a77173b2..8cdef327 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/MastodonAccountController.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/MastodonAccountController.cs @@ -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 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 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 GetRelationships([FromQuery(Name = "id")] List 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 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); + } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/Relationship.cs b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/Relationship.cs new file mode 100644 index 00000000..54ff6512 --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/Relationship.cs @@ -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? Languages { get; set; } + + [J("id")] public required string Id { get; set; } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Database/Tables/User.cs b/Iceshrimp.Backend/Core/Database/Tables/User.cs index 734c255b..c26adfd8 100644 --- a/Iceshrimp.Backend/Core/Database/Tables/User.cs +++ b/Iceshrimp.Backend/Core/Database/Tables/User.cs @@ -319,6 +319,11 @@ public class User : IEntity { [InverseProperty(nameof(FollowRequest.Follower))] public virtual ICollection OutgoingFollowRequests { get; set; } = new List(); + [Projectable] + public virtual IEnumerable ReceivedFollowRequests => IncomingFollowRequests.Select(p => p.Follower); + + [Projectable] public virtual IEnumerable SentFollowRequests => OutgoingFollowRequests.Select(p => p.Followee); + [InverseProperty(nameof(Tables.Following.Followee))] public virtual ICollection IncomingFollowRelationships { get; set; } = new List(); @@ -451,6 +456,18 @@ public class User : IEntity { [InverseProperty(nameof(Webhook.User))] public virtual ICollection Webhooks { get; set; } = new List(); + [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; + } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Extensions/ModelBinderProviderExtensions.cs b/Iceshrimp.Backend/Core/Extensions/ModelBinderProviderExtensions.cs index c38218ec..802e9fec 100644 --- a/Iceshrimp.Backend/Core/Extensions/ModelBinderProviderExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/ModelBinderProviderExtensions.cs @@ -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; diff --git a/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs b/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs index 75344069..bda4f9b8 100644 --- a/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs @@ -90,6 +90,13 @@ public static class NoteQueryableExtensions { p.Renote.IsVisibleFor(user))); } + public static IQueryable PrecomputeRelationshipData(this IQueryable 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 FilterBlocked(this IQueryable query, User user) { return query.Where(note => !note.User.IsBlocking(user) && !note.User.IsBlockedBy(user)) .Where(note => note.Renote == null || diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityDeliverService.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityDeliverService.cs index bcc4809b..e7357da9 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityDeliverService.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityDeliverService.cs @@ -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 + }); + } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityHandlerService.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityHandlerService.cs index afcf3f44..fcd290fd 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityHandlerService.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityHandlerService.cs @@ -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); diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityRenderer.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityRenderer.cs index 05379839..5098f364 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityRenderer.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityRenderer.cs @@ -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) { }; } - 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}"; + } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/UserRenderer.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/UserRenderer.cs index 9f690892..4a9e9136 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/UserRenderer.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/UserRenderer.cs @@ -12,8 +12,11 @@ namespace Iceshrimp.Backend.Core.Federation.ActivityPub; public class UserRenderer(IOptions config, DatabaseContext db) { public async Task 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); diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActivity.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActivity.cs index 54393077..4cfe1a22 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActivity.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActivity.cs @@ -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; \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Helpers/MastodonOauthHelpers.cs b/Iceshrimp.Backend/Core/Helpers/MastodonOauthHelpers.cs index e6e245c2..a6e18355 100644 --- a/Iceshrimp.Backend/Core/Helpers/MastodonOauthHelpers.cs +++ b/Iceshrimp.Backend/Core/Helpers/MastodonOauthHelpers.cs @@ -51,16 +51,15 @@ public static class MastodonOauthHelpers { public static IEnumerable ExpandScopes(IEnumerable scopes) { var res = new List(); - 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(); } diff --git a/Iceshrimp.Backend/Core/Middleware/AuthenticationMiddleware.cs b/Iceshrimp.Backend/Core/Middleware/AuthenticationMiddleware.cs index b5e6c93d..fa19f73c 100644 --- a/Iceshrimp.Backend/Core/Middleware/AuthenticationMiddleware.cs +++ b/Iceshrimp.Backend/Core/Middleware/AuthenticationMiddleware.cs @@ -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);