[backend/masto-client] Add follow/unfollow/relationship endpoints

This commit is contained in:
Laura Hausmann 2024-02-05 22:41:46 +01:00
parent e31a0719f4
commit 8c9e6ef56c
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
12 changed files with 351 additions and 21 deletions

View file

@ -1,13 +1,16 @@
using Iceshrimp.Backend.Controllers.Mastodon.Attributes; using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
using Iceshrimp.Backend.Controllers.Mastodon.Schemas; using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
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.Federation.ActivityPub;
using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using MastodonUserRenderer = Iceshrimp.Backend.Controllers.Mastodon.Renderers.UserRenderer;
namespace Iceshrimp.Backend.Controllers.Mastodon; namespace Iceshrimp.Backend.Controllers.Mastodon;
@ -16,10 +19,15 @@ namespace Iceshrimp.Backend.Controllers.Mastodon;
[Authenticate] [Authenticate]
[EnableRateLimiting("sliding")] [EnableRateLimiting("sliding")]
[Produces("application/json")] [Produces("application/json")]
public class MastodonAccountController(DatabaseContext db, UserRenderer userRenderer) : Controller { public class MastodonAccountController(
[Authorize("read:accounts")] DatabaseContext db,
MastodonUserRenderer userRenderer,
ActivityRenderer activityRenderer,
UserRenderer apUserRenderer,
ActivityDeliverService deliverSvc
) : Controller {
[HttpGet("verify_credentials")] [HttpGet("verify_credentials")]
[Produces("application/json")] [Authorize("read:accounts")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Account))] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Account))]
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))] [ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))] [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
@ -30,7 +38,6 @@ public class MastodonAccountController(DatabaseContext db, UserRenderer userRend
} }
[HttpGet("{id}")] [HttpGet("{id}")]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Account))] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Account))]
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))] [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
public async Task<IActionResult> GetUser(string id) { public async Task<IActionResult> GetUser(string id) {
@ -39,4 +46,177 @@ public class MastodonAccountController(DatabaseContext db, UserRenderer userRend
var res = await userRenderer.RenderAsync(user); var res = await userRenderer.RenderAsync(user);
return Ok(res); 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);
}
} }

View file

@ -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; }
}

View file

@ -319,6 +319,11 @@ public class User : IEntity {
[InverseProperty(nameof(FollowRequest.Follower))] [InverseProperty(nameof(FollowRequest.Follower))]
public virtual ICollection<FollowRequest> OutgoingFollowRequests { get; set; } = new List<FollowRequest>(); 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))] [InverseProperty(nameof(Tables.Following.Followee))]
public virtual ICollection<Following> IncomingFollowRelationships { get; set; } = new List<Following>(); public virtual ICollection<Following> IncomingFollowRelationships { get; set; } = new List<Following>();
@ -451,6 +456,18 @@ public class User : IEntity {
[InverseProperty(nameof(Webhook.User))] [InverseProperty(nameof(Webhook.User))]
public virtual ICollection<Webhook> Webhooks { get; set; } = new List<Webhook>(); 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] [Key]
[Column("id")] [Column("id")]
[StringLength(32)] [StringLength(32)]
@ -468,9 +485,38 @@ public class User : IEntity {
[Projectable] [Projectable]
public bool IsFollowing(User user) => Following.Contains(user); 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] [Projectable]
public bool IsMutedBy(User user) => MutedBy.Contains(user); public bool IsMutedBy(User user) => MutedBy.Contains(user);
[Projectable] [Projectable]
public bool IsMuting(User user) => Muting.Contains(user); 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;
}
} }

View file

