[backend/drive] Proxy remote media by default
This commit is contained in:
parent
241486a778
commit
113bd98b0e
40 changed files with 581 additions and 278 deletions
|
@ -3,12 +3,14 @@ using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
|
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
|
||||||
using Iceshrimp.MfmSharp;
|
using Iceshrimp.MfmSharp;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Components.PublicPreview.Renderers;
|
namespace Iceshrimp.Backend.Components.PublicPreview.Renderers;
|
||||||
|
|
||||||
public readonly record struct MfmRenderData(MarkupString Html, List<MfmInlineMedia> InlineMedia);
|
public readonly record struct MfmRenderData(MarkupString Html, List<MfmInlineMedia> InlineMedia);
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
public class MfmRenderer(MfmConverter converter) : ISingletonService
|
public class MfmRenderer(MfmConverter converter) : ISingletonService
|
||||||
{
|
{
|
||||||
public async Task<MfmRenderData?> RenderAsync(
|
public async Task<MfmRenderData?> RenderAsync(
|
||||||
|
|
|
@ -3,6 +3,7 @@ using Iceshrimp.Backend.Core.Configuration;
|
||||||
using Iceshrimp.Backend.Core.Database;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
|
using Iceshrimp.Backend.Core.Services;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
@ -12,6 +13,7 @@ public class NoteRenderer(
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
UserRenderer userRenderer,
|
UserRenderer userRenderer,
|
||||||
MfmRenderer mfm,
|
MfmRenderer mfm,
|
||||||
|
MediaProxyService mediaProxy,
|
||||||
IOptions<Config.InstanceSection> instance,
|
IOptions<Config.InstanceSection> instance,
|
||||||
IOptionsSnapshot<Config.SecuritySection> security
|
IOptionsSnapshot<Config.SecuritySection> security
|
||||||
) : IScopedService
|
) : IScopedService
|
||||||
|
@ -102,7 +104,7 @@ public class NoteRenderer(
|
||||||
.Select(f => new PreviewAttachment
|
.Select(f => new PreviewAttachment
|
||||||
{
|
{
|
||||||
MimeType = f.Type,
|
MimeType = f.Type,
|
||||||
Url = f.AccessUrl,
|
Url = mediaProxy.GetProxyUrl(f),
|
||||||
Name = f.Name,
|
Name = f.Name,
|
||||||
Alt = f.Comment,
|
Alt = f.Comment,
|
||||||
Sensitive = f.IsSensitive
|
Sensitive = f.IsSensitive
|
||||||
|
|
|
@ -33,8 +33,8 @@ public class UserRenderer(
|
||||||
Username = user.Username,
|
Username = user.Username,
|
||||||
Host = user.Host ?? instance.Value.AccountDomain,
|
Host = user.Host ?? instance.Value.AccountDomain,
|
||||||
Url = user.UserProfile?.Url ?? user.Uri ?? user.PublicUrlPath,
|
Url = user.UserProfile?.Url ?? user.Uri ?? user.PublicUrlPath,
|
||||||
AvatarUrl = user.AvatarUrl ?? user.IdenticonUrlPath,
|
AvatarUrl = user.GetAvatarUrl(instance.Value),
|
||||||
BannerUrl = user.BannerUrl,
|
BannerUrl = user.GetBannerUrl(instance.Value),
|
||||||
RawDisplayName = user.DisplayName,
|
RawDisplayName = user.DisplayName,
|
||||||
DisplayName = await mfm.RenderSimpleAsync(user.DisplayName, user.Host, mentions, emoji[user.Id], "span"),
|
DisplayName = await mfm.RenderSimpleAsync(user.DisplayName, user.Host, mentions, emoji[user.Id], "span"),
|
||||||
Bio = await mfm.RenderSimpleAsync(user.UserProfile?.Description, user.Host, mentions, emoji[user.Id], "span"),
|
Bio = await mfm.RenderSimpleAsync(user.UserProfile?.Description, user.Host, mentions, emoji[user.Id], "span"),
|
||||||
|
|
|
@ -151,7 +151,12 @@ public class ActivityPubController(
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||||
public async Task<ActionResult<JObject>> GetUser(string id)
|
public async Task<ActionResult<JObject>> GetUser(string id)
|
||||||
{
|
{
|
||||||
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id);
|
var user = await db.Users
|
||||||
|
.IncludeCommonProperties()
|
||||||
|
.Include(p => p.Avatar)
|
||||||
|
.Include(p => p.Banner)
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == id);
|
||||||
|
|
||||||
if (user == null) throw GracefulException.NotFound("User not found");
|
if (user == null) throw GracefulException.NotFound("User not found");
|
||||||
if (user.IsRemoteUser)
|
if (user.IsRemoteUser)
|
||||||
{
|
{
|
||||||
|
@ -282,6 +287,8 @@ public class ActivityPubController(
|
||||||
|
|
||||||
var user = await db.Users
|
var user = await db.Users
|
||||||
.IncludeCommonProperties()
|
.IncludeCommonProperties()
|
||||||
|
.Include(p => p.Avatar)
|
||||||
|
.Include(p => p.Banner)
|
||||||
.FirstOrDefaultAsync(p => p.UsernameLower == acct.ToLowerInvariant() && p.IsLocalUser);
|
.FirstOrDefaultAsync(p => p.UsernameLower == acct.ToLowerInvariant() && p.IsLocalUser);
|
||||||
|
|
||||||
if (user == null) throw GracefulException.NotFound("User not found");
|
if (user == null) throw GracefulException.NotFound("User not found");
|
||||||
|
@ -327,7 +334,7 @@ public class ActivityPubController(
|
||||||
{
|
{
|
||||||
Id = emoji.GetPublicUri(config.Value),
|
Id = emoji.GetPublicUri(config.Value),
|
||||||
Name = emoji.Name,
|
Name = emoji.Name,
|
||||||
Image = new ASImage { Url = new ASLink(emoji.PublicUrl) }
|
Image = new ASImage { Url = new ASLink(emoji.RawPublicUrl) }
|
||||||
};
|
};
|
||||||
|
|
||||||
return LdHelpers.Compact(rendered);
|
return LdHelpers.Compact(rendered);
|
||||||
|
|
|
@ -107,7 +107,6 @@ public class AccountController(
|
||||||
var avatar = await driveSvc.StoreFileAsync(request.Avatar.OpenReadStream(), user, rq);
|
var avatar = await driveSvc.StoreFileAsync(request.Avatar.OpenReadStream(), user, rq);
|
||||||
user.Avatar = avatar;
|
user.Avatar = avatar;
|
||||||
user.AvatarBlurhash = avatar.Blurhash;
|
user.AvatarBlurhash = avatar.Blurhash;
|
||||||
user.AvatarUrl = avatar.AccessUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.Banner != null)
|
if (request.Banner != null)
|
||||||
|
@ -121,7 +120,6 @@ public class AccountController(
|
||||||
var banner = await driveSvc.StoreFileAsync(request.Banner.OpenReadStream(), user, rq);
|
var banner = await driveSvc.StoreFileAsync(request.Banner.OpenReadStream(), user, rq);
|
||||||
user.Banner = banner;
|
user.Banner = banner;
|
||||||
user.BannerBlurhash = banner.Blurhash;
|
user.BannerBlurhash = banner.Blurhash;
|
||||||
user.BannerUrl = banner.AccessUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user = await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
user = await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
||||||
|
@ -139,7 +137,6 @@ public class AccountController(
|
||||||
var id = user.AvatarId;
|
var id = user.AvatarId;
|
||||||
|
|
||||||
user.AvatarId = null;
|
user.AvatarId = null;
|
||||||
user.AvatarUrl = null;
|
|
||||||
user.AvatarBlurhash = null;
|
user.AvatarBlurhash = null;
|
||||||
|
|
||||||
db.Update(user);
|
db.Update(user);
|
||||||
|
@ -161,7 +158,6 @@ public class AccountController(
|
||||||
var id = user.BannerId;
|
var id = user.BannerId;
|
||||||
|
|
||||||
user.BannerId = null;
|
user.BannerId = null;
|
||||||
user.BannerUrl = null;
|
|
||||||
user.BannerBlurhash = null;
|
user.BannerBlurhash = null;
|
||||||
|
|
||||||
db.Update(user);
|
db.Update(user);
|
||||||
|
|
|
@ -21,7 +21,11 @@ namespace Iceshrimp.Backend.Controllers.Mastodon;
|
||||||
[EnableCors("mastodon")]
|
[EnableCors("mastodon")]
|
||||||
[EnableRateLimiting("sliding")]
|
[EnableRateLimiting("sliding")]
|
||||||
[Produces(MediaTypeNames.Application.Json)]
|
[Produces(MediaTypeNames.Application.Json)]
|
||||||
public class InstanceController(DatabaseContext db, MetaService meta) : ControllerBase
|
public class InstanceController(
|
||||||
|
IOptions<Config.InstanceSection> instance,
|
||||||
|
DatabaseContext db,
|
||||||
|
MetaService meta
|
||||||
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet("/api/v1/instance")]
|
[HttpGet("/api/v1/instance")]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
|
@ -51,9 +55,10 @@ public class InstanceController(DatabaseContext db, MetaService meta) : Controll
|
||||||
public async Task<InstanceInfoV2Response> GetInstanceInfoV2([FromServices] IOptionsSnapshot<Config> config)
|
public async Task<InstanceInfoV2Response> GetInstanceInfoV2([FromServices] IOptionsSnapshot<Config> config)
|
||||||
{
|
{
|
||||||
var cutoff = DateTime.UtcNow - TimeSpan.FromDays(30);
|
var cutoff = DateTime.UtcNow - TimeSpan.FromDays(30);
|
||||||
var activeMonth = await db.Users.LongCountAsync(p => p.IsLocalUser &&
|
var activeMonth =
|
||||||
!Constants.SystemUsers.Contains(p.UsernameLower) &&
|
await db.Users.LongCountAsync(p => p.IsLocalUser
|
||||||
p.LastActiveDate > cutoff);
|
&& !Constants.SystemUsers.Contains(p.UsernameLower)
|
||||||
|
&& p.LastActiveDate > cutoff);
|
||||||
|
|
||||||
var (instanceName, instanceDescription, adminContact) =
|
var (instanceName, instanceDescription, adminContact) =
|
||||||
await meta.GetManyAsync(MetaEntity.InstanceName, MetaEntity.InstanceDescription,
|
await meta.GetManyAsync(MetaEntity.InstanceName, MetaEntity.InstanceDescription,
|
||||||
|
@ -74,8 +79,8 @@ public class InstanceController(DatabaseContext db, MetaService meta) : Controll
|
||||||
{
|
{
|
||||||
Id = p.Id,
|
Id = p.Id,
|
||||||
Shortcode = p.Name,
|
Shortcode = p.Name,
|
||||||
Url = p.PublicUrl,
|
Url = p.GetAccessUrl(instance.Value),
|
||||||
StaticUrl = p.PublicUrl, //TODO
|
StaticUrl = p.GetAccessUrl(instance.Value), //TODO
|
||||||
VisibleInPicker = true,
|
VisibleInPicker = true,
|
||||||
Category = p.Category
|
Category = p.Category
|
||||||
})
|
})
|
||||||
|
|
|
@ -23,7 +23,11 @@ namespace Iceshrimp.Backend.Controllers.Mastodon;
|
||||||
[EnableCors("mastodon")]
|
[EnableCors("mastodon")]
|
||||||
[EnableRateLimiting("sliding")]
|
[EnableRateLimiting("sliding")]
|
||||||
[Produces(MediaTypeNames.Application.Json)]
|
[Produces(MediaTypeNames.Application.Json)]
|
||||||
public class MediaController(DriveService driveSvc, DatabaseContext db) : ControllerBase
|
public class MediaController(
|
||||||
|
DriveService driveSvc,
|
||||||
|
DatabaseContext db,
|
||||||
|
AttachmentRenderer attachmentRenderer
|
||||||
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
[MaxRequestSizeIsMaxUploadSize]
|
[MaxRequestSizeIsMaxUploadSize]
|
||||||
[HttpPost("/api/v1/media")]
|
[HttpPost("/api/v1/media")]
|
||||||
|
@ -40,7 +44,7 @@ public class MediaController(DriveService driveSvc, DatabaseContext db) : Contro
|
||||||
MimeType = request.File.ContentType
|
MimeType = request.File.ContentType
|
||||||
};
|
};
|
||||||
var file = await driveSvc.StoreFileAsync(request.File.OpenReadStream(), user, rq);
|
var file = await driveSvc.StoreFileAsync(request.File.OpenReadStream(), user, rq);
|
||||||
return AttachmentRenderer.Render(file);
|
return attachmentRenderer.Render(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("/api/v1/media/{id}")]
|
[HttpPut("/api/v1/media/{id}")]
|
||||||
|
@ -51,12 +55,12 @@ public class MediaController(DriveService driveSvc, DatabaseContext db) : Contro
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Id == id && p.User == user) ??
|
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Id == id && p.User == user)
|
||||||
throw GracefulException.RecordNotFound();
|
?? throw GracefulException.RecordNotFound();
|
||||||
file.Comment = request.Description;
|
file.Comment = request.Description;
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
return AttachmentRenderer.Render(file);
|
return attachmentRenderer.Render(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("/api/v1/media/{id}")]
|
[HttpGet("/api/v1/media/{id}")]
|
||||||
|
@ -65,10 +69,10 @@ public class MediaController(DriveService driveSvc, DatabaseContext db) : Contro
|
||||||
public async Task<AttachmentEntity> GetAttachment(string id)
|
public async Task<AttachmentEntity> GetAttachment(string id)
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Id == id && p.User == user) ??
|
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Id == id && p.User == user)
|
||||||
throw GracefulException.RecordNotFound();
|
?? throw GracefulException.RecordNotFound();
|
||||||
|
|
||||||
return AttachmentRenderer.Render(file);
|
return attachmentRenderer.Render(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("/api/v2/media/{id}")]
|
[HttpPut("/api/v2/media/{id}")]
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
|
using Iceshrimp.Backend.Core.Services;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
namespace Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
||||||
|
|
||||||
public static class AttachmentRenderer
|
public class AttachmentRenderer(MediaProxyService mediaProxy) : ISingletonService
|
||||||
{
|
{
|
||||||
public static AttachmentEntity Render(DriveFile file) => new()
|
public AttachmentEntity Render(DriveFile file, bool proxy = true) => new()
|
||||||
{
|
{
|
||||||
Id = file.Id,
|
Id = file.Id,
|
||||||
Type = AttachmentEntity.GetType(file.Type),
|
Type = AttachmentEntity.GetType(file.Type),
|
||||||
Url = file.AccessUrl,
|
Url = proxy ? mediaProxy.GetProxyUrl(file) : file.RawAccessUrl,
|
||||||
Blurhash = file.Blurhash,
|
Blurhash = file.Blurhash,
|
||||||
PreviewUrl = file.ThumbnailAccessUrl,
|
PreviewUrl = proxy ? mediaProxy.GetThumbnailProxyUrl(file) : file.RawThumbnailAccessUrl,
|
||||||
Description = file.Comment,
|
Description = file.Comment,
|
||||||
RemoteUrl = file.Uri,
|
RemoteUrl = file.Uri,
|
||||||
Sensitive = file.IsSensitive,
|
Sensitive = file.IsSensitive,
|
||||||
|
|
|
@ -19,7 +19,8 @@ public class NoteRenderer(
|
||||||
PollRenderer pollRenderer,
|
PollRenderer pollRenderer,
|
||||||
MfmConverter mfmConverter,
|
MfmConverter mfmConverter,
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
EmojiService emojiSvc
|
EmojiService emojiSvc,
|
||||||
|
AttachmentRenderer attachmentRenderer
|
||||||
) : IScopedService
|
) : IScopedService
|
||||||
{
|
{
|
||||||
private static readonly FilterResultEntity InaccessibleFilter = new()
|
private static readonly FilterResultEntity InaccessibleFilter = new()
|
||||||
|
@ -296,7 +297,7 @@ public class NoteRenderer(
|
||||||
if (notes.Count == 0) return [];
|
if (notes.Count == 0) return [];
|
||||||
var ids = notes.SelectMany(n => n.FileIds).Distinct();
|
var ids = notes.SelectMany(n => n.FileIds).Distinct();
|
||||||
return await db.DriveFiles.Where(p => ids.Contains(p.Id))
|
return await db.DriveFiles.Where(p => ids.Contains(p.Id))
|
||||||
.Select(f => AttachmentRenderer.Render(f))
|
.Select(f => attachmentRenderer.Render(f, true))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -305,7 +306,7 @@ public class NoteRenderer(
|
||||||
var ids = fileIds.Distinct().ToList();
|
var ids = fileIds.Distinct().ToList();
|
||||||
if (ids.Count == 0) return [];
|
if (ids.Count == 0) return [];
|
||||||
return await db.DriveFiles.Where(p => ids.Contains(p.Id))
|
return await db.DriveFiles.Where(p => ids.Contains(p.Id))
|
||||||
.Select(f => AttachmentRenderer.Render(f))
|
.Select(f => attachmentRenderer.Render(f, true))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -354,8 +355,8 @@ public class NoteRenderer(
|
||||||
{
|
{
|
||||||
var hit = await emojiSvc.ResolveEmojiAsync(item.Name);
|
var hit = await emojiSvc.ResolveEmojiAsync(item.Name);
|
||||||
if (hit == null) continue;
|
if (hit == null) continue;
|
||||||
item.Url = hit.PublicUrl;
|
item.Url = hit.GetAccessUrl(config.Value);
|
||||||
item.StaticUrl = hit.PublicUrl;
|
item.StaticUrl = hit.GetAccessUrl(config.Value);
|
||||||
item.Name = item.Name.Trim(':');
|
item.Name = item.Name.Trim(':');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -422,8 +423,8 @@ public class NoteRenderer(
|
||||||
{
|
{
|
||||||
Id = p.Id,
|
Id = p.Id,
|
||||||
Shortcode = p.Name.Trim(':'),
|
Shortcode = p.Name.Trim(':'),
|
||||||
Url = p.PublicUrl,
|
Url = p.GetAccessUrl(config.Value),
|
||||||
StaticUrl = p.PublicUrl, //TODO
|
StaticUrl = p.GetAccessUrl(config.Value), //TODO
|
||||||
VisibleInPicker = true,
|
VisibleInPicker = true,
|
||||||
Category = p.Category
|
Category = p.Category
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||||
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
||||||
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
using Iceshrimp.Backend.Core.Database;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
using Iceshrimp.Backend.Core.Services;
|
using Iceshrimp.Backend.Core.Services;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
namespace Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
||||||
|
|
||||||
public class NotificationRenderer(
|
public class NotificationRenderer(
|
||||||
|
IOptions<Config.InstanceSection> instance,
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
NoteRenderer noteRenderer,
|
NoteRenderer noteRenderer,
|
||||||
UserRenderer userRenderer
|
UserRenderer userRenderer
|
||||||
|
@ -45,7 +48,7 @@ public class NotificationRenderer(
|
||||||
var parts = notification.Reaction.Trim(':').Split('@');
|
var parts = notification.Reaction.Trim(':').Split('@');
|
||||||
emojiUrl = await db.Emojis
|
emojiUrl = await db.Emojis
|
||||||
.Where(e => e.Name == parts[0] && e.Host == (parts.Length > 1 ? parts[1] : null))
|
.Where(e => e.Name == parts[0] && e.Host == (parts.Length > 1 ? parts[1] : null))
|
||||||
.Select(e => e.PublicUrl)
|
.Select(e => e.GetAccessUrl(instance.Value))
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,7 +110,7 @@ public class NotificationRenderer(
|
||||||
var emojiUrls = await urlQ.Select(e => new
|
var emojiUrls = await urlQ.Select(e => new
|
||||||
{
|
{
|
||||||
Name = $":{e.Name}{(e.Host != null ? "@" + e.Host : "")}:",
|
Name = $":{e.Name}{(e.Host != null ? "@" + e.Host : "")}:",
|
||||||
Url = e.PublicUrl
|
Url = e.GetAccessUrl(instance.Value)
|
||||||
})
|
})
|
||||||
.ToArrayAsync()
|
.ToArrayAsync()
|
||||||
.ContinueWithResult(res => res.DistinctBy(e => e.Name)
|
.ContinueWithResult(res => res.DistinctBy(e => e.Name)
|
||||||
|
|
|
@ -49,7 +49,7 @@ public class UserRenderer(
|
||||||
{
|
{
|
||||||
Id = user.Id,
|
Id = user.Id,
|
||||||
DisplayName = user.DisplayName ?? user.Username,
|
DisplayName = user.DisplayName ?? user.Username,
|
||||||
AvatarUrl = user.AvatarUrl ?? user.GetIdenticonUrlPng(config.Value),
|
AvatarUrl = user.GetAvatarUrl(config.Value),
|
||||||
Username = user.Username,
|
Username = user.Username,
|
||||||
Acct = acct,
|
Acct = acct,
|
||||||
FullyQualifiedName = $"{user.Username}@{user.Host ?? config.Value.AccountDomain}",
|
FullyQualifiedName = $"{user.Username}@{user.Host ?? config.Value.AccountDomain}",
|
||||||
|
@ -61,9 +61,9 @@ public class UserRenderer(
|
||||||
Note = (await mfmConverter.ToHtmlAsync(profile?.Description ?? "", mentions, user.Host)).Html,
|
Note = (await mfmConverter.ToHtmlAsync(profile?.Description ?? "", mentions, user.Host)).Html,
|
||||||
Url = profile?.Url ?? user.Uri ?? user.GetPublicUrl(config.Value),
|
Url = profile?.Url ?? user.Uri ?? user.GetPublicUrl(config.Value),
|
||||||
Uri = user.Uri ?? user.GetPublicUri(config.Value),
|
Uri = user.Uri ?? user.GetPublicUri(config.Value),
|
||||||
AvatarStaticUrl = user.AvatarUrl ?? user.GetIdenticonUrlPng(config.Value), //TODO
|
AvatarStaticUrl = user.GetAvatarUrl(config.Value), //TODO
|
||||||
HeaderUrl = user.BannerUrl ?? _transparent,
|
HeaderUrl = user.GetBannerUrl(config.Value) ?? _transparent,
|
||||||
HeaderStaticUrl = user.BannerUrl ?? _transparent, //TODO
|
HeaderStaticUrl = user.GetBannerUrl(config.Value) ?? _transparent, //TODO
|
||||||
MovedToAccount = null, //TODO
|
MovedToAccount = null, //TODO
|
||||||
IsBot = user.IsBot,
|
IsBot = user.IsBot,
|
||||||
IsDiscoverable = user.IsExplorable,
|
IsDiscoverable = user.IsExplorable,
|
||||||
|
@ -73,8 +73,8 @@ public class UserRenderer(
|
||||||
|
|
||||||
if (localUser is null && security.Value.PublicPreview == Enums.PublicPreview.RestrictedNoMedia) //TODO
|
if (localUser is null && security.Value.PublicPreview == Enums.PublicPreview.RestrictedNoMedia) //TODO
|
||||||
{
|
{
|
||||||
res.AvatarUrl = user.GetIdenticonUrlPng(config.Value);
|
res.AvatarUrl = user.GetIdenticonUrl(config.Value);
|
||||||
res.AvatarStaticUrl = user.GetIdenticonUrlPng(config.Value);
|
res.AvatarStaticUrl = user.GetIdenticonUrl(config.Value);
|
||||||
res.HeaderUrl = _transparent;
|
res.HeaderUrl = _transparent;
|
||||||
res.HeaderStaticUrl = _transparent;
|
res.HeaderStaticUrl = _transparent;
|
||||||
}
|
}
|
||||||
|
@ -108,8 +108,8 @@ public class UserRenderer(
|
||||||
{
|
{
|
||||||
Id = p.Id,
|
Id = p.Id,
|
||||||
Shortcode = p.Name,
|
Shortcode = p.Name,
|
||||||
Url = p.PublicUrl,
|
Url = p.GetAccessUrl(config.Value),
|
||||||
StaticUrl = p.PublicUrl, //TODO
|
StaticUrl = p.GetAccessUrl(config.Value), //TODO
|
||||||
VisibleInPicker = true,
|
VisibleInPicker = true,
|
||||||
Category = p.Category
|
Category = p.Category
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,11 +3,13 @@ using System.Net.Mime;
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||||
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
||||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||||
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
using Iceshrimp.Backend.Core.Database;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Microsoft.AspNetCore.Cors;
|
using Microsoft.AspNetCore.Cors;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Pleroma;
|
namespace Iceshrimp.Backend.Controllers.Pleroma;
|
||||||
|
|
||||||
|
@ -15,7 +17,7 @@ namespace Iceshrimp.Backend.Controllers.Pleroma;
|
||||||
[EnableCors("mastodon")]
|
[EnableCors("mastodon")]
|
||||||
[EnableRateLimiting("sliding")]
|
[EnableRateLimiting("sliding")]
|
||||||
[Produces(MediaTypeNames.Application.Json)]
|
[Produces(MediaTypeNames.Application.Json)]
|
||||||
public class EmojiController(DatabaseContext db) : ControllerBase
|
public class EmojiController(IOptions<Config.InstanceSection> instance, DatabaseContext db) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet("/api/v1/pleroma/emoji")]
|
[HttpGet("/api/v1/pleroma/emoji")]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
|
@ -26,7 +28,7 @@ public class EmojiController(DatabaseContext db) : ControllerBase
|
||||||
.Select(p => KeyValuePair.Create(p.Name,
|
.Select(p => KeyValuePair.Create(p.Name,
|
||||||
new PleromaEmojiEntity
|
new PleromaEmojiEntity
|
||||||
{
|
{
|
||||||
ImageUrl = p.PublicUrl,
|
ImageUrl = p.GetAccessUrl(instance.Value),
|
||||||
Tags = new[] { p.Category ?? "" }
|
Tags = new[] { p.Category ?? "" }
|
||||||
}))
|
}))
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
|
|
|
@ -3,6 +3,7 @@ using System.Net.Mime;
|
||||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||||
using Iceshrimp.Backend.Core.Configuration;
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
using Iceshrimp.Backend.Core.Database;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
using Iceshrimp.Backend.Core.Helpers;
|
using Iceshrimp.Backend.Core.Helpers;
|
||||||
using Iceshrimp.Backend.Core.Middleware;
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
using Iceshrimp.Backend.Core.Queues;
|
using Iceshrimp.Backend.Core.Queues;
|
||||||
|
@ -10,6 +11,7 @@ using Iceshrimp.Backend.Core.Services;
|
||||||
using Iceshrimp.Shared.Schemas.Web;
|
using Iceshrimp.Shared.Schemas.Web;
|
||||||
using Microsoft.AspNetCore.Cors;
|
using Microsoft.AspNetCore.Cors;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
@ -23,63 +25,78 @@ public class DriveController(
|
||||||
IOptionsSnapshot<Config.StorageSection> options,
|
IOptionsSnapshot<Config.StorageSection> options,
|
||||||
ILogger<DriveController> logger,
|
ILogger<DriveController> logger,
|
||||||
DriveService driveSvc,
|
DriveService driveSvc,
|
||||||
QueueService queueSvc
|
QueueService queueSvc,
|
||||||
|
HttpClient httpClient
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
|
private const string CacheControl = "max-age=31536000, immutable";
|
||||||
|
|
||||||
[EnableCors("drive")]
|
[EnableCors("drive")]
|
||||||
[HttpGet("/files/{accessKey}")]
|
[EnableRateLimiting("proxy")]
|
||||||
|
[HttpGet("/files/{accessKey}/{version?}")]
|
||||||
[ProducesResults(HttpStatusCode.OK, HttpStatusCode.NoContent)]
|
[ProducesResults(HttpStatusCode.OK, HttpStatusCode.NoContent)]
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||||
public async Task<IActionResult> GetFileByAccessKey(string accessKey)
|
public async Task<IActionResult> GetFileByAccessKey(string accessKey, string? version)
|
||||||
{
|
{
|
||||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.AccessKey == accessKey ||
|
return await GetFileByAccessKey(accessKey, version, null);
|
||||||
p.PublicAccessKey == accessKey ||
|
}
|
||||||
p.ThumbnailAccessKey == accessKey);
|
|
||||||
if (file == null)
|
[EnableCors("drive")]
|
||||||
|
[EnableRateLimiting("proxy")]
|
||||||
|
[HttpGet("/media/emoji/{id}")]
|
||||||
|
[ProducesResults(HttpStatusCode.OK, HttpStatusCode.NoContent)]
|
||||||
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||||
|
public async Task<IActionResult> GetEmojiById(string id)
|
||||||
|
{
|
||||||
|
var emoji = await db.Emojis.FirstOrDefaultAsync(p => p.Id == id)
|
||||||
|
?? throw GracefulException.NotFound("Emoji not found");
|
||||||
|
|
||||||
|
if (!options.Value.ProxyRemoteMedia)
|
||||||
|
return Redirect(emoji.RawPublicUrl);
|
||||||
|
|
||||||
|
return await ProxyAsync(emoji.RawPublicUrl, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[EnableCors("drive")]
|
||||||
|
[EnableRateLimiting("proxy")]
|
||||||
|
[HttpGet("/avatars/{userId}/{version}")]
|
||||||
|
[ProducesResults(HttpStatusCode.OK, HttpStatusCode.NoContent)]
|
||||||
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||||
|
public async Task<IActionResult> GetAvatarByUserId(string userId, string? version)
|
||||||
|
{
|
||||||
|
var user = await db.Users.Include(p => p.Avatar).FirstOrDefaultAsync(p => p.Id == userId)
|
||||||
|
?? throw GracefulException.NotFound("User not found");
|
||||||
|
|
||||||
|
if (user.Avatar is null)
|
||||||
{
|
{
|
||||||
Response.Headers.CacheControl = "max-age=86400";
|
var stream = await IdenticonHelper.GetIdenticonAsync(user.Id);
|
||||||
throw GracefulException.NotFound("File not found");
|
Response.Headers.CacheControl = CacheControl;
|
||||||
|
return new InlineFileStreamResult(stream, "image/png", $"{user.Id}.png", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.StoredInternal)
|
if (!options.Value.ProxyRemoteMedia)
|
||||||
{
|
return Redirect(user.Avatar.RawThumbnailAccessUrl);
|
||||||
var pathBase = options.Value.Local?.Path;
|
|
||||||
if (string.IsNullOrWhiteSpace(pathBase))
|
|
||||||
{
|
|
||||||
logger.LogError("Failed to get file {accessKey} from local storage: path does not exist", accessKey);
|
|
||||||
throw GracefulException.NotFound("File not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
var path = Path.Join(pathBase, accessKey);
|
return await GetFileByAccessKey(user.Avatar.AccessKey, "thumbnail", user.Avatar);
|
||||||
var stream = System.IO.File.OpenRead(path);
|
}
|
||||||
|
|
||||||
Response.Headers.CacheControl = "max-age=31536000, immutable";
|
[EnableCors("drive")]
|
||||||
Response.Headers.XContentTypeOptions = "nosniff";
|
[EnableRateLimiting("proxy")]
|
||||||
return Constants.BrowserSafeMimeTypes.Contains(file.Type)
|
[HttpGet("/banners/{userId}/{version}")]
|
||||||
? new InlineFileStreamResult(stream, file.Type, file.Name, true)
|
[ProducesResults(HttpStatusCode.OK, HttpStatusCode.NoContent)]
|
||||||
: File(stream, file.Type, file.Name, true);
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||||
}
|
public async Task<IActionResult> GetBannerByUserId(string userId, string? version)
|
||||||
else
|
{
|
||||||
{
|
var user = await db.Users.Include(p => p.Banner).FirstOrDefaultAsync(p => p.Id == userId)
|
||||||
if (file.IsLink)
|
?? throw GracefulException.NotFound("User not found");
|
||||||
{
|
|
||||||
//TODO: handle remove media proxying
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
var stream = await objectStorage.GetFileAsync(accessKey);
|
if (user.Banner is null)
|
||||||
if (stream == null)
|
return NoContent();
|
||||||
{
|
|
||||||
logger.LogError("Failed to get file {accessKey} from object storage", accessKey);
|
|
||||||
throw GracefulException.NotFound("File not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
Response.Headers.CacheControl = "max-age=31536000, immutable";
|
if (!options.Value.ProxyRemoteMedia)
|
||||||
Response.Headers.XContentTypeOptions = "nosniff";
|
return Redirect(user.Banner.RawThumbnailAccessUrl);
|
||||||
return Constants.BrowserSafeMimeTypes.Contains(file.Type)
|
|
||||||
? new InlineFileStreamResult(stream, file.Type, file.Name, true)
|
return await GetFileByAccessKey(user.Banner.AccessKey, "thumbnail", user.Banner);
|
||||||
: File(stream, file.Type, file.Name, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
@ -110,14 +127,14 @@ public class DriveController(
|
||||||
public async Task<DriveFileResponse> GetFileById(string id)
|
public async Task<DriveFileResponse> GetFileById(string id)
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.User == user && p.Id == id) ??
|
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.User == user && p.Id == id)
|
||||||
throw GracefulException.NotFound("File not found");
|
?? throw GracefulException.NotFound("File not found");
|
||||||
|
|
||||||
return new DriveFileResponse
|
return new DriveFileResponse
|
||||||
{
|
{
|
||||||
Id = file.Id,
|
Id = file.Id,
|
||||||
Url = file.AccessUrl,
|
Url = file.RawAccessUrl,
|
||||||
ThumbnailUrl = file.ThumbnailAccessUrl,
|
ThumbnailUrl = file.RawThumbnailAccessUrl,
|
||||||
Filename = file.Name,
|
Filename = file.Name,
|
||||||
ContentType = file.Type,
|
ContentType = file.Type,
|
||||||
Description = file.Comment,
|
Description = file.Comment,
|
||||||
|
@ -134,14 +151,14 @@ public class DriveController(
|
||||||
public async Task<DriveFileResponse> GetFileByHash(string sha256)
|
public async Task<DriveFileResponse> GetFileByHash(string sha256)
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.User == user && p.Sha256 == sha256) ??
|
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.User == user && p.Sha256 == sha256)
|
||||||
throw GracefulException.NotFound("File not found");
|
?? throw GracefulException.NotFound("File not found");
|
||||||
|
|
||||||
return new DriveFileResponse
|
return new DriveFileResponse
|
||||||
{
|
{
|
||||||
Id = file.Id,
|
Id = file.Id,
|
||||||
Url = file.AccessUrl,
|
Url = file.RawAccessUrl,
|
||||||
ThumbnailUrl = file.ThumbnailAccessUrl,
|
ThumbnailUrl = file.RawThumbnailAccessUrl,
|
||||||
Filename = file.Name,
|
Filename = file.Name,
|
||||||
ContentType = file.Type,
|
ContentType = file.Type,
|
||||||
Description = file.Comment,
|
Description = file.Comment,
|
||||||
|
@ -159,8 +176,8 @@ public class DriveController(
|
||||||
public async Task<DriveFileResponse> UpdateFile(string id, UpdateDriveFileRequest request)
|
public async Task<DriveFileResponse> UpdateFile(string id, UpdateDriveFileRequest request)
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.User == user && p.Id == id) ??
|
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.User == user && p.Id == id)
|
||||||
throw GracefulException.NotFound("File not found");
|
?? throw GracefulException.NotFound("File not found");
|
||||||
|
|
||||||
file.Name = request.Filename ?? file.Name;
|
file.Name = request.Filename ?? file.Name;
|
||||||
file.IsSensitive = request.Sensitive ?? file.IsSensitive;
|
file.IsSensitive = request.Sensitive ?? file.IsSensitive;
|
||||||
|
@ -178,8 +195,8 @@ public class DriveController(
|
||||||
public async Task<IActionResult> DeleteFile(string id)
|
public async Task<IActionResult> DeleteFile(string id)
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.User == user && p.Id == id) ??
|
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.User == user && p.Id == id)
|
||||||
throw GracefulException.NotFound("File not found");
|
?? throw GracefulException.NotFound("File not found");
|
||||||
|
|
||||||
if (await db.Users.AnyAsync(p => p.Avatar == file || p.Banner == file))
|
if (await db.Users.AnyAsync(p => p.Avatar == file || p.Banner == file))
|
||||||
throw GracefulException.UnprocessableEntity("Refusing to delete file: used in banner or avatar");
|
throw GracefulException.UnprocessableEntity("Refusing to delete file: used in banner or avatar");
|
||||||
|
@ -193,4 +210,104 @@ public class DriveController(
|
||||||
|
|
||||||
return StatusCode(StatusCodes.Status202Accepted);
|
return StatusCode(StatusCodes.Status202Accepted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<IActionResult> GetFileByAccessKey(string accessKey, string? version, DriveFile? file)
|
||||||
|
{
|
||||||
|
file ??= await db.DriveFiles.FirstOrDefaultAsync(p => p.AccessKey == accessKey
|
||||||
|
|| p.PublicAccessKey == accessKey
|
||||||
|
|| p.ThumbnailAccessKey == accessKey);
|
||||||
|
if (file == null)
|
||||||
|
{
|
||||||
|
Response.Headers.CacheControl = "max-age=86400";
|
||||||
|
throw GracefulException.NotFound("File not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.IsLink)
|
||||||
|
{
|
||||||
|
if (!options.Value.ProxyRemoteMedia)
|
||||||
|
return NoContent();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fetchUrl = version is "thumbnail"
|
||||||
|
? file.RawThumbnailAccessUrl
|
||||||
|
: file.RawAccessUrl;
|
||||||
|
|
||||||
|
var filename = file.AccessKey == accessKey || file.Name.EndsWith(".webp")
|
||||||
|
? file.Name
|
||||||
|
: $"{file.Name}.webp";
|
||||||
|
|
||||||
|
return await ProxyAsync(fetchUrl, file.Type, filename);
|
||||||
|
}
|
||||||
|
catch (Exception e) when (e is not GracefulException)
|
||||||
|
{
|
||||||
|
throw GracefulException.BadGateway($"Failed to proxy request: {e.Message}", suppressLog: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.StoredInternal)
|
||||||
|
{
|
||||||
|
var pathBase = options.Value.Local?.Path;
|
||||||
|
if (string.IsNullOrWhiteSpace(pathBase))
|
||||||
|
{
|
||||||
|
logger.LogError("Failed to get file {accessKey} from local storage: path does not exist", accessKey);
|
||||||
|
throw GracefulException.NotFound("File not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = Path.Join(pathBase, accessKey);
|
||||||
|
var stream = System.IO.File.OpenRead(path);
|
||||||
|
|
||||||
|
Response.Headers.CacheControl = CacheControl;
|
||||||
|
Response.Headers.XContentTypeOptions = "nosniff";
|
||||||
|
|
||||||
|
return Constants.BrowserSafeMimeTypes.Contains(file.Type)
|
||||||
|
? new InlineFileStreamResult(stream, file.Type, file.Name, true)
|
||||||
|
: File(stream, file.Type, file.Name, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var stream = await objectStorage.GetFileAsync(accessKey);
|
||||||
|
if (stream == null)
|
||||||
|
{
|
||||||
|
logger.LogError("Failed to get file {accessKey} from object storage", accessKey);
|
||||||
|
throw GracefulException.NotFound("File not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
Response.Headers.CacheControl = CacheControl;
|
||||||
|
Response.Headers.XContentTypeOptions = "nosniff";
|
||||||
|
|
||||||
|
return Constants.BrowserSafeMimeTypes.Contains(file.Type)
|
||||||
|
? new InlineFileStreamResult(stream, file.Type, file.Name, true)
|
||||||
|
: File(stream, file.Type, file.Name, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IActionResult> ProxyAsync(string url, string? expectedMediaType, string? filename)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// @formatter:off
|
||||||
|
var res = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
|
||||||
|
if (!res.IsSuccessStatusCode)
|
||||||
|
throw GracefulException.BadGateway($"Failed to proxy request: response status was {res.StatusCode}", suppressLog: true);
|
||||||
|
if (res.Content.Headers.ContentType?.MediaType is not { } mediaType)
|
||||||
|
throw GracefulException.BadGateway("Failed to proxy request: remote didn't return Content-Type");
|
||||||
|
if (expectedMediaType != null && mediaType != expectedMediaType && !Constants.BrowserSafeMimeTypes.Contains(mediaType))
|
||||||
|
throw GracefulException.BadGateway("Failed to proxy request: content type mismatch", suppressLog: true);
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
Response.Headers.CacheControl = CacheControl;
|
||||||
|
Response.Headers.XContentTypeOptions = "nosniff";
|
||||||
|
|
||||||
|
var stream = await res.Content.ReadAsStreamAsync();
|
||||||
|
|
||||||
|
return Constants.BrowserSafeMimeTypes.Contains(mediaType)
|
||||||
|
? new InlineFileStreamResult(stream, mediaType, filename, true)
|
||||||
|
: File(stream, mediaType, filename, true);
|
||||||
|
}
|
||||||
|
catch (Exception e) when (e is not GracefulException)
|
||||||
|
{
|
||||||
|
throw GracefulException.BadGateway($"Failed to proxy request: {e.Message}", suppressLog: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||||
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
using Iceshrimp.Backend.Core.Database;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Backend.Core.Middleware;
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
using Iceshrimp.Backend.Core.Services;
|
using Iceshrimp.Backend.Core.Services;
|
||||||
|
@ -8,6 +9,7 @@ using Iceshrimp.Shared.Schemas.Web;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Web;
|
namespace Iceshrimp.Backend.Controllers.Web;
|
||||||
|
|
||||||
|
@ -18,6 +20,7 @@ namespace Iceshrimp.Backend.Controllers.Web;
|
||||||
[Route("/api/iceshrimp/emoji")]
|
[Route("/api/iceshrimp/emoji")]
|
||||||
[Produces(MediaTypeNames.Application.Json)]
|
[Produces(MediaTypeNames.Application.Json)]
|
||||||
public class EmojiController(
|
public class EmojiController(
|
||||||
|
IOptions<Config.InstanceSection> instance,
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
EmojiService emojiSvc,
|
EmojiService emojiSvc,
|
||||||
EmojiImportService emojiImportSvc
|
EmojiImportService emojiImportSvc
|
||||||
|
@ -36,7 +39,7 @@ public class EmojiController(
|
||||||
Uri = p.Uri,
|
Uri = p.Uri,
|
||||||
Aliases = p.Aliases,
|
Aliases = p.Aliases,
|
||||||
Category = p.Category,
|
Category = p.Category,
|
||||||
PublicUrl = p.PublicUrl,
|
PublicUrl = p.GetAccessUrl(instance.Value),
|
||||||
License = p.License,
|
License = p.License,
|
||||||
Sensitive = p.Sensitive
|
Sensitive = p.Sensitive
|
||||||
})
|
})
|
||||||
|
@ -58,7 +61,7 @@ public class EmojiController(
|
||||||
Uri = emoji.Uri,
|
Uri = emoji.Uri,
|
||||||
Aliases = emoji.Aliases,
|
Aliases = emoji.Aliases,
|
||||||
Category = emoji.Category,
|
Category = emoji.Category,
|
||||||
PublicUrl = emoji.PublicUrl,
|
PublicUrl = emoji.GetAccessUrl(instance.Value),
|
||||||
License = emoji.License,
|
License = emoji.License,
|
||||||
Sensitive = emoji.Sensitive
|
Sensitive = emoji.Sensitive
|
||||||
};
|
};
|
||||||
|
@ -80,7 +83,7 @@ public class EmojiController(
|
||||||
Uri = emoji.Uri,
|
Uri = emoji.Uri,
|
||||||
Aliases = [],
|
Aliases = [],
|
||||||
Category = null,
|
Category = null,
|
||||||
PublicUrl = emoji.PublicUrl,
|
PublicUrl = emoji.GetAccessUrl(instance.Value),
|
||||||
License = null,
|
License = null,
|
||||||
Sensitive = false
|
Sensitive = false
|
||||||
};
|
};
|
||||||
|
@ -106,7 +109,7 @@ public class EmojiController(
|
||||||
Uri = cloned.Uri,
|
Uri = cloned.Uri,
|
||||||
Aliases = [],
|
Aliases = [],
|
||||||
Category = null,
|
Category = null,
|
||||||
PublicUrl = cloned.PublicUrl,
|
PublicUrl = cloned.GetAccessUrl(instance.Value),
|
||||||
License = null,
|
License = null,
|
||||||
Sensitive = cloned.Sensitive
|
Sensitive = cloned.Sensitive
|
||||||
};
|
};
|
||||||
|
@ -141,7 +144,7 @@ public class EmojiController(
|
||||||
Uri = emoji.Uri,
|
Uri = emoji.Uri,
|
||||||
Aliases = emoji.Aliases,
|
Aliases = emoji.Aliases,
|
||||||
Category = emoji.Category,
|
Category = emoji.Category,
|
||||||
PublicUrl = emoji.PublicUrl,
|
PublicUrl = emoji.GetAccessUrl(instance.Value),
|
||||||
License = emoji.License,
|
License = emoji.License,
|
||||||
Sensitive = emoji.Sensitive
|
Sensitive = emoji.Sensitive
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||||
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
using Iceshrimp.Backend.Core.Middleware;
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
using Iceshrimp.Backend.Core.Services;
|
using Iceshrimp.Backend.Core.Services;
|
||||||
using Iceshrimp.Shared.Schemas.Web;
|
using Iceshrimp.Shared.Schemas.Web;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Web;
|
namespace Iceshrimp.Backend.Controllers.Web;
|
||||||
|
|
||||||
|
@ -16,7 +18,11 @@ namespace Iceshrimp.Backend.Controllers.Web;
|
||||||
[EnableRateLimiting("sliding")]
|
[EnableRateLimiting("sliding")]
|
||||||
[Route("/api/iceshrimp/profile")]
|
[Route("/api/iceshrimp/profile")]
|
||||||
[Produces(MediaTypeNames.Application.Json)]
|
[Produces(MediaTypeNames.Application.Json)]
|
||||||
public class ProfileController(UserService userSvc, DriveService driveSvc) : ControllerBase
|
public class ProfileController(
|
||||||
|
IOptions<Config.InstanceSection> instance,
|
||||||
|
UserService userSvc,
|
||||||
|
DriveService driveSvc
|
||||||
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
|
@ -86,7 +92,7 @@ public class ProfileController(UserService userSvc, DriveService driveSvc) : Con
|
||||||
public string GetAvatarUrl()
|
public string GetAvatarUrl()
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
return user.AvatarUrl ?? "";
|
return user.AvatarId != null ? user.GetAvatarUrl(instance.Value) : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("avatar")]
|
[HttpPost("avatar")]
|
||||||
|
@ -113,7 +119,6 @@ public class ProfileController(UserService userSvc, DriveService driveSvc) : Con
|
||||||
|
|
||||||
user.Avatar = avatar;
|
user.Avatar = avatar;
|
||||||
user.AvatarBlurhash = avatar.Blurhash;
|
user.AvatarBlurhash = avatar.Blurhash;
|
||||||
user.AvatarUrl = avatar.AccessUrl;
|
|
||||||
|
|
||||||
await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
||||||
}
|
}
|
||||||
|
@ -133,7 +138,6 @@ public class ProfileController(UserService userSvc, DriveService driveSvc) : Con
|
||||||
|
|
||||||
user.Avatar = null;
|
user.Avatar = null;
|
||||||
user.AvatarBlurhash = null;
|
user.AvatarBlurhash = null;
|
||||||
user.AvatarUrl = null;
|
|
||||||
|
|
||||||
await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
||||||
}
|
}
|
||||||
|
@ -143,7 +147,7 @@ public class ProfileController(UserService userSvc, DriveService driveSvc) : Con
|
||||||
public string GetBannerUrl()
|
public string GetBannerUrl()
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
return user.BannerUrl ?? "";
|
return user.GetBannerUrl(instance.Value) ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("banner")]
|
[HttpPost("banner")]
|
||||||
|
@ -170,7 +174,6 @@ public class ProfileController(UserService userSvc, DriveService driveSvc) : Con
|
||||||
|
|
||||||
user.Banner = banner;
|
user.Banner = banner;
|
||||||
user.BannerBlurhash = banner.Blurhash;
|
user.BannerBlurhash = banner.Blurhash;
|
||||||
user.BannerUrl = banner.AccessUrl;
|
|
||||||
|
|
||||||
await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
||||||
}
|
}
|
||||||
|
@ -190,7 +193,6 @@ public class ProfileController(UserService userSvc, DriveService driveSvc) : Con
|
||||||
|
|
||||||
user.Banner = null;
|
user.Banner = null;
|
||||||
user.BannerBlurhash = null;
|
user.BannerBlurhash = null;
|
||||||
user.BannerUrl = null;
|
|
||||||
|
|
||||||
await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ public class NoteRenderer(
|
||||||
UserRenderer userRenderer,
|
UserRenderer userRenderer,
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
EmojiService emojiSvc,
|
EmojiService emojiSvc,
|
||||||
|
MediaProxyService mediaProxy,
|
||||||
IOptions<Config.InstanceSection> config
|
IOptions<Config.InstanceSection> config
|
||||||
) : IScopedService
|
) : IScopedService
|
||||||
{
|
{
|
||||||
|
@ -114,8 +115,8 @@ public class NoteRenderer(
|
||||||
return files.Select(p => new NoteAttachment
|
return files.Select(p => new NoteAttachment
|
||||||
{
|
{
|
||||||
Id = p.Id,
|
Id = p.Id,
|
||||||
Url = p.AccessUrl,
|
Url = mediaProxy.GetProxyUrl(p),
|
||||||
ThumbnailUrl = p.ThumbnailAccessUrl,
|
ThumbnailUrl = mediaProxy.GetThumbnailProxyUrl(p),
|
||||||
ContentType = p.Type,
|
ContentType = p.Type,
|
||||||
Blurhash = p.Blurhash,
|
Blurhash = p.Blurhash,
|
||||||
AltText = p.Comment,
|
AltText = p.Comment,
|
||||||
|
@ -141,7 +142,7 @@ public class NoteRenderer(
|
||||||
i.User == user),
|
i.User == user),
|
||||||
Name = p.First().Reaction,
|
Name = p.First().Reaction,
|
||||||
Url = null,
|
Url = null,
|
||||||
Sensitive = false,
|
Sensitive = false
|
||||||
})
|
})
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
@ -149,7 +150,7 @@ public class NoteRenderer(
|
||||||
{
|
{
|
||||||
var hit = await emojiSvc.ResolveEmojiAsync(item.Name);
|
var hit = await emojiSvc.ResolveEmojiAsync(item.Name);
|
||||||
if (hit == null) continue;
|
if (hit == null) continue;
|
||||||
item.Url = hit.PublicUrl;
|
item.Url = hit.GetAccessUrl(config.Value);
|
||||||
item.Sensitive = hit.Sensitive;
|
item.Sensitive = hit.Sensitive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,7 +194,7 @@ public class NoteRenderer(
|
||||||
Uri = p.Uri,
|
Uri = p.Uri,
|
||||||
Aliases = p.Aliases,
|
Aliases = p.Aliases,
|
||||||
Category = p.Category,
|
Category = p.Category,
|
||||||
PublicUrl = p.PublicUrl,
|
PublicUrl = p.GetAccessUrl(config.Value),
|
||||||
License = p.License,
|
License = p.License,
|
||||||
Sensitive = p.Sensitive
|
Sensitive = p.Sensitive
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,33 +1,38 @@
|
||||||
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
using Iceshrimp.Backend.Core.Services;
|
using Iceshrimp.Backend.Core.Services;
|
||||||
using Iceshrimp.Shared.Schemas.Web;
|
using Iceshrimp.Shared.Schemas.Web;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using static Iceshrimp.Shared.Schemas.Web.NotificationResponse;
|
using static Iceshrimp.Shared.Schemas.Web.NotificationResponse;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Web.Renderers;
|
namespace Iceshrimp.Backend.Controllers.Web.Renderers;
|
||||||
|
|
||||||
public class NotificationRenderer(UserRenderer userRenderer, NoteRenderer noteRenderer, EmojiService emojiSvc) : IScopedService
|
public class NotificationRenderer(
|
||||||
|
IOptions<Config.InstanceSection> instance,
|
||||||
|
UserRenderer userRenderer,
|
||||||
|
NoteRenderer noteRenderer,
|
||||||
|
EmojiService emojiSvc
|
||||||
|
) : IScopedService
|
||||||
{
|
{
|
||||||
private static NotificationResponse Render(Notification notification, NotificationRendererDto data)
|
private static NotificationResponse Render(Notification notification, NotificationRendererDto data)
|
||||||
{
|
{
|
||||||
var user = notification.Notifier != null
|
var user = notification.Notifier != null
|
||||||
? data.Users?.First(p => p.Id == notification.Notifier.Id) ??
|
? data.Users?.First(p => p.Id == notification.Notifier.Id)
|
||||||
throw new Exception("DTO didn't contain the notifier")
|
?? throw new Exception("DTO didn't contain the notifier")
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var note = notification.Note != null
|
var note = notification.Note != null
|
||||||
? data.Notes?.First(p => p.Id == notification.Note.Id) ??
|
? data.Notes?.First(p => p.Id == notification.Note.Id) ?? throw new Exception("DTO didn't contain the note")
|
||||||
throw new Exception("DTO didn't contain the note")
|
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var bite = notification.Bite != null
|
var bite = notification.Bite != null
|
||||||
? data.Bites?.First(p => p.Id == notification.Bite.Id) ??
|
? data.Bites?.First(p => p.Id == notification.Bite.Id) ?? throw new Exception("DTO didn't contain the bite")
|
||||||
throw new Exception("DTO didn't contain the bite")
|
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var reaction = notification.Reaction != null
|
var reaction = notification.Reaction != null
|
||||||
? data.Reactions?.First(p => p.Name == notification.Reaction) ??
|
? data.Reactions?.First(p => p.Name == notification.Reaction)
|
||||||
throw new Exception("DTO didn't contain the reaction")
|
?? throw new Exception("DTO didn't contain the reaction")
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return new NotificationResponse
|
return new NotificationResponse
|
||||||
|
@ -49,9 +54,9 @@ public class NotificationRenderer(UserRenderer userRenderer, NoteRenderer noteRe
|
||||||
{
|
{
|
||||||
var data = new NotificationRendererDto
|
var data = new NotificationRendererDto
|
||||||
{
|
{
|
||||||
Users = await GetUsersAsync([notification]),
|
Users = await GetUsersAsync([notification]),
|
||||||
Notes = await GetNotesAsync([notification], localUser),
|
Notes = await GetNotesAsync([notification], localUser),
|
||||||
Bites = GetBites([notification]),
|
Bites = GetBites([notification]),
|
||||||
Reactions = await GetReactionsAsync([notification])
|
Reactions = await GetReactionsAsync([notification])
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -101,15 +106,32 @@ public class NotificationRenderer(UserRenderer userRenderer, NoteRenderer noteRe
|
||||||
{
|
{
|
||||||
var reactions = notifications.Select(p => p.Reaction).NotNull().ToList();
|
var reactions = notifications.Select(p => p.Reaction).NotNull().ToList();
|
||||||
|
|
||||||
var emojis = reactions.Where(p => !EmojiService.IsCustomEmoji(p)).Select(p => new ReactionResponse { Name = p, Url = null, Sensitive = false }).ToList();
|
var emojis = reactions.Where(p => !EmojiService.IsCustomEmoji(p))
|
||||||
var custom = reactions.Where(EmojiService.IsCustomEmoji).ToArray();
|
.Select(p => new ReactionResponse
|
||||||
|
{
|
||||||
|
Name = p,
|
||||||
|
Url = null,
|
||||||
|
Sensitive = false
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
var custom = reactions.Where(EmojiService.IsCustomEmoji).ToArray();
|
||||||
|
|
||||||
foreach (var s in custom)
|
foreach (var s in custom)
|
||||||
{
|
{
|
||||||
var emoji = await emojiSvc.ResolveEmojiAsync(s);
|
var emoji = await emojiSvc.ResolveEmojiAsync(s);
|
||||||
var reaction = emoji != null
|
var reaction = emoji != null
|
||||||
? new ReactionResponse { Name = s, Url = emoji.PublicUrl, Sensitive = emoji.Sensitive }
|
? new ReactionResponse
|
||||||
: new ReactionResponse { Name = s, Url = null, Sensitive = false };
|
{
|
||||||
|
Name = s,
|
||||||
|
Url = emoji.GetAccessUrl(instance.Value),
|
||||||
|
Sensitive = emoji.Sensitive
|
||||||
|
}
|
||||||
|
: new ReactionResponse
|
||||||
|
{
|
||||||
|
Name = s,
|
||||||
|
Url = null,
|
||||||
|
Sensitive = false
|
||||||
|
};
|
||||||
|
|
||||||
emojis.Add(reaction);
|
emojis.Add(reaction);
|
||||||
}
|
}
|
||||||
|
@ -124,9 +146,9 @@ public class NotificationRenderer(UserRenderer userRenderer, NoteRenderer noteRe
|
||||||
var notificationsList = notifications.ToList();
|
var notificationsList = notifications.ToList();
|
||||||
var data = new NotificationRendererDto
|
var data = new NotificationRendererDto
|
||||||
{
|
{
|
||||||
Users = await GetUsersAsync(notificationsList),
|
Users = await GetUsersAsync(notificationsList),
|
||||||
Notes = await GetNotesAsync(notificationsList, user),
|
Notes = await GetNotesAsync(notificationsList, user),
|
||||||
Bites = GetBites(notificationsList),
|
Bites = GetBites(notificationsList),
|
||||||
Reactions = await GetReactionsAsync(notificationsList)
|
Reactions = await GetReactionsAsync(notificationsList)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -135,9 +157,9 @@ public class NotificationRenderer(UserRenderer userRenderer, NoteRenderer noteRe
|
||||||
|
|
||||||
private class NotificationRendererDto
|
private class NotificationRendererDto
|
||||||
{
|
{
|
||||||
public List<NoteResponse>? Notes;
|
public List<NoteResponse>? Notes;
|
||||||
public List<UserResponse>? Users;
|
public List<UserResponse>? Users;
|
||||||
public List<BiteResponse>? Bites;
|
public List<BiteResponse>? Bites;
|
||||||
public List<ReactionResponse>? Reactions;
|
public List<ReactionResponse>? Reactions;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -27,8 +27,8 @@ public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseConte
|
||||||
Username = user.Username,
|
Username = user.Username,
|
||||||
Host = user.Host,
|
Host = user.Host,
|
||||||
DisplayName = user.DisplayName,
|
DisplayName = user.DisplayName,
|
||||||
AvatarUrl = user.AvatarUrl ?? user.GetIdenticonUrl(config.Value),
|
AvatarUrl = user.GetAvatarUrl(config.Value),
|
||||||
BannerUrl = user.BannerUrl,
|
BannerUrl = user.GetBannerUrl(config.Value),
|
||||||
InstanceName = instanceName,
|
InstanceName = instanceName,
|
||||||
InstanceIconUrl = instanceIcon,
|
InstanceIconUrl = instanceIcon,
|
||||||
Emojis = emoji,
|
Emojis = emoji,
|
||||||
|
@ -78,7 +78,7 @@ public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseConte
|
||||||
Uri = p.Uri,
|
Uri = p.Uri,
|
||||||
Aliases = p.Aliases,
|
Aliases = p.Aliases,
|
||||||
Category = p.Category,
|
Category = p.Category,
|
||||||
PublicUrl = p.PublicUrl,
|
PublicUrl = p.GetAccessUrl(config.Value),
|
||||||
License = p.License,
|
License = p.License,
|
||||||
Sensitive = p.Sensitive
|
Sensitive = p.Sensitive
|
||||||
})
|
})
|
||||||
|
|
|
@ -85,9 +85,10 @@ public sealed class Config
|
||||||
public readonly long? MaxUploadSizeBytes;
|
public readonly long? MaxUploadSizeBytes;
|
||||||
public readonly TimeSpan? MediaRetentionTimeSpan;
|
public readonly TimeSpan? MediaRetentionTimeSpan;
|
||||||
|
|
||||||
public bool CleanAvatars = false;
|
public bool CleanAvatars { get; init; } = false;
|
||||||
public bool CleanBanners = false;
|
public bool CleanBanners { get; init; } = false;
|
||||||
public Enums.FileStorage Provider { get; init; } = Enums.FileStorage.Local;
|
public bool ProxyRemoteMedia { get; init; } = true;
|
||||||
|
public Enums.FileStorage Provider { get; init; } = Enums.FileStorage.Local;
|
||||||
|
|
||||||
[Obsolete("This property is for backwards compatibility only, use StorageSection.Provider instead", true)]
|
[Obsolete("This property is for backwards compatibility only, use StorageSection.Provider instead", true)]
|
||||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||||
|
|
|
@ -747,6 +747,7 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
.HasColumnName("id");
|
.HasColumnName("id");
|
||||||
|
|
||||||
b.Property<string>("AccessKey")
|
b.Property<string>("AccessKey")
|
||||||
|
.IsRequired()
|
||||||
.HasMaxLength(256)
|
.HasMaxLength(256)
|
||||||
.HasColumnType("character varying(256)")
|
.HasColumnType("character varying(256)")
|
||||||
.HasColumnName("accessKey");
|
.HasColumnName("accessKey");
|
||||||
|
@ -3998,12 +3999,6 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
.HasColumnName("avatarId")
|
.HasColumnName("avatarId")
|
||||||
.HasComment("The ID of avatar DriveFile.");
|
.HasComment("The ID of avatar DriveFile.");
|
||||||
|
|
||||||
b.Property<string>("AvatarUrl")
|
|
||||||
.HasMaxLength(512)
|
|
||||||
.HasColumnType("character varying(512)")
|
|
||||||
.HasColumnName("avatarUrl")
|
|
||||||
.HasComment("The URL of the avatar DriveFile");
|
|
||||||
|
|
||||||
b.Property<string>("BannerBlurhash")
|
b.Property<string>("BannerBlurhash")
|
||||||
.HasMaxLength(128)
|
.HasMaxLength(128)
|
||||||
.HasColumnType("character varying(128)")
|
.HasColumnType("character varying(128)")
|
||||||
|
@ -4016,12 +4011,6 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
.HasColumnName("bannerId")
|
.HasColumnName("bannerId")
|
||||||
.HasComment("The ID of banner DriveFile.");
|
.HasComment("The ID of banner DriveFile.");
|
||||||
|
|
||||||
b.Property<string>("BannerUrl")
|
|
||||||
.HasMaxLength(512)
|
|
||||||
.HasColumnType("character varying(512)")
|
|
||||||
.HasColumnName("bannerUrl")
|
|
||||||
.HasComment("The URL of the banner DriveFile");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("createdAt")
|
.HasColumnName("createdAt")
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
[DbContext(typeof(DatabaseContext))]
|
||||||
|
[Migration("20241123224718_MakeDriveFileAccessKeyNonOptional")]
|
||||||
|
public partial class MakeDriveFileAccessKeyNonOptional : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
UPDATE "drive_file" SET "accessKey" = gen_random_uuid() WHERE "accessKey" IS NULL;
|
||||||
|
""");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "accessKey",
|
||||||
|
table: "drive_file",
|
||||||
|
type: "character varying(256)",
|
||||||
|
maxLength: 256,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(256)",
|
||||||
|
oldMaxLength: 256,
|
||||||
|
oldNullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "accessKey",
|
||||||
|
table: "drive_file",
|
||||||
|
type: "character varying(256)",
|
||||||
|
maxLength: 256,
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(256)",
|
||||||
|
oldMaxLength: 256);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
[DbContext(typeof(DatabaseContext))]
|
||||||
|
[Migration("20250109095535_RemoteUserAvatarBannerUrlColumns")]
|
||||||
|
public partial class RemoteUserAvatarBannerUrlColumns : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "avatarUrl",
|
||||||
|
table: "user");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "bannerUrl",
|
||||||
|
table: "user");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "avatarUrl",
|
||||||
|
table: "user",
|
||||||
|
type: "character varying(512)",
|
||||||
|
maxLength: 512,
|
||||||
|
nullable: true,
|
||||||
|
comment: "The URL of the avatar DriveFile");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "bannerUrl",
|
||||||
|
table: "user",
|
||||||
|
type: "character varying(512)",
|
||||||
|
maxLength: 512,
|
||||||
|
nullable: true,
|
||||||
|
comment: "The URL of the banner DriveFile");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
UPDATE "user" SET "avatarUrl" = (SELECT COALESCE("webpublicUrl", "url") FROM "drive_file" WHERE "id" = "user"."avatarId");
|
||||||
|
UPDATE "user" SET "bannerUrl" = (SELECT COALESCE("webpublicUrl", "url") FROM "drive_file" WHERE "id" = "user"."bannerId");
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -106,7 +106,7 @@ public class DriveFile : IEntity
|
||||||
|
|
||||||
[Column("accessKey")]
|
[Column("accessKey")]
|
||||||
[StringLength(256)]
|
[StringLength(256)]
|
||||||
public string? AccessKey { get; set; }
|
public string AccessKey { get; set; } = null!;
|
||||||
|
|
||||||
[Column("thumbnailAccessKey")]
|
[Column("thumbnailAccessKey")]
|
||||||
[StringLength(256)]
|
[StringLength(256)]
|
||||||
|
@ -189,8 +189,8 @@ public class DriveFile : IEntity
|
||||||
[InverseProperty(nameof(Tables.User.Banner))]
|
[InverseProperty(nameof(Tables.User.Banner))]
|
||||||
public virtual User? UserBanner { get; set; }
|
public virtual User? UserBanner { get; set; }
|
||||||
|
|
||||||
[NotMapped] public string AccessUrl => PublicUrl ?? Url;
|
[NotMapped] public string RawAccessUrl => PublicUrl ?? Url;
|
||||||
[NotMapped] public string ThumbnailAccessUrl => ThumbnailUrl ?? PublicUrl ?? Url;
|
[NotMapped] public string RawThumbnailAccessUrl => ThumbnailUrl ?? PublicUrl ?? Url;
|
||||||
|
|
||||||
[Key]
|
[Key]
|
||||||
[Column("id")]
|
[Column("id")]
|
||||||
|
|
|
@ -40,7 +40,7 @@ public class Emoji
|
||||||
|
|
||||||
[Column("publicUrl")]
|
[Column("publicUrl")]
|
||||||
[StringLength(512)]
|
[StringLength(512)]
|
||||||
public string PublicUrl { get; set; } = null!;
|
public string RawPublicUrl { get; set; } = null!;
|
||||||
|
|
||||||
[Column("license")]
|
[Column("license")]
|
||||||
[StringLength(1024)]
|
[StringLength(1024)]
|
||||||
|
@ -69,13 +69,16 @@ public class Emoji
|
||||||
? $"https://{config.WebDomain}/emoji/{Name}"
|
? $"https://{config.WebDomain}/emoji/{Name}"
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
public string GetAccessUrl(Config.InstanceSection config)
|
||||||
|
=> $"https://{config.WebDomain}/media/emoji/{Id}";
|
||||||
|
|
||||||
private class EntityTypeConfiguration : IEntityTypeConfiguration<Emoji>
|
private class EntityTypeConfiguration : IEntityTypeConfiguration<Emoji>
|
||||||
{
|
{
|
||||||
public void Configure(EntityTypeBuilder<Emoji> entity)
|
public void Configure(EntityTypeBuilder<Emoji> entity)
|
||||||
{
|
{
|
||||||
entity.Property(e => e.Aliases).HasDefaultValueSql("'{}'::character varying[]");
|
entity.Property(e => e.Aliases).HasDefaultValueSql("'{}'::character varying[]");
|
||||||
entity.Property(e => e.Height).HasComment("Image height");
|
entity.Property(e => e.Height).HasComment("Image height");
|
||||||
entity.Property(e => e.PublicUrl).HasDefaultValueSql("''::character varying");
|
entity.Property(e => e.RawPublicUrl).HasDefaultValueSql("''::character varying");
|
||||||
entity.Property(e => e.Width).HasComment("Image width");
|
entity.Property(e => e.Width).HasComment("Image width");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -242,13 +242,6 @@ public class User : IEntity
|
||||||
[Column("speakAsCat")]
|
[Column("speakAsCat")]
|
||||||
public bool SpeakAsCat { get; set; }
|
public bool SpeakAsCat { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The URL of the avatar DriveFile
|
|
||||||
/// </summary>
|
|
||||||
[Column("avatarUrl")]
|
|
||||||
[StringLength(512)]
|
|
||||||
public string? AvatarUrl { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The blurhash of the avatar DriveFile
|
/// The blurhash of the avatar DriveFile
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -256,13 +249,6 @@ public class User : IEntity
|
||||||
[StringLength(128)]
|
[StringLength(128)]
|
||||||
public string? AvatarBlurhash { get; set; }
|
public string? AvatarBlurhash { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The URL of the banner DriveFile
|
|
||||||
/// </summary>
|
|
||||||
[Column("bannerUrl")]
|
|
||||||
[StringLength(512)]
|
|
||||||
public string? BannerUrl { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The blurhash of the banner DriveFile
|
/// The blurhash of the banner DriveFile
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -587,12 +573,12 @@ public class User : IEntity
|
||||||
|
|
||||||
[Projectable]
|
[Projectable]
|
||||||
public bool HasInteractedWith(Note note) =>
|
public bool HasInteractedWith(Note note) =>
|
||||||
HasLiked(note) ||
|
HasLiked(note)
|
||||||
HasReacted(note) ||
|
|| HasReacted(note)
|
||||||
HasBookmarked(note) ||
|
|| HasBookmarked(note)
|
||||||
HasReplied(note) ||
|
|| HasReplied(note)
|
||||||
HasRenoted(note) ||
|
|| HasRenoted(note)
|
||||||
HasVoted(note);
|
|| HasVoted(note);
|
||||||
|
|
||||||
[Projectable]
|
[Projectable]
|
||||||
public bool ProhibitInteractionWith(User user) => IsBlocking(user) || IsBlockedBy(user);
|
public bool ProhibitInteractionWith(User user) => IsBlocking(user) || IsBlockedBy(user);
|
||||||
|
@ -623,11 +609,10 @@ public class User : IEntity
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetPublicUrl(Config.InstanceSection config) => GetPublicUrl(config.WebDomain);
|
public string GetPublicUrl(Config.InstanceSection config) => GetPublicUrl(config.WebDomain);
|
||||||
public string GetPublicUri(Config.InstanceSection config) => GetPublicUri(config.WebDomain);
|
public string GetPublicUri(Config.InstanceSection config) => GetPublicUri(config.WebDomain);
|
||||||
public string GetUriOrPublicUri(Config.InstanceSection config) => GetUriOrPublicUri(config.WebDomain);
|
public string GetUriOrPublicUri(Config.InstanceSection config) => GetUriOrPublicUri(config.WebDomain);
|
||||||
public string GetIdenticonUrl(Config.InstanceSection config) => GetIdenticonUrl(config.WebDomain);
|
public string GetIdenticonUrl(Config.InstanceSection config) => GetIdenticonUrl(config.WebDomain);
|
||||||
public string GetIdenticonUrlPng(Config.InstanceSection config) => GetIdenticonUrl(config.WebDomain) + ".png";
|
|
||||||
|
|
||||||
public string GetPublicUri(string webDomain) => Host == null
|
public string GetPublicUri(string webDomain) => Host == null
|
||||||
? $"https://{webDomain}/users/{Id}"
|
? $"https://{webDomain}/users/{Id}"
|
||||||
|
@ -643,6 +628,12 @@ public class User : IEntity
|
||||||
|
|
||||||
public string GetIdenticonUrl(string webDomain) => $"https://{webDomain}{IdenticonUrlPath}";
|
public string GetIdenticonUrl(string webDomain) => $"https://{webDomain}{IdenticonUrlPath}";
|
||||||
|
|
||||||
|
public string GetAvatarUrl(Config.InstanceSection config)
|
||||||
|
=> $"https://{config.WebDomain}/avatars/{Id}/{AvatarId ?? "identicon"}";
|
||||||
|
|
||||||
|
public string? GetBannerUrl(Config.InstanceSection config)
|
||||||
|
=> BannerId != null ? $"https://{config.WebDomain}/banners/{Id}/{BannerId}" : null;
|
||||||
|
|
||||||
private class EntityTypeConfiguration : IEntityTypeConfiguration<User>
|
private class EntityTypeConfiguration : IEntityTypeConfiguration<User>
|
||||||
{
|
{
|
||||||
public void Configure(EntityTypeBuilder<User> entity)
|
public void Configure(EntityTypeBuilder<User> entity)
|
||||||
|
@ -650,10 +641,8 @@ public class User : IEntity
|
||||||
entity.Property(e => e.AlsoKnownAs).HasComment("URIs the user is known as too");
|
entity.Property(e => e.AlsoKnownAs).HasComment("URIs the user is known as too");
|
||||||
entity.Property(e => e.AvatarBlurhash).HasComment("The blurhash of the avatar DriveFile");
|
entity.Property(e => e.AvatarBlurhash).HasComment("The blurhash of the avatar DriveFile");
|
||||||
entity.Property(e => e.AvatarId).HasComment("The ID of avatar DriveFile.");
|
entity.Property(e => e.AvatarId).HasComment("The ID of avatar DriveFile.");
|
||||||
entity.Property(e => e.AvatarUrl).HasComment("The URL of the avatar DriveFile");
|
|
||||||
entity.Property(e => e.BannerBlurhash).HasComment("The blurhash of the banner DriveFile");
|
entity.Property(e => e.BannerBlurhash).HasComment("The blurhash of the banner DriveFile");
|
||||||
entity.Property(e => e.BannerId).HasComment("The ID of banner DriveFile.");
|
entity.Property(e => e.BannerId).HasComment("The ID of banner DriveFile.");
|
||||||
entity.Property(e => e.BannerUrl).HasComment("The URL of the banner DriveFile");
|
|
||||||
entity.Property(e => e.CreatedAt).HasComment("The created date of the User.");
|
entity.Property(e => e.CreatedAt).HasComment("The created date of the User.");
|
||||||
entity.Property(e => e.DriveCapacityOverrideMb).HasComment("Overrides user drive capacity limit");
|
entity.Property(e => e.DriveCapacityOverrideMb).HasComment("Overrides user drive capacity limit");
|
||||||
entity.Property(e => e.Emojis).HasDefaultValueSql("'{}'::character varying[]");
|
entity.Property(e => e.Emojis).HasDefaultValueSql("'{}'::character varying[]");
|
||||||
|
|
|
@ -248,11 +248,21 @@ public static class ServiceExtensions
|
||||||
QueueLimit = 0
|
QueueLimit = 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var proxy = new SlidingWindowRateLimiterOptions
|
||||||
|
{
|
||||||
|
PermitLimit = 10,
|
||||||
|
SegmentsPerWindow = 10,
|
||||||
|
Window = TimeSpan.FromSeconds(10),
|
||||||
|
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
|
||||||
|
QueueLimit = 0
|
||||||
|
};
|
||||||
|
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
options.AddPolicy("sliding", ctx => RateLimitPartition.GetSlidingWindowLimiter(ctx.GetRateLimitPartition(false),_ => sliding));
|
options.AddPolicy("sliding", ctx => RateLimitPartition.GetSlidingWindowLimiter(ctx.GetRateLimitPartition(false),_ => sliding));
|
||||||
options.AddPolicy("auth", ctx => RateLimitPartition.GetSlidingWindowLimiter(ctx.GetRateLimitPartition(false), _ => auth));
|
options.AddPolicy("auth", ctx => RateLimitPartition.GetSlidingWindowLimiter(ctx.GetRateLimitPartition(false), _ => auth));
|
||||||
options.AddPolicy("strict", ctx => RateLimitPartition.GetSlidingWindowLimiter(ctx.GetRateLimitPartition(true), _ => strict));
|
options.AddPolicy("strict", ctx => RateLimitPartition.GetSlidingWindowLimiter(ctx.GetRateLimitPartition(true), _ => strict));
|
||||||
options.AddPolicy("imports", ctx => RateLimitPartition.GetSlidingWindowLimiter(ctx.GetRateLimitPartition(true), _ => imports));
|
options.AddPolicy("imports", ctx => RateLimitPartition.GetSlidingWindowLimiter(ctx.GetRateLimitPartition(true), _ => imports));
|
||||||
|
options.AddPolicy("proxy", ctx => RateLimitPartition.GetSlidingWindowLimiter(ctx.GetRateLimitPartition(true), _ => proxy));
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
||||||
options.OnRejected = async (context, token) =>
|
options.OnRejected = async (context, token) =>
|
||||||
|
|
|
@ -92,7 +92,7 @@ public class ActivityRenderer(
|
||||||
{
|
{
|
||||||
Id = emoji.GetPublicUriOrNull(config.Value),
|
Id = emoji.GetPublicUriOrNull(config.Value),
|
||||||
Name = name,
|
Name = name,
|
||||||
Image = new ASImage { Url = new ASLink(emoji.PublicUrl) }
|
Image = new ASImage { Url = new ASLink(emoji.RawPublicUrl) }
|
||||||
};
|
};
|
||||||
|
|
||||||
res.Tags = [e];
|
res.Tags = [e];
|
||||||
|
|
|
@ -105,7 +105,7 @@ public class NoteRenderer(
|
||||||
{
|
{
|
||||||
Id = e.GetPublicUri(config.Value),
|
Id = e.GetPublicUri(config.Value),
|
||||||
Name = e.Name,
|
Name = e.Name,
|
||||||
Image = new ASImage { Url = new ASLink(e.PublicUrl) }
|
Image = new ASImage { Url = new ASLink(e.RawPublicUrl) }
|
||||||
}))
|
}))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
@ -119,14 +119,14 @@ public class NoteRenderer(
|
||||||
var attachments = driveFiles?.Select(p => new ASDocument
|
var attachments = driveFiles?.Select(p => new ASDocument
|
||||||
{
|
{
|
||||||
Sensitive = p.IsSensitive,
|
Sensitive = p.IsSensitive,
|
||||||
Url = new ASLink(p.AccessUrl),
|
Url = new ASLink(p.RawAccessUrl),
|
||||||
MediaType = p.Type,
|
MediaType = p.Type,
|
||||||
Description = p.Comment
|
Description = p.Comment
|
||||||
})
|
})
|
||||||
.Cast<ASAttachment>()
|
.Cast<ASAttachment>()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var inlineMedia = driveFiles?.Select(p => new MfmInlineMedia(MfmInlineMedia.GetType(p.Type), p.AccessUrl, p.Comment))
|
var inlineMedia = driveFiles?.Select(p => new MfmInlineMedia(MfmInlineMedia.GetType(p.Type), p.RawAccessUrl, p.Comment))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var quoteUri = note.IsQuote ? note.Renote?.Uri ?? note.Renote?.GetPublicUriOrNull(config.Value) : null;
|
var quoteUri = note.IsQuote ? note.Renote?.Uri ?? note.Renote?.GetPublicUriOrNull(config.Value) : null;
|
||||||
|
|
|
@ -40,6 +40,19 @@ public class UserRenderer(
|
||||||
|
|
||||||
if (keypair == null) throw new Exception("User has no keypair");
|
if (keypair == null) throw new Exception("User has no keypair");
|
||||||
|
|
||||||
|
// Fetch avatar/banner relations if missing
|
||||||
|
if ((user.Avatar == null && user.AvatarId != null) || (user.Banner == null && user.BannerId != null))
|
||||||
|
{
|
||||||
|
var newUser = await db.Users
|
||||||
|
.IncludeCommonProperties()
|
||||||
|
.Include(p => p.Avatar)
|
||||||
|
.Include(p => p.Banner)
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == user.Id);
|
||||||
|
|
||||||
|
if (newUser != null)
|
||||||
|
user = newUser;
|
||||||
|
}
|
||||||
|
|
||||||
var id = user.GetPublicUri(config.Value);
|
var id = user.GetPublicUri(config.Value);
|
||||||
var type = Constants.SystemUsers.Contains(user.UsernameLower)
|
var type = Constants.SystemUsers.Contains(user.UsernameLower)
|
||||||
? ASActor.Types.Application
|
? ASActor.Types.Application
|
||||||
|
@ -61,7 +74,7 @@ public class UserRenderer(
|
||||||
{
|
{
|
||||||
Id = e.GetPublicUri(config.Value),
|
Id = e.GetPublicUri(config.Value),
|
||||||
Name = e.Name,
|
Name = e.Name,
|
||||||
Image = new ASImage { Url = new ASLink(e.PublicUrl) }
|
Image = new ASImage { Url = new ASLink(e.RawPublicUrl) }
|
||||||
}))
|
}))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
@ -97,11 +110,11 @@ public class UserRenderer(
|
||||||
AlsoKnownAs = user.AlsoKnownAs?.Select(p => new ASLink(p)).ToList(),
|
AlsoKnownAs = user.AlsoKnownAs?.Select(p => new ASLink(p)).ToList(),
|
||||||
MovedTo = user.MovedToUri is not null ? new ASLink(user.MovedToUri) : null,
|
MovedTo = user.MovedToUri is not null ? new ASLink(user.MovedToUri) : null,
|
||||||
Featured = new ASOrderedCollection($"{id}/collections/featured"),
|
Featured = new ASOrderedCollection($"{id}/collections/featured"),
|
||||||
Avatar = user.AvatarUrl != null
|
Avatar = user.Avatar != null
|
||||||
? new ASImage { Url = new ASLink(user.AvatarUrl) }
|
? new ASImage { Url = new ASLink(user.Avatar.RawAccessUrl) }
|
||||||
: null,
|
: null,
|
||||||
Banner = user.BannerUrl != null
|
Banner = user.Banner != null
|
||||||
? new ASImage { Url = new ASLink(user.BannerUrl) }
|
? new ASImage { Url = new ASLink(user.Banner.RawAccessUrl) }
|
||||||
: null,
|
: null,
|
||||||
Endpoints = new ASEndpoints { SharedInbox = new ASObjectBase($"https://{config.Value.WebDomain}/inbox") },
|
Endpoints = new ASEndpoints { SharedInbox = new ASObjectBase($"https://{config.Value.WebDomain}/inbox") },
|
||||||
PublicKey = new ASPublicKey
|
PublicKey = new ASPublicKey
|
||||||
|
|
|
@ -1,27 +1,15 @@
|
||||||
using System.IO.Hashing;
|
using System.IO.Hashing;
|
||||||
using System.Net;
|
|
||||||
using System.Net.Mime;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
|
||||||
using Microsoft.AspNetCore.Cors;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.Drawing.Processing;
|
using SixLabors.ImageSharp.Drawing.Processing;
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
using SixLabors.ImageSharp.Processing;
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Web;
|
namespace Iceshrimp.Backend.Core.Helpers;
|
||||||
|
|
||||||
[ApiController]
|
public static class IdenticonHelper
|
||||||
[EnableCors("drive")]
|
|
||||||
[Route("/identicon/{id}")]
|
|
||||||
[Route("/identicon/{id}.png")]
|
|
||||||
[Produces(MediaTypeNames.Image.Png)]
|
|
||||||
public class IdenticonController : ControllerBase
|
|
||||||
{
|
{
|
||||||
[HttpGet]
|
public static async Task<Stream> GetIdenticonAsync(string id)
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
public async Task GetIdenticon(string id)
|
|
||||||
{
|
{
|
||||||
using var image = new Image<Rgb24>(Size, Size);
|
using var image = new Image<Rgb24>(Size, Size);
|
||||||
|
|
||||||
|
@ -74,9 +62,10 @@ public class IdenticonController : ControllerBase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Response.Headers.CacheControl = "max-age=31536000, immutable";
|
var stream = new MemoryStream();
|
||||||
Response.Headers.ContentType = "image/png";
|
await image.SaveAsPngAsync(stream);
|
||||||
await image.SaveAsPngAsync(Response.Body);
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Color definitions & Constants
|
#region Color definitions & Constants
|
|
@ -8,6 +8,7 @@ using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing;
|
using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing;
|
||||||
using Iceshrimp.MfmSharp;
|
using Iceshrimp.MfmSharp;
|
||||||
|
using Iceshrimp.Backend.Core.Services;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using MfmHtmlParser = Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing.HtmlParser;
|
using MfmHtmlParser = Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing.HtmlParser;
|
||||||
using HtmlParser = AngleSharp.Html.Parser.HtmlParser;
|
using HtmlParser = AngleSharp.Html.Parser.HtmlParser;
|
||||||
|
@ -41,7 +42,8 @@ public readonly record struct HtmlMfmData(string Mfm, List<MfmInlineMedia> Inlin
|
||||||
public readonly record struct MfmHtmlData(string Html, List<MfmInlineMedia> InlineMedia);
|
public readonly record struct MfmHtmlData(string Html, List<MfmInlineMedia> InlineMedia);
|
||||||
|
|
||||||
public class MfmConverter(
|
public class MfmConverter(
|
||||||
IOptions<Config.InstanceSection> config
|
IOptions<Config.InstanceSection> config,
|
||||||
|
MediaProxyService mediaProxy
|
||||||
) : ISingletonService
|
) : ISingletonService
|
||||||
{
|
{
|
||||||
public AsyncLocal<bool> SupportsHtmlFormatting { get; } = new();
|
public AsyncLocal<bool> SupportsHtmlFormatting { get; } = new();
|
||||||
|
@ -310,7 +312,7 @@ public class MfmConverter(
|
||||||
{
|
{
|
||||||
var el = document.CreateElement("span");
|
var el = document.CreateElement("span");
|
||||||
var inner = document.CreateElement("img");
|
var inner = document.CreateElement("img");
|
||||||
inner.SetAttribute("src", hit.PublicUrl);
|
inner.SetAttribute("src", mediaProxy.GetProxyUrl(hit));
|
||||||
inner.SetAttribute("alt", hit.Name);
|
inner.SetAttribute("alt", hit.Name);
|
||||||
el.AppendChild(inner);
|
el.AppendChild(inner);
|
||||||
el.ClassList.Add("emoji");
|
el.ClassList.Add("emoji");
|
||||||
|
|
|
@ -241,6 +241,9 @@ public class GracefulException(
|
||||||
new(HttpStatusCode.MisdirectedRequest, HttpStatusCode.MisdirectedRequest.ToString(),
|
new(HttpStatusCode.MisdirectedRequest, HttpStatusCode.MisdirectedRequest.ToString(),
|
||||||
"This server is not configured to respond to this request.", null, true, true);
|
"This server is not configured to respond to this request.", null, true, true);
|
||||||
|
|
||||||
|
public static GracefulException BadGateway(string message, string? details = null, bool suppressLog = false) =>
|
||||||
|
new(HttpStatusCode.BadGateway, HttpStatusCode.BadGateway.ToString(), message, details, suppressLog);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This is intended for cases where no error occured, but the request needs to be aborted early (e.g. WebFinger
|
/// This is intended for cases where no error occured, but the request needs to be aborted early (e.g. WebFinger
|
||||||
/// returning 410 Gone)
|
/// returning 410 Gone)
|
||||||
|
|
|
@ -69,8 +69,7 @@ public class BackgroundTaskQueue(int parallelism)
|
||||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Id == jobData.DriveFileId, token);
|
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Id == jobData.DriveFileId, token);
|
||||||
if (file == null) return;
|
if (file == null) return;
|
||||||
|
|
||||||
var deduplicated = file.AccessKey != null &&
|
var deduplicated = await db.DriveFiles.AnyAsync(p => p.Id != file.Id &&
|
||||||
await db.DriveFiles.AnyAsync(p => p.Id != file.Id &&
|
|
||||||
p.AccessKey == file.AccessKey &&
|
p.AccessKey == file.AccessKey &&
|
||||||
!p.IsLink,
|
!p.IsLink,
|
||||||
token);
|
token);
|
||||||
|
|
|
@ -110,7 +110,7 @@ public class DriveService(
|
||||||
Filename = filename,
|
Filename = filename,
|
||||||
IsSensitive = sensitive,
|
IsSensitive = sensitive,
|
||||||
Comment = description,
|
Comment = description,
|
||||||
MimeType = CleanMimeType(mimeType ?? res.Content.Headers.ContentType?.MediaType)
|
MimeType = CleanMimeType(res.Content.Headers.ContentType?.MediaType ?? mimeType)
|
||||||
};
|
};
|
||||||
|
|
||||||
var input = await res.Content.ReadAsStreamAsync();
|
var input = await res.Content.ReadAsStreamAsync();
|
||||||
|
@ -148,7 +148,8 @@ public class DriveService(
|
||||||
Url = uri,
|
Url = uri,
|
||||||
Name = new Uri(uri).AbsolutePath.Split('/').LastOrDefault() ?? "",
|
Name = new Uri(uri).AbsolutePath.Split('/').LastOrDefault() ?? "",
|
||||||
Comment = description,
|
Comment = description,
|
||||||
Type = CleanMimeType(mimeType ?? "application/octet-stream")
|
Type = CleanMimeType(mimeType ?? "application/octet-stream"),
|
||||||
|
AccessKey = Guid.NewGuid().ToStringLower()
|
||||||
};
|
};
|
||||||
|
|
||||||
db.Add(file);
|
db.Add(file);
|
||||||
|
@ -191,7 +192,8 @@ public class DriveService(
|
||||||
Comment = request.Comment,
|
Comment = request.Comment,
|
||||||
Type = CleanMimeType(request.MimeType),
|
Type = CleanMimeType(request.MimeType),
|
||||||
RequestHeaders = request.RequestHeaders,
|
RequestHeaders = request.RequestHeaders,
|
||||||
RequestIp = request.RequestIp
|
RequestIp = request.RequestIp,
|
||||||
|
AccessKey = Guid.NewGuid().ToStringLower() + Path.GetExtension(request.Filename)
|
||||||
};
|
};
|
||||||
|
|
||||||
db.Add(file);
|
db.Add(file);
|
||||||
|
@ -331,7 +333,7 @@ public class DriveService(
|
||||||
Sha256 = digest,
|
Sha256 = digest,
|
||||||
Size = buf.Length,
|
Size = buf.Length,
|
||||||
IsLink = !shouldStore,
|
IsLink = !shouldStore,
|
||||||
AccessKey = original?.accessKey,
|
AccessKey = original?.accessKey ?? Guid.NewGuid().ToStringLower(),
|
||||||
IsSensitive = request.IsSensitive,
|
IsSensitive = request.IsSensitive,
|
||||||
StoredInternal = storedInternal,
|
StoredInternal = storedInternal,
|
||||||
Src = request.Source,
|
Src = request.Source,
|
||||||
|
@ -461,21 +463,13 @@ public class DriveService(
|
||||||
file.PublicMimeType = null;
|
file.PublicMimeType = null;
|
||||||
file.StoredInternal = false;
|
file.StoredInternal = false;
|
||||||
|
|
||||||
await db.Users.Where(p => p.AvatarId == file.Id)
|
|
||||||
.ExecuteUpdateAsync(p => p.SetProperty(u => u.AvatarUrl, file.Uri), token);
|
|
||||||
await db.Users.Where(p => p.BannerId == file.Id)
|
|
||||||
.ExecuteUpdateAsync(p => p.SetProperty(u => u.BannerUrl, file.Uri), token);
|
|
||||||
await db.SaveChangesAsync(token);
|
await db.SaveChangesAsync(token);
|
||||||
|
|
||||||
if (file.AccessKey != null)
|
var deduplicated =
|
||||||
{
|
await db.DriveFiles.AnyAsync(p => p.Id != file.Id && p.AccessKey == file.AccessKey && !p.IsLink, token);
|
||||||
var deduplicated = await db.DriveFiles
|
|
||||||
.AnyAsync(p => p.Id != file.Id && p.AccessKey == file.AccessKey && !p.IsLink,
|
|
||||||
token);
|
|
||||||
|
|
||||||
if (deduplicated)
|
if (deduplicated)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (storedInternal)
|
if (storedInternal)
|
||||||
{
|
{
|
||||||
|
|
|
@ -55,7 +55,7 @@ public partial class EmojiService(
|
||||||
Category = category,
|
Category = category,
|
||||||
UpdatedAt = DateTime.UtcNow,
|
UpdatedAt = DateTime.UtcNow,
|
||||||
OriginalUrl = driveFile.Url,
|
OriginalUrl = driveFile.Url,
|
||||||
PublicUrl = driveFile.AccessUrl,
|
RawPublicUrl = driveFile.RawAccessUrl,
|
||||||
Width = driveFile.Properties.Width,
|
Width = driveFile.Properties.Width,
|
||||||
Height = driveFile.Properties.Height,
|
Height = driveFile.Properties.Height,
|
||||||
Sensitive = false
|
Sensitive = false
|
||||||
|
@ -81,7 +81,7 @@ public partial class EmojiService(
|
||||||
Name = existing.Name,
|
Name = existing.Name,
|
||||||
UpdatedAt = DateTime.UtcNow,
|
UpdatedAt = DateTime.UtcNow,
|
||||||
OriginalUrl = driveFile.Url,
|
OriginalUrl = driveFile.Url,
|
||||||
PublicUrl = driveFile.AccessUrl,
|
RawPublicUrl = driveFile.RawAccessUrl,
|
||||||
Width = driveFile.Properties.Width,
|
Width = driveFile.Properties.Width,
|
||||||
Height = driveFile.Properties.Height,
|
Height = driveFile.Properties.Height,
|
||||||
Sensitive = existing.Sensitive
|
Sensitive = existing.Sensitive
|
||||||
|
@ -132,7 +132,7 @@ public partial class EmojiService(
|
||||||
Name = emojo.Name ?? throw new Exception("emojo.Name must not be null at this stage"),
|
Name = emojo.Name ?? throw new Exception("emojo.Name must not be null at this stage"),
|
||||||
UpdatedAt = DateTime.UtcNow,
|
UpdatedAt = DateTime.UtcNow,
|
||||||
OriginalUrl = emojo.Image?.Url?.Link ?? throw new Exception("Emoji.Image has no url"),
|
OriginalUrl = emojo.Image?.Url?.Link ?? throw new Exception("Emoji.Image has no url"),
|
||||||
PublicUrl = emojo.Image.Url.Link,
|
RawPublicUrl = emojo.Image.Url.Link,
|
||||||
Uri = emojo.Id,
|
Uri = emojo.Id,
|
||||||
Sensitive = false
|
Sensitive = false
|
||||||
};
|
};
|
||||||
|
|
52
Iceshrimp.Backend/Core/Services/MediaProxyService.cs
Normal file
52
Iceshrimp.Backend/Core/Services/MediaProxyService.cs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using static Iceshrimp.Backend.Core.Configuration.Enums;
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Core.Services;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
public class MediaProxyService(
|
||||||
|
IOptions<Config.InstanceSection> instance,
|
||||||
|
IOptionsMonitor<Config.StorageSection> storage
|
||||||
|
) : ISingletonService
|
||||||
|
{
|
||||||
|
public string? GetProxyUrl(string? url, string? accessKey, bool thumbnail = false, string route = "files")
|
||||||
|
{
|
||||||
|
if (!storage.CurrentValue.ProxyRemoteMedia || url is null || accessKey is null) return url;
|
||||||
|
|
||||||
|
// Don't proxy local / object storage urls
|
||||||
|
if (
|
||||||
|
storage.CurrentValue.Provider is FileStorage.ObjectStorage
|
||||||
|
&& storage.CurrentValue.ObjectStorage?.AccessUrl is { } accessUrl
|
||||||
|
&& (url.StartsWith(accessUrl) || url.StartsWith($"https://{instance.Value.WebDomain}/"))
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetProxyUrlInternal($"{route}/{accessKey}", thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetProxyUrl(DriveFile file, bool thumbnail)
|
||||||
|
{
|
||||||
|
var url = thumbnail ? file.RawThumbnailAccessUrl : file.RawAccessUrl;
|
||||||
|
if (file.UserHost is null || !file.IsLink)
|
||||||
|
return url;
|
||||||
|
|
||||||
|
return GetProxyUrlInternal($"files/{file.AccessKey}", thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetProxyUrl(Emoji emoji) => emoji.Host is null
|
||||||
|
? emoji.RawPublicUrl
|
||||||
|
: GetProxyUrlInternal($"emoji/{emoji.Id}", thumbnail: false);
|
||||||
|
|
||||||
|
public string GetProxyUrl(DriveFile file) => GetProxyUrl(file, thumbnail: false);
|
||||||
|
public string GetThumbnailProxyUrl(DriveFile file) => GetProxyUrl(file, thumbnail: true);
|
||||||
|
|
||||||
|
private string GetProxyUrlInternal(string route, bool thumbnail) => thumbnail
|
||||||
|
? $"https://{instance.Value.WebDomain}/{route}/thumbnail"
|
||||||
|
: $"https://{instance.Value.WebDomain}/{route}";
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Carbon.Storage;
|
using Carbon.Storage;
|
||||||
using Iceshrimp.Backend.Core.Configuration;
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
|
@ -89,10 +89,11 @@ public class ObjectStorageService(IOptions<Config.StorageSection> config, HttpCl
|
||||||
if (_bucket == null) throw new Exception("Refusing to upload to object storage with invalid configuration");
|
if (_bucket == null) throw new Exception("Refusing to upload to object storage with invalid configuration");
|
||||||
|
|
||||||
var properties = (_acl ?? BlobProperties.Empty).ToDictionary();
|
var properties = (_acl ?? BlobProperties.Empty).ToDictionary();
|
||||||
var contentDisposition = new ContentDispositionHeaderValue("inline") { FileName = filename }.ToString();
|
var contentDisposition = new ContentDispositionHeaderValue("inline");
|
||||||
|
contentDisposition.SetHttpFileName(filename);
|
||||||
|
|
||||||
properties.Add("Content-Type", contentType);
|
properties.Add("Content-Type", contentType);
|
||||||
properties.Add("Content-Disposition", contentDisposition);
|
properties.Add("Content-Disposition", contentDisposition.ToString());
|
||||||
|
|
||||||
IBlob blob = data.Length > 0
|
IBlob blob = data.Length > 0
|
||||||
? new Blob(GetKeyWithPrefix(key), data, properties)
|
? new Blob(GetKeyWithPrefix(key), data, properties)
|
||||||
|
|
|
@ -92,7 +92,6 @@ public class StorageMaintenanceService(
|
||||||
// defer deletions in case an error occurs
|
// defer deletions in case an error occurs
|
||||||
List<string> deletionQueue = [];
|
List<string> deletionQueue = [];
|
||||||
|
|
||||||
if (file.AccessKey != null)
|
|
||||||
{
|
{
|
||||||
var path = Path.Join(pathBase, file.AccessKey);
|
var path = Path.Join(pathBase, file.AccessKey);
|
||||||
var stream = File.OpenRead(path);
|
var stream = File.OpenRead(path);
|
||||||
|
@ -179,10 +178,6 @@ public class StorageMaintenanceService(
|
||||||
}
|
}
|
||||||
|
|
||||||
await driveSvc.ExpireFileAsync(file);
|
await driveSvc.ExpireFileAsync(file);
|
||||||
await db.Users.Where(p => p.AvatarId == file.Id)
|
|
||||||
.ExecuteUpdateAsync(p => p.SetProperty(u => u.AvatarUrl, file.Uri));
|
|
||||||
await db.Users.Where(p => p.BannerId == file.Id)
|
|
||||||
.ExecuteUpdateAsync(p => p.SetProperty(u => u.BannerUrl, file.Uri));
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,10 +213,6 @@ public class StorageMaintenanceService(
|
||||||
if (dryRun) continue;
|
if (dryRun) continue;
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
await db.Users.Where(p => p.AvatarId == file.Id)
|
|
||||||
.ExecuteUpdateAsync(p => p.SetProperty(u => u.AvatarUrl, file.AccessUrl));
|
|
||||||
await db.Users.Where(p => p.BannerId == file.Id)
|
|
||||||
.ExecuteUpdateAsync(p => p.SetProperty(u => u.BannerUrl, file.AccessUrl));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dryRun)
|
if (dryRun)
|
||||||
|
|
|
@ -464,9 +464,6 @@ public class UserService(
|
||||||
user.AvatarBlurhash = avatar?.Blurhash;
|
user.AvatarBlurhash = avatar?.Blurhash;
|
||||||
user.BannerBlurhash = banner?.Blurhash;
|
user.BannerBlurhash = banner?.Blurhash;
|
||||||
|
|
||||||
user.AvatarUrl = avatar?.Url;
|
|
||||||
user.BannerUrl = banner?.Url;
|
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
return async () =>
|
return async () =>
|
||||||
|
|
|
@ -167,6 +167,10 @@ MediaRetention = 30d
|
||||||
CleanAvatars = false
|
CleanAvatars = false
|
||||||
CleanBanners = false
|
CleanBanners = false
|
||||||
|
|
||||||
|
;; Whether to proxy remote media. This can prevent leaking the IP address of users, at the cost of higher bandwidth use.
|
||||||
|
;; It is recommended to disable this for instances hosted on residential connections.
|
||||||
|
ProxyRemoteMedia = true
|
||||||
|
|
||||||
[Storage:Local]
|
[Storage:Local]
|
||||||
;; Path where media is stored at. Must be writable for the service user.
|
;; Path where media is stored at. Must be writable for the service user.
|
||||||
Path = /path/to/media/location
|
Path = /path/to/media/location
|
||||||
|
|
Loading…
Add table
Reference in a new issue