@ -1,3 +1,4 @@
using System.Collections;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
@ -11,12 +12,19 @@ public static class ModelBinderProviderExtensions {
ComplexObjectModelBinderProvider complexProvider) ComplexObjectModelBinderProvider complexProvider)
throw new Exception("Failed to set up hybrid model binding provider"); 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 hybridProvider = new HybridModelBinderProvider(bodyProvider, complexProvider);
var queryProvider = new QueryCollectionModelBinderProvider(collectionProvider);
providers.Insert(0, hybridProvider); providers.Insert(0, hybridProvider);
providers.Insert(1, queryProvider);
} }
} }
//TODO: this doesn't work with QueryCollectionModelBinderProvider yet
public class HybridModelBinderProvider( public class HybridModelBinderProvider(
IModelBinderProvider bodyProvider, IModelBinderProvider bodyProvider,
IModelBinderProvider complexProvider) : IModelBinderProvider { IModelBinderProvider complexProvider) : IModelBinderProvider {
@ -33,10 +41,17 @@ public class HybridModelBinderProvider(
} }
} }
public class HybridModelBinder( public class QueryCollectionModelBinderProvider(IModelBinderProvider provider) : IModelBinderProvider {
IModelBinder? bodyBinder, public IModelBinder? GetBinder(ModelBinderProviderContext context) {
IModelBinder? complexBinder if (context.BindingInfo.BindingSource == null) return null;
) : IModelBinder { 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) { public async Task BindModelAsync(ModelBindingContext bindingContext) {
if (bodyBinder != null && bindingContext is if (bodyBinder != null && bindingContext is
{ IsTopLevelObject: true, HttpContext.Request: { HasFormContentType: false, ContentLength: > 0 } }) { { 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)] [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)]
public class FromHybridAttribute : Attribute, IBindingSourceMetadata { public class FromHybridAttribute : Attribute, IBindingSourceMetadata {
public BindingSource BindingSource => HybridBindingSource.Hybrid; public BindingSource BindingSource => HybridBindingSource.Hybrid;

View file

@ -90,6 +90,13 @@ public static class NoteQueryableExtensions {
p.Renote.IsVisibleFor(user))); 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) { public static IQueryable<Note> FilterBlocked(this IQueryable<Note> query, User user) {
return query.Where(note => !note.User.IsBlocking(user) && !note.User.IsBlockedBy(user)) return query.Where(note => !note.User.IsBlocking(user) && !note.User.IsBlockedBy(user))
.Where(note => note.Renote == null || .Where(note => note.Renote == null ||

View file

@ -2,6 +2,7 @@ using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Federation.ActivityStreams; using Iceshrimp.Backend.Core.Federation.ActivityStreams;
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Queues; using Iceshrimp.Backend.Core.Queues;
using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Services;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -37,4 +38,23 @@ public class ActivityDeliverService(
UserId = actor.Id 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
});
}
} }

View file

@ -78,7 +78,7 @@ public class ActivityHandlerService(
} }
var acceptActivity = activityRenderer.RenderAccept(followeeActor, var acceptActivity = activityRenderer.RenderAccept(followeeActor,
activityRenderer.RenderFollow(followerActor, ActivityRenderer.RenderFollow(followerActor,
followeeActor, requestId)); followeeActor, requestId));
var keypair = await db.UserKeypairs.FirstAsync(p => p.User == followee); var keypair = await db.UserKeypairs.FirstAsync(p => p.User == followee);
var payload = await acceptActivity.SignAndCompactAsync(keypair); var payload = await acceptActivity.SignAndCompactAsync(keypair);

View file

@ -1,4 +1,5 @@
using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -25,14 +26,17 @@ public class ActivityRenderer(IOptions<Config.InstanceSection> config) {
}; };
} }
public ASActivity RenderFollow(ASObject followerActor, ASObject followeeActor, string requestId) { public static ASFollow RenderFollow(ASObject followerActor, ASObject followeeActor, string requestId) {
return new ASActivity { return new ASFollow {
Id = requestId, Id = requestId,
Type = ASActivity.Types.Follow,
Actor = new ASActor { Actor = new ASActor {
Id = followerActor.Id Id = followerActor.Id
}, },
Object = followeeActor Object = followeeActor
}; };
} }
public string RenderFollowId(User follower, User followee) {
return $"https://{config.Value.WebDomain}/follows/{follower.Id}/{followee.Id}";
}
} }

View file

@ -12,8 +12,11 @@ namespace Iceshrimp.Backend.Core.Federation.ActivityPub;
public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseContext db) { public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseContext db) {
public async Task<ASActor> RenderAsync(User user) { public async Task<ASActor> RenderAsync(User user) {
if (user.Host != null) if (user.Host != null) {
throw new GracefulException("Refusing to render remote user"); 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 profile = await db.UserProfiles.FirstOrDefaultAsync(p => p.User == user);
var keypair = await db.UserKeypairs.FirstOrDefaultAsync(p => p.User == user); var keypair = await db.UserKeypairs.FirstOrDefaultAsync(p => p.User == user);

View file

@ -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>; public sealed class ASActivityConverter : ASSerializer.ListSingleObjectConverter<ASActivity>;

View file

@ -51,16 +51,15 @@ public static class MastodonOauthHelpers {
public static IEnumerable<string> ExpandScopes(IEnumerable<string> scopes) { public static IEnumerable<string> ExpandScopes(IEnumerable<string> scopes) {
var res = new List<string>(); var res = new List<string>();
foreach (var scope in scopes) { foreach (var scope in scopes)
if (scope == "read") if (scope == "read")
res.AddRange(ReadScopes); res.AddRange(ReadScopes);
if (scope == "write") else if (scope == "write")
res.AddRange(WriteScopes); res.AddRange(WriteScopes);
if (scope == "follow") else if (scope == "follow")
res.AddRange(FollowScopes); res.AddRange(FollowScopes);
else else
res.Add(scope); res.Add(scope);
}
return res.Distinct(); return res.Distinct();
} }

View file

@ -27,7 +27,7 @@ public class AuthenticationMiddleware(DatabaseContext db) : IMiddleware {
.Include(p => p.User) .Include(p => p.User)
.ThenInclude(p => p.UserProfile) .ThenInclude(p => p.UserProfile)
.Include(p => p.App) .Include(p => p.App)
.FirstOrDefaultAsync(p => p.Token == header && p.Active); .FirstOrDefaultAsync(p => p.Token == token && p.Active);
if (oauthToken == null) { if (oauthToken == null) {
await next(ctx); await next(ctx);