Compare commits
1 commit
dev
...
release-fi
Author | SHA1 | Date | |
---|---|---|---|
![]() |
77709fd90d |
190 changed files with 1458 additions and 4652 deletions
|
@ -1,7 +1,7 @@
|
||||||
on:
|
on:
|
||||||
release:
|
push:
|
||||||
types:
|
branches:
|
||||||
- published
|
- release-fixup
|
||||||
jobs:
|
jobs:
|
||||||
build-artifacts:
|
build-artifacts:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
|
@ -22,12 +22,7 @@ jobs:
|
||||||
run: make release-artifacts "ARCHIVE_BASENAME=$REPO" "ARCHIVE_VERSION=$RELEASE_VERSION" VERBOSE=true DEP_VULN_WERROR=true
|
run: make release-artifacts "ARCHIVE_BASENAME=$REPO" "ARCHIVE_VERSION=$RELEASE_VERSION" VERBOSE=true DEP_VULN_WERROR=true
|
||||||
env:
|
env:
|
||||||
REPO: ${{ github.event.repository.name }}
|
REPO: ${{ github.event.repository.name }}
|
||||||
RELEASE_VERSION: ${{ github.ref_name }}
|
RELEASE_VERSION: v2025.1-beta5.patch1
|
||||||
- name: Upload artifacts
|
|
||||||
uses: actions/release-action@main
|
|
||||||
with:
|
|
||||||
files: artifacts/**
|
|
||||||
api_key: '${{secrets.RELEASE_TOKEN}}'
|
|
||||||
- name: Set environment variables
|
- name: Set environment variables
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
@ -35,16 +30,7 @@ jobs:
|
||||||
REPO="iceshrimp.dev/${GITHUB_REPOSITORY@L}"
|
REPO="iceshrimp.dev/${GITHUB_REPOSITORY@L}"
|
||||||
|
|
||||||
# We always want to tag :{version} and :pre, but only tag :latest for stable releases, and (temporarily) v2024.1-beta releases
|
# We always want to tag :{version} and :pre, but only tag :latest for stable releases, and (temporarily) v2024.1-beta releases
|
||||||
TAGS="-t $REPO:$GITHUB_REF_NAME -t $REPO:pre"
|
TAGS="-t $REPO:v2025.1-beta5.patch1 -t $REPO:latest -t $REPO:pre"
|
||||||
|
|
||||||
# The first section below can be safely removed once v2025.1 hits stable
|
|
||||||
if [[ "$GITHUB_REF_NAME" == "v2025.1-beta"* ]]; then
|
|
||||||
TAGS="$TAGS -t $REPO:latest"
|
|
||||||
elif [[ "$GITHUB_REF_NAME" == *"-beta"* ]] || [[ "$GITHUB_REF_NAME" == *"-pre"* ]]; then
|
|
||||||
:
|
|
||||||
else
|
|
||||||
TAGS="$TAGS -t $REPO:latest"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Finally, we pass the computed tags back to the actions environment
|
# Finally, we pass the computed tags back to the actions environment
|
||||||
echo "TAGS=$TAGS" >> "${GITHUB_ENV}"
|
echo "TAGS=$TAGS" >> "${GITHUB_ENV}"
|
||||||
|
|
|
@ -2,11 +2,6 @@
|
||||||
<profile version="1.0">
|
<profile version="1.0">
|
||||||
<option name="myName" value="Project Default" />
|
<option name="myName" value="Project Default" />
|
||||||
<inspection_tool class="HttpUrlsUsage" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
<inspection_tool class="HttpUrlsUsage" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
|
||||||
<option name="processCode" value="true" />
|
|
||||||
<option name="processLiterals" value="true" />
|
|
||||||
<option name="processComments" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="SqlNoDataSourceInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
<inspection_tool class="SqlNoDataSourceInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</component>
|
40
CHANGELOG.md
40
CHANGELOG.md
|
@ -1,43 +1,3 @@
|
||||||
## v2025.1-beta5.patch2.security1
|
|
||||||
This is a security hotfix release. It's identical to v2025.1-beta5.patch2, except for the security mitigations listed below. Upgrading is strongly recommended for all server operators.
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
- Updated SixLabors.ImageSharp to 3.1.7 (addressing [GHSA-2cmq-823j-5qj8](https://github.com/advisories/GHSA-2cmq-823j-5qj8))
|
|
||||||
|
|
||||||
### Attribution
|
|
||||||
This release was made possible by project contributors: Laura Hausmann
|
|
||||||
|
|
||||||
## v2025.1-beta5.patch2
|
|
||||||
This is a hotfix release. It's identical to v2025.1-beta5.patch1, except for a bunch of bugfixes. Upgrading is strongly recommended for all server operators running v2025.1-beta5 or v2025.1-beta5.patch1.
|
|
||||||
|
|
||||||
### Blazor Frontend
|
|
||||||
- The compose dialog send button no longer gets stuck on "Sent!"
|
|
||||||
- The compose dialog can no longer be closed while files are uploading or the composed note is being created
|
|
||||||
- Alt text no longer overflows the avatar box when the avatar fails to load
|
|
||||||
- Firefox no longer displays the avatar alt text while the image is loading
|
|
||||||
- If an avatar fails to load, the user's identicon is loaded as a fallback image
|
|
||||||
- Avatars now take up the same amount of space while they're loading
|
|
||||||
- The contrast of poll results has been improved
|
|
||||||
- An error dialog is displayed when poll voting fails
|
|
||||||
- Quotes no longer have extraneous whitespace above their content
|
|
||||||
- A frontend crash related to early JSInterop use has been fixed
|
|
||||||
- Profile fields now get rendered with an improved layout which no longer breaks on overflow
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
- Link verification now works with `rel="me"` links in the `<head>` section, and works with MFM/aliased links
|
|
||||||
- Voting on a poll using the Web API no longer fails
|
|
||||||
- A bug related to LDLocalizedString serialization has been resolved
|
|
||||||
|
|
||||||
### Mastodon client API
|
|
||||||
- `/api/v2/instance` now returns the streaming API URL in the correct format
|
|
||||||
|
|
||||||
### Miscellaneous
|
|
||||||
- Release builds no longer use compiled EF models, fixing API errors & frontend page load issues
|
|
||||||
- Docker builds are getting tagged as :latest again
|
|
||||||
|
|
||||||
### Attribution
|
|
||||||
This release was made possible by project contributors: Kopper, Laura Hausmann, Lilian & pancakes
|
|
||||||
|
|
||||||
## v2025.1-beta5.patch1
|
## v2025.1-beta5.patch1
|
||||||
This is a hotfix release. It's identical to v2025.1-beta5, except for a bunch of bugfixes. Upgrading is strongly recommended for all server operators running v2025.1-beta5.
|
This is a hotfix release. It's identical to v2025.1-beta5, except for a bunch of bugfixes. Upgrading is strongly recommended for all server operators running v2025.1-beta5.
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
<!-- Version metadata -->
|
<!-- Version metadata -->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<VersionPrefix>2025.1</VersionPrefix>
|
<VersionPrefix>2025.1</VersionPrefix>
|
||||||
<VersionSuffix>beta5.patch2.security1</VersionSuffix>
|
<VersionSuffix>beta5.patch1</VersionSuffix>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
new("/admin/users", "User management", Icons.Users),
|
new("/admin/users", "User management", Icons.Users),
|
||||||
new("/admin/federation", "Federation control", Icons.Graph),
|
new("/admin/federation", "Federation control", Icons.Graph),
|
||||||
new("/admin/relays", "Relays", Icons.FastForward),
|
new("/admin/relays", "Relays", Icons.FastForward),
|
||||||
new("/admin/tasks", "Cron tasks", Icons.Timer),
|
|
||||||
new("/admin/plugins", "Plugins", Icons.Plug)
|
new("/admin/plugins", "Plugins", Icons.Plug)
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ using Iceshrimp.Backend.Components.PublicPreview.Schemas;
|
||||||
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.Helpers.LibMfm.Conversion;
|
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
|
||||||
using Iceshrimp.Backend.Core.Services;
|
|
||||||
using Iceshrimp.MfmSharp;
|
using Iceshrimp.MfmSharp;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
@ -12,9 +11,9 @@ 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]
|
[UsedImplicitly]
|
||||||
public class MfmRenderer(MfmConverter converter, FlagService flags) : ISingletonService
|
public class MfmRenderer(MfmConverter converter) : ISingletonService
|
||||||
{
|
{
|
||||||
public MfmRenderData? Render(
|
public async Task<MfmRenderData?> RenderAsync(
|
||||||
string? text, string? host, List<Note.MentionedUser> mentions, List<Emoji> emoji, string rootElement,
|
string? text, string? host, List<Note.MentionedUser> mentions, List<Emoji> emoji, string rootElement,
|
||||||
List<PreviewAttachment>? media = null
|
List<PreviewAttachment>? media = null
|
||||||
)
|
)
|
||||||
|
@ -23,18 +22,18 @@ public class MfmRenderer(MfmConverter converter, FlagService flags) : ISingleton
|
||||||
var parsed = MfmParser.Parse(text);
|
var parsed = MfmParser.Parse(text);
|
||||||
|
|
||||||
// Ensure we are rendering HTML markup (AsyncLocal)
|
// Ensure we are rendering HTML markup (AsyncLocal)
|
||||||
flags.SupportsHtmlFormatting.Value = true;
|
converter.SupportsHtmlFormatting.Value = true;
|
||||||
flags.SupportsInlineMedia.Value = true;
|
converter.SupportsInlineMedia.Value = true;
|
||||||
|
|
||||||
var mfmInlineMedia = media?.Select(m => new MfmInlineMedia(MfmInlineMedia.GetType(m.MimeType), m.Url, m.Alt)).ToList();
|
var mfmInlineMedia = media?.Select(m => new MfmInlineMedia(MfmInlineMedia.GetType(m.MimeType), m.Url, m.Alt)).ToList();
|
||||||
var serialized = converter.ToHtml(parsed, mentions, host, emoji: emoji, rootElement: rootElement, media: mfmInlineMedia);
|
var serialized = await converter.ToHtmlAsync(parsed, mentions, host, emoji: emoji, rootElement: rootElement, media: mfmInlineMedia);
|
||||||
|
|
||||||
return new MfmRenderData(new MarkupString(serialized.Html), serialized.InlineMedia);
|
return new MfmRenderData(new MarkupString(serialized.Html), serialized.InlineMedia);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MarkupString? RenderSimple(string? text, string? host, List<Note.MentionedUser> mentions, List<Emoji> emoji, string rootElement)
|
public async Task<MarkupString?> RenderSimpleAsync(string? text, string? host, List<Note.MentionedUser> mentions, List<Emoji> emoji, string rootElement)
|
||||||
{
|
{
|
||||||
var rendered = Render(text, host, mentions, emoji, rootElement);
|
var rendered = await RenderAsync(text, host, mentions, emoji, rootElement);
|
||||||
return rendered?.Html;
|
return rendered?.Html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,16 +30,16 @@ public class NoteRenderer(
|
||||||
var attachments = await GetAttachmentsAsync(allNotes);
|
var attachments = await GetAttachmentsAsync(allNotes);
|
||||||
var polls = await GetPollsAsync(allNotes);
|
var polls = await GetPollsAsync(allNotes);
|
||||||
|
|
||||||
return Render(note, users, mentions, emoji, attachments, polls);
|
return await RenderAsync(note, users, mentions, emoji, attachments, polls);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PreviewNote Render(
|
private async Task<PreviewNote> RenderAsync(
|
||||||
Note note, List<PreviewUser> users, Dictionary<string, List<Note.MentionedUser>> mentions,
|
Note note, List<PreviewUser> users, Dictionary<string, List<Note.MentionedUser>> mentions,
|
||||||
Dictionary<string, List<Emoji>> emoji, Dictionary<string, List<PreviewAttachment>?> attachments,
|
Dictionary<string, List<Emoji>> emoji, Dictionary<string, List<PreviewAttachment>?> attachments,
|
||||||
Dictionary<string, PreviewPoll> polls
|
Dictionary<string, PreviewPoll> polls
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var renderedText = mfm.Render(note.Text, note.User.Host, mentions[note.Id], emoji[note.Id], "span", attachments[note.Id]);
|
var renderedText = await mfm.RenderAsync(note.Text, note.User.Host, mentions[note.Id], emoji[note.Id], "span", attachments[note.Id]);
|
||||||
var inlineMediaUrls = renderedText?.InlineMedia.Select(m => m.Src).ToArray() ?? [];
|
var inlineMediaUrls = renderedText?.InlineMedia.Select(m => m.Src).ToArray() ?? [];
|
||||||
|
|
||||||
var res = new PreviewNote
|
var res = new PreviewNote
|
||||||
|
@ -48,7 +48,6 @@ public class NoteRenderer(
|
||||||
Text = renderedText?.Html,
|
Text = renderedText?.Html,
|
||||||
Cw = note.Cw,
|
Cw = note.Cw,
|
||||||
RawText = note.Text,
|
RawText = note.Text,
|
||||||
Uri = note.Uri ?? note.GetPublicUri(instance.Value),
|
|
||||||
QuoteUrl = note.Renote?.Url ?? note.Renote?.Uri ?? note.Renote?.GetPublicUriOrNull(instance.Value),
|
QuoteUrl = note.Renote?.Url ?? note.Renote?.Uri ?? note.Renote?.GetPublicUriOrNull(instance.Value),
|
||||||
QuoteInaccessible = note.Renote?.VisibilityIsPublicOrHome == false,
|
QuoteInaccessible = note.Renote?.VisibilityIsPublicOrHome == false,
|
||||||
Attachments = attachments[note.Id]?.Where(p => !inlineMediaUrls.Contains(p.Url)).ToList(),
|
Attachments = attachments[note.Id]?.Where(p => !inlineMediaUrls.Contains(p.Url)).ToList(),
|
||||||
|
@ -143,6 +142,8 @@ public class NoteRenderer(
|
||||||
var emoji = await GetEmojiAsync(allNotes);
|
var emoji = await GetEmojiAsync(allNotes);
|
||||||
var attachments = await GetAttachmentsAsync(allNotes);
|
var attachments = await GetAttachmentsAsync(allNotes);
|
||||||
var polls = await GetPollsAsync(allNotes);
|
var polls = await GetPollsAsync(allNotes);
|
||||||
return notes.Select(p => Render(p, users, mentions, emoji, attachments, polls)).ToList();
|
return await notes.Select(p => RenderAsync(p, users, mentions, emoji, attachments, polls))
|
||||||
|
.AwaitAllAsync()
|
||||||
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,10 +19,10 @@ public class UserRenderer(
|
||||||
{
|
{
|
||||||
if (user == null) return null;
|
if (user == null) return null;
|
||||||
var emoji = await GetEmojiAsync([user]);
|
var emoji = await GetEmojiAsync([user]);
|
||||||
return Render(user, emoji);
|
return await RenderAsync(user, emoji);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PreviewUser Render(User user, Dictionary<string, List<Emoji>> emoji)
|
private async Task<PreviewUser> RenderAsync(User user, Dictionary<string, List<Emoji>> emoji)
|
||||||
{
|
{
|
||||||
var mentions = user.UserProfile?.Mentions ?? [];
|
var mentions = user.UserProfile?.Mentions ?? [];
|
||||||
|
|
||||||
|
@ -32,13 +32,12 @@ public class UserRenderer(
|
||||||
Id = user.Id,
|
Id = user.Id,
|
||||||
Username = user.Username,
|
Username = user.Username,
|
||||||
Host = user.Host ?? instance.Value.AccountDomain,
|
Host = user.Host ?? instance.Value.AccountDomain,
|
||||||
Uri = user.GetUriOrPublicUri(instance.Value),
|
|
||||||
Url = user.UserProfile?.Url ?? user.Uri ?? user.PublicUrlPath,
|
Url = user.UserProfile?.Url ?? user.Uri ?? user.PublicUrlPath,
|
||||||
AvatarUrl = user.GetAvatarUrl(instance.Value),
|
AvatarUrl = user.GetAvatarUrl(instance.Value),
|
||||||
BannerUrl = user.GetBannerUrl(instance.Value),
|
BannerUrl = user.GetBannerUrl(instance.Value),
|
||||||
RawDisplayName = user.DisplayName,
|
RawDisplayName = user.DisplayName,
|
||||||
DisplayName = mfm.RenderSimple(user.DisplayName, user.Host, mentions, emoji[user.Id], "span"),
|
DisplayName = await mfm.RenderSimpleAsync(user.DisplayName, user.Host, mentions, emoji[user.Id], "span"),
|
||||||
Bio = mfm.RenderSimple(user.UserProfile?.Description, user.Host, mentions, emoji[user.Id], "span"),
|
Bio = await mfm.RenderSimpleAsync(user.UserProfile?.Description, user.Host, mentions, emoji[user.Id], "span"),
|
||||||
MovedToUri = user.MovedToUri
|
MovedToUri = user.MovedToUri
|
||||||
};
|
};
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
@ -64,6 +63,6 @@ public class UserRenderer(
|
||||||
public async Task<List<PreviewUser>> RenderManyAsync(List<User> users)
|
public async Task<List<PreviewUser>> RenderManyAsync(List<User> users)
|
||||||
{
|
{
|
||||||
var emoji = await GetEmojiAsync(users);
|
var emoji = await GetEmojiAsync(users);
|
||||||
return users.Select(p => Render(p, emoji)).ToList();
|
return await users.Select(p => RenderAsync(p, emoji)).AwaitAllAsync().ToListAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -8,7 +8,6 @@ public class PreviewNote
|
||||||
public required string? RawText;
|
public required string? RawText;
|
||||||
public required MarkupString? Text;
|
public required MarkupString? Text;
|
||||||
public required string? Cw;
|
public required string? Cw;
|
||||||
public required string? Uri;
|
|
||||||
public required string? QuoteUrl;
|
public required string? QuoteUrl;
|
||||||
public required bool QuoteInaccessible;
|
public required bool QuoteInaccessible;
|
||||||
public required List<PreviewAttachment>? Attachments;
|
public required List<PreviewAttachment>? Attachments;
|
||||||
|
|
|
@ -11,7 +11,6 @@ public class PreviewUser
|
||||||
public required string Username;
|
public required string Username;
|
||||||
public required string Host;
|
public required string Host;
|
||||||
public required string Url;
|
public required string Url;
|
||||||
public required string Uri;
|
|
||||||
public required string AvatarUrl;
|
public required string AvatarUrl;
|
||||||
public required string? BannerUrl;
|
public required string? BannerUrl;
|
||||||
public required string? MovedToUri;
|
public required string? MovedToUri;
|
||||||
|
|
|
@ -55,7 +55,7 @@ public class AnnouncementController(DatabaseContext db, MfmConverter mfmConverte
|
||||||
})
|
})
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
res.ForEach(p => p.Content = mfmConverter.ToHtml(p.Content, [], null).Html);
|
await res.Select(async p => p.Content = (await mfmConverter.ToHtmlAsync(p.Content, [], null)).Html).AwaitAllAsync();
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Mime;
|
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.Shared.Attributes;
|
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||||
using Iceshrimp.Backend.Core.Database;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
|
@ -9,7 +8,6 @@ using Iceshrimp.Backend.Core.Extensions;
|
||||||
using Iceshrimp.Backend.Core.Helpers;
|
using Iceshrimp.Backend.Core.Helpers;
|
||||||
using Iceshrimp.Backend.Core.Middleware;
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
using Iceshrimp.Backend.Core.Services;
|
using Iceshrimp.Backend.Core.Services;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Cors;
|
using Microsoft.AspNetCore.Cors;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
@ -159,44 +157,4 @@ public class AuthController(DatabaseContext db, MetaService meta) : ControllerBa
|
||||||
|
|
||||||
return new object();
|
return new object();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authenticate]
|
|
||||||
[HttpGet("/api/oauth_tokens.json")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
public async Task<List<PleromaOauthTokenEntity>> GetOauthTokens()
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
var oauthTokens = await db.OauthTokens
|
|
||||||
.Where(p => p.User == user)
|
|
||||||
.Include(oauthToken => oauthToken.App)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
List<PleromaOauthTokenEntity> result = [];
|
|
||||||
foreach (var token in oauthTokens)
|
|
||||||
{
|
|
||||||
result.Add(new PleromaOauthTokenEntity()
|
|
||||||
{
|
|
||||||
Id = token.Id,
|
|
||||||
AppName = token.App.Name,
|
|
||||||
ValidUntil = token.CreatedAt + TimeSpan.FromDays(365 * 100)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Authenticate]
|
|
||||||
[HttpDelete("/api/oauth_tokens/{id}")]
|
|
||||||
[ProducesResults(HttpStatusCode.Created)]
|
|
||||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.Forbidden)]
|
|
||||||
public async Task RevokeOauthTokenPleroma(string id)
|
|
||||||
{
|
|
||||||
var token = await db.OauthTokens.FirstOrDefaultAsync(p => p.Id == id) ??
|
|
||||||
throw GracefulException.Forbidden("You are not authorized to revoke this token");
|
|
||||||
|
|
||||||
db.Remove(token);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
Response.StatusCode = 201;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,29 +0,0 @@
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
|
||||||
using Iceshrimp.Backend.Core.Middleware;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon;
|
|
||||||
|
|
||||||
[Route("/api/v1/preferences")]
|
|
||||||
[Authenticate]
|
|
||||||
[MastodonApiController]
|
|
||||||
public class PreferencesController : ControllerBase
|
|
||||||
{
|
|
||||||
[Authorize("read:accounts")]
|
|
||||||
[HttpGet]
|
|
||||||
public PreferencesEntity GetPreferences()
|
|
||||||
{
|
|
||||||
var settings = HttpContext.GetUserOrFail().UserSettings;
|
|
||||||
var visibility = StatusEntity.EncodeVisibility(settings?.DefaultNoteVisibility ?? Note.NoteVisibility.Public);
|
|
||||||
|
|
||||||
return new PreferencesEntity
|
|
||||||
{
|
|
||||||
PostingDefaultVisibility = visibility,
|
|
||||||
PostingDefaultSensitive = settings?.AlwaysMarkSensitive ?? false,
|
|
||||||
ReadingExpandMedia = "default",
|
|
||||||
ReadingExpandSpoilers = false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -20,8 +20,7 @@ public class NoteRenderer(
|
||||||
MfmConverter mfmConverter,
|
MfmConverter mfmConverter,
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
EmojiService emojiSvc,
|
EmojiService emojiSvc,
|
||||||
AttachmentRenderer attachmentRenderer,
|
AttachmentRenderer attachmentRenderer
|
||||||
FlagService flags
|
|
||||||
) : IScopedService
|
) : IScopedService
|
||||||
{
|
{
|
||||||
private static readonly FilterResultEntity InaccessibleFilter = new()
|
private static readonly FilterResultEntity InaccessibleFilter = new()
|
||||||
|
@ -153,7 +152,7 @@ public class NoteRenderer(
|
||||||
{
|
{
|
||||||
if (text != null || quoteUri != null || quoteInaccessible || replyInaccessible)
|
if (text != null || quoteUri != null || quoteInaccessible || replyInaccessible)
|
||||||
{
|
{
|
||||||
(content, inlineMedia) = mfmConverter.ToHtml(text ?? "", mentionedUsers, note.UserHost, quoteUri,
|
(content, inlineMedia) = await mfmConverter.ToHtmlAsync(text ?? "", mentionedUsers, note.UserHost, quoteUri,
|
||||||
quoteInaccessible, replyInaccessible, media: inlineMedia);
|
quoteInaccessible, replyInaccessible, media: inlineMedia);
|
||||||
|
|
||||||
attachments.RemoveAll(attachment => inlineMedia.Any(inline => inline.Src == (attachment.RemoteUrl ?? attachment.Url)));
|
attachments.RemoveAll(attachment => inlineMedia.Any(inline => inline.Src == (attachment.RemoteUrl ?? attachment.Url)));
|
||||||
|
@ -171,20 +170,6 @@ public class NoteRenderer(
|
||||||
? (data?.Polls ?? await GetPollsAsync([note], user)).FirstOrDefault(p => p.Id == note.Id)
|
? (data?.Polls ?? await GetPollsAsync([note], user)).FirstOrDefault(p => p.Id == note.Id)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var visibility = flags.IsPleroma.Value && note.LocalOnly
|
|
||||||
? "local"
|
|
||||||
: StatusEntity.EncodeVisibility(note.Visibility);
|
|
||||||
|
|
||||||
var pleromaExtensions = flags.IsPleroma.Value
|
|
||||||
? new PleromaStatusExtensions
|
|
||||||
{
|
|
||||||
LocalOnly = note.LocalOnly,
|
|
||||||
Reactions = reactions,
|
|
||||||
ConversationId = note.ThreadId,
|
|
||||||
ThreadMuted = muted
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
|
|
||||||
var res = new StatusEntity
|
var res = new StatusEntity
|
||||||
{
|
{
|
||||||
Id = note.Id,
|
Id = note.Id,
|
||||||
|
@ -209,7 +194,7 @@ public class NoteRenderer(
|
||||||
IsMuted = muted,
|
IsMuted = muted,
|
||||||
IsSensitive = sensitive,
|
IsSensitive = sensitive,
|
||||||
ContentWarning = cw ?? "",
|
ContentWarning = cw ?? "",
|
||||||
Visibility = visibility,
|
Visibility = StatusEntity.EncodeVisibility(note.Visibility),
|
||||||
Content = content,
|
Content = content,
|
||||||
Text = text,
|
Text = text,
|
||||||
Mentions = mentions,
|
Mentions = mentions,
|
||||||
|
@ -220,7 +205,7 @@ public class NoteRenderer(
|
||||||
Reactions = reactions,
|
Reactions = reactions,
|
||||||
Tags = tags,
|
Tags = tags,
|
||||||
Filtered = filterResult,
|
Filtered = filterResult,
|
||||||
Pleroma = pleromaExtensions
|
Pleroma = new PleromaStatusExtensions { Reactions = reactions, ConversationId = note.ThreadId }
|
||||||
};
|
};
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
@ -258,7 +243,7 @@ public class NoteRenderer(
|
||||||
_ => MfmInlineMedia.MediaType.Other
|
_ => MfmInlineMedia.MediaType.Other
|
||||||
}, p.RemoteUrl ?? p.Url, p.Description)).ToList();
|
}, p.RemoteUrl ?? p.Url, p.Description)).ToList();
|
||||||
|
|
||||||
(var content, inlineMedia) = mfmConverter.ToHtml(edit.Text ?? "", mentionedUsers, note.UserHost, media: inlineMedia);
|
(var content, inlineMedia) = await mfmConverter.ToHtmlAsync(edit.Text ?? "", mentionedUsers, note.UserHost, media: inlineMedia);
|
||||||
files.RemoveAll(attachment => inlineMedia.Any(inline => inline.Src == (attachment.RemoteUrl ?? attachment.Url)));
|
files.RemoveAll(attachment => inlineMedia.Any(inline => inline.Src == (attachment.RemoteUrl ?? attachment.Url)));
|
||||||
|
|
||||||
var entry = new StatusEdit
|
var entry = new StatusEdit
|
||||||
|
|
|
@ -5,7 +5,6 @@ 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 Iceshrimp.EntityFrameworkCore.Extensions;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
@ -15,8 +14,7 @@ public class NotificationRenderer(
|
||||||
IOptions<Config.InstanceSection> instance,
|
IOptions<Config.InstanceSection> instance,
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
NoteRenderer noteRenderer,
|
NoteRenderer noteRenderer,
|
||||||
UserRenderer userRenderer,
|
UserRenderer userRenderer
|
||||||
FlagService flags
|
|
||||||
) : IScopedService
|
) : IScopedService
|
||||||
{
|
{
|
||||||
public async Task<NotificationEntity> RenderAsync(
|
public async Task<NotificationEntity> RenderAsync(
|
||||||
|
@ -29,13 +27,13 @@ public class NotificationRenderer(
|
||||||
var targetNote = notification.Note;
|
var targetNote = notification.Note;
|
||||||
|
|
||||||
var note = targetNote != null
|
var note = targetNote != null
|
||||||
? statuses?.FirstOrDefault(p => p.Id == targetNote.Id)
|
? statuses?.FirstOrDefault(p => p.Id == targetNote.Id) ??
|
||||||
?? await noteRenderer.RenderAsync(targetNote, user, Filter.FilterContext.Notifications,
|
await noteRenderer.RenderAsync(targetNote, user, Filter.FilterContext.Notifications,
|
||||||
new NoteRenderer.NoteRendererDto { Accounts = accounts })
|
new NoteRenderer.NoteRendererDto { Accounts = accounts })
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var notifier = accounts?.FirstOrDefault(p => p.Id == dbNotifier.Id)
|
var notifier = accounts?.FirstOrDefault(p => p.Id == dbNotifier.Id) ??
|
||||||
?? await userRenderer.RenderAsync(dbNotifier, user);
|
await userRenderer.RenderAsync(dbNotifier, user);
|
||||||
|
|
||||||
string? emojiUrl = null;
|
string? emojiUrl = null;
|
||||||
if (notification.Reaction != null)
|
if (notification.Reaction != null)
|
||||||
|
@ -64,9 +62,7 @@ public class NotificationRenderer(
|
||||||
CreatedAt = notification.CreatedAt.ToStringIso8601Like(),
|
CreatedAt = notification.CreatedAt.ToStringIso8601Like(),
|
||||||
Emoji = notification.Reaction,
|
Emoji = notification.Reaction,
|
||||||
EmojiUrl = emojiUrl,
|
EmojiUrl = emojiUrl,
|
||||||
Pleroma = flags.IsPleroma.Value
|
Pleroma = new PleromaNotificationExtensions { IsSeen = notification.IsRead }
|
||||||
? new PleromaNotificationExtensions { IsSeen = notification.IsRead }
|
|
||||||
: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
@ -101,28 +97,27 @@ public class NotificationRenderer(
|
||||||
.Select(p =>
|
.Select(p =>
|
||||||
{
|
{
|
||||||
var parts = p.Reaction!.Trim(':').Split('@');
|
var parts = p.Reaction!.Trim(':').Split('@');
|
||||||
return (name: parts[0], host: parts.Length > 1 ? parts[1] : null);
|
return new { Name = parts[0], Host = parts.Length > 1 ? parts[1] : null };
|
||||||
})
|
});
|
||||||
.Distinct()
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
var expr = ExpressionExtensions.False<Emoji>();
|
|
||||||
expr = parts.Aggregate(expr, (current, part) => current.Or(p => p.Name == part.name && p.Host == part.host));
|
|
||||||
|
|
||||||
// https://github.com/dotnet/efcore/issues/31492
|
// https://github.com/dotnet/efcore/issues/31492
|
||||||
var emojiUrls = await db.Emojis
|
//TODO: is there a better way of expressing this using LINQ?
|
||||||
.Where(expr)
|
IQueryable<Emoji> urlQ = db.Emojis;
|
||||||
.Select(e => new
|
foreach (var part in parts)
|
||||||
|
urlQ = urlQ.Concat(db.Emojis.Where(e => e.Name == part.Name && e.Host == part.Host));
|
||||||
|
|
||||||
|
//TODO: can we somehow optimize this to do the dedupe database side?
|
||||||
|
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.GetAccessUrl(instance.Value)
|
Url = e.GetAccessUrl(instance.Value)
|
||||||
})
|
})
|
||||||
.ToDictionaryAsync(e => e.Name, e => e.Url);
|
.ToArrayAsync()
|
||||||
|
.ContinueWithResult(res => res.DistinctBy(e => e.Name)
|
||||||
|
.ToDictionary(e => e.Name, e => e.Url));
|
||||||
|
|
||||||
var res = await notificationList
|
return await notificationList
|
||||||
.Select(p => RenderAsync(p, user, isPleroma, accounts, notes, emojiUrls))
|
.Select(p => RenderAsync(p, user, isPleroma, accounts, notes, emojiUrls))
|
||||||
.AwaitAllAsync();
|
.AwaitAllAsync();
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,11 +1,9 @@
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||||
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
|
||||||
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.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.Backend.Core.Services;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
@ -15,8 +13,7 @@ public class UserRenderer(
|
||||||
IOptions<Config.InstanceSection> config,
|
IOptions<Config.InstanceSection> config,
|
||||||
IOptionsSnapshot<Config.SecuritySection> security,
|
IOptionsSnapshot<Config.SecuritySection> security,
|
||||||
MfmConverter mfmConverter,
|
MfmConverter mfmConverter,
|
||||||
DatabaseContext db,
|
DatabaseContext db
|
||||||
FlagService flags
|
|
||||||
) : IScopedService
|
) : IScopedService
|
||||||
{
|
{
|
||||||
private readonly string _transparent = $"https://{config.Value.WebDomain}/assets/transparent.png";
|
private readonly string _transparent = $"https://{config.Value.WebDomain}/assets/transparent.png";
|
||||||
|
@ -34,15 +31,18 @@ public class UserRenderer(
|
||||||
|
|
||||||
var profileEmoji = data?.Emoji.Where(p => user.Emojis.Contains(p.Id)).ToList() ?? await GetEmojiAsync([user]);
|
var profileEmoji = data?.Emoji.Where(p => user.Emojis.Contains(p.Id)).ToList() ?? await GetEmojiAsync([user]);
|
||||||
var mentions = profile?.Mentions ?? [];
|
var mentions = profile?.Mentions ?? [];
|
||||||
var fields = profile?.Fields
|
var fields = profile != null
|
||||||
.Select(p => new Field
|
? await profile.Fields
|
||||||
|
.Select(async p => new Field
|
||||||
{
|
{
|
||||||
Name = p.Name,
|
Name = p.Name,
|
||||||
Value = (mfmConverter.ToHtml(p.Value, mentions, user.Host)).Html,
|
Value = (await mfmConverter.ToHtmlAsync(p.Value, mentions, user.Host)).Html,
|
||||||
VerifiedAt = p.IsVerified.HasValue && p.IsVerified.Value
|
VerifiedAt = p.IsVerified.HasValue && p.IsVerified.Value
|
||||||
? DateTime.Now.ToStringIso8601Like()
|
? DateTime.Now.ToStringIso8601Like()
|
||||||
: null
|
: null
|
||||||
});
|
})
|
||||||
|
.AwaitAllAsync()
|
||||||
|
: null;
|
||||||
|
|
||||||
var fieldsSource = source
|
var fieldsSource = source
|
||||||
? profile?.Fields.Select(p => new Field { Name = p.Name, Value = p.Value }).ToList() ?? []
|
? profile?.Fields.Select(p => new Field { Name = p.Name, Value = p.Value }).ToList() ?? []
|
||||||
|
@ -51,25 +51,6 @@ public class UserRenderer(
|
||||||
var avatarAlt = data?.AvatarAlt.GetValueOrDefault(user.Id);
|
var avatarAlt = data?.AvatarAlt.GetValueOrDefault(user.Id);
|
||||||
var bannerAlt = data?.BannerAlt.GetValueOrDefault(user.Id);
|
var bannerAlt = data?.BannerAlt.GetValueOrDefault(user.Id);
|
||||||
|
|
||||||
string? favicon;
|
|
||||||
string? softwareName;
|
|
||||||
string? softwareVersion;
|
|
||||||
if (user.IsRemoteUser)
|
|
||||||
{
|
|
||||||
var instInfo = await db.Instances
|
|
||||||
.Where(p => p.Host == user.Host)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
favicon = instInfo?.FaviconUrl;
|
|
||||||
softwareName = instInfo?.SoftwareName;
|
|
||||||
softwareVersion = instInfo?.SoftwareVersion;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
favicon = config.Value.WebDomain + "/_content/Iceshrimp.Assets.Branding/favicon.png";
|
|
||||||
softwareName = "iceshrimp";
|
|
||||||
softwareVersion = config.Value.Version;
|
|
||||||
}
|
|
||||||
|
|
||||||
var res = new AccountEntity
|
var res = new AccountEntity
|
||||||
{
|
{
|
||||||
Id = user.Id,
|
Id = user.Id,
|
||||||
|
@ -80,11 +61,10 @@ public class UserRenderer(
|
||||||
FullyQualifiedName = $"{user.Username}@{user.Host ?? config.Value.AccountDomain}",
|
FullyQualifiedName = $"{user.Username}@{user.Host ?? config.Value.AccountDomain}",
|
||||||
IsLocked = user.IsLocked,
|
IsLocked = user.IsLocked,
|
||||||
CreatedAt = user.CreatedAt.ToStringIso8601Like(),
|
CreatedAt = user.CreatedAt.ToStringIso8601Like(),
|
||||||
LastStatusAt = user.LastNoteAt?.ToStringIso8601Like(),
|
|
||||||
FollowersCount = user.FollowersCount,
|
FollowersCount = user.FollowersCount,
|
||||||
FollowingCount = user.FollowingCount,
|
FollowingCount = user.FollowingCount,
|
||||||
StatusesCount = user.NotesCount,
|
StatusesCount = user.NotesCount,
|
||||||
Note = mfmConverter.ToHtml(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.GetAvatarUrl(config.Value), //TODO
|
AvatarStaticUrl = user.GetAvatarUrl(config.Value), //TODO
|
||||||
|
@ -96,30 +76,7 @@ public class UserRenderer(
|
||||||
IsBot = user.IsBot,
|
IsBot = user.IsBot,
|
||||||
IsDiscoverable = user.IsExplorable,
|
IsDiscoverable = user.IsExplorable,
|
||||||
Fields = fields?.ToList() ?? [],
|
Fields = fields?.ToList() ?? [],
|
||||||
Emoji = profileEmoji,
|
Emoji = profileEmoji
|
||||||
Pleroma = flags?.IsPleroma.Value == true
|
|
||||||
? new PleromaUserExtensions
|
|
||||||
{
|
|
||||||
IsAdmin = user.IsAdmin,
|
|
||||||
IsModerator = user.IsModerator,
|
|
||||||
Favicon = favicon!
|
|
||||||
} : null,
|
|
||||||
Akkoma = flags?.IsPleroma.Value == true
|
|
||||||
? new AkkomaUserExtensions
|
|
||||||
{
|
|
||||||
Instance = new AkkomaInstanceEntity
|
|
||||||
{
|
|
||||||
Name = user.Host ?? config.Value.AccountDomain,
|
|
||||||
NodeInfo = new AkkomaNodeInfoEntity
|
|
||||||
{
|
|
||||||
Software = new AkkomaNodeInfoSoftwareEntity
|
|
||||||
{
|
|
||||||
Name = softwareName,
|
|
||||||
Version = softwareVersion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} : null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (localUser is null && security.Value.PublicPreview == Enums.PublicPreview.RestrictedNoMedia) //TODO
|
if (localUser is null && security.Value.PublicPreview == Enums.PublicPreview.RestrictedNoMedia) //TODO
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
using System.Net;
|
|
||||||
using System.Net.Mime;
|
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
|
||||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
|
||||||
using Iceshrimp.Backend.Core.Database;
|
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
|
||||||
using Iceshrimp.Backend.Core.Middleware;
|
|
||||||
using Iceshrimp.Backend.Core.Services;
|
|
||||||
using Microsoft.AspNetCore.Cors;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon;
|
|
||||||
|
|
||||||
[Route("/api/v1/reports")]
|
|
||||||
[Authenticate]
|
|
||||||
[MastodonApiController]
|
|
||||||
[EnableCors("mastodon")]
|
|
||||||
[EnableRateLimiting("sliding")]
|
|
||||||
[Produces(MediaTypeNames.Application.Json)]
|
|
||||||
public class ReportController(ReportService reportSvc, DatabaseContext db, UserRenderer userRenderer) : ControllerBase
|
|
||||||
{
|
|
||||||
[Authorize("write:reports")]
|
|
||||||
[HttpPost]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
|
|
||||||
public async Task<ReportEntity> FileReport([FromHybrid] ReportSchemas.FileReportRequest request)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
if (request.Comment.Length > 2048)
|
|
||||||
throw GracefulException.BadRequest("Comment length must not exceed 2048 characters");
|
|
||||||
if (request.AccountId == user.Id)
|
|
||||||
throw GracefulException.BadRequest("You cannot report yourself");
|
|
||||||
|
|
||||||
var target = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == request.AccountId)
|
|
||||||
?? throw GracefulException.NotFound("Target user not found");
|
|
||||||
|
|
||||||
var notes = await db.Notes.Where(p => request.StatusIds.Contains(p.Id)).ToListAsync();
|
|
||||||
if (notes.Any(p => p.UserId != target.Id))
|
|
||||||
throw GracefulException.BadRequest("Note author does not match target user");
|
|
||||||
|
|
||||||
var report = await reportSvc.CreateReportAsync(user, target, notes, request.Comment);
|
|
||||||
var targetAccount = await userRenderer.RenderAsync(report.TargetUser, user);
|
|
||||||
|
|
||||||
return new ReportEntity
|
|
||||||
{
|
|
||||||
Id = report.Id,
|
|
||||||
Category = "other",
|
|
||||||
Comment = report.Comment,
|
|
||||||
Forwarded = report.Forwarded,
|
|
||||||
ActionTaken = report.Resolved,
|
|
||||||
CreatedAt = report.CreatedAt.ToStringIso8601Like(),
|
|
||||||
TargetAccount = targetAccount,
|
|
||||||
RuleIds = null,
|
|
||||||
StatusIds = request.StatusIds,
|
|
||||||
ActionTakenAt = null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,3 @@
|
||||||
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
|
||||||
using Iceshrimp.Shared.Helpers;
|
using Iceshrimp.Shared.Helpers;
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||||
|
|
||||||
|
@ -29,9 +28,6 @@ public class AccountEntity : IIdentifiable
|
||||||
[J("source")] public AccountSource? Source { get; set; }
|
[J("source")] public AccountSource? Source { get; set; }
|
||||||
[J("emojis")] public required List<EmojiEntity> Emoji { get; set; }
|
[J("emojis")] public required List<EmojiEntity> Emoji { get; set; }
|
||||||
[J("id")] public required string Id { get; set; }
|
[J("id")] public required string Id { get; set; }
|
||||||
[J("last_status_at")] public string? LastStatusAt { get; set; }
|
|
||||||
[J("pleroma")] public required PleromaUserExtensions? Pleroma { get; set; }
|
|
||||||
[J("akkoma")] public required AkkomaUserExtensions? Akkoma { get; set; }
|
|
||||||
|
|
||||||
[J("avatar_description")] public required string AvatarDescription { get; set; }
|
[J("avatar_description")] public required string AvatarDescription { get; set; }
|
||||||
[J("header_description")] public required string HeaderDescription { get; set; }
|
[J("header_description")] public required string HeaderDescription { get; set; }
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
||||||
using Iceshrimp.Backend.Core.Middleware;
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
using Iceshrimp.Shared.Helpers;
|
using Iceshrimp.Shared.Helpers;
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||||
using JI = System.Text.Json.Serialization.JsonIgnoreAttribute;
|
|
||||||
using static Iceshrimp.Backend.Core.Database.Tables.Notification;
|
using static Iceshrimp.Backend.Core.Database.Tables.Notification;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||||
|
@ -15,12 +13,10 @@ public class NotificationEntity : IIdentifiable
|
||||||
[J("account")] public required AccountEntity Notifier { get; set; }
|
[J("account")] public required AccountEntity Notifier { get; set; }
|
||||||
[J("status")] public required StatusEntity? Note { get; set; }
|
[J("status")] public required StatusEntity? Note { get; set; }
|
||||||
[J("id")] public required string Id { get; set; }
|
[J("id")] public required string Id { get; set; }
|
||||||
|
[J("pleroma")] public required PleromaNotificationExtensions Pleroma { get; set; }
|
||||||
[J("emoji")] public string? Emoji { get; set; }
|
[J("emoji")] public string? Emoji { get; set; }
|
||||||
[J("emoji_url")] public string? EmojiUrl { get; set; }
|
[J("emoji_url")] public string? EmojiUrl { get; set; }
|
||||||
|
|
||||||
[J("pleroma")] [JI(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
||||||
public required PleromaNotificationExtensions? Pleroma { get; set; }
|
|
||||||
|
|
||||||
public static string EncodeType(NotificationType type, bool isPleroma)
|
public static string EncodeType(NotificationType type, bool isPleroma)
|
||||||
{
|
{
|
||||||
return type switch
|
return type switch
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
|
||||||
|
|
||||||
public class PreferencesEntity
|
|
||||||
{
|
|
||||||
[J("posting:default:visibility")] public required string PostingDefaultVisibility { get; set; }
|
|
||||||
[J("posting:default:sensitive")] public required bool PostingDefaultSensitive { get; set; }
|
|
||||||
[J("posting:default:language")] public string? PostingDefaultLanguage => null;
|
|
||||||
[J("reading:expand:media")] public required string ReadingExpandMedia { get; set; }
|
|
||||||
[J("reading:expand:spoilers")] public required bool ReadingExpandSpoilers { get; set; }
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
|
||||||
|
|
||||||
public class ReportEntity
|
|
||||||
{
|
|
||||||
[J("id")] public required string Id { get; set; }
|
|
||||||
[J("action_taken")] public required bool ActionTaken { get; set; }
|
|
||||||
[J("action_taken_at")] public string? ActionTakenAt { get; set; }
|
|
||||||
[J("category")] public required string Category { get; set; }
|
|
||||||
[J("comment")] public required string Comment { get; set; }
|
|
||||||
[J("forwarded")] public required bool Forwarded { get; set; }
|
|
||||||
[J("created_at")] public required string CreatedAt { get; set; }
|
|
||||||
[J("status_ids")] public string[]? StatusIds { get; set; }
|
|
||||||
[J("rule_ids")] public string[]? RuleIds { get; set; }
|
|
||||||
[J("target_account")] public required AccountEntity TargetAccount { get; set; }
|
|
||||||
}
|
|
|
@ -56,8 +56,7 @@ public class StatusEntity : IIdentifiable, ICloneable
|
||||||
public object Clone() => MemberwiseClone();
|
public object Clone() => MemberwiseClone();
|
||||||
[J("id")] public required string Id { get; set; }
|
[J("id")] public required string Id { get; set; }
|
||||||
|
|
||||||
[J("pleroma")] [JI(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
[J("pleroma")] public required PleromaStatusExtensions Pleroma { get; set; }
|
||||||
public required PleromaStatusExtensions? Pleroma { get; set; }
|
|
||||||
|
|
||||||
public static string EncodeVisibility(Note.NoteVisibility visibility)
|
public static string EncodeVisibility(Note.NoteVisibility visibility)
|
||||||
{
|
{
|
||||||
|
|
|
@ -33,7 +33,7 @@ public class InstanceInfoV1Response(
|
||||||
[J("invites_enabled")] public bool RegsInvite => config.Security.Registrations == Enums.Registrations.Invite;
|
[J("invites_enabled")] public bool RegsInvite => config.Security.Registrations == Enums.Registrations.Invite;
|
||||||
[J("approval_required")] public bool RegsClosed => config.Security.Registrations == Enums.Registrations.Closed;
|
[J("approval_required")] public bool RegsClosed => config.Security.Registrations == Enums.Registrations.Closed;
|
||||||
|
|
||||||
[J("urls")] public InstanceUrlsV1 Urls => new(config.Instance);
|
[J("urls")] public InstanceUrls Urls => new(config.Instance);
|
||||||
[J("configuration")] public InstanceConfigurationV1 Configuration => new(config.Instance);
|
[J("configuration")] public InstanceConfigurationV1 Configuration => new(config.Instance);
|
||||||
|
|
||||||
[J("pleroma")] public required PleromaInstanceExtensions Pleroma { get; set; }
|
[J("pleroma")] public required PleromaInstanceExtensions Pleroma { get; set; }
|
||||||
|
@ -43,7 +43,7 @@ public class InstanceInfoV1Response(
|
||||||
//TODO: add the rest
|
//TODO: add the rest
|
||||||
}
|
}
|
||||||
|
|
||||||
public class InstanceUrlsV1(Config.InstanceSection config)
|
public class InstanceUrls(Config.InstanceSection config)
|
||||||
{
|
{
|
||||||
[J("streaming_api")] public string StreamingApi => $"wss://{config.WebDomain}";
|
[J("streaming_api")] public string StreamingApi => $"wss://{config.WebDomain}";
|
||||||
}
|
}
|
||||||
|
@ -98,5 +98,5 @@ public class InstancePollConfiguration
|
||||||
|
|
||||||
public class InstanceReactionConfiguration
|
public class InstanceReactionConfiguration
|
||||||
{
|
{
|
||||||
[J("max_reactions")] public int MaxOptions => 100;
|
[J("max_reactions")] public int MaxOptions => 1;
|
||||||
}
|
}
|
|
@ -39,12 +39,7 @@ public class InstanceConfigurationV2(Config.InstanceSection config)
|
||||||
[J("media_attachments")] public InstanceMediaConfiguration Media => new();
|
[J("media_attachments")] public InstanceMediaConfiguration Media => new();
|
||||||
[J("polls")] public InstancePollConfiguration Polls => new();
|
[J("polls")] public InstancePollConfiguration Polls => new();
|
||||||
[J("reactions")] public InstanceReactionConfiguration Reactions => new();
|
[J("reactions")] public InstanceReactionConfiguration Reactions => new();
|
||||||
[J("urls")] public InstanceUrlsV2 Urls => new(config);
|
[J("urls")] public InstanceUrls Urls => new(config);
|
||||||
}
|
|
||||||
|
|
||||||
public class InstanceUrlsV2(Config.InstanceSection config)
|
|
||||||
{
|
|
||||||
[J("streaming")] public string StreamingApi => $"wss://{config.WebDomain}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class InstanceRegistrations(Config.SecuritySection config)
|
public class InstanceRegistrations(Config.SecuritySection config)
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
|
||||||
using JR = System.Text.Json.Serialization.JsonRequiredAttribute;
|
|
||||||
using B = Microsoft.AspNetCore.Mvc.BindPropertyAttribute;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
|
||||||
|
|
||||||
public abstract class ReportSchemas
|
|
||||||
{
|
|
||||||
public class FileReportRequest
|
|
||||||
{
|
|
||||||
[B(Name = "account_id")]
|
|
||||||
[J("account_id")]
|
|
||||||
[JR]
|
|
||||||
public string AccountId { get; set; } = null!;
|
|
||||||
|
|
||||||
[B(Name = "status_ids")]
|
|
||||||
[J("status_ids")]
|
|
||||||
public string[] StatusIds { get; set; } = [];
|
|
||||||
|
|
||||||
[B(Name = "comment")]
|
|
||||||
[J("comment")]
|
|
||||||
public string Comment { get; set; } = "";
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,6 +8,7 @@ using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
using Iceshrimp.Backend.Core.Events;
|
using Iceshrimp.Backend.Core.Events;
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
using Iceshrimp.Backend.Core.Helpers;
|
using Iceshrimp.Backend.Core.Helpers;
|
||||||
|
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
|
||||||
using Iceshrimp.Backend.Core.Services;
|
using Iceshrimp.Backend.Core.Services;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@ -393,10 +394,9 @@ public sealed class WebSocketConnection(
|
||||||
|
|
||||||
private void InitializeScopeLocalParameters(IServiceScope scope)
|
private void InitializeScopeLocalParameters(IServiceScope scope)
|
||||||
{
|
{
|
||||||
var flags = scope.ServiceProvider.GetRequiredService<FlagService>();
|
var mfmConverter = scope.ServiceProvider.GetRequiredService<MfmConverter>();
|
||||||
flags.SupportsHtmlFormatting.Value = Token.SupportsHtmlFormatting;
|
mfmConverter.SupportsHtmlFormatting.Value = Token.SupportsHtmlFormatting;
|
||||||
flags.SupportsInlineMedia.Value = Token.SupportsInlineMedia;
|
mfmConverter.SupportsInlineMedia.Value = Token.SupportsInlineMedia;
|
||||||
flags.IsPleroma.Value = Token.IsPleroma;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CloseAsync(WebSocketCloseStatus status)
|
public async Task CloseAsync(WebSocketCloseStatus status)
|
||||||
|
|
|
@ -1,106 +0,0 @@
|
||||||
using System.Net;
|
|
||||||
using System.Net.Mime;
|
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
|
||||||
using Iceshrimp.Backend.Controllers.Pleroma.Schemas;
|
|
||||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
|
||||||
using Iceshrimp.Backend.Controllers.Web.Renderers;
|
|
||||||
using Iceshrimp.Backend.Core.Database;
|
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
|
||||||
using Iceshrimp.Backend.Core.Middleware;
|
|
||||||
using Microsoft.AspNetCore.Cors;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using NoteRenderer = Iceshrimp.Backend.Controllers.Mastodon.Renderers.NoteRenderer;
|
|
||||||
using UserRenderer = Iceshrimp.Backend.Controllers.Mastodon.Renderers.UserRenderer;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Pleroma;
|
|
||||||
|
|
||||||
[MastodonApiController]
|
|
||||||
[Authenticate]
|
|
||||||
[EnableCors("mastodon")]
|
|
||||||
[EnableRateLimiting("sliding")]
|
|
||||||
[Produces(MediaTypeNames.Application.Json)]
|
|
||||||
public class AdminController(
|
|
||||||
DatabaseContext db,
|
|
||||||
ReportRenderer reportRenderer,
|
|
||||||
NoteRenderer noteRenderer,
|
|
||||||
UserRenderer userRenderer
|
|
||||||
) : ControllerBase
|
|
||||||
{
|
|
||||||
[HttpGet("/api/v1/pleroma/admin/reports")]
|
|
||||||
[Authenticate("admin:read:reports")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
public async Task<ReportsQuery> GetReports()
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
var reports = await db.Reports
|
|
||||||
.IncludeCommonProperties()
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var rendered = await reportRenderer.RenderManyAsync(reports);
|
|
||||||
|
|
||||||
var reportsList = new List<Reports>();
|
|
||||||
foreach (var r in rendered)
|
|
||||||
{
|
|
||||||
var reActor = await db.Users
|
|
||||||
.IncludeCommonProperties()
|
|
||||||
.Where(p => p.Id == r.Reporter.Id)
|
|
||||||
.RenderAllForMastodonAsync(userRenderer, user);
|
|
||||||
|
|
||||||
var reTarget = await db.Users
|
|
||||||
.IncludeCommonProperties()
|
|
||||||
.Where(p => p.Id == r.TargetUser.Id)
|
|
||||||
.RenderAllForMastodonAsync(userRenderer, user);
|
|
||||||
|
|
||||||
foreach (var n in r.Notes)
|
|
||||||
{
|
|
||||||
var note = await db.Notes
|
|
||||||
.IncludeCommonProperties()
|
|
||||||
.Where(p => p.Id == n.Id)
|
|
||||||
.RenderAllForMastodonAsync(noteRenderer, user);
|
|
||||||
|
|
||||||
reportsList.Add(new Reports()
|
|
||||||
{
|
|
||||||
Account = reTarget.FirstOrDefault()!,
|
|
||||||
Actor = reActor.FirstOrDefault()!,
|
|
||||||
Id = r.Id,
|
|
||||||
CreatedAt = r.CreatedAt,
|
|
||||||
State = r.Resolved ? "resolved" : "open",
|
|
||||||
Content = r.Comment,
|
|
||||||
Statuses = note,
|
|
||||||
Notes = [] // unsupported
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var resps = new ReportsQuery()
|
|
||||||
{
|
|
||||||
Total = reportsList.Count,
|
|
||||||
Reports = reportsList
|
|
||||||
};
|
|
||||||
|
|
||||||
return resps;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPatch("/api/v1/pleroma/admin/reports")]
|
|
||||||
[Authenticate("admin:read:reports")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
|
||||||
// ReSharper disable once AsyncVoidMethod
|
|
||||||
public async Task<ReportsQuery>? SetReportState(ReportsQuery query)
|
|
||||||
{
|
|
||||||
foreach (var list in query.Reports)
|
|
||||||
{
|
|
||||||
var report = await db.Reports.Where(p => p.Id == list.Id).FirstOrDefaultAsync()
|
|
||||||
?? throw GracefulException.NotFound("Report not found");
|
|
||||||
|
|
||||||
report.Resolved = list.State is "resolved" or "closed";
|
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
|
||||||
|
|
||||||
public class AkkomaInstanceEntity
|
|
||||||
{
|
|
||||||
[J("name")] public required string Name { get; set; }
|
|
||||||
[J("nodeinfo")] public required AkkomaNodeInfoEntity NodeInfo { get; set; }
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
|
||||||
|
|
||||||
public class AkkomaNodeInfoEntity
|
|
||||||
{
|
|
||||||
[J("software")] public required AkkomaNodeInfoSoftwareEntity Software { get; set; }
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
|
||||||
|
|
||||||
public class AkkomaNodeInfoSoftwareEntity
|
|
||||||
{
|
|
||||||
[J("name")] public required string? Name { get; set; }
|
|
||||||
[J("version")] public required string? Version { get; set; }
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
|
||||||
|
|
||||||
[Keyless]
|
|
||||||
public class AkkomaUserExtensions
|
|
||||||
{
|
|
||||||
[J("instance")] public required AkkomaInstanceEntity Instance { get; set; }
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
using System.Runtime.InteropServices.JavaScript;
|
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
|
||||||
|
|
||||||
public class PleromaOauthTokenEntity
|
|
||||||
{
|
|
||||||
[J("id")] public required string Id { get; set; }
|
|
||||||
[J("valid_until")] public required DateTime ValidUntil { get; set; }
|
|
||||||
[J("app_name")] public required string? AppName { get; set; }
|
|
||||||
}
|
|
|
@ -7,6 +7,4 @@ public class PleromaStatusExtensions
|
||||||
{
|
{
|
||||||
[J("emoji_reactions")] public required List<ReactionEntity> Reactions { get; set; }
|
[J("emoji_reactions")] public required List<ReactionEntity> Reactions { get; set; }
|
||||||
[J("conversation_id")] public required string ConversationId { get; set; }
|
[J("conversation_id")] public required string ConversationId { get; set; }
|
||||||
[J("local")] public required bool LocalOnly { get; set; }
|
|
||||||
[J("thread_muted")] public required bool ThreadMuted { get; set; }
|
|
||||||
}
|
}
|
|
@ -1,12 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
|
||||||
|
|
||||||
[Keyless]
|
|
||||||
public class PleromaUserExtensions
|
|
||||||
{
|
|
||||||
[J("is_admin")] public required bool IsAdmin { get; set; }
|
|
||||||
[J("is_moderator")] public required bool IsModerator { get; set; }
|
|
||||||
[J("favicon")] public required string Favicon { get; set; }
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas;
|
|
||||||
|
|
||||||
public class ReportsQuery
|
|
||||||
{
|
|
||||||
[J("total")] public int Total { get; set; }
|
|
||||||
[J("reports")] public required List<Reports> Reports { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Reports
|
|
||||||
{
|
|
||||||
[J("account")] public AccountEntity? Account { get; set; }
|
|
||||||
[J("actor")] public AccountEntity? Actor { get; set; }
|
|
||||||
[J("id")] public required string Id { get; set; }
|
|
||||||
[J("created_at")] public DateTime? CreatedAt { get; set; }
|
|
||||||
[J("state")] public required string State { get; set; }
|
|
||||||
[J("content")] public string? Content { get; set; }
|
|
||||||
[J("statuses")] public IEnumerable<StatusEntity>? Statuses { get; set; }
|
|
||||||
[J("notes")] public string[]? Notes { get; set; }
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
|
||||||
using Microsoft.AspNetCore.Http.Metadata;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Shared.Attributes;
|
|
||||||
|
|
||||||
public class NoRequestSizeLimitAttribute : Attribute, IFormOptionsMetadata, IResourceFilter
|
|
||||||
{
|
|
||||||
public void OnResourceExecuting(ResourceExecutingContext context)
|
|
||||||
{
|
|
||||||
var feature = context.HttpContext.Features.Get<IHttpMaxRequestBodySizeFeature>() ??
|
|
||||||
throw new Exception("Failed to get IHttpMaxRequestBodySizeFeature");
|
|
||||||
feature.MaxRequestBodySize = long.MaxValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnResourceExecuted(ResourceExecutedContext context) { }
|
|
||||||
|
|
||||||
public bool? BufferBody => null;
|
|
||||||
public int? MemoryBufferThreshold => null;
|
|
||||||
public long? BufferBodyLengthLimit => long.MaxValue;
|
|
||||||
public int? ValueCountLimit => null;
|
|
||||||
public int? KeyLengthLimit => null;
|
|
||||||
public int? ValueLengthLimit => null;
|
|
||||||
public int? MultipartBoundaryLengthLimit => null;
|
|
||||||
public int? MultipartHeadersCountLimit => null;
|
|
||||||
public int? MultipartHeadersLengthLimit => null;
|
|
||||||
public long? MultipartBodyLengthLimit => long.MaxValue;
|
|
||||||
}
|
|
|
@ -298,24 +298,6 @@ public class AdminController(
|
||||||
await new MediaCleanupTask().InvokeAsync(scope.ServiceProvider);
|
await new MediaCleanupTask().InvokeAsync(scope.ServiceProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("tasks/{id}/run")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
|
||||||
public void RunCronTask([FromServices] CronService cronSvc, string id)
|
|
||||||
{
|
|
||||||
var task = cronSvc.Tasks.FirstOrDefault(p => p.Task.GetType().FullName == id)
|
|
||||||
?? throw GracefulException.NotFound("Task not found");
|
|
||||||
|
|
||||||
Task.Factory.StartNew(async () =>
|
|
||||||
{
|
|
||||||
await cronSvc.RunCronTaskAsync(task.Task, task.Trigger);
|
|
||||||
task.Trigger.UpdateNextTrigger();
|
|
||||||
},
|
|
||||||
CancellationToken.None,
|
|
||||||
TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning,
|
|
||||||
TaskScheduler.Default);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("policy")]
|
[HttpGet("policy")]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
public async Task<List<string>> GetAvailablePolicies() => await policySvc.GetAvailablePoliciesAsync();
|
public async Task<List<string>> GetAvailablePolicies() => await policySvc.GetAvailablePoliciesAsync();
|
||||||
|
|
|
@ -40,7 +40,7 @@ public class EmojiController(
|
||||||
Id = p.Id,
|
Id = p.Id,
|
||||||
Name = p.Name,
|
Name = p.Name,
|
||||||
Uri = p.Uri,
|
Uri = p.Uri,
|
||||||
Tags = p.Tags,
|
Aliases = p.Aliases,
|
||||||
Category = p.Category,
|
Category = p.Category,
|
||||||
PublicUrl = p.GetAccessUrl(instance.Value),
|
PublicUrl = p.GetAccessUrl(instance.Value),
|
||||||
License = p.License,
|
License = p.License,
|
||||||
|
@ -53,20 +53,16 @@ public class EmojiController(
|
||||||
[Authorize("role:moderator")]
|
[Authorize("role:moderator")]
|
||||||
[RestPagination(100, 500)]
|
[RestPagination(100, 500)]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
public async Task<PaginationWrapper<List<EmojiResponse>>> GetRemoteEmoji(
|
public async Task<PaginationWrapper<List<EmojiResponse>>> GetRemoteEmoji(PaginationQuery pq)
|
||||||
[FromQuery] string? name, [FromQuery] string? host, PaginationQuery pq
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
var res = await db.Emojis
|
var res = await db.Emojis
|
||||||
.Where(p => p.Host != null
|
.Where(p => p.Host != null)
|
||||||
&& (string.IsNullOrWhiteSpace(host) || p.Host.ToLower().Contains(host.ToLower()))
|
|
||||||
&& (string.IsNullOrWhiteSpace(name) || p.Name.ToLower().Contains(name.ToLower())))
|
|
||||||
.Select(p => new EmojiResponse
|
.Select(p => new EmojiResponse
|
||||||
{
|
{
|
||||||
Id = p.Id,
|
Id = p.Id,
|
||||||
Name = p.Name,
|
Name = p.Name,
|
||||||
Uri = p.Uri,
|
Uri = p.Uri,
|
||||||
Tags = p.Tags,
|
Aliases = p.Aliases,
|
||||||
Category = p.Host,
|
Category = p.Host,
|
||||||
PublicUrl = p.GetAccessUrl(instance.Value),
|
PublicUrl = p.GetAccessUrl(instance.Value),
|
||||||
License = p.License,
|
License = p.License,
|
||||||
|
@ -91,7 +87,7 @@ public class EmojiController(
|
||||||
Id = p.Id,
|
Id = p.Id,
|
||||||
Name = p.Name,
|
Name = p.Name,
|
||||||
Uri = p.Uri,
|
Uri = p.Uri,
|
||||||
Tags = p.Tags,
|
Aliases = p.Aliases,
|
||||||
Category = p.Host,
|
Category = p.Host,
|
||||||
PublicUrl = p.GetAccessUrl(instance.Value),
|
PublicUrl = p.GetAccessUrl(instance.Value),
|
||||||
License = p.License,
|
License = p.License,
|
||||||
|
@ -133,7 +129,7 @@ public class EmojiController(
|
||||||
Id = emoji.Id,
|
Id = emoji.Id,
|
||||||
Name = emoji.Name,
|
Name = emoji.Name,
|
||||||
Uri = emoji.Uri,
|
Uri = emoji.Uri,
|
||||||
Tags = emoji.Tags,
|
Aliases = emoji.Aliases,
|
||||||
Category = emoji.Category,
|
Category = emoji.Category,
|
||||||
PublicUrl = emoji.GetAccessUrl(instance.Value),
|
PublicUrl = emoji.GetAccessUrl(instance.Value),
|
||||||
License = emoji.License,
|
License = emoji.License,
|
||||||
|
@ -155,7 +151,7 @@ public class EmojiController(
|
||||||
Id = emoji.Id,
|
Id = emoji.Id,
|
||||||
Name = emoji.Name,
|
Name = emoji.Name,
|
||||||
Uri = emoji.Uri,
|
Uri = emoji.Uri,
|
||||||
Tags = [],
|
Aliases = [],
|
||||||
Category = null,
|
Category = null,
|
||||||
PublicUrl = emoji.GetAccessUrl(instance.Value),
|
PublicUrl = emoji.GetAccessUrl(instance.Value),
|
||||||
License = null,
|
License = null,
|
||||||
|
@ -181,7 +177,7 @@ public class EmojiController(
|
||||||
Id = cloned.Id,
|
Id = cloned.Id,
|
||||||
Name = cloned.Name,
|
Name = cloned.Name,
|
||||||
Uri = cloned.Uri,
|
Uri = cloned.Uri,
|
||||||
Tags = [],
|
Aliases = [],
|
||||||
Category = null,
|
Category = null,
|
||||||
PublicUrl = cloned.GetAccessUrl(instance.Value),
|
PublicUrl = cloned.GetAccessUrl(instance.Value),
|
||||||
License = null,
|
License = null,
|
||||||
|
@ -191,7 +187,7 @@ public class EmojiController(
|
||||||
|
|
||||||
[HttpPost("import")]
|
[HttpPost("import")]
|
||||||
[Authorize("role:moderator")]
|
[Authorize("role:moderator")]
|
||||||
[NoRequestSizeLimit]
|
[DisableRequestSizeLimit]
|
||||||
[ProducesResults(HttpStatusCode.Accepted)]
|
[ProducesResults(HttpStatusCode.Accepted)]
|
||||||
public async Task<AcceptedResult> ImportEmoji(IFormFile file)
|
public async Task<AcceptedResult> ImportEmoji(IFormFile file)
|
||||||
{
|
{
|
||||||
|
@ -204,10 +200,10 @@ public class EmojiController(
|
||||||
[Authorize("role:moderator")]
|
[Authorize("role:moderator")]
|
||||||
[Consumes(MediaTypeNames.Application.Json)]
|
[Consumes(MediaTypeNames.Application.Json)]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||||
public async Task<EmojiResponse> UpdateEmoji(string id, UpdateEmojiRequest request)
|
public async Task<EmojiResponse> UpdateEmoji(string id, UpdateEmojiRequest request)
|
||||||
{
|
{
|
||||||
var emoji = await emojiSvc.UpdateLocalEmojiAsync(id, request.Name, request.Tags, request.Category,
|
var emoji = await emojiSvc.UpdateLocalEmojiAsync(id, request.Name, request.Aliases, request.Category,
|
||||||
request.License, request.Sensitive)
|
request.License, request.Sensitive)
|
||||||
?? throw GracefulException.NotFound("Emoji not found");
|
?? throw GracefulException.NotFound("Emoji not found");
|
||||||
|
|
||||||
|
@ -216,7 +212,7 @@ public class EmojiController(
|
||||||
Id = emoji.Id,
|
Id = emoji.Id,
|
||||||
Name = emoji.Name,
|
Name = emoji.Name,
|
||||||
Uri = emoji.Uri,
|
Uri = emoji.Uri,
|
||||||
Tags = emoji.Tags,
|
Aliases = emoji.Aliases,
|
||||||
Category = emoji.Category,
|
Category = emoji.Category,
|
||||||
PublicUrl = emoji.GetAccessUrl(instance.Value),
|
PublicUrl = emoji.GetAccessUrl(instance.Value),
|
||||||
License = emoji.License,
|
License = emoji.License,
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||||
using Iceshrimp.Backend.Controllers.Shared.Schemas;
|
|
||||||
using Iceshrimp.Backend.Controllers.Web.Renderers;
|
|
||||||
using Iceshrimp.Backend.Core.Database;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
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 Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
@ -16,21 +13,15 @@ namespace Iceshrimp.Backend.Controllers.Web;
|
||||||
[Authorize("role:moderator")]
|
[Authorize("role:moderator")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/api/iceshrimp/moderation")]
|
[Route("/api/iceshrimp/moderation")]
|
||||||
public class ModerationController(
|
public class ModerationController(DatabaseContext db, NoteService noteSvc, UserService userSvc) : ControllerBase
|
||||||
DatabaseContext db,
|
|
||||||
NoteService noteSvc,
|
|
||||||
UserService userSvc,
|
|
||||||
ReportRenderer reportRenderer,
|
|
||||||
ReportService reportSvc
|
|
||||||
) : ControllerBase
|
|
||||||
{
|
{
|
||||||
[HttpPost("notes/{id}/delete")]
|
[HttpPost("notes/{id}/delete")]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||||
public async Task DeleteNote(string id)
|
public async Task DeleteNote(string id)
|
||||||
{
|
{
|
||||||
var note = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id)
|
var note = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id) ??
|
||||||
?? throw GracefulException.NotFound("Note not found");
|
throw GracefulException.NotFound("Note not found");
|
||||||
|
|
||||||
await noteSvc.DeleteNoteAsync(note);
|
await noteSvc.DeleteNoteAsync(note);
|
||||||
}
|
}
|
||||||
|
@ -40,8 +31,8 @@ public class ModerationController(
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||||
public async Task SuspendUser(string id)
|
public async Task SuspendUser(string id)
|
||||||
{
|
{
|
||||||
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id && !p.IsSystemUser)
|
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id && !p.IsSystemUser) ??
|
||||||
?? throw GracefulException.NotFound("User not found");
|
throw GracefulException.NotFound("User not found");
|
||||||
|
|
||||||
if (user == HttpContext.GetUserOrFail())
|
if (user == HttpContext.GetUserOrFail())
|
||||||
throw GracefulException.BadRequest("You cannot suspend yourself.");
|
throw GracefulException.BadRequest("You cannot suspend yourself.");
|
||||||
|
@ -54,8 +45,8 @@ public class ModerationController(
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||||
public async Task UnsuspendUser(string id)
|
public async Task UnsuspendUser(string id)
|
||||||
{
|
{
|
||||||
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id && !p.IsSystemUser)
|
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id && !p.IsSystemUser) ??
|
||||||
?? throw GracefulException.NotFound("User not found");
|
throw GracefulException.NotFound("User not found");
|
||||||
|
|
||||||
if (user == HttpContext.GetUserOrFail())
|
if (user == HttpContext.GetUserOrFail())
|
||||||
throw GracefulException.BadRequest("You cannot unsuspend yourself.");
|
throw GracefulException.BadRequest("You cannot unsuspend yourself.");
|
||||||
|
@ -68,8 +59,8 @@ public class ModerationController(
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||||
public async Task DeleteUser(string id)
|
public async Task DeleteUser(string id)
|
||||||
{
|
{
|
||||||
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id && !p.IsSystemUser)
|
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id && !p.IsSystemUser) ??
|
||||||
?? throw GracefulException.NotFound("User not found");
|
throw GracefulException.NotFound("User not found");
|
||||||
|
|
||||||
if (user == HttpContext.GetUserOrFail())
|
if (user == HttpContext.GetUserOrFail())
|
||||||
throw GracefulException.BadRequest("You cannot delete yourself.");
|
throw GracefulException.BadRequest("You cannot delete yourself.");
|
||||||
|
@ -82,60 +73,9 @@ public class ModerationController(
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||||
public async Task PurgeUser(string id)
|
public async Task PurgeUser(string id)
|
||||||
{
|
{
|
||||||
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id && !p.IsSystemUser)
|
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id && !p.IsSystemUser) ??
|
||||||
?? throw GracefulException.NotFound("User not found");
|
throw GracefulException.NotFound("User not found");
|
||||||
|
|
||||||
await userSvc.PurgeUserAsync(user);
|
await userSvc.PurgeUserAsync(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("reports")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.BadRequest)]
|
|
||||||
[LinkPagination(20, 40)]
|
|
||||||
public async Task<IEnumerable<ReportResponse>> GetReports(PaginationQuery pq, bool resolved = false)
|
|
||||||
{
|
|
||||||
var reports = await db.Reports
|
|
||||||
.IncludeCommonProperties()
|
|
||||||
.Where(p => p.Resolved == resolved)
|
|
||||||
.Paginate(pq, ControllerContext)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return await reportRenderer.RenderManyAsync(reports);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("reports/{id}/resolve")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
|
||||||
public async Task ResolveReport(string id)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
var report = await db.Reports.FirstOrDefaultAsync(p => p.Id == id)
|
|
||||||
?? throw GracefulException.NotFound("Report not found");
|
|
||||||
|
|
||||||
report.Assignee = user;
|
|
||||||
report.Resolved = true;
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("reports/{id}/forward")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
|
|
||||||
public async Task ForwardReport(string id, [FromBody] NoteReportRequest? request)
|
|
||||||
{
|
|
||||||
var report = await db.Reports
|
|
||||||
.Include(p => p.TargetUser)
|
|
||||||
.Include(p => p.Notes)
|
|
||||||
.FirstOrDefaultAsync(p => p.Id == id)
|
|
||||||
?? throw GracefulException.NotFound("Report not found");
|
|
||||||
|
|
||||||
if (report.TargetUserHost == null)
|
|
||||||
throw GracefulException.BadRequest("Cannot forward report to local instance");
|
|
||||||
if (report.Forwarded)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await reportSvc.ForwardReportAsync(report, request?.Comment);
|
|
||||||
|
|
||||||
report.Forwarded = true;
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -30,8 +30,7 @@ public class NoteController(
|
||||||
UserRenderer userRenderer,
|
UserRenderer userRenderer,
|
||||||
CacheService cache,
|
CacheService cache,
|
||||||
BiteService biteSvc,
|
BiteService biteSvc,
|
||||||
PollService pollSvc,
|
PollService pollSvc
|
||||||
ReportService reportSvc
|
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
|
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
|
||||||
|
@ -159,10 +158,8 @@ public class NoteController(
|
||||||
.FirstOrDefaultAsync() ??
|
.FirstOrDefaultAsync() ??
|
||||||
throw GracefulException.NotFound("Note not found");
|
throw GracefulException.NotFound("Note not found");
|
||||||
|
|
||||||
name = name.Trim(':');
|
|
||||||
|
|
||||||
var users = await db.NoteReactions
|
var users = await db.NoteReactions
|
||||||
.Where(p => p.Note == note && (p.Reaction == $":{name}:" || p.Reaction == name))
|
.Where(p => p.Note == note && p.Reaction == $":{name.Trim(':')}:")
|
||||||
.Include(p => p.User.UserProfile)
|
.Include(p => p.User.UserProfile)
|
||||||
.Select(p => p.User)
|
.Select(p => p.User)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
@ -518,14 +515,15 @@ public class NoteController(
|
||||||
throw GracefulException.BadRequest("At least one vote must be included");
|
throw GracefulException.BadRequest("At least one vote must be included");
|
||||||
|
|
||||||
var target = await db.Notes
|
var target = await db.Notes
|
||||||
.IncludeCommonProperties()
|
.Where(p => p.Id == id)
|
||||||
.Include(p => p.Poll)
|
|
||||||
.Where(p => p.Id == id && p.Poll != null)
|
|
||||||
.EnsureVisibleFor(user)
|
.EnsureVisibleFor(user)
|
||||||
|
.Include(p => p.Poll)
|
||||||
.FirstOrDefaultAsync()
|
.FirstOrDefaultAsync()
|
||||||
?? throw GracefulException.NotFound("Poll not found");
|
?? throw GracefulException.NotFound("Note not found");
|
||||||
|
if (target.Poll == null)
|
||||||
|
throw GracefulException.NotFound("Poll not found");
|
||||||
|
|
||||||
if (target.Poll!.ExpiresAt != null && target.Poll.ExpiresAt < DateTime.UtcNow)
|
if (target.Poll.ExpiresAt != null && target.Poll.ExpiresAt < DateTime.UtcNow)
|
||||||
throw GracefulException.NotFound("Poll has expired");
|
throw GracefulException.NotFound("Poll has expired");
|
||||||
|
|
||||||
var voted = await db.PollVotes
|
var voted = await db.PollVotes
|
||||||
|
@ -646,18 +644,4 @@ public class NoteController(
|
||||||
|
|
||||||
return await noteRenderer.RenderOne(note, user);
|
return await noteRenderer.RenderOne(note, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/report")]
|
|
||||||
[Authenticate]
|
|
||||||
[Authorize]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
|
||||||
public async Task ReportNote(string id, [FromBody] NoteReportRequest request)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
var note = await db.Notes.Include(p => p.User).EnsureVisibleFor(user).FirstOrDefaultAsync(p => p.Id == id)
|
|
||||||
?? throw GracefulException.NotFound("Note not found");
|
|
||||||
|
|
||||||
await reportSvc.CreateReportAsync(user, note.User, [note], request.Comment);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -38,7 +38,6 @@ public class NoteRenderer(
|
||||||
res.Filtered = new NoteFilteredSchema
|
res.Filtered = new NoteFilteredSchema
|
||||||
{
|
{
|
||||||
Id = filtered.Value.filter.Id,
|
Id = filtered.Value.filter.Id,
|
||||||
Name = filtered.Value.filter.Name,
|
|
||||||
Keyword = filtered.Value.keyword,
|
Keyword = filtered.Value.keyword,
|
||||||
Hide = filtered.Value.filter.Action == Filter.FilterAction.Hide
|
Hide = filtered.Value.filter.Action == Filter.FilterAction.Hide
|
||||||
};
|
};
|
||||||
|
@ -77,8 +76,8 @@ public class NoteRenderer(
|
||||||
var attachments =
|
var attachments =
|
||||||
(data?.Attachments ?? await GetAttachmentsAsync([note])).Where(p => note.FileIds.Contains(p.Id));
|
(data?.Attachments ?? await GetAttachmentsAsync([note])).Where(p => note.FileIds.Contains(p.Id));
|
||||||
var reactions = (data?.Reactions ?? await GetReactionsAsync([note], user)).Where(p => p.NoteId == note.Id);
|
var reactions = (data?.Reactions ?? await GetReactionsAsync([note], user)).Where(p => p.NoteId == note.Id);
|
||||||
var liked = data?.LikedNotes?.Contains(note.Id)
|
var liked = data?.LikedNotes?.Contains(note.Id) ??
|
||||||
?? await db.NoteLikes.AnyAsync(p => p.Note == note && p.User == user);
|
await db.NoteLikes.AnyAsync(p => p.Note == note && p.User == user);
|
||||||
var emoji = data?.Emoji?.Where(p => note.Emojis.Contains(p.Id)).ToList() ?? await GetEmojiAsync([note]);
|
var emoji = data?.Emoji?.Where(p => note.Emojis.Contains(p.Id)).ToList() ?? await GetEmojiAsync([note]);
|
||||||
var poll = (data?.Polls ?? await GetPollsAsync([note], user)).FirstOrDefault(p => p.NoteId == note.Id);
|
var poll = (data?.Polls ?? await GetPollsAsync([note], user)).FirstOrDefault(p => p.NoteId == note.Id);
|
||||||
|
|
||||||
|
@ -91,7 +90,6 @@ public class NoteRenderer(
|
||||||
Text = note.Text,
|
Text = note.Text,
|
||||||
Cw = note.Cw,
|
Cw = note.Cw,
|
||||||
Visibility = (NoteVisibility)note.Visibility,
|
Visibility = (NoteVisibility)note.Visibility,
|
||||||
LocalOnly = note.LocalOnly,
|
|
||||||
User = noteUser,
|
User = noteUser,
|
||||||
Attachments = attachments.ToList(),
|
Attachments = attachments.ToList(),
|
||||||
Reactions = reactions.ToList(),
|
Reactions = reactions.ToList(),
|
||||||
|
@ -124,8 +122,7 @@ public class NoteRenderer(
|
||||||
ContentType = p.Type,
|
ContentType = p.Type,
|
||||||
Blurhash = p.Blurhash,
|
Blurhash = p.Blurhash,
|
||||||
AltText = p.Comment,
|
AltText = p.Comment,
|
||||||
IsSensitive = p.IsSensitive,
|
IsSensitive = p.IsSensitive
|
||||||
FileName = p.Name
|
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
@ -141,12 +138,10 @@ public class NoteRenderer(
|
||||||
.Select(p => new NoteReactionSchema
|
.Select(p => new NoteReactionSchema
|
||||||
{
|
{
|
||||||
NoteId = p.First().NoteId,
|
NoteId = p.First().NoteId,
|
||||||
Count =
|
Count = (int)counts[p.First().NoteId].GetValueOrDefault(p.First().Reaction, 1),
|
||||||
(int)counts[p.First().NoteId].GetValueOrDefault(p.First().Reaction, 1),
|
Reacted = db.NoteReactions.Any(i => i.NoteId == p.First().NoteId &&
|
||||||
Reacted =
|
i.Reaction == p.First().Reaction &&
|
||||||
db.NoteReactions.Any(i => i.NoteId == p.First().NoteId
|
i.User == user),
|
||||||
&& i.Reaction == p.First().Reaction
|
|
||||||
&& i.User == user),
|
|
||||||
Name = p.First().Reaction,
|
Name = p.First().Reaction,
|
||||||
Url = null,
|
Url = null,
|
||||||
Sensitive = false
|
Sensitive = false
|
||||||
|
@ -199,7 +194,7 @@ public class NoteRenderer(
|
||||||
Id = p.Id,
|
Id = p.Id,
|
||||||
Name = p.Name,
|
Name = p.Name,
|
||||||
Uri = p.Uri,
|
Uri = p.Uri,
|
||||||
Tags = p.Tags,
|
Aliases = p.Aliases,
|
||||||
Category = p.Category,
|
Category = p.Category,
|
||||||
PublicUrl = p.GetAccessUrl(config.Value),
|
PublicUrl = p.GetAccessUrl(config.Value),
|
||||||
License = p.License,
|
License = p.License,
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
using Iceshrimp.Backend.Core.Configuration;
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
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 Iceshrimp.EntityFrameworkCore.Extensions;
|
|
||||||
using Iceshrimp.Shared.Schemas.Web;
|
using Iceshrimp.Shared.Schemas.Web;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using static Iceshrimp.Shared.Schemas.Web.NotificationResponse;
|
using static Iceshrimp.Shared.Schemas.Web.NotificationResponse;
|
||||||
|
|
||||||
|
@ -15,7 +12,7 @@ public class NotificationRenderer(
|
||||||
IOptions<Config.InstanceSection> instance,
|
IOptions<Config.InstanceSection> instance,
|
||||||
UserRenderer userRenderer,
|
UserRenderer userRenderer,
|
||||||
NoteRenderer noteRenderer,
|
NoteRenderer noteRenderer,
|
||||||
DatabaseContext db
|
EmojiService emojiSvc
|
||||||
) : IScopedService
|
) : IScopedService
|
||||||
{
|
{
|
||||||
private static NotificationResponse Render(Notification notification, NotificationRendererDto data)
|
private static NotificationResponse Render(Notification notification, NotificationRendererDto data)
|
||||||
|
@ -117,44 +114,21 @@ public class NotificationRenderer(
|
||||||
Sensitive = false
|
Sensitive = false
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
var custom = reactions.Where(EmojiService.IsCustomEmoji).ToArray();
|
||||||
var custom = reactions.Where(EmojiService.IsCustomEmoji)
|
|
||||||
.Select(p =>
|
|
||||||
{
|
|
||||||
var parts = p.Trim(':').Split('@');
|
|
||||||
return (name: parts[0], host: parts.Length > 1 ? parts[1] : null);
|
|
||||||
})
|
|
||||||
.Distinct()
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
var expr = ExpressionExtensions.False<Emoji>();
|
|
||||||
expr = custom.Aggregate(expr, (current, part) => current.Or(p => p.Name == part.name && p.Host == part.host));
|
|
||||||
|
|
||||||
// https://github.com/dotnet/efcore/issues/31492
|
|
||||||
var emojiUrls = await db.Emojis
|
|
||||||
.Where(expr)
|
|
||||||
.Select(e => new
|
|
||||||
{
|
|
||||||
Name = $":{e.Name}{(e.Host != null ? "@" + e.Host : "")}:",
|
|
||||||
Url = e.GetAccessUrl(instance.Value),
|
|
||||||
e.Sensitive
|
|
||||||
})
|
|
||||||
.ToDictionaryAsync(e => e.Name, e => new { e.Url, e.Sensitive });
|
|
||||||
|
|
||||||
foreach (var s in custom)
|
foreach (var s in custom)
|
||||||
{
|
{
|
||||||
var name = s.host != null ? $":{s.name}@{s.host}:" : $":{s.name}:";
|
var emoji = await emojiSvc.ResolveEmojiAsync(s);
|
||||||
emojiUrls.TryGetValue(name, out var emoji);
|
|
||||||
var reaction = emoji != null
|
var reaction = emoji != null
|
||||||
? new ReactionResponse
|
? new ReactionResponse
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = s,
|
||||||
Url = emoji.Url,
|
Url = emoji.GetAccessUrl(instance.Value),
|
||||||
Sensitive = emoji.Sensitive
|
Sensitive = emoji.Sensitive
|
||||||
}
|
}
|
||||||
: new ReactionResponse
|
: new ReactionResponse
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = s,
|
||||||
Url = null,
|
Url = null,
|
||||||
Sensitive = false
|
Sensitive = false
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
|
||||||
using Iceshrimp.Shared.Schemas.Web;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Web.Renderers;
|
|
||||||
|
|
||||||
public class ReportRenderer(UserRenderer userRenderer, NoteRenderer noteRenderer) : IScopedService
|
|
||||||
{
|
|
||||||
private static ReportResponse Render(Report report, ReportRendererDto data)
|
|
||||||
{
|
|
||||||
var noteIds = report.Notes.Select(i => i.Id).ToArray();
|
|
||||||
return new ReportResponse
|
|
||||||
{
|
|
||||||
Id = report.Id,
|
|
||||||
CreatedAt = report.CreatedAt,
|
|
||||||
Comment = report.Comment,
|
|
||||||
Forwarded = report.Forwarded,
|
|
||||||
Resolved = report.Resolved,
|
|
||||||
Assignee = data.Users.FirstOrDefault(p => p.Id == report.AssigneeId),
|
|
||||||
TargetUser = data.Users.First(p => p.Id == report.TargetUserId),
|
|
||||||
Reporter = data.Users.First(p => p.Id == report.ReporterId),
|
|
||||||
Notes = data.Notes.Where(p => noteIds.Contains(p.Id)).ToArray()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ReportResponse> RenderOneAsync(Report report)
|
|
||||||
{
|
|
||||||
return Render(report, await BuildDtoAsync(report));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<ReportResponse>> RenderManyAsync(IEnumerable<Report> reports)
|
|
||||||
{
|
|
||||||
var arr = reports.ToArray();
|
|
||||||
var data = await BuildDtoAsync(arr);
|
|
||||||
return arr.Select(p => Render(p, data));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<UserResponse[]> GetUsersAsync(IEnumerable<User> users)
|
|
||||||
=> await userRenderer.RenderManyAsync(users).ToArrayAsync();
|
|
||||||
|
|
||||||
private async Task<NoteResponse[]> GetNotesAsync(IEnumerable<Note> notes)
|
|
||||||
=> await noteRenderer.RenderManyAsync(notes, null).ToArrayAsync();
|
|
||||||
|
|
||||||
private async Task<ReportRendererDto> BuildDtoAsync(params Report[] reports)
|
|
||||||
{
|
|
||||||
var notes = await GetNotesAsync(reports.SelectMany(p => p.Notes));
|
|
||||||
var users = notes.Select(p => p.User).DistinctBy(p => p.Id).ToList();
|
|
||||||
|
|
||||||
var missingUsers = reports.Select(p => p.TargetUser)
|
|
||||||
.Concat(reports.Select(p => p.Assignee))
|
|
||||||
.Concat(reports.Select(p => p.Reporter))
|
|
||||||
.NotNull()
|
|
||||||
.DistinctBy(p => p.Id)
|
|
||||||
.ExceptBy(users.Select(p => p.Id), p => p.Id);
|
|
||||||
|
|
||||||
users.AddRange(await GetUsersAsync(missingUsers));
|
|
||||||
|
|
||||||
return new ReportRendererDto { Users = users.ToArray(), Notes = notes };
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ReportRendererDto
|
|
||||||
{
|
|
||||||
public required UserResponse[] Users;
|
|
||||||
public required NoteResponse[] Notes;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -51,10 +51,7 @@ public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseConte
|
||||||
var bannerAlt = await GetBannerAltAsync([user]);
|
var bannerAlt = await GetBannerAltAsync([user]);
|
||||||
var data = new UserRendererDto
|
var data = new UserRendererDto
|
||||||
{
|
{
|
||||||
Emojis = emojis,
|
Emojis = emojis, InstanceData = instanceData, AvatarAlt = avatarAlt, BannerAlt = bannerAlt
|
||||||
InstanceData = instanceData,
|
|
||||||
AvatarAlt = avatarAlt,
|
|
||||||
BannerAlt = bannerAlt
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return Render(user, data);
|
return Render(user, data);
|
||||||
|
@ -110,7 +107,7 @@ public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseConte
|
||||||
Id = p.Id,
|
Id = p.Id,
|
||||||
Name = p.Name,
|
Name = p.Name,
|
||||||
Uri = p.Uri,
|
Uri = p.Uri,
|
||||||
Tags = p.Tags,
|
Aliases = p.Aliases,
|
||||||
Category = p.Category,
|
Category = p.Category,
|
||||||
PublicUrl = p.GetAccessUrl(config.Value),
|
PublicUrl = p.GetAccessUrl(config.Value),
|
||||||
License = p.License,
|
License = p.License,
|
||||||
|
|
|
@ -42,94 +42,4 @@ public class TimelineController(DatabaseContext db, NoteRenderer noteRenderer, C
|
||||||
return await noteRenderer.RenderManyAsync(notes.EnforceRenoteReplyVisibility(), user,
|
return await noteRenderer.RenderManyAsync(notes.EnforceRenoteReplyVisibility(), user,
|
||||||
Filter.FilterContext.Home);
|
Filter.FilterContext.Home);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("local")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
public async Task<IEnumerable<NoteResponse>> GetLocalTimeline(PaginationQuery pq)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
var notes = await db.Notes.IncludeCommonProperties()
|
|
||||||
.Where(p => p.UserHost == null)
|
|
||||||
.EnsureVisibleFor(user)
|
|
||||||
.FilterHidden(user, db)
|
|
||||||
.FilterMutedThreads(user, db)
|
|
||||||
.Paginate(pq, ControllerContext)
|
|
||||||
.PrecomputeVisibilities(user)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return await noteRenderer.RenderManyAsync(notes.EnforceRenoteReplyVisibility(), user,
|
|
||||||
Filter.FilterContext.Public);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("social")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
public async Task<IEnumerable<NoteResponse>> GetSocialTimeline(PaginationQuery pq)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
var heuristic = await QueryableTimelineExtensions.GetHeuristicAsync(user, db, cache);
|
|
||||||
var notes = await db.Notes.IncludeCommonProperties()
|
|
||||||
.FilterByFollowingOwnAndLocal(user, db, heuristic)
|
|
||||||
.EnsureVisibleFor(user)
|
|
||||||
.FilterHidden(user, db, filterHiddenListMembers: true)
|
|
||||||
.FilterMutedThreads(user, db)
|
|
||||||
.Paginate(pq, ControllerContext)
|
|
||||||
.PrecomputeVisibilities(user)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return await noteRenderer.RenderManyAsync(notes.EnforceRenoteReplyVisibility(), user,
|
|
||||||
Filter.FilterContext.Public);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("bubble")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
public async Task<IEnumerable<NoteResponse>> GetBubbleTimeline(PaginationQuery pq)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
var notes = await db.Notes.IncludeCommonProperties()
|
|
||||||
.Where(p => db.BubbleInstances.Any(i => i.Host == p.UserHost))
|
|
||||||
.EnsureVisibleFor(user)
|
|
||||||
.FilterHidden(user, db, filterHiddenListMembers: true)
|
|
||||||
.FilterMutedThreads(user, db)
|
|
||||||
.Paginate(pq, ControllerContext)
|
|
||||||
.PrecomputeVisibilities(user)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return await noteRenderer.RenderManyAsync(notes.EnforceRenoteReplyVisibility(), user,
|
|
||||||
Filter.FilterContext.Public);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("global")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
public async Task<IEnumerable<NoteResponse>> GetGlobalTimeline(PaginationQuery pq)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
var notes = await db.Notes.IncludeCommonProperties()
|
|
||||||
.EnsureVisibleFor(user)
|
|
||||||
.FilterHidden(user, db)
|
|
||||||
.FilterMutedThreads(user, db)
|
|
||||||
.Paginate(pq, ControllerContext)
|
|
||||||
.PrecomputeVisibilities(user)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return await noteRenderer.RenderManyAsync(notes.EnforceRenoteReplyVisibility(), user,
|
|
||||||
Filter.FilterContext.Public);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("remote/{instance}")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
public async Task<IEnumerable<NoteResponse>> GetRemoteTimeline(string instance, PaginationQuery pq)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
var notes = await db.Notes.IncludeCommonProperties()
|
|
||||||
.Where(p => p.UserHost == instance)
|
|
||||||
.EnsureVisibleFor(user)
|
|
||||||
.FilterHidden(user, db)
|
|
||||||
.FilterMutedThreads(user, db)
|
|
||||||
.Paginate(pq, ControllerContext)
|
|
||||||
.PrecomputeVisibilities(user)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return await noteRenderer.RenderManyAsync(notes.EnforceRenoteReplyVisibility(), user,
|
|
||||||
Filter.FilterContext.Public);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -117,48 +117,6 @@ public class UserController(
|
||||||
await biteSvc.BiteAsync(user, target);
|
await biteSvc.BiteAsync(user, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/block")]
|
|
||||||
[Authenticate]
|
|
||||||
[Authorize]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
|
|
||||||
public async Task BlockUser(string id)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
if (user.Id == id)
|
|
||||||
throw GracefulException.BadRequest("You cannot block yourself");
|
|
||||||
|
|
||||||
var blockee = await db.Users
|
|
||||||
.Where(p => p.Id == id)
|
|
||||||
.IncludeCommonProperties()
|
|
||||||
.PrecomputeRelationshipData(user)
|
|
||||||
.FirstOrDefaultAsync()
|
|
||||||
?? throw GracefulException.RecordNotFound();
|
|
||||||
|
|
||||||
await userSvc.BlockUserAsync(user, blockee);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/unblock")]
|
|
||||||
[Authenticate]
|
|
||||||
[Authorize]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
|
|
||||||
public async Task UnblockUser(string id)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
if (user.Id == id)
|
|
||||||
throw GracefulException.BadRequest("You cannot unblock yourself");
|
|
||||||
|
|
||||||
var blockee = await db.Users
|
|
||||||
.Where(p => p.Id == id)
|
|
||||||
.IncludeCommonProperties()
|
|
||||||
.PrecomputeRelationshipData(user)
|
|
||||||
.FirstOrDefaultAsync()
|
|
||||||
?? throw GracefulException.RecordNotFound();
|
|
||||||
|
|
||||||
await userSvc.UnblockUserAsync(user, blockee);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/follow")]
|
[HttpPost("{id}/follow")]
|
||||||
[Authenticate]
|
[Authenticate]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
@ -183,51 +141,6 @@ public class UserController(
|
||||||
await userSvc.FollowUserAsync(user, followee);
|
await userSvc.FollowUserAsync(user, followee);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/mute")]
|
|
||||||
[Authenticate]
|
|
||||||
[Authorize]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
|
|
||||||
public async Task MuteUser(string id, [FromQuery] DateTime? expires)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
if (user.Id == id)
|
|
||||||
throw GracefulException.BadRequest("You cannot mute yourself");
|
|
||||||
|
|
||||||
if (expires?.ToUniversalTime() <= DateTime.UtcNow.AddMinutes(1))
|
|
||||||
throw GracefulException.BadRequest("Mute expiration must be in the future");
|
|
||||||
|
|
||||||
var mutee = await db.Users
|
|
||||||
.Where(p => p.Id == id)
|
|
||||||
.IncludeCommonProperties()
|
|
||||||
.PrecomputeRelationshipData(user)
|
|
||||||
.FirstOrDefaultAsync()
|
|
||||||
?? throw GracefulException.RecordNotFound();
|
|
||||||
|
|
||||||
await userSvc.MuteUserAsync(user, mutee, expires?.ToUniversalTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/unmute")]
|
|
||||||
[Authenticate]
|
|
||||||
[Authorize]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
|
|
||||||
public async Task UnmuteUser(string id)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
if (user.Id == id)
|
|
||||||
throw GracefulException.BadRequest("You cannot unmute yourself");
|
|
||||||
|
|
||||||
var mutee = await db.Users
|
|
||||||
.Where(p => p.Id == id)
|
|
||||||
.IncludeCommonProperties()
|
|
||||||
.PrecomputeRelationshipData(user)
|
|
||||||
.FirstOrDefaultAsync()
|
|
||||||
?? throw GracefulException.RecordNotFound();
|
|
||||||
|
|
||||||
await userSvc.UnmuteUserAsync(user, mutee);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/refetch")]
|
[HttpPost("{id}/refetch")]
|
||||||
[Authenticate]
|
[Authenticate]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
|
|
@ -55,7 +55,6 @@ public sealed class Config
|
||||||
public sealed class SecuritySection
|
public sealed class SecuritySection
|
||||||
{
|
{
|
||||||
public bool AuthorizedFetch { get; init; } = true;
|
public bool AuthorizedFetch { get; init; } = true;
|
||||||
public bool ValidateRequestSignatures { get; init; } = true;
|
|
||||||
public bool AttachLdSignatures { get; init; } = false;
|
public bool AttachLdSignatures { get; init; } = false;
|
||||||
public bool AcceptLdSignatures { get; init; } = false;
|
public bool AcceptLdSignatures { get; init; } = false;
|
||||||
public bool AllowLoopback { get; init; } = false;
|
public bool AllowLoopback { get; init; } = false;
|
||||||
|
|
|
@ -16,7 +16,7 @@ namespace Iceshrimp.Backend.Core.Database;
|
||||||
public class DatabaseContext(DbContextOptions<DatabaseContext> options)
|
public class DatabaseContext(DbContextOptions<DatabaseContext> options)
|
||||||
: DbContext(options), IDataProtectionKeyContext
|
: DbContext(options), IDataProtectionKeyContext
|
||||||
{
|
{
|
||||||
public virtual DbSet<Report> Reports { get; init; } = null!;
|
public virtual DbSet<AbuseUserReport> AbuseUserReports { get; init; } = null!;
|
||||||
public virtual DbSet<Announcement> Announcements { get; init; } = null!;
|
public virtual DbSet<Announcement> Announcements { get; init; } = null!;
|
||||||
public virtual DbSet<AnnouncementRead> AnnouncementReads { get; init; } = null!;
|
public virtual DbSet<AnnouncementRead> AnnouncementReads { get; init; } = null!;
|
||||||
public virtual DbSet<Antenna> Antennas { get; init; } = null!;
|
public virtual DbSet<Antenna> Antennas { get; init; } = null!;
|
||||||
|
@ -92,7 +92,6 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options)
|
||||||
public virtual DbSet<Filter> Filters { get; init; } = null!;
|
public virtual DbSet<Filter> Filters { get; init; } = null!;
|
||||||
public virtual DbSet<PluginStoreEntry> PluginStore { get; init; } = null!;
|
public virtual DbSet<PluginStoreEntry> PluginStore { get; init; } = null!;
|
||||||
public virtual DbSet<PolicyConfiguration> PolicyConfiguration { get; init; } = null!;
|
public virtual DbSet<PolicyConfiguration> PolicyConfiguration { get; init; } = null!;
|
||||||
public virtual DbSet<BubbleInstance> BubbleInstances { get; init; } = null!;
|
|
||||||
public virtual DbSet<DataProtectionKey> DataProtectionKeys { get; init; } = null!;
|
public virtual DbSet<DataProtectionKey> DataProtectionKeys { get; init; } = null!;
|
||||||
|
|
||||||
public static NpgsqlDataSource GetDataSource(Config.DatabaseSection config)
|
public static NpgsqlDataSource GetDataSource(Config.DatabaseSection config)
|
||||||
|
@ -108,8 +107,7 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options)
|
||||||
Database = config.Database,
|
Database = config.Database,
|
||||||
MaxPoolSize = config.MaxConnections,
|
MaxPoolSize = config.MaxConnections,
|
||||||
Multiplexing = config.Multiplexing,
|
Multiplexing = config.Multiplexing,
|
||||||
Options = "-c jit=off",
|
Options = "-c jit=off"
|
||||||
ApplicationName = "Iceshrimp.NET"
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "9.0.2")
|
.HasAnnotation("ProductVersion", "9.0.1")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "antenna_src_enum", new[] { "home", "all", "users", "list", "group", "instances" });
|
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "antenna_src_enum", new[] { "home", "all", "users", "list", "group", "instances" });
|
||||||
|
@ -36,6 +36,84 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "pg_trgm");
|
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "pg_trgm");
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AbuseUserReport", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("AssigneeId")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("assigneeId");
|
||||||
|
|
||||||
|
b.Property<string>("Comment")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)")
|
||||||
|
.HasColumnName("comment");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("createdAt")
|
||||||
|
.HasComment("The created date of the AbuseUserReport.");
|
||||||
|
|
||||||
|
b.Property<bool>("Forwarded")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("forwarded");
|
||||||
|
|
||||||
|
b.Property<string>("ReporterHost")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)")
|
||||||
|
.HasColumnName("reporterHost")
|
||||||
|
.HasComment("[Denormalized]");
|
||||||
|
|
||||||
|
b.Property<string>("ReporterId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("reporterId");
|
||||||
|
|
||||||
|
b.Property<bool>("Resolved")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("resolved");
|
||||||
|
|
||||||
|
b.Property<string>("TargetUserHost")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)")
|
||||||
|
.HasColumnName("targetUserHost")
|
||||||
|
.HasComment("[Denormalized]");
|
||||||
|
|
||||||
|
b.Property<string>("TargetUserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("targetUserId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AssigneeId");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("ReporterHost");
|
||||||
|
|
||||||
|
b.HasIndex("ReporterId");
|
||||||
|
|
||||||
|
b.HasIndex("Resolved");
|
||||||
|
|
||||||
|
b.HasIndex("TargetUserHost");
|
||||||
|
|
||||||
|
b.HasIndex("TargetUserId");
|
||||||
|
|
||||||
|
b.ToTable("abuse_user_report");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AllowedInstance", b =>
|
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AllowedInstance", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Host")
|
b.Property<string>("Host")
|
||||||
|
@ -409,18 +487,6 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
b.ToTable("blocking");
|
b.ToTable("blocking");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.BubbleInstance", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Host")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)")
|
|
||||||
.HasColumnName("host");
|
|
||||||
|
|
||||||
b.HasKey("Host");
|
|
||||||
|
|
||||||
b.ToTable("bubble_instance");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.CacheEntry", b =>
|
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.CacheEntry", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Key")
|
b.Property<string>("Key")
|
||||||
|
@ -912,6 +978,13 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
.HasColumnType("character varying(32)")
|
.HasColumnType("character varying(32)")
|
||||||
.HasColumnName("id");
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<List<string>>("Aliases")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("character varying(128)[]")
|
||||||
|
.HasColumnName("aliases")
|
||||||
|
.HasDefaultValueSql("'{}'::character varying[]");
|
||||||
|
|
||||||
b.Property<string>("Category")
|
b.Property<string>("Category")
|
||||||
.HasMaxLength(128)
|
.HasMaxLength(128)
|
||||||
.HasColumnType("character varying(128)")
|
.HasColumnType("character varying(128)")
|
||||||
|
@ -956,13 +1029,6 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
.HasColumnType("boolean")
|
.HasColumnType("boolean")
|
||||||
.HasColumnName("sensitive");
|
.HasColumnName("sensitive");
|
||||||
|
|
||||||
b.PrimitiveCollection<List<string>>("Tags")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("character varying(128)[]")
|
|
||||||
.HasColumnName("tags")
|
|
||||||
.HasDefaultValueSql("'{}'::character varying[]");
|
|
||||||
|
|
||||||
b.Property<string>("Type")
|
b.Property<string>("Type")
|
||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
.HasColumnType("character varying(64)")
|
.HasColumnType("character varying(64)")
|
||||||
|
@ -993,16 +1059,6 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
|
|
||||||
NpgsqlIndexBuilderExtensions.AreNullsDistinct(b.HasIndex("Name", "Host"), false);
|
NpgsqlIndexBuilderExtensions.AreNullsDistinct(b.HasIndex("Name", "Host"), false);
|
||||||
|
|
||||||
b.HasIndex(new[] { "Host" }, "GIN_TRGM_emoji_host");
|
|
||||||
|
|
||||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex(new[] { "Host" }, "GIN_TRGM_emoji_host"), "gin");
|
|
||||||
NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex(new[] { "Host" }, "GIN_TRGM_emoji_host"), new[] { "gin_trgm_ops" });
|
|
||||||
|
|
||||||
b.HasIndex(new[] { "Name" }, "GIN_TRGM_emoji_name");
|
|
||||||
|
|
||||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex(new[] { "Name" }, "GIN_TRGM_emoji_name"), "gin");
|
|
||||||
NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex(new[] { "Name" }, "GIN_TRGM_emoji_name"), new[] { "gin_trgm_ops" });
|
|
||||||
|
|
||||||
b.ToTable("emoji");
|
b.ToTable("emoji");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3814,84 +3870,6 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
b.ToTable("renote_muting");
|
b.ToTable("renote_muting");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Report", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("character varying(32)")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b.Property<string>("AssigneeId")
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("character varying(32)")
|
|
||||||
.HasColumnName("assigneeId");
|
|
||||||
|
|
||||||
b.Property<string>("Comment")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)")
|
|
||||||
.HasColumnName("comment");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone")
|
|
||||||
.HasColumnName("createdAt")
|
|
||||||
.HasComment("The created date of the Report.");
|
|
||||||
|
|
||||||
b.Property<bool>("Forwarded")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("boolean")
|
|
||||||
.HasDefaultValue(false)
|
|
||||||
.HasColumnName("forwarded");
|
|
||||||
|
|
||||||
b.Property<string>("ReporterHost")
|
|
||||||
.HasMaxLength(512)
|
|
||||||
.HasColumnType("character varying(512)")
|
|
||||||
.HasColumnName("reporterHost")
|
|
||||||
.HasComment("[Denormalized]");
|
|
||||||
|
|
||||||
b.Property<string>("ReporterId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("character varying(32)")
|
|
||||||
.HasColumnName("reporterId");
|
|
||||||
|
|
||||||
b.Property<bool>("Resolved")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("boolean")
|
|
||||||
.HasDefaultValue(false)
|
|
||||||
.HasColumnName("resolved");
|
|
||||||
|
|
||||||
b.Property<string>("TargetUserHost")
|
|
||||||
.HasMaxLength(512)
|
|
||||||
.HasColumnType("character varying(512)")
|
|
||||||
.HasColumnName("targetUserHost")
|
|
||||||
.HasComment("[Denormalized]");
|
|
||||||
|
|
||||||
b.Property<string>("TargetUserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("character varying(32)")
|
|
||||||
.HasColumnName("targetUserId");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("AssigneeId");
|
|
||||||
|
|
||||||
b.HasIndex("CreatedAt");
|
|
||||||
|
|
||||||
b.HasIndex("ReporterHost");
|
|
||||||
|
|
||||||
b.HasIndex("ReporterId");
|
|
||||||
|
|
||||||
b.HasIndex("Resolved");
|
|
||||||
|
|
||||||
b.HasIndex("TargetUserHost");
|
|
||||||
|
|
||||||
b.HasIndex("TargetUserId");
|
|
||||||
|
|
||||||
b.ToTable("report");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Rule", b =>
|
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Rule", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
|
@ -4202,10 +4180,6 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("lastFetchedAt");
|
.HasColumnName("lastFetchedAt");
|
||||||
|
|
||||||
b.Property<DateTime?>("LastNoteAt")
|
|
||||||
.HasColumnType("timestamp with time zone")
|
|
||||||
.HasColumnName("lastNoteAt");
|
|
||||||
|
|
||||||
b.Property<string>("MovedToUri")
|
b.Property<string>("MovedToUri")
|
||||||
.HasMaxLength(512)
|
.HasMaxLength(512)
|
||||||
.HasColumnType("character varying(512)")
|
.HasColumnType("character varying(512)")
|
||||||
|
@ -4944,19 +4918,30 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
b.ToTable("data_protection_keys", (string)null);
|
b.ToTable("data_protection_keys", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("reported_note", b =>
|
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AbuseUserReport", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("note_id")
|
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Assignee")
|
||||||
.HasColumnType("character varying(32)");
|
.WithMany("AbuseUserReportAssignees")
|
||||||
|
.HasForeignKey("AssigneeId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
b.Property<string>("report_id")
|
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Reporter")
|
||||||
.HasColumnType("character varying(32)");
|
.WithMany("AbuseUserReportReporters")
|
||||||
|
.HasForeignKey("ReporterId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
b.HasKey("note_id", "report_id");
|
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "TargetUser")
|
||||||
|
.WithMany("AbuseUserReportTargetUsers")
|
||||||
|
.HasForeignKey("TargetUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
b.HasIndex("report_id");
|
b.Navigation("Assignee");
|
||||||
|
|
||||||
b.ToTable("reported_note");
|
b.Navigation("Reporter");
|
||||||
|
|
||||||
|
b.Navigation("TargetUser");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AnnouncementRead", b =>
|
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AnnouncementRead", b =>
|
||||||
|
@ -5731,32 +5716,6 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
b.Navigation("Muter");
|
b.Navigation("Muter");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Report", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Assignee")
|
|
||||||
.WithMany("AbuseUserReportAssignees")
|
|
||||||
.HasForeignKey("AssigneeId")
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
|
|
||||||
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Reporter")
|
|
||||||
.WithMany("AbuseUserReportReporters")
|
|
||||||
.HasForeignKey("ReporterId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "TargetUser")
|
|
||||||
.WithMany("AbuseUserReportTargetUsers")
|
|
||||||
.HasForeignKey("TargetUserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Assignee");
|
|
||||||
|
|
||||||
b.Navigation("Reporter");
|
|
||||||
|
|
||||||
b.Navigation("TargetUser");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Session", b =>
|
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Session", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User")
|
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User")
|
||||||
|
@ -5967,21 +5926,6 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
b.Navigation("User");
|
b.Navigation("User");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("reported_note", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Note", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("note_id")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Report", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("report_id")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Announcement", b =>
|
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Announcement", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("AnnouncementReads");
|
b.Navigation("AnnouncementReads");
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Database.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
[DbContext(typeof(DatabaseContext))]
|
|
||||||
[Migration("20250304222123_RenameEmojiTagsColumn")]
|
|
||||||
public partial class RenameEmojiTagsColumn : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.RenameColumn(
|
|
||||||
name: "aliases",
|
|
||||||
table: "emoji",
|
|
||||||
newName: "tags");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.RenameColumn(
|
|
||||||
name: "tags",
|
|
||||||
table: "emoji",
|
|
||||||
newName: "aliases");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Database.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
[DbContext(typeof(DatabaseContext))]
|
|
||||||
[Migration("20250223092435_AddUserLastNoteAt")]
|
|
||||||
public partial class AddUserLastNoteAt : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddColumn<DateTime>(
|
|
||||||
name: "lastNoteAt",
|
|
||||||
table: "user",
|
|
||||||
type: "timestamp with time zone",
|
|
||||||
nullable: true);
|
|
||||||
|
|
||||||
migrationBuilder.Sql("""UPDATE "user" SET "lastNoteAt" = (SELECT note."createdAt" FROM "note" WHERE "note"."userId" = "user"."id" ORDER BY "note"."id" DESC LIMIT 1);""");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "lastNoteAt",
|
|
||||||
table: "user");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Database.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
[DbContext(typeof(DatabaseContext))]
|
|
||||||
[Migration("20250307145204_EmojiNameHostIndex")]
|
|
||||||
public partial class EmojiNameHostIndex : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "GIN_TRGM_emoji_host",
|
|
||||||
table: "emoji",
|
|
||||||
column: "host")
|
|
||||||
.Annotation("Npgsql:IndexMethod", "gin")
|
|
||||||
.Annotation("Npgsql:IndexOperators", new[] { "gin_trgm_ops" });
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "GIN_TRGM_emoji_name",
|
|
||||||
table: "emoji",
|
|
||||||
column: "name")
|
|
||||||
.Annotation("Npgsql:IndexMethod", "gin")
|
|
||||||
.Annotation("Npgsql:IndexOperators", new[] { "gin_trgm_ops" });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropIndex(
|
|
||||||
name: "GIN_TRGM_emoji_host",
|
|
||||||
table: "emoji");
|
|
||||||
|
|
||||||
migrationBuilder.DropIndex(
|
|
||||||
name: "GIN_TRGM_emoji_name",
|
|
||||||
table: "emoji");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,242 +0,0 @@
|
||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Database.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
[DbContext(typeof(DatabaseContext))]
|
|
||||||
[Migration("20250310231413_RefactorReportsSchema")]
|
|
||||||
public partial class RefactorReportsSchema : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropForeignKey(
|
|
||||||
name: "FK_abuse_user_report_user_assigneeId",
|
|
||||||
table: "abuse_user_report");
|
|
||||||
|
|
||||||
migrationBuilder.DropForeignKey(
|
|
||||||
name: "FK_abuse_user_report_user_reporterId",
|
|
||||||
table: "abuse_user_report");
|
|
||||||
|
|
||||||
migrationBuilder.DropForeignKey(
|
|
||||||
name: "FK_abuse_user_report_user_targetUserId",
|
|
||||||
table: "abuse_user_report");
|
|
||||||
|
|
||||||
migrationBuilder.DropPrimaryKey(
|
|
||||||
name: "PK_abuse_user_report",
|
|
||||||
table: "abuse_user_report");
|
|
||||||
|
|
||||||
migrationBuilder.RenameTable(
|
|
||||||
name: "abuse_user_report",
|
|
||||||
newName: "report");
|
|
||||||
|
|
||||||
migrationBuilder.RenameIndex(
|
|
||||||
name: "IX_abuse_user_report_targetUserId",
|
|
||||||
table: "report",
|
|
||||||
newName: "IX_report_targetUserId");
|
|
||||||
|
|
||||||
migrationBuilder.RenameIndex(
|
|
||||||
name: "IX_abuse_user_report_targetUserHost",
|
|
||||||
table: "report",
|
|
||||||
newName: "IX_report_targetUserHost");
|
|
||||||
|
|
||||||
migrationBuilder.RenameIndex(
|
|
||||||
name: "IX_abuse_user_report_resolved",
|
|
||||||
table: "report",
|
|
||||||
newName: "IX_report_resolved");
|
|
||||||
|
|
||||||
migrationBuilder.RenameIndex(
|
|
||||||
name: "IX_abuse_user_report_reporterId",
|
|
||||||
table: "report",
|
|
||||||
newName: "IX_report_reporterId");
|
|
||||||
|
|
||||||
migrationBuilder.RenameIndex(
|
|
||||||
name: "IX_abuse_user_report_reporterHost",
|
|
||||||
table: "report",
|
|
||||||
newName: "IX_report_reporterHost");
|
|
||||||
|
|
||||||
migrationBuilder.RenameIndex(
|
|
||||||
name: "IX_abuse_user_report_createdAt",
|
|
||||||
table: "report",
|
|
||||||
newName: "IX_report_createdAt");
|
|
||||||
|
|
||||||
migrationBuilder.RenameIndex(
|
|
||||||
name: "IX_abuse_user_report_assigneeId",
|
|
||||||
table: "report",
|
|
||||||
newName: "IX_report_assigneeId");
|
|
||||||
|
|
||||||
migrationBuilder.AlterColumn<DateTime>(
|
|
||||||
name: "createdAt",
|
|
||||||
table: "report",
|
|
||||||
type: "timestamp with time zone",
|
|
||||||
nullable: false,
|
|
||||||
comment: "The created date of the Report.",
|
|
||||||
oldClrType: typeof(DateTime),
|
|
||||||
oldType: "timestamp with time zone",
|
|
||||||
oldComment: "The created date of the AbuseUserReport.");
|
|
||||||
|
|
||||||
migrationBuilder.AddPrimaryKey(
|
|
||||||
name: "PK_report",
|
|
||||||
table: "report",
|
|
||||||
column: "id");
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "reported_note",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
note_id = table.Column<string>(type: "character varying(32)", nullable: false),
|
|
||||||
report_id = table.Column<string>(type: "character varying(32)", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_reported_note", x => new { x.note_id, x.report_id });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_reported_note_note_note_id",
|
|
||||||
column: x => x.note_id,
|
|
||||||
principalTable: "note",
|
|
||||||
principalColumn: "id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_reported_note_report_report_id",
|
|
||||||
column: x => x.report_id,
|
|
||||||
principalTable: "report",
|
|
||||||
principalColumn: "id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_reported_note_report_id",
|
|
||||||
table: "reported_note",
|
|
||||||
column: "report_id");
|
|
||||||
|
|
||||||
migrationBuilder.AddForeignKey(
|
|
||||||
name: "FK_report_user_assigneeId",
|
|
||||||
table: "report",
|
|
||||||
column: "assigneeId",
|
|
||||||
principalTable: "user",
|
|
||||||
principalColumn: "id",
|
|
||||||
onDelete: ReferentialAction.SetNull);
|
|
||||||
|
|
||||||
migrationBuilder.AddForeignKey(
|
|
||||||
name: "FK_report_user_reporterId",
|
|
||||||
table: "report",
|
|
||||||
column: "reporterId",
|
|
||||||
principalTable: "user",
|
|
||||||
principalColumn: "id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
|
|
||||||
migrationBuilder.AddForeignKey(
|
|
||||||
name: "FK_report_user_targetUserId",
|
|
||||||
table: "report",
|
|
||||||
column: "targetUserId",
|
|
||||||
principalTable: "user",
|
|
||||||
principalColumn: "id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropForeignKey(
|
|
||||||
name: "FK_report_user_assigneeId",
|
|
||||||
table: "report");
|
|
||||||
|
|
||||||
migrationBuilder.DropForeignKey(
|
|
||||||
name: "FK_report_user_reporterId",
|
|
||||||
table: "report");
|
|
||||||
|
|
||||||
migrationBuilder.DropForeignKey(
|
|
||||||
name: "FK_report_user_targetUserId",
|
|
||||||
table: "report");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "reported_note");
|
|
||||||
|
|
||||||
migrationBuilder.DropPrimaryKey(
|
|
||||||
name: "PK_report",
|
|
||||||
table: "report");
|
|
||||||
|
|
||||||
migrationBuilder.RenameTable(
|
|
||||||
name: "report",
|
|
||||||
newName: "abuse_user_report");
|
|
||||||
|
|
||||||
migrationBuilder.RenameIndex(
|
|
||||||
name: "IX_report_targetUserId",
|
|
||||||
table: "abuse_user_report",
|
|
||||||
newName: "IX_abuse_user_report_targetUserId");
|
|
||||||
|
|
||||||
migrationBuilder.RenameIndex(
|
|
||||||
name: "IX_report_targetUserHost",
|
|
||||||
table: "abuse_user_report",
|
|
||||||
newName: "IX_abuse_user_report_targetUserHost");
|
|
||||||
|
|
||||||
migrationBuilder.RenameIndex(
|
|
||||||
name: "IX_report_resolved",
|
|
||||||
table: "abuse_user_report",
|
|
||||||
newName: "IX_abuse_user_report_resolved");
|
|
||||||
|
|
||||||
migrationBuilder.RenameIndex(
|
|
||||||
name: "IX_report_reporterId",
|
|
||||||
table: "abuse_user_report",
|
|
||||||
newName: "IX_abuse_user_report_reporterId");
|
|
||||||
|
|
||||||
migrationBuilder.RenameIndex(
|
|
||||||
name: "IX_report_reporterHost",
|
|
||||||
table: "abuse_user_report",
|
|
||||||
newName: "IX_abuse_user_report_reporterHost");
|
|
||||||
|
|
||||||
migrationBuilder.RenameIndex(
|
|
||||||
name: "IX_report_createdAt",
|
|
||||||
table: "abuse_user_report",
|
|
||||||
newName: "IX_abuse_user_report_createdAt");
|
|
||||||
|
|
||||||
migrationBuilder.RenameIndex(
|
|
||||||
name: "IX_report_assigneeId",
|
|
||||||
table: "abuse_user_report",
|
|
||||||
newName: "IX_abuse_user_report_assigneeId");
|
|
||||||
|
|
||||||
migrationBuilder.AlterColumn<DateTime>(
|
|
||||||
name: "createdAt",
|
|
||||||
table: "abuse_user_report",
|
|
||||||
type: "timestamp with time zone",
|
|
||||||
nullable: false,
|
|
||||||
comment: "The created date of the AbuseUserReport.",
|
|
||||||
oldClrType: typeof(DateTime),
|
|
||||||
oldType: "timestamp with time zone",
|
|
||||||
oldComment: "The created date of the Report.");
|
|
||||||
|
|
||||||
migrationBuilder.AddPrimaryKey(
|
|
||||||
name: "PK_abuse_user_report",
|
|
||||||
table: "abuse_user_report",
|
|
||||||
column: "id");
|
|
||||||
|
|
||||||
migrationBuilder.AddForeignKey(
|
|
||||||
name: "FK_abuse_user_report_user_assigneeId",
|
|
||||||
table: "abuse_user_report",
|
|
||||||
column: "assigneeId",
|
|
||||||
principalTable: "user",
|
|
||||||
principalColumn: "id",
|
|
||||||
onDelete: ReferentialAction.SetNull);
|
|
||||||
|
|
||||||
migrationBuilder.AddForeignKey(
|
|
||||||
name: "FK_abuse_user_report_user_reporterId",
|
|
||||||
table: "abuse_user_report",
|
|
||||||
column: "reporterId",
|
|
||||||
principalTable: "user",
|
|
||||||
principalColumn: "id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
|
|
||||||
migrationBuilder.AddForeignKey(
|
|
||||||
name: "FK_abuse_user_report_user_targetUserId",
|
|
||||||
table: "abuse_user_report",
|
|
||||||
column: "targetUserId",
|
|
||||||
principalTable: "user",
|
|
||||||
principalColumn: "id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Database.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
[DbContext(typeof(DatabaseContext))]
|
|
||||||
[Migration("20250322222922_AddRecommendedInstanceTable")]
|
|
||||||
public partial class AddRecommendedInstanceTable : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "recommended_instance",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
host = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_recommended_instance", x => x.host);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "recommended_instance");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Database.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
[DbContext(typeof(DatabaseContext))]
|
|
||||||
[Migration("20250323112015_RenameBubbleInstanceTable")]
|
|
||||||
public partial class RenameBubbleInstanceTable : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropPrimaryKey(
|
|
||||||
name: "PK_recommended_instance",
|
|
||||||
table: "recommended_instance");
|
|
||||||
|
|
||||||
migrationBuilder.RenameTable(
|
|
||||||
name: "recommended_instance",
|
|
||||||
newName: "bubble_instance");
|
|
||||||
|
|
||||||
migrationBuilder.AddPrimaryKey(
|
|
||||||
name: "PK_bubble_instance",
|
|
||||||
table: "bubble_instance",
|
|
||||||
column: "host");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropPrimaryKey(
|
|
||||||
name: "PK_bubble_instance",
|
|
||||||
table: "bubble_instance");
|
|
||||||
|
|
||||||
migrationBuilder.RenameTable(
|
|
||||||
name: "bubble_instance",
|
|
||||||
newName: "recommended_instance");
|
|
||||||
|
|
||||||
migrationBuilder.AddPrimaryKey(
|
|
||||||
name: "PK_recommended_instance",
|
|
||||||
table: "recommended_instance",
|
|
||||||
column: "host");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +1,18 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using Iceshrimp.EntityFrameworkCore.Extensions;
|
|
||||||
using Iceshrimp.Shared.Helpers;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Database.Tables;
|
namespace Iceshrimp.Backend.Core.Database.Tables;
|
||||||
|
|
||||||
[Table("report")]
|
[Table("abuse_user_report")]
|
||||||
[Index(nameof(ReporterId))]
|
[Index(nameof(ReporterId))]
|
||||||
[Index(nameof(Resolved))]
|
[Index(nameof(Resolved))]
|
||||||
[Index(nameof(TargetUserHost))]
|
[Index(nameof(TargetUserHost))]
|
||||||
[Index(nameof(TargetUserId))]
|
[Index(nameof(TargetUserId))]
|
||||||
[Index(nameof(CreatedAt))]
|
[Index(nameof(CreatedAt))]
|
||||||
[Index(nameof(ReporterHost))]
|
[Index(nameof(ReporterHost))]
|
||||||
public class Report : IIdentifiable
|
public class AbuseUserReport
|
||||||
{
|
{
|
||||||
[Key]
|
[Key]
|
||||||
[Column("id")]
|
[Column("id")]
|
||||||
|
@ -22,7 +20,7 @@ public class Report : IIdentifiable
|
||||||
public string Id { get; set; } = null!;
|
public string Id { get; set; } = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The created date of the Report.
|
/// The created date of the AbuseUserReport.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Column("createdAt")]
|
[Column("createdAt")]
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
|
@ -73,13 +71,11 @@ public class Report : IIdentifiable
|
||||||
[InverseProperty(nameof(User.AbuseUserReportTargetUsers))]
|
[InverseProperty(nameof(User.AbuseUserReportTargetUsers))]
|
||||||
public virtual User TargetUser { get; set; } = null!;
|
public virtual User TargetUser { get; set; } = null!;
|
||||||
|
|
||||||
public virtual ICollection<Note> Notes { get; set; } = new List<Note>();
|
private class EntityTypeConfiguration : IEntityTypeConfiguration<AbuseUserReport>
|
||||||
|
|
||||||
private class EntityTypeConfiguration : IEntityTypeConfiguration<Report>
|
|
||||||
{
|
{
|
||||||
public void Configure(EntityTypeBuilder<Report> entity)
|
public void Configure(EntityTypeBuilder<AbuseUserReport> entity)
|
||||||
{
|
{
|
||||||
entity.Property(e => e.CreatedAt).HasComment("The created date of the Report.");
|
entity.Property(e => e.CreatedAt).HasComment("The created date of the AbuseUserReport.");
|
||||||
entity.Property(e => e.Forwarded).HasDefaultValue(false);
|
entity.Property(e => e.Forwarded).HasDefaultValue(false);
|
||||||
entity.Property(e => e.ReporterHost).HasComment("[Denormalized]");
|
entity.Property(e => e.ReporterHost).HasComment("[Denormalized]");
|
||||||
entity.Property(e => e.Resolved).HasDefaultValue(false);
|
entity.Property(e => e.Resolved).HasDefaultValue(false);
|
||||||
|
@ -96,10 +92,6 @@ public class Report : IIdentifiable
|
||||||
entity.HasOne(d => d.TargetUser)
|
entity.HasOne(d => d.TargetUser)
|
||||||
.WithMany(p => p.AbuseUserReportTargetUsers)
|
.WithMany(p => p.AbuseUserReportTargetUsers)
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
entity.HasMany(p => p.Notes)
|
|
||||||
.WithMany()
|
|
||||||
.UsingEntity("reported_note", "report_id", "note_id", DeleteBehavior.Cascade);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -30,8 +30,8 @@ public class Emoji
|
||||||
|
|
||||||
[Column("type")] [StringLength(64)] public string? Type { get; set; }
|
[Column("type")] [StringLength(64)] public string? Type { get; set; }
|
||||||
|
|
||||||
[Column("tags", TypeName = "character varying(128)[]")]
|
[Column("aliases", TypeName = "character varying(128)[]")]
|
||||||
public List<string> Tags { get; set; } = [];
|
public List<string> Aliases { get; set; } = [];
|
||||||
|
|
||||||
[Column("category")]
|
[Column("category")]
|
||||||
[StringLength(128)]
|
[StringLength(128)]
|
||||||
|
@ -75,18 +75,11 @@ public class Emoji
|
||||||
{
|
{
|
||||||
public void Configure(EntityTypeBuilder<Emoji> entity)
|
public void Configure(EntityTypeBuilder<Emoji> entity)
|
||||||
{
|
{
|
||||||
entity.Property(e => e.Tags).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.RawPublicUrl).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");
|
||||||
|
|
||||||
entity.HasIndex(e => e.Name, "GIN_TRGM_emoji_name")
|
|
||||||
.HasMethod("gin")
|
|
||||||
.HasOperators("gin_trgm_ops");
|
|
||||||
entity.HasIndex(e => e.Host, "GIN_TRGM_emoji_host")
|
|
||||||
.HasMethod("gin")
|
|
||||||
.HasOperators("gin_trgm_ops");
|
|
||||||
|
|
||||||
// This index must be NULLS NOT DISTINCT to make having multiple local emoji with the same name cause a constraint failure
|
// This index must be NULLS NOT DISTINCT to make having multiple local emoji with the same name cause a constraint failure
|
||||||
entity.HasIndex(nameof(Name), nameof(Host)).IsUnique().AreNullsDistinct(false);
|
entity.HasIndex(nameof(Name), nameof(Host)).IsUnique().AreNullsDistinct(false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Database.Tables;
|
|
||||||
|
|
||||||
[Table("bubble_instance")]
|
|
||||||
public class BubbleInstance
|
|
||||||
{
|
|
||||||
[Key]
|
|
||||||
[Column("host")]
|
|
||||||
[StringLength(256)]
|
|
||||||
public string Host { get; set; } = null!;
|
|
||||||
}
|
|
|
@ -41,7 +41,6 @@ public class User : IIdentifiable
|
||||||
[Column("lastFetchedAt")] public DateTime? LastFetchedAt { get; set; }
|
[Column("lastFetchedAt")] public DateTime? LastFetchedAt { get; set; }
|
||||||
|
|
||||||
[Column("outboxFetchedAt")] public DateTime? OutboxFetchedAt { get; set; }
|
[Column("outboxFetchedAt")] public DateTime? OutboxFetchedAt { get; set; }
|
||||||
[Column("lastNoteAt")] public DateTime? LastNoteAt { get; set; }
|
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
[Projectable]
|
[Projectable]
|
||||||
|
@ -260,14 +259,14 @@ public class User : IIdentifiable
|
||||||
|
|
||||||
[Column("splitDomainResolved")] public bool SplitDomainResolved { get; set; }
|
[Column("splitDomainResolved")] public bool SplitDomainResolved { get; set; }
|
||||||
|
|
||||||
[InverseProperty(nameof(Report.Assignee))]
|
[InverseProperty(nameof(AbuseUserReport.Assignee))]
|
||||||
public virtual ICollection<Report> AbuseUserReportAssignees { get; set; } = new List<Report>();
|
public virtual ICollection<AbuseUserReport> AbuseUserReportAssignees { get; set; } = new List<AbuseUserReport>();
|
||||||
|
|
||||||
[InverseProperty(nameof(Report.Reporter))]
|
[InverseProperty(nameof(AbuseUserReport.Reporter))]
|
||||||
public virtual ICollection<Report> AbuseUserReportReporters { get; set; } = new List<Report>();
|
public virtual ICollection<AbuseUserReport> AbuseUserReportReporters { get; set; } = new List<AbuseUserReport>();
|
||||||
|
|
||||||
[InverseProperty(nameof(Report.TargetUser))]
|
[InverseProperty(nameof(AbuseUserReport.TargetUser))]
|
||||||
public virtual ICollection<Report> AbuseUserReportTargetUsers { get; set; } = new List<Report>();
|
public virtual ICollection<AbuseUserReport> AbuseUserReportTargetUsers { get; set; } = new List<AbuseUserReport>();
|
||||||
|
|
||||||
[InverseProperty(nameof(AnnouncementRead.User))]
|
[InverseProperty(nameof(AnnouncementRead.User))]
|
||||||
public virtual ICollection<AnnouncementRead> AnnouncementReads { get; set; } = new List<AnnouncementRead>();
|
public virtual ICollection<AnnouncementRead> AnnouncementReads { get; set; } = new List<AnnouncementRead>();
|
||||||
|
|
|
@ -655,14 +655,5 @@ public static class QueryableExtensions
|
||||||
.Include(p => p.Bite);
|
.Include(p => p.Bite);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IQueryable<Report> IncludeCommonProperties(this IQueryable<Report> query)
|
|
||||||
{
|
|
||||||
return query.Include(p => p.Reporter.UserProfile)
|
|
||||||
.Include(p => p.TargetUser.UserProfile)
|
|
||||||
.Include(p => p.Assignee.UserProfile)
|
|
||||||
.Include(p => p.Notes)
|
|
||||||
.ThenInclude(p => p.User.UserProfile);
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma warning restore CS8602 // Dereference of a possibly null reference.
|
#pragma warning restore CS8602 // Dereference of a possibly null reference.
|
||||||
}
|
}
|
|
@ -35,7 +35,6 @@ public static class QueryableFtsExtensions
|
||||||
InstanceFilter instanceFilter => current.ApplyInstanceFilter(instanceFilter, config),
|
InstanceFilter instanceFilter => current.ApplyInstanceFilter(instanceFilter, config),
|
||||||
MentionFilter mentionFilter => current.ApplyMentionFilter(mentionFilter, config, db),
|
MentionFilter mentionFilter => current.ApplyMentionFilter(mentionFilter, config, db),
|
||||||
MiscFilter miscFilter => current.ApplyMiscFilter(miscFilter, user),
|
MiscFilter miscFilter => current.ApplyMiscFilter(miscFilter, user),
|
||||||
VisibilityFilter visibilityFilter => current.ApplyVisibilityFilter(visibilityFilter),
|
|
||||||
ReplyFilter replyFilter => current.ApplyReplyFilter(replyFilter, config, db),
|
ReplyFilter replyFilter => current.ApplyReplyFilter(replyFilter, config, db),
|
||||||
CwFilter cwFilter => current.ApplyCwFilter(cwFilter, caseSensitivity, matchType),
|
CwFilter cwFilter => current.ApplyCwFilter(cwFilter, caseSensitivity, matchType),
|
||||||
WordFilter wordFilter => current.ApplyWordFilter(wordFilter, caseSensitivity, matchType),
|
WordFilter wordFilter => current.ApplyWordFilter(wordFilter, caseSensitivity, matchType),
|
||||||
|
@ -82,9 +81,7 @@ public static class QueryableFtsExtensions
|
||||||
|
|
||||||
private static IQueryable<Note> ApplyMultiWordFilter(
|
private static IQueryable<Note> ApplyMultiWordFilter(
|
||||||
this IQueryable<Note> query, MultiWordFilter filter, CaseFilterType caseSensitivity, MatchFilterType matchType
|
this IQueryable<Note> query, MultiWordFilter filter, CaseFilterType caseSensitivity, MatchFilterType matchType
|
||||||
) => filter.Negated
|
) => query.Where(p => p.FtsQueryOneOf(filter.Values, caseSensitivity, matchType));
|
||||||
? query.Where(p => !p.FtsQueryOneOf(filter.Values, caseSensitivity, matchType))
|
|
||||||
: query.Where(p => p.FtsQueryOneOf(filter.Values, caseSensitivity, matchType));
|
|
||||||
|
|
||||||
private static IQueryable<Note> ApplyFromFilters(
|
private static IQueryable<Note> ApplyFromFilters(
|
||||||
this IQueryable<Note> query, List<FromFilter> filters, Config.InstanceSection config, DatabaseContext db
|
this IQueryable<Note> query, List<FromFilter> filters, Config.InstanceSection config, DatabaseContext db
|
||||||
|
@ -166,25 +163,6 @@ public static class QueryableFtsExtensions
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IQueryable<Note> ApplyVisibilityFilter(this IQueryable<Note> query, VisibilityFilter filter)
|
|
||||||
{
|
|
||||||
if (filter.Value is VisibilityFilterType.Local)
|
|
||||||
return query.Where(p => p.LocalOnly == !filter.Negated);
|
|
||||||
|
|
||||||
var visibility = filter.Value switch
|
|
||||||
{
|
|
||||||
VisibilityFilterType.Public => Note.NoteVisibility.Public,
|
|
||||||
VisibilityFilterType.Home => Note.NoteVisibility.Home,
|
|
||||||
VisibilityFilterType.Followers => Note.NoteVisibility.Followers,
|
|
||||||
VisibilityFilterType.Specified => Note.NoteVisibility.Specified,
|
|
||||||
_ => throw new ArgumentOutOfRangeException()
|
|
||||||
};
|
|
||||||
|
|
||||||
return filter.Negated
|
|
||||||
? query.Where(p => p.Visibility != visibility)
|
|
||||||
: query.Where(p => p.Visibility == visibility);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")]
|
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")]
|
||||||
private static IQueryable<Note> ApplyFollowersFilter(this IQueryable<Note> query, User user, bool negated)
|
private static IQueryable<Note> ApplyFollowersFilter(this IQueryable<Note> query, User user, bool negated)
|
||||||
=> query.Where(p => negated ? !p.User.IsFollowing(user) : p.User.IsFollowing(user));
|
=> query.Where(p => negated ? !p.User.IsFollowing(user) : p.User.IsFollowing(user));
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq.Expressions;
|
|
||||||
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.Services;
|
using Iceshrimp.Backend.Core.Services;
|
||||||
using Iceshrimp.EntityFrameworkCore.Extensions;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Extensions;
|
namespace Iceshrimp.Backend.Core.Extensions;
|
||||||
|
@ -20,25 +18,16 @@ public static class QueryableTimelineExtensions
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
return heuristic < Cutoff
|
return heuristic < Cutoff
|
||||||
? query.Where(FollowingAndOwnLowFreqExpr(user, db))
|
? query.FollowingAndOwnLowFreq(user, db)
|
||||||
: query.Where(note => note.User == user || note.User.IsFollowedBy(user));
|
: query.Where(note => note.User == user || note.User.IsFollowedBy(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IQueryable<Note> FilterByFollowingOwnAndLocal(
|
private static IQueryable<Note> FollowingAndOwnLowFreq(this IQueryable<Note> query, User user, DatabaseContext db)
|
||||||
this IQueryable<Note> query, User user, DatabaseContext db, int heuristic
|
=> query.Where(note => db.Followings
|
||||||
)
|
|
||||||
{
|
|
||||||
return heuristic < Cutoff
|
|
||||||
? query.Where(FollowingAndOwnLowFreqExpr(user, db).Or(p => p.UserHost == null))
|
|
||||||
: query.Where(note => note.User == user || note.User.IsFollowedBy(user));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Expression<Func<Note,bool>> FollowingAndOwnLowFreqExpr(User user, DatabaseContext db)
|
|
||||||
=> note => db.Followings
|
|
||||||
.Where(p => p.Follower == user)
|
.Where(p => p.Follower == user)
|
||||||
.Select(p => p.FolloweeId)
|
.Select(p => p.FolloweeId)
|
||||||
.Concat(new[] { user.Id })
|
.Concat(new[] { user.Id })
|
||||||
.Contains(note.UserId);
|
.Contains(note.UserId));
|
||||||
|
|
||||||
public static IQueryable<User> NeedsTimelineHeuristicUpdate(
|
public static IQueryable<User> NeedsTimelineHeuristicUpdate(
|
||||||
this IQueryable<User> query, DatabaseContext db, TimeSpan maxRemainingTtl
|
this IQueryable<User> query, DatabaseContext db, TimeSpan maxRemainingTtl
|
||||||
|
@ -71,7 +60,7 @@ public static class QueryableTimelineExtensions
|
||||||
//TODO: maybe we should express this as a ratio between matching and non-matching posts
|
//TODO: maybe we should express this as a ratio between matching and non-matching posts
|
||||||
return await db.Notes
|
return await db.Notes
|
||||||
.Where(p => p.CreatedAt > latestNote.CreatedAt - TimeSpan.FromDays(7))
|
.Where(p => p.CreatedAt > latestNote.CreatedAt - TimeSpan.FromDays(7))
|
||||||
.Where(FollowingAndOwnLowFreqExpr(user, db))
|
.FollowingAndOwnLowFreq(user, db)
|
||||||
.OrderByDescending(p => p.Id)
|
.OrderByDescending(p => p.Id)
|
||||||
.Take(Cutoff + 1)
|
.Take(Cutoff + 1)
|
||||||
.CountAsync();
|
.CountAsync();
|
||||||
|
|
|
@ -28,35 +28,4 @@ public static class StreamExtensions
|
||||||
|
|
||||||
ValueTask<int> DoReadAsync() => source.ReadAsync(new Memory<byte>(buffer), cancellationToken);
|
ValueTask<int> DoReadAsync() => source.ReadAsync(new Memory<byte>(buffer), cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// We can't trust the Content-Length header, and it might be null.
|
|
||||||
/// This makes sure that we only ever read up to maxLength into memory.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="stream">The response content stream</param>
|
|
||||||
/// <param name="maxLength">The maximum length to buffer (null = unlimited)</param>
|
|
||||||
/// <param name="contentLength">The content length, if known</param>
|
|
||||||
/// <param name="token">A CancellationToken, if applicable</param>
|
|
||||||
/// <returns>Either a buffered MemoryStream, or Stream.Null</returns>
|
|
||||||
public static async Task<Stream> GetSafeStreamOrNullAsync(
|
|
||||||
this Stream stream, long? maxLength, long? contentLength, CancellationToken token = default
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (maxLength is 0) return Stream.Null;
|
|
||||||
if (contentLength > maxLength) return Stream.Null;
|
|
||||||
|
|
||||||
MemoryStream buf = new();
|
|
||||||
if (contentLength < maxLength)
|
|
||||||
maxLength = contentLength.Value;
|
|
||||||
|
|
||||||
await stream.CopyToAsync(buf, maxLength, token);
|
|
||||||
if (maxLength == null || buf.Length <= maxLength)
|
|
||||||
{
|
|
||||||
buf.Seek(0, SeekOrigin.Begin);
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
await buf.DisposeAsync();
|
|
||||||
return Stream.Null;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -61,11 +61,6 @@ public static class TaskExtensions
|
||||||
return (await task).ToList();
|
return (await task).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<T[]> ToArrayAsync<T>(this Task<IEnumerable<T>> task)
|
|
||||||
{
|
|
||||||
return (await task).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task ContinueWithResult(this Task task, Action continuation)
|
public static async Task ContinueWithResult(this Task task, Action continuation)
|
||||||
{
|
{
|
||||||
await task;
|
await task;
|
||||||
|
|
|
@ -2,33 +2,5 @@ namespace Iceshrimp.Backend.Core.Extensions;
|
||||||
|
|
||||||
public static class TimeSpanExtensions
|
public static class TimeSpanExtensions
|
||||||
{
|
{
|
||||||
private static readonly long Seconds = TimeSpan.FromMinutes(1).Ticks;
|
|
||||||
private static readonly long Minutes = TimeSpan.FromHours(1).Ticks;
|
|
||||||
private static readonly long Hours = TimeSpan.FromDays(1).Ticks;
|
|
||||||
|
|
||||||
public static long GetTotalMilliseconds(this TimeSpan timeSpan) => Convert.ToInt64(timeSpan.TotalMilliseconds);
|
public static long GetTotalMilliseconds(this TimeSpan timeSpan) => Convert.ToInt64(timeSpan.TotalMilliseconds);
|
||||||
|
|
||||||
public static string ToDisplayString(this TimeSpan timeSpan, bool singleNumber = true)
|
|
||||||
{
|
|
||||||
if (timeSpan.Ticks < Seconds)
|
|
||||||
{
|
|
||||||
var seconds = (int)timeSpan.TotalSeconds;
|
|
||||||
return seconds == 1 ? singleNumber ? "1 second" : "second" : $"{seconds} seconds";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeSpan.Ticks < Minutes)
|
|
||||||
{
|
|
||||||
var minutes = (int)timeSpan.TotalMinutes;
|
|
||||||
return minutes == 1 ? singleNumber ? "1 minute" : "minute" : $"{minutes} minutes";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeSpan.Ticks < Hours)
|
|
||||||
{
|
|
||||||
var hours = (int)timeSpan.TotalHours;
|
|
||||||
return hours == 1 ? singleNumber ? "1 hour" : "hour" : $"{hours} hours";
|
|
||||||
}
|
|
||||||
|
|
||||||
var days = (int)timeSpan.TotalDays;
|
|
||||||
return days == 1 ? singleNumber ? "1 day" : "day" : $"{days} days";
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -4,7 +4,6 @@ using System.Runtime.InteropServices;
|
||||||
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.Migrations;
|
using Iceshrimp.Backend.Core.Database.Migrations;
|
||||||
using Iceshrimp.Backend.Core.Helpers;
|
|
||||||
using Iceshrimp.Backend.Core.Middleware;
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
using Iceshrimp.Backend.Core.Services;
|
using Iceshrimp.Backend.Core.Services;
|
||||||
using Iceshrimp.Backend.Core.Services.ImageProcessing;
|
using Iceshrimp.Backend.Core.Services.ImageProcessing;
|
||||||
|
@ -62,9 +61,6 @@ public static class WebApplicationExtensions
|
||||||
app.MapScalarApiReference("/scalar", options =>
|
app.MapScalarApiReference("/scalar", options =>
|
||||||
{
|
{
|
||||||
options.WithTitle("Iceshrimp API documentation")
|
options.WithTitle("Iceshrimp API documentation")
|
||||||
.AddDocument("iceshrimp", "Iceshrimp.NET")
|
|
||||||
.AddDocument("federation", "Federation")
|
|
||||||
.AddDocument("mastodon", "Mastodon")
|
|
||||||
.WithOpenApiRoutePattern("/openapi/{documentName}.json")
|
.WithOpenApiRoutePattern("/openapi/{documentName}.json")
|
||||||
.WithModels(false)
|
.WithModels(false)
|
||||||
.WithCustomCss("""
|
.WithCustomCss("""
|
||||||
|
@ -232,106 +228,13 @@ public static class WebApplicationExtensions
|
||||||
Environment.Exit(0);
|
Environment.Exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
string[] userMgmtCommands =
|
|
||||||
[
|
|
||||||
"--create-user", "--create-admin-user", "--reset-password", "--grant-admin", "--revoke-admin"
|
|
||||||
];
|
|
||||||
|
|
||||||
if (args.FirstOrDefault(userMgmtCommands.Contains) is { } cmd)
|
|
||||||
{
|
|
||||||
if (args is not [not null, var username])
|
|
||||||
{
|
|
||||||
app.Logger.LogError("Invalid syntax. Usage: {cmd} <username>", cmd);
|
|
||||||
Environment.Exit(1);
|
|
||||||
return null!;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmd is "--create-user" or "--create-admin-user")
|
|
||||||
{
|
|
||||||
var password = CryptographyHelpers.GenerateRandomString(16);
|
|
||||||
app.Logger.LogInformation("Creating user {username}...", username);
|
|
||||||
var userSvc = provider.GetRequiredService<UserService>();
|
|
||||||
await userSvc.CreateLocalUserAsync(username, password, null, force: true);
|
|
||||||
|
|
||||||
if (args[0] is "--create-admin-user")
|
|
||||||
{
|
|
||||||
await db.Users.Where(p => p.Username == username)
|
|
||||||
.ExecuteUpdateAsync(p => p.SetProperty(i => i.IsAdmin, true));
|
|
||||||
|
|
||||||
app.Logger.LogInformation("Successfully created admin user.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
app.Logger.LogInformation("Successfully created user.");
|
|
||||||
}
|
|
||||||
|
|
||||||
app.Logger.LogInformation("Username: {username}", username);
|
|
||||||
app.Logger.LogInformation("Password: {password}", password);
|
|
||||||
Environment.Exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmd is "--reset-password")
|
|
||||||
{
|
|
||||||
var settings = await db.UserSettings
|
|
||||||
.FirstOrDefaultAsync(p => p.User.UsernameLower == username.ToLowerInvariant());
|
|
||||||
|
|
||||||
if (settings == null)
|
|
||||||
{
|
|
||||||
app.Logger.LogError("User {username} not found.", username);
|
|
||||||
Environment.Exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.Logger.LogInformation("Resetting password for user {username}...", username);
|
|
||||||
|
|
||||||
var password = CryptographyHelpers.GenerateRandomString(16);
|
|
||||||
settings.Password = AuthHelpers.HashPassword(password);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
app.Logger.LogInformation("Password for user {username} was reset to: {password}", username, password);
|
|
||||||
Environment.Exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmd is "--grant-admin")
|
|
||||||
{
|
|
||||||
var user = await db.Users.FirstOrDefaultAsync(p => p.UsernameLower == username.ToLowerInvariant());
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
app.Logger.LogError("User {username} not found.", username);
|
|
||||||
Environment.Exit(1);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
user.IsAdmin = true;
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
app.Logger.LogInformation("Granted admin privileges to user {username}.", username);
|
|
||||||
Environment.Exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmd is "--revoke-admin")
|
|
||||||
{
|
|
||||||
var user = await db.Users.FirstOrDefaultAsync(p => p.UsernameLower == username.ToLowerInvariant());
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
app.Logger.LogError("User {username} not found.", username);
|
|
||||||
Environment.Exit(1);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
user.IsAdmin = false;
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
app.Logger.LogInformation("Revoked admin privileges of user {username}.", username);
|
|
||||||
Environment.Exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var storageConfig = app.Configuration.GetSection("Storage").Get<Config.StorageSection>() ??
|
var storageConfig = app.Configuration.GetSection("Storage").Get<Config.StorageSection>() ??
|
||||||
throw new Exception("Failed to read Storage config section");
|
throw new Exception("Failed to read Storage config section");
|
||||||
|
|
||||||
if (storageConfig.Provider == Enums.FileStorage.Local)
|
if (storageConfig.Provider == Enums.FileStorage.Local)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(storageConfig.Local?.Path) || !Directory.Exists(storageConfig.Local.Path))
|
if (string.IsNullOrWhiteSpace(storageConfig.Local?.Path) ||
|
||||||
|
!Directory.Exists(storageConfig.Local.Path))
|
||||||
{
|
{
|
||||||
app.Logger.LogCritical("Local storage path does not exist");
|
app.Logger.LogCritical("Local storage path does not exist");
|
||||||
Environment.Exit(1);
|
Environment.Exit(1);
|
||||||
|
|
|
@ -28,8 +28,7 @@ public class ActivityHandlerService(
|
||||||
FollowupTaskService followupTaskSvc,
|
FollowupTaskService followupTaskSvc,
|
||||||
EmojiService emojiSvc,
|
EmojiService emojiSvc,
|
||||||
EventService eventSvc,
|
EventService eventSvc,
|
||||||
RelayService relaySvc,
|
RelayService relaySvc
|
||||||
ReportService reportSvc
|
|
||||||
) : IScopedService
|
) : IScopedService
|
||||||
{
|
{
|
||||||
public async Task PerformActivityAsync(ASActivity activity, string? inboxUserId, string? authenticatedUserId)
|
public async Task PerformActivityAsync(ASActivity activity, string? inboxUserId, string? authenticatedUserId)
|
||||||
|
@ -87,7 +86,6 @@ public class ActivityHandlerService(
|
||||||
ASUndo undo => HandleUndoAsync(undo, resolvedActor),
|
ASUndo undo => HandleUndoAsync(undo, resolvedActor),
|
||||||
ASUnfollow unfollow => HandleUnfollowAsync(unfollow, resolvedActor),
|
ASUnfollow unfollow => HandleUnfollowAsync(unfollow, resolvedActor),
|
||||||
ASUpdate update => HandleUpdateAsync(update, resolvedActor),
|
ASUpdate update => HandleUpdateAsync(update, resolvedActor),
|
||||||
ASFlag flag => HandleFlagAsync(flag, resolvedActor),
|
|
||||||
|
|
||||||
// Separated for readability
|
// Separated for readability
|
||||||
_ => throw GracefulException.UnprocessableEntity($"Activity type {activity.Type} is unknown")
|
_ => throw GracefulException.UnprocessableEntity($"Activity type {activity.Type} is unknown")
|
||||||
|
@ -110,8 +108,8 @@ public class ActivityHandlerService(
|
||||||
if (activity.Object == null)
|
if (activity.Object == null)
|
||||||
throw GracefulException.UnprocessableEntity("Create activity object was null");
|
throw GracefulException.UnprocessableEntity("Create activity object was null");
|
||||||
|
|
||||||
activity.Object = await objectResolver.ResolveObjectAsync(activity.Object, actor.Uri) as ASNote
|
activity.Object = await objectResolver.ResolveObjectAsync(activity.Object, actor.Uri) as ASNote ??
|
||||||
?? throw GracefulException.UnprocessableEntity("Failed to resolve create object");
|
throw GracefulException.UnprocessableEntity("Failed to resolve create object");
|
||||||
|
|
||||||
using (await NoteService.GetNoteProcessLockAsync(activity.Object.Id))
|
using (await NoteService.GetNoteProcessLockAsync(activity.Object.Id))
|
||||||
await noteSvc.ProcessNoteAsync(activity.Object, actor, inboxUser);
|
await noteSvc.ProcessNoteAsync(activity.Object, actor, inboxUser);
|
||||||
|
@ -223,8 +221,8 @@ public class ActivityHandlerService(
|
||||||
if (await db.Followings.AnyAsync(p => p.Followee == actor && p.FollowerId == ids[0]))
|
if (await db.Followings.AnyAsync(p => p.Followee == actor && p.FollowerId == ids[0]))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
throw GracefulException.UnprocessableEntity($"No follow or follow request matching follower '{ids[0]}'"
|
throw GracefulException.UnprocessableEntity($"No follow or follow request matching follower '{ids[0]}'" +
|
||||||
+ $"and followee '{actor.Id}' found");
|
$"and followee '{actor.Id}' found");
|
||||||
}
|
}
|
||||||
|
|
||||||
await userSvc.AcceptFollowRequestAsync(request);
|
await userSvc.AcceptFollowRequestAsync(request);
|
||||||
|
@ -274,7 +272,8 @@ public class ActivityHandlerService(
|
||||||
|
|
||||||
await db.Notifications
|
await db.Notifications
|
||||||
.Where(p => p.Type == Notification.NotificationType.FollowRequestAccepted)
|
.Where(p => p.Type == Notification.NotificationType.FollowRequestAccepted)
|
||||||
.Where(p => p.Notifiee == resolvedFollower && p.Notifier == resolvedActor)
|
.Where(p => p.Notifiee == resolvedFollower &&
|
||||||
|
p.Notifier == resolvedActor)
|
||||||
.ExecuteDeleteAsync();
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
await db.UserListMembers
|
await db.UserListMembers
|
||||||
|
@ -402,8 +401,8 @@ public class ActivityHandlerService(
|
||||||
User = resolvedActor,
|
User = resolvedActor,
|
||||||
UserHost = resolvedActor.Host,
|
UserHost = resolvedActor.Host,
|
||||||
TargetBite =
|
TargetBite =
|
||||||
await db.Bites.FirstAsync(p => p.UserHost == null
|
await db.Bites.FirstAsync(p => p.UserHost == null &&
|
||||||
&& p.Id == Bite.GetIdFromPublicUri(targetBite.Id, config.Value))
|
p.Id == Bite.GetIdFromPublicUri(targetBite.Id, config.Value))
|
||||||
},
|
},
|
||||||
null => throw GracefulException.UnprocessableEntity($"Failed to resolve bite target {activity.Target.Id}"),
|
null => throw GracefulException.UnprocessableEntity($"Failed to resolve bite target {activity.Target.Id}"),
|
||||||
_ when activity.To?.Id != null => new Bite
|
_ when activity.To?.Id != null => new Bite
|
||||||
|
@ -420,9 +419,9 @@ public class ActivityHandlerService(
|
||||||
//TODO: more fallback
|
//TODO: more fallback
|
||||||
};
|
};
|
||||||
|
|
||||||
if ((dbBite.TargetUser?.IsRemoteUser ?? false)
|
if ((dbBite.TargetUser?.IsRemoteUser ?? false) ||
|
||||||
|| (dbBite.TargetNote?.User.IsRemoteUser ?? false)
|
(dbBite.TargetNote?.User.IsRemoteUser ?? false) ||
|
||||||
|| (dbBite.TargetBite?.User.IsRemoteUser ?? false))
|
(dbBite.TargetBite?.User.IsRemoteUser ?? false))
|
||||||
throw GracefulException.Accepted("Ignoring bite for remote user");
|
throw GracefulException.Accepted("Ignoring bite for remote user");
|
||||||
|
|
||||||
var finalTarget = dbBite.TargetUser ?? dbBite.TargetNote?.User ?? dbBite.TargetBite?.User;
|
var finalTarget = dbBite.TargetUser ?? dbBite.TargetNote?.User ?? dbBite.TargetBite?.User;
|
||||||
|
@ -521,45 +520,14 @@ public class ActivityHandlerService(
|
||||||
var targetUri = target.Uri ?? target.GetPublicUri(config.Value.WebDomain);
|
var targetUri = target.Uri ?? target.GetPublicUri(config.Value.WebDomain);
|
||||||
var aliases = target.AlsoKnownAs ?? [];
|
var aliases = target.AlsoKnownAs ?? [];
|
||||||
if (!aliases.Contains(sourceUri))
|
if (!aliases.Contains(sourceUri))
|
||||||
throw GracefulException.UnprocessableEntity("Refusing to process move activity:"
|
throw GracefulException.UnprocessableEntity("Refusing to process move activity:" +
|
||||||
+ "source uri not listed in target aliases");
|
"source uri not listed in target aliases");
|
||||||
|
|
||||||
source.MovedToUri = targetUri;
|
source.MovedToUri = targetUri;
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
await userSvc.MoveRelationshipsAsync(source, target, sourceUri, targetUri);
|
await userSvc.MoveRelationshipsAsync(source, target, sourceUri, targetUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleFlagAsync(ASFlag flag, User resolvedActor)
|
|
||||||
{
|
|
||||||
if (resolvedActor.IsLocalUser)
|
|
||||||
throw GracefulException.UnprocessableEntity("Refusing to process locally originating report via AP");
|
|
||||||
if (flag.Object is not { Length: > 0 })
|
|
||||||
throw GracefulException.UnprocessableEntity("ASFlag activity does not reference any objects");
|
|
||||||
|
|
||||||
var candidates = flag.Object.Take(25)
|
|
||||||
.Select(p => p.Id)
|
|
||||||
.NotNull()
|
|
||||||
.Where(p => p.StartsWith($"https://{config.Value.WebDomain}/users/")
|
|
||||||
|| p.StartsWith($"https://{config.Value.WebDomain}/notes/"))
|
|
||||||
.Select(p => p[$"https://{config.Value.WebDomain}/notes/".Length..])
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
var userMatch = await db.Users.FirstOrDefaultAsync(p => p.IsLocalUser && candidates.Contains(p.Id));
|
|
||||||
var noteMatches = await db.Notes.Where(p => p.UserHost == null && candidates.Contains(p.Id))
|
|
||||||
.Include(note => note.User)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
if (userMatch == null && noteMatches.Count == 0)
|
|
||||||
throw GracefulException.UnprocessableEntity("ASFlag activity object resolution yielded zero results");
|
|
||||||
|
|
||||||
userMatch ??= noteMatches[0].User;
|
|
||||||
|
|
||||||
if (noteMatches.Count != 0 && noteMatches.Any(p => p.User != userMatch))
|
|
||||||
throw GracefulException.UnprocessableEntity("Refusing to process ASFlag: note author mismatch");
|
|
||||||
|
|
||||||
await reportSvc.CreateReportAsync(resolvedActor, userMatch, noteMatches, flag.Content ?? "");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UnfollowAsync(ASActor followeeActor, User follower)
|
private async Task UnfollowAsync(ASActor followeeActor, User follower)
|
||||||
{
|
{
|
||||||
//TODO: send reject? or do we not want to copy that part of the old ap core
|
//TODO: send reject? or do we not want to copy that part of the old ap core
|
||||||
|
@ -590,9 +558,9 @@ public class ActivityHandlerService(
|
||||||
});
|
});
|
||||||
|
|
||||||
await db.Notifications
|
await db.Notifications
|
||||||
.Where(p => p.Type == Notification.NotificationType.Follow
|
.Where(p => p.Type == Notification.NotificationType.Follow &&
|
||||||
&& p.Notifiee == followee
|
p.Notifiee == followee &&
|
||||||
&& p.Notifier == follower)
|
p.Notifier == follower)
|
||||||
.ExecuteDeleteAsync();
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
eventSvc.RaiseUserUnfollowed(this, follower, followee);
|
eventSvc.RaiseUserUnfollowed(this, follower, followee);
|
||||||
|
|
|
@ -239,12 +239,4 @@ public class ActivityRenderer(
|
||||||
PublishedAt = bite.CreatedAt,
|
PublishedAt = bite.CreatedAt,
|
||||||
To = userRenderer.RenderLite(fallbackTo)
|
To = userRenderer.RenderLite(fallbackTo)
|
||||||
};
|
};
|
||||||
|
|
||||||
public ASFlag RenderFlag(User actor, User user, IEnumerable<Note> notes, string comment) => new()
|
|
||||||
{
|
|
||||||
Id = GenerateActivityId(),
|
|
||||||
Actor = userRenderer.RenderLite(actor),
|
|
||||||
Object = notes.Select(noteRenderer.RenderLite).Prepend<ASObject>(userRenderer.RenderLite(user)).ToArray(),
|
|
||||||
Content = comment
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -182,7 +182,7 @@ public class NoteRenderer(
|
||||||
To = to,
|
To = to,
|
||||||
Tags = tags,
|
Tags = tags,
|
||||||
Attachments = attachments,
|
Attachments = attachments,
|
||||||
Content = text != null ? mfmConverter.ToHtml(text, mentions, note.UserHost, media: inlineMedia).Html : null,
|
Content = text != null ? (await mfmConverter.ToHtmlAsync(text, mentions, note.UserHost, media: inlineMedia)).Html : null,
|
||||||
Summary = note.Cw,
|
Summary = note.Cw,
|
||||||
Source = rawText != null
|
Source = rawText != null
|
||||||
? new ASNoteSource { Content = rawText, MediaType = "text/x.misskeymarkdown" }
|
? new ASNoteSource { Content = rawText, MediaType = "text/x.misskeymarkdown" }
|
||||||
|
@ -214,7 +214,7 @@ public class NoteRenderer(
|
||||||
To = to,
|
To = to,
|
||||||
Tags = tags,
|
Tags = tags,
|
||||||
Attachments = attachments,
|
Attachments = attachments,
|
||||||
Content = text != null ? mfmConverter.ToHtml(text, mentions, note.UserHost, media: inlineMedia).Html : null,
|
Content = text != null ? (await mfmConverter.ToHtmlAsync(text, mentions, note.UserHost, media: inlineMedia)).Html : null,
|
||||||
Summary = note.Cw,
|
Summary = note.Cw,
|
||||||
Source = rawText != null
|
Source = rawText != null
|
||||||
? new ASNoteSource { Content = rawText, MediaType = "text/x.misskeymarkdown" }
|
? new ASNoteSource { Content = rawText, MediaType = "text/x.misskeymarkdown" }
|
||||||
|
|
|
@ -5,7 +5,6 @@ using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
||||||
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
|
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
|
||||||
using Iceshrimp.MfmSharp;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
@ -85,7 +84,7 @@ public class UserRenderer(
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var summary = profile?.Description != null
|
var summary = profile?.Description != null
|
||||||
? mfmConverter.ToHtml(profile.Description, profile.Mentions, user.Host).Html
|
? (await mfmConverter.ToHtmlAsync(profile.Description, profile.Mentions, user.Host)).Html
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var pronouns = profile?.Pronouns != null ? new LDLocalizedString { Values = profile.Pronouns! } : null;
|
var pronouns = profile?.Pronouns != null ? new LDLocalizedString { Values = profile.Pronouns! } : null;
|
||||||
|
|
|
@ -35,7 +35,6 @@ public class ASActivity : ASObjectWithId
|
||||||
public const string Like = $"{Ns}#Like";
|
public const string Like = $"{Ns}#Like";
|
||||||
public const string Block = $"{Ns}#Block";
|
public const string Block = $"{Ns}#Block";
|
||||||
public const string Move = $"{Ns}#Move";
|
public const string Move = $"{Ns}#Move";
|
||||||
public const string Flag = $"{Ns}#Flag";
|
|
||||||
|
|
||||||
// Extensions
|
// Extensions
|
||||||
public const string Bite = "https://ns.mia.jetzt/as#Bite";
|
public const string Bite = "https://ns.mia.jetzt/as#Bite";
|
||||||
|
@ -43,23 +42,6 @@ public class ASActivity : ASObjectWithId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ASActivityWithObjectArray : ASActivity
|
|
||||||
{
|
|
||||||
private ASObject[]? _object;
|
|
||||||
|
|
||||||
[J($"{Constants.ActivityStreamsNs}#object")]
|
|
||||||
[JC(typeof(ASObjectArrayConverter))]
|
|
||||||
public new ASObject[]? Object
|
|
||||||
{
|
|
||||||
get => _object;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_object = value;
|
|
||||||
base.Object = value?.FirstOrDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ASCreate : ASActivity
|
public class ASCreate : ASActivity
|
||||||
{
|
{
|
||||||
public ASCreate() => Type = Types.Create;
|
public ASCreate() => Type = Types.Create;
|
||||||
|
@ -146,15 +128,6 @@ public class ASBlock : ASActivity
|
||||||
public ASBlock() => Type = Types.Block;
|
public ASBlock() => Type = Types.Block;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ASFlag : ASActivityWithObjectArray
|
|
||||||
{
|
|
||||||
public ASFlag() => Type = Types.Flag;
|
|
||||||
|
|
||||||
[J($"{Constants.ActivityStreamsNs}#content")]
|
|
||||||
[JC(typeof(VC))]
|
|
||||||
public string? Content { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ASLike : ASActivity
|
public class ASLike : ASActivity
|
||||||
{
|
{
|
||||||
public ASLike() => Type = Types.Like;
|
public ASLike() => Type = Types.Like;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using Iceshrimp.Backend.Core.Configuration;
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using J = Newtonsoft.Json.JsonPropertyAttribute;
|
using J = Newtonsoft.Json.JsonPropertyAttribute;
|
||||||
|
@ -55,7 +54,6 @@ public class ASObject : ASObjectBase
|
||||||
ASActivity.Types.EmojiReact => token.ToObject<ASEmojiReact>(),
|
ASActivity.Types.EmojiReact => token.ToObject<ASEmojiReact>(),
|
||||||
ASActivity.Types.Block => token.ToObject<ASBlock>(),
|
ASActivity.Types.Block => token.ToObject<ASBlock>(),
|
||||||
ASActivity.Types.Move => token.ToObject<ASMove>(),
|
ASActivity.Types.Move => token.ToObject<ASMove>(),
|
||||||
ASActivity.Types.Flag => token.ToObject<ASFlag>(),
|
|
||||||
_ => token.ToObject<ASObject>()
|
_ => token.ToObject<ASObject>()
|
||||||
};
|
};
|
||||||
case JTokenType.Array:
|
case JTokenType.Array:
|
||||||
|
@ -133,40 +131,3 @@ internal sealed class ASObjectConverter : JsonConverter
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class ASObjectArrayConverter : JsonConverter
|
|
||||||
{
|
|
||||||
public override bool CanWrite => false;
|
|
||||||
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override object? ReadJson(
|
|
||||||
JsonReader reader, Type objectType, object? existingValue,
|
|
||||||
JsonSerializer serializer
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonToken.StartArray)
|
|
||||||
{
|
|
||||||
var obj = JArray.Load(reader);
|
|
||||||
var arr = obj.Select(ASObject.Deserialize).NotNull().ToArray();
|
|
||||||
return arr.Length > 0 ? arr : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader.TokenType == JsonToken.StartObject)
|
|
||||||
{
|
|
||||||
var obj = JObject.Load(reader);
|
|
||||||
var deserialized = ASObject.Deserialize(obj);
|
|
||||||
return deserialized != null ? new[]{ deserialized } : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Exception("this shouldn't happen");
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -24,8 +24,6 @@ public class LDLocalizedString
|
||||||
{
|
{
|
||||||
Values = [];
|
Values = [];
|
||||||
|
|
||||||
if (value == null) return;
|
|
||||||
|
|
||||||
// this is required to create a non-Map field for non-JsonLD remotes.
|
// this is required to create a non-Map field for non-JsonLD remotes.
|
||||||
Values.Add("", value);
|
Values.Add("", value);
|
||||||
|
|
||||||
|
@ -33,7 +31,7 @@ public class LDLocalizedString
|
||||||
{
|
{
|
||||||
language = NormalizeLanguageCode(language);
|
language = NormalizeLanguageCode(language);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(language))
|
if (language != null && language != "")
|
||||||
Values.Add(language, value);
|
Values.Add(language, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -286,7 +284,7 @@ public class LocalizedValueObjectConverter : JsonConverter<LDLocalizedString>
|
||||||
|
|
||||||
public override void WriteJson(JsonWriter writer, LDLocalizedString? value, JsonSerializer serializer)
|
public override void WriteJson(JsonWriter writer, LDLocalizedString? value, JsonSerializer serializer)
|
||||||
{
|
{
|
||||||
if (value == null || value.Values.Count < 1)
|
if (value == null)
|
||||||
{
|
{
|
||||||
writer.WriteNull();
|
writer.WriteNull();
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -16,7 +16,7 @@ public static class BlurhashHelper
|
||||||
/// <param name="componentsX">The number of components used on the X-Axis for the DCT</param>
|
/// <param name="componentsX">The number of components used on the X-Axis for the DCT</param>
|
||||||
/// <param name="componentsY">The number of components used on the Y-Axis for the DCT</param>
|
/// <param name="componentsY">The number of components used on the Y-Axis for the DCT</param>
|
||||||
/// <returns>The resulting Blurhash string</returns>
|
/// <returns>The resulting Blurhash string</returns>
|
||||||
public static string Encode(ReadOnlySpan2D<Rgb24> pixels, int componentsX, int componentsY)
|
public static string Encode(Span2D<Rgb24> pixels, int componentsX, int componentsY)
|
||||||
{
|
{
|
||||||
if (componentsX < 1) throw new ArgumentException("componentsX needs to be at least 1");
|
if (componentsX < 1) throw new ArgumentException("componentsX needs to be at least 1");
|
||||||
if (componentsX > 9) throw new ArgumentException("componentsX needs to be at most 9");
|
if (componentsX > 9) throw new ArgumentException("componentsX needs to be at most 9");
|
||||||
|
|
|
@ -2,14 +2,13 @@ using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using AngleSharp;
|
using AngleSharp;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using AngleSharp.Html.Dom;
|
using AngleSharp.Html.Parser;
|
||||||
using Iceshrimp.Backend.Core.Configuration;
|
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.Helpers.LibMfm.Parsing;
|
using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing;
|
||||||
using Iceshrimp.MfmSharp;
|
using Iceshrimp.MfmSharp;
|
||||||
using Iceshrimp.Backend.Core.Services;
|
using Iceshrimp.Backend.Core.Services;
|
||||||
using Iceshrimp.MfmSharp.Helpers;
|
|
||||||
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;
|
||||||
|
@ -44,19 +43,13 @@ public readonly record struct MfmHtmlData(string Html, List<MfmInlineMedia> Inli
|
||||||
|
|
||||||
public class MfmConverter(
|
public class MfmConverter(
|
||||||
IOptions<Config.InstanceSection> config,
|
IOptions<Config.InstanceSection> config,
|
||||||
MediaProxyService mediaProxy,
|
MediaProxyService mediaProxy
|
||||||
FlagService flags
|
|
||||||
) : ISingletonService
|
) : ISingletonService
|
||||||
{
|
{
|
||||||
private static readonly HtmlParser Parser = new();
|
public AsyncLocal<bool> SupportsHtmlFormatting { get; } = new();
|
||||||
|
public AsyncLocal<bool> SupportsInlineMedia { get; } = new();
|
||||||
|
|
||||||
private static readonly Lazy<IHtmlDocument> OwnerDocument =
|
public static async Task<HtmlMfmData> FromHtmlAsync(
|
||||||
new(() => Parser.ParseDocument(ReadOnlyMemory<char>.Empty));
|
|
||||||
|
|
||||||
private static IElement CreateElement(string name) => OwnerDocument.Value.CreateElement(name);
|
|
||||||
private static IText CreateTextNode(string data) => OwnerDocument.Value.CreateTextNode(data);
|
|
||||||
|
|
||||||
public static HtmlMfmData FromHtml(
|
|
||||||
string? html, List<Note.MentionedUser>? mentions = null, List<string>? hashtags = null
|
string? html, List<Note.MentionedUser>? mentions = null, List<string>? hashtags = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
@ -73,7 +66,7 @@ public class MfmConverter(
|
||||||
// Ensure compatibility with AP servers that send CRLF or CR instead of LF-style newlines
|
// Ensure compatibility with AP servers that send CRLF or CR instead of LF-style newlines
|
||||||
html = html.ReplaceLineEndings("\n");
|
html = html.ReplaceLineEndings("\n");
|
||||||
|
|
||||||
var dom = Parser.ParseDocument(html);
|
var dom = await new HtmlParser().ParseDocumentAsync(html);
|
||||||
if (dom.Body == null) return new HtmlMfmData("", media);
|
if (dom.Body == null) return new HtmlMfmData("", media);
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
|
@ -82,7 +75,7 @@ public class MfmConverter(
|
||||||
return new HtmlMfmData(sb.ToString().Trim(), media);
|
return new HtmlMfmData(sb.ToString().Trim(), media);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<string> ExtractMentionsFromHtml(string? html)
|
public static async Task<List<string>> ExtractMentionsFromHtmlAsync(string? html)
|
||||||
{
|
{
|
||||||
if (html == null) return [];
|
if (html == null) return [];
|
||||||
|
|
||||||
|
@ -90,7 +83,7 @@ public class MfmConverter(
|
||||||
var regex = new Regex(@"<br\s?\/?>\r?\n", RegexOptions.IgnoreCase);
|
var regex = new Regex(@"<br\s?\/?>\r?\n", RegexOptions.IgnoreCase);
|
||||||
html = regex.Replace(html, "\n");
|
html = regex.Replace(html, "\n");
|
||||||
|
|
||||||
var dom = Parser.ParseDocument(html);
|
var dom = await new HtmlParser().ParseDocumentAsync(html);
|
||||||
if (dom.Body == null) return [];
|
if (dom.Body == null) return [];
|
||||||
|
|
||||||
var parser = new HtmlMentionsExtractor();
|
var parser = new HtmlMentionsExtractor();
|
||||||
|
@ -100,26 +93,28 @@ public class MfmConverter(
|
||||||
return parser.Mentions;
|
return parser.Mentions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MfmHtmlData ToHtml(
|
public async Task<MfmHtmlData> ToHtmlAsync(
|
||||||
IMfmNode[] nodes, List<Note.MentionedUser> mentions, string? host, string? quoteUri = null,
|
IMfmNode[] nodes, List<Note.MentionedUser> mentions, string? host, string? quoteUri = null,
|
||||||
bool quoteInaccessible = false, bool replyInaccessible = false, string rootElement = "p",
|
bool quoteInaccessible = false, bool replyInaccessible = false, string rootElement = "p",
|
||||||
List<Emoji>? emoji = null, List<MfmInlineMedia>? media = null
|
List<Emoji>? emoji = null, List<MfmInlineMedia>? media = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var element = CreateElement(rootElement);
|
var context = BrowsingContext.New();
|
||||||
|
var document = await context.OpenNewAsync();
|
||||||
|
var element = document.CreateElement(rootElement);
|
||||||
var hasContent = nodes.Length > 0;
|
var hasContent = nodes.Length > 0;
|
||||||
|
|
||||||
if (replyInaccessible)
|
if (replyInaccessible)
|
||||||
{
|
{
|
||||||
var wrapper = CreateElement("span");
|
var wrapper = document.CreateElement("span");
|
||||||
var re = CreateElement("span");
|
var re = document.CreateElement("span");
|
||||||
re.TextContent = "RE: \ud83d\udd12"; // lock emoji
|
re.TextContent = "RE: \ud83d\udd12"; // lock emoji
|
||||||
wrapper.AppendChild(re);
|
wrapper.AppendChild(re);
|
||||||
|
|
||||||
if (hasContent)
|
if (hasContent)
|
||||||
{
|
{
|
||||||
wrapper.AppendChild(CreateElement("br"));
|
wrapper.AppendChild(document.CreateElement("br"));
|
||||||
wrapper.AppendChild(CreateElement("br"));
|
wrapper.AppendChild(document.CreateElement("br"));
|
||||||
}
|
}
|
||||||
|
|
||||||
element.AppendChild(wrapper);
|
element.AppendChild(wrapper);
|
||||||
|
@ -127,23 +122,23 @@ public class MfmConverter(
|
||||||
|
|
||||||
var usedMedia = new List<MfmInlineMedia>();
|
var usedMedia = new List<MfmInlineMedia>();
|
||||||
foreach (var node in nodes)
|
foreach (var node in nodes)
|
||||||
element.AppendNodes(FromMfmNode(node, mentions, host, usedMedia, emoji, media));
|
element.AppendNodes(FromMfmNode(document, node, mentions, host, usedMedia, emoji, media));
|
||||||
|
|
||||||
if (quoteUri != null)
|
if (quoteUri != null)
|
||||||
{
|
{
|
||||||
var a = CreateElement("a");
|
var a = document.CreateElement("a");
|
||||||
a.SetAttribute("href", quoteUri);
|
a.SetAttribute("href", quoteUri);
|
||||||
a.TextContent = quoteUri.StartsWith("https://") ? quoteUri[8..] : quoteUri[7..];
|
a.TextContent = quoteUri.StartsWith("https://") ? quoteUri[8..] : quoteUri[7..];
|
||||||
var quote = CreateElement("span");
|
var quote = document.CreateElement("span");
|
||||||
quote.ClassList.Add("quote-inline");
|
quote.ClassList.Add("quote-inline");
|
||||||
|
|
||||||
if (hasContent)
|
if (hasContent)
|
||||||
{
|
{
|
||||||
quote.AppendChild(CreateElement("br"));
|
quote.AppendChild(document.CreateElement("br"));
|
||||||
quote.AppendChild(CreateElement("br"));
|
quote.AppendChild(document.CreateElement("br"));
|
||||||
}
|
}
|
||||||
|
|
||||||
var re = CreateElement("span");
|
var re = document.CreateElement("span");
|
||||||
re.TextContent = "RE: ";
|
re.TextContent = "RE: ";
|
||||||
quote.AppendChild(re);
|
quote.AppendChild(re);
|
||||||
quote.AppendChild(a);
|
quote.AppendChild(a);
|
||||||
|
@ -151,47 +146,39 @@ public class MfmConverter(
|
||||||
}
|
}
|
||||||
else if (quoteInaccessible)
|
else if (quoteInaccessible)
|
||||||
{
|
{
|
||||||
var wrapper = CreateElement("span");
|
var wrapper = document.CreateElement("span");
|
||||||
var re = CreateElement("span");
|
var re = document.CreateElement("span");
|
||||||
re.TextContent = "RE: \ud83d\udd12"; // lock emoji
|
re.TextContent = "RE: \ud83d\udd12"; // lock emoji
|
||||||
|
|
||||||
if (hasContent)
|
if (hasContent)
|
||||||
{
|
{
|
||||||
wrapper.AppendChild(CreateElement("br"));
|
wrapper.AppendChild(document.CreateElement("br"));
|
||||||
wrapper.AppendChild(CreateElement("br"));
|
wrapper.AppendChild(document.CreateElement("br"));
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapper.AppendChild(re);
|
wrapper.AppendChild(re);
|
||||||
element.AppendChild(wrapper);
|
element.AppendChild(wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new MfmHtmlData(element.ToHtml(), usedMedia);
|
await using var sw = new StringWriter();
|
||||||
|
await element.ToHtmlAsync(sw);
|
||||||
|
return new MfmHtmlData(sw.ToString(), usedMedia);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MfmHtmlData ToHtml(
|
public async Task<MfmHtmlData> ToHtmlAsync(
|
||||||
string mfm, List<Note.MentionedUser> mentions, string? host, string? quoteUri = null,
|
string mfm, List<Note.MentionedUser> mentions, string? host, string? quoteUri = null,
|
||||||
bool quoteInaccessible = false, bool replyInaccessible = false, string rootElement = "p",
|
bool quoteInaccessible = false, bool replyInaccessible = false, string rootElement = "p",
|
||||||
List<Emoji>? emoji = null, List<MfmInlineMedia>? media = null
|
List<Emoji>? emoji = null, List<MfmInlineMedia>? media = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var nodes = MfmParser.Parse(mfm);
|
var nodes = MfmParser.Parse(mfm);
|
||||||
return ToHtml(nodes, mentions, host, quoteUri, quoteInaccessible, replyInaccessible, rootElement, emoji, media);
|
return await ToHtmlAsync(nodes, mentions, host, quoteUri, quoteInaccessible,
|
||||||
}
|
replyInaccessible, rootElement, emoji, media);
|
||||||
|
|
||||||
public string ProfileFieldToHtml(MfmUrlNode node)
|
|
||||||
{
|
|
||||||
var parsed = FromMfmNode(node, [], null, []);
|
|
||||||
if (parsed is not IHtmlAnchorElement el)
|
|
||||||
return parsed.ToHtml();
|
|
||||||
|
|
||||||
el.SetAttribute("rel", "me nofollow noopener");
|
|
||||||
el.SetAttribute("target", "_blank");
|
|
||||||
|
|
||||||
return el.ToHtml();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private INode FromMfmNode(
|
private INode FromMfmNode(
|
||||||
IMfmNode node, List<Note.MentionedUser> mentions, string? host, List<MfmInlineMedia> usedMedia,
|
IDocument document, IMfmNode node, List<Note.MentionedUser> mentions, string? host,
|
||||||
|
List<MfmInlineMedia> usedMedia,
|
||||||
List<Emoji>? emoji = null, List<MfmInlineMedia>? media = null
|
List<Emoji>? emoji = null, List<MfmInlineMedia>? media = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
@ -207,9 +194,9 @@ public class MfmConverter(
|
||||||
{
|
{
|
||||||
usedMedia.Add(current);
|
usedMedia.Add(current);
|
||||||
|
|
||||||
if (!flags.SupportsInlineMedia.Value || current.Type == MfmInlineMedia.MediaType.Other)
|
if (!SupportsInlineMedia.Value || current.Type == MfmInlineMedia.MediaType.Other)
|
||||||
{
|
{
|
||||||
var el = CreateElement("a");
|
var el = document.CreateElement("a");
|
||||||
el.SetAttribute("href", current.Src);
|
el.SetAttribute("href", current.Src);
|
||||||
|
|
||||||
if (current.Type == MfmInlineMedia.MediaType.Other)
|
if (current.Type == MfmInlineMedia.MediaType.Other)
|
||||||
|
@ -236,7 +223,7 @@ public class MfmConverter(
|
||||||
_ => throw new ArgumentOutOfRangeException()
|
_ => throw new ArgumentOutOfRangeException()
|
||||||
};
|
};
|
||||||
|
|
||||||
var el = CreateElement(nodeName);
|
var el = document.CreateElement(nodeName);
|
||||||
el.SetAttribute("src", current.Src);
|
el.SetAttribute("src", current.Src);
|
||||||
el.SetAttribute("alt", current.Alt);
|
el.SetAttribute("alt", current.Alt);
|
||||||
return el;
|
return el;
|
||||||
|
@ -245,16 +232,16 @@ public class MfmConverter(
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
var el = CreateInlineFormattingElement("i");
|
var el = CreateInlineFormattingElement(document, "i");
|
||||||
AddHtmlMarkup(el, "*");
|
AddHtmlMarkup(document, el, "*");
|
||||||
AppendChildren(el, node, mentions, host, usedMedia);
|
AppendChildren(el, document, node, mentions, host, usedMedia);
|
||||||
AddHtmlMarkup(el, "*");
|
AddHtmlMarkup(document, el, "*");
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case MfmFnNode { Name: "unixtime" } fn:
|
case MfmFnNode { Name: "unixtime" } fn:
|
||||||
{
|
{
|
||||||
var el = CreateInlineFormattingElement("i");
|
var el = CreateInlineFormattingElement(document, "i");
|
||||||
|
|
||||||
if (fn.Children.Length != 1 || fn.Children.FirstOrDefault() is not MfmTextNode textNode)
|
if (fn.Children.Length != 1 || fn.Children.FirstOrDefault() is not MfmTextNode textNode)
|
||||||
return Fallback();
|
return Fallback();
|
||||||
|
@ -269,55 +256,55 @@ public class MfmConverter(
|
||||||
|
|
||||||
IElement Fallback()
|
IElement Fallback()
|
||||||
{
|
{
|
||||||
AddHtmlMarkup(el, "*");
|
AddHtmlMarkup(document, el, "*");
|
||||||
AppendChildren(el, node, mentions, host, usedMedia);
|
AppendChildren(el, document, node, mentions, host, usedMedia);
|
||||||
AddHtmlMarkup(el, "*");
|
AddHtmlMarkup(document, el, "*");
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case MfmBoldNode:
|
case MfmBoldNode:
|
||||||
{
|
{
|
||||||
var el = CreateInlineFormattingElement("b");
|
var el = CreateInlineFormattingElement(document, "b");
|
||||||
AddHtmlMarkup(el, "**");
|
AddHtmlMarkup(document, el, "**");
|
||||||
AppendChildren(el, node, mentions, host, usedMedia);
|
AppendChildren(el, document, node, mentions, host, usedMedia);
|
||||||
AddHtmlMarkup(el, "**");
|
AddHtmlMarkup(document, el, "**");
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
case MfmSmallNode:
|
case MfmSmallNode:
|
||||||
{
|
{
|
||||||
var el = CreateElement("small");
|
var el = document.CreateElement("small");
|
||||||
AppendChildren(el, node, mentions, host, usedMedia);
|
AppendChildren(el, document, node, mentions, host, usedMedia);
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
case MfmStrikeNode:
|
case MfmStrikeNode:
|
||||||
{
|
{
|
||||||
var el = CreateInlineFormattingElement("del");
|
var el = CreateInlineFormattingElement(document, "del");
|
||||||
AddHtmlMarkup(el, "~~");
|
AddHtmlMarkup(document, el, "~~");
|
||||||
AppendChildren(el, node, mentions, host, usedMedia);
|
AppendChildren(el, document, node, mentions, host, usedMedia);
|
||||||
AddHtmlMarkup(el, "~~");
|
AddHtmlMarkup(document, el, "~~");
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
case MfmItalicNode:
|
case MfmItalicNode:
|
||||||
case MfmFnNode:
|
case MfmFnNode:
|
||||||
{
|
{
|
||||||
var el = CreateInlineFormattingElement("i");
|
var el = CreateInlineFormattingElement(document, "i");
|
||||||
AddHtmlMarkup(el, "*");
|
AddHtmlMarkup(document, el, "*");
|
||||||
AppendChildren(el, node, mentions, host, usedMedia);
|
AppendChildren(el, document, node, mentions, host, usedMedia);
|
||||||
AddHtmlMarkup(el, "*");
|
AddHtmlMarkup(document, el, "*");
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
case MfmCodeBlockNode codeBlockNode:
|
case MfmCodeBlockNode codeBlockNode:
|
||||||
{
|
{
|
||||||
var el = CreateInlineFormattingElement("pre");
|
var el = CreateInlineFormattingElement(document, "pre");
|
||||||
var inner = CreateInlineFormattingElement("code");
|
var inner = CreateInlineFormattingElement(document, "code");
|
||||||
inner.TextContent = codeBlockNode.Code;
|
inner.TextContent = codeBlockNode.Code;
|
||||||
el.AppendNodes(inner);
|
el.AppendNodes(inner);
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
case MfmCenterNode:
|
case MfmCenterNode:
|
||||||
{
|
{
|
||||||
var el = CreateElement("div");
|
var el = document.CreateElement("div");
|
||||||
AppendChildren(el, node, mentions, host, usedMedia);
|
AppendChildren(el, document, node, mentions, host, usedMedia);
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
case MfmEmojiCodeNode emojiCodeNode:
|
case MfmEmojiCodeNode emojiCodeNode:
|
||||||
|
@ -325,8 +312,8 @@ public class MfmConverter(
|
||||||
var punyHost = host?.ToPunycodeLower();
|
var punyHost = host?.ToPunycodeLower();
|
||||||
if (emoji?.FirstOrDefault(p => p.Name == emojiCodeNode.Name && p.Host == punyHost) is { } hit)
|
if (emoji?.FirstOrDefault(p => p.Name == emojiCodeNode.Name && p.Host == punyHost) is { } hit)
|
||||||
{
|
{
|
||||||
var el = CreateElement("span");
|
var el = document.CreateElement("span");
|
||||||
var inner = CreateElement("img");
|
var inner = document.CreateElement("img");
|
||||||
inner.SetAttribute("src", mediaProxy.GetProxyUrl(hit));
|
inner.SetAttribute("src", mediaProxy.GetProxyUrl(hit));
|
||||||
inner.SetAttribute("alt", hit.Name);
|
inner.SetAttribute("alt", hit.Name);
|
||||||
el.AppendChild(inner);
|
el.AppendChild(inner);
|
||||||
|
@ -334,11 +321,11 @@ public class MfmConverter(
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
return CreateTextNode($"\u200B:{emojiCodeNode.Name}:\u200B");
|
return document.CreateTextNode($"\u200B:{emojiCodeNode.Name}:\u200B");
|
||||||
}
|
}
|
||||||
case MfmHashtagNode hashtagNode:
|
case MfmHashtagNode hashtagNode:
|
||||||
{
|
{
|
||||||
var el = CreateElement("a");
|
var el = document.CreateElement("a");
|
||||||
el.SetAttribute("href", $"https://{config.Value.WebDomain}/tags/{hashtagNode.Hashtag}");
|
el.SetAttribute("href", $"https://{config.Value.WebDomain}/tags/{hashtagNode.Hashtag}");
|
||||||
el.TextContent = $"#{hashtagNode.Hashtag}";
|
el.TextContent = $"#{hashtagNode.Hashtag}";
|
||||||
el.SetAttribute("rel", "tag");
|
el.SetAttribute("rel", "tag");
|
||||||
|
@ -347,32 +334,32 @@ public class MfmConverter(
|
||||||
}
|
}
|
||||||
case MfmInlineCodeNode inlineCodeNode:
|
case MfmInlineCodeNode inlineCodeNode:
|
||||||
{
|
{
|
||||||
var el = CreateInlineFormattingElement("code");
|
var el = CreateInlineFormattingElement(document, "code");
|
||||||
el.TextContent = inlineCodeNode.Code;
|
el.TextContent = inlineCodeNode.Code;
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
case MfmInlineMathNode inlineMathNode:
|
case MfmInlineMathNode inlineMathNode:
|
||||||
{
|
{
|
||||||
var el = CreateInlineFormattingElement("code");
|
var el = CreateInlineFormattingElement(document, "code");
|
||||||
el.TextContent = inlineMathNode.Formula;
|
el.TextContent = inlineMathNode.Formula;
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
case MfmMathBlockNode mathBlockNode:
|
case MfmMathBlockNode mathBlockNode:
|
||||||
{
|
{
|
||||||
var el = CreateInlineFormattingElement("code");
|
var el = CreateInlineFormattingElement(document, "code");
|
||||||
el.TextContent = mathBlockNode.Formula;
|
el.TextContent = mathBlockNode.Formula;
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
case MfmLinkNode linkNode:
|
case MfmLinkNode linkNode:
|
||||||
{
|
{
|
||||||
var el = CreateElement("a");
|
var el = document.CreateElement("a");
|
||||||
el.SetAttribute("href", linkNode.Url);
|
el.SetAttribute("href", linkNode.Url);
|
||||||
el.TextContent = linkNode.Text;
|
el.TextContent = linkNode.Text;
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
case MfmMentionNode mentionNode:
|
case MfmMentionNode mentionNode:
|
||||||
{
|
{
|
||||||
var el = CreateElement("span");
|
var el = document.CreateElement("span");
|
||||||
|
|
||||||
// Fall back to object host, as localpart-only mentions are relative to the instance the note originated from
|
// Fall back to object host, as localpart-only mentions are relative to the instance the note originated from
|
||||||
var finalHost = mentionNode.Host ?? host ?? config.Value.AccountDomain;
|
var finalHost = mentionNode.Host ?? host ?? config.Value.AccountDomain;
|
||||||
|
@ -393,10 +380,10 @@ public class MfmConverter(
|
||||||
{
|
{
|
||||||
el.ClassList.Add("h-card");
|
el.ClassList.Add("h-card");
|
||||||
el.SetAttribute("translate", "no");
|
el.SetAttribute("translate", "no");
|
||||||
var a = CreateElement("a");
|
var a = document.CreateElement("a");
|
||||||
a.ClassList.Add("u-url", "mention");
|
a.ClassList.Add("u-url", "mention");
|
||||||
a.SetAttribute("href", mention.Url ?? mention.Uri);
|
a.SetAttribute("href", mention.Url ?? mention.Uri);
|
||||||
var span = CreateElement("span");
|
var span = document.CreateElement("span");
|
||||||
span.TextContent = $"@{mention.Username}";
|
span.TextContent = $"@{mention.Username}";
|
||||||
a.AppendChild(span);
|
a.AppendChild(span);
|
||||||
el.AppendChild(a);
|
el.AppendChild(a);
|
||||||
|
@ -406,25 +393,25 @@ public class MfmConverter(
|
||||||
}
|
}
|
||||||
case MfmQuoteNode:
|
case MfmQuoteNode:
|
||||||
{
|
{
|
||||||
var el = CreateInlineFormattingElement("blockquote");
|
var el = CreateInlineFormattingElement(document, "blockquote");
|
||||||
AddHtmlMarkup(el, "> ");
|
AddHtmlMarkup(document, el, "> ");
|
||||||
AppendChildren(el, node, mentions, host, usedMedia);
|
AppendChildren(el, document, node, mentions, host, usedMedia);
|
||||||
AddHtmlMarkupTag(el, "br");
|
AddHtmlMarkupTag(document, el, "br");
|
||||||
AddHtmlMarkupTag(el, "br");
|
AddHtmlMarkupTag(document, el, "br");
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
case MfmTextNode textNode:
|
case MfmTextNode textNode:
|
||||||
{
|
{
|
||||||
var el = CreateElement("span");
|
var el = document.CreateElement("span");
|
||||||
var nodes = textNode.Text.Split("\r\n")
|
var nodes = textNode.Text.Split("\r\n")
|
||||||
.SelectMany(p => p.Split('\r'))
|
.SelectMany(p => p.Split('\r'))
|
||||||
.SelectMany(p => p.Split('\n'))
|
.SelectMany(p => p.Split('\n'))
|
||||||
.Select(CreateTextNode);
|
.Select(document.CreateTextNode);
|
||||||
|
|
||||||
foreach (var htmlNode in nodes)
|
foreach (var htmlNode in nodes)
|
||||||
{
|
{
|
||||||
el.AppendNodes(htmlNode);
|
el.AppendNodes(htmlNode);
|
||||||
el.AppendNodes(CreateElement("br"));
|
el.AppendNodes(document.CreateElement("br"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.LastChild != null)
|
if (el.LastChild != null)
|
||||||
|
@ -433,25 +420,17 @@ public class MfmConverter(
|
||||||
}
|
}
|
||||||
case MfmUrlNode urlNode:
|
case MfmUrlNode urlNode:
|
||||||
{
|
{
|
||||||
if (
|
var el = document.CreateElement("a");
|
||||||
!Uri.TryCreate(urlNode.Url, UriKind.Absolute, out var uri)
|
|
||||||
|| uri is not { Scheme: "http" or "https" }
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var fallbackEl = CreateElement("span");
|
|
||||||
fallbackEl.TextContent = urlNode.Url;
|
|
||||||
return fallbackEl;
|
|
||||||
}
|
|
||||||
|
|
||||||
var el = CreateElement("a");
|
|
||||||
el.SetAttribute("href", urlNode.Url);
|
el.SetAttribute("href", urlNode.Url);
|
||||||
el.TextContent = uri.ToMfmDisplayString();
|
var prefix = urlNode.Url.StartsWith("https://") ? "https://" : "http://";
|
||||||
|
var length = prefix.Length;
|
||||||
|
el.TextContent = urlNode.Url[length..];
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
case MfmPlainNode:
|
case MfmPlainNode:
|
||||||
{
|
{
|
||||||
var el = CreateElement("span");
|
var el = document.CreateElement("span");
|
||||||
AppendChildren(el, node, mentions, host, usedMedia);
|
AppendChildren(el, document, node, mentions, host, usedMedia);
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -462,32 +441,32 @@ public class MfmConverter(
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AppendChildren(
|
private void AppendChildren(
|
||||||
INode element, IMfmNode parent,
|
INode element, IDocument document, IMfmNode parent,
|
||||||
List<Note.MentionedUser> mentions, string? host, List<MfmInlineMedia> usedMedia,
|
List<Note.MentionedUser> mentions, string? host, List<MfmInlineMedia> usedMedia,
|
||||||
List<Emoji>? emoji = null, List<MfmInlineMedia>? media = null
|
List<Emoji>? emoji = null, List<MfmInlineMedia>? media = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
foreach (var node in parent.Children)
|
foreach (var node in parent.Children)
|
||||||
element.AppendNodes(FromMfmNode(node, mentions, host, usedMedia, emoji, media));
|
element.AppendNodes(FromMfmNode(document, node, mentions, host, usedMedia, emoji, media));
|
||||||
}
|
}
|
||||||
|
|
||||||
private IElement CreateInlineFormattingElement(string name)
|
private IElement CreateInlineFormattingElement(IDocument document, string name)
|
||||||
{
|
{
|
||||||
return CreateElement(flags.SupportsHtmlFormatting.Value ? name : "span");
|
return document.CreateElement(SupportsHtmlFormatting.Value ? name : "span");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddHtmlMarkup(IElement node, string chars)
|
private void AddHtmlMarkup(IDocument document, IElement node, string chars)
|
||||||
{
|
{
|
||||||
if (flags.SupportsHtmlFormatting.Value) return;
|
if (SupportsHtmlFormatting.Value) return;
|
||||||
var el = CreateElement("span");
|
var el = document.CreateElement("span");
|
||||||
el.AppendChild(CreateTextNode(chars));
|
el.AppendChild(document.CreateTextNode(chars));
|
||||||
node.AppendChild(el);
|
node.AppendChild(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddHtmlMarkupTag(IElement node, string tag)
|
private void AddHtmlMarkupTag(IDocument document, IElement node, string tag)
|
||||||
{
|
{
|
||||||
if (flags.SupportsHtmlFormatting.Value) return;
|
if (SupportsHtmlFormatting.Value) return;
|
||||||
var el = CreateElement(tag);
|
var el = document.CreateElement(tag);
|
||||||
node.AppendChild(el);
|
node.AppendChild(el);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,6 @@ public static class StartupHelpers
|
||||||
{
|
{
|
||||||
Console.WriteLine($"""
|
Console.WriteLine($"""
|
||||||
Usage: ./{typeof(Program).Assembly.GetName().Name} [options...]
|
Usage: ./{typeof(Program).Assembly.GetName().Name} [options...]
|
||||||
|
|
||||||
General options & commands:
|
|
||||||
-h, -?, --help Prints information on available command line arguments.
|
-h, -?, --help Prints information on available command line arguments.
|
||||||
--migrate Applies pending migrations.
|
--migrate Applies pending migrations.
|
||||||
--migrate-and-start Applies pending migrations, then starts the application.
|
--migrate-and-start Applies pending migrations, then starts the application.
|
||||||
|
@ -28,15 +26,6 @@ public static class StartupHelpers
|
||||||
instead of http on the specified port.
|
instead of http on the specified port.
|
||||||
--environment <env> Specifies the ASP.NET Core environment. Available options
|
--environment <env> Specifies the ASP.NET Core environment. Available options
|
||||||
are 'Development' and 'Production'.
|
are 'Development' and 'Production'.
|
||||||
|
|
||||||
User management commands:
|
|
||||||
--create-user <username> Creates a new user with the specified username
|
|
||||||
and a randomly generated password.
|
|
||||||
--create-admin-user <username> Creates a new admin user with the specified
|
|
||||||
username and a randomly generated password.
|
|
||||||
--reset-password <username> Resets the password of the specified user.
|
|
||||||
--grant-admin <username> Grants admin privileges to the specified user.
|
|
||||||
--revoke-admin <username> Revokes admin privileges of the specified user.
|
|
||||||
""");
|
""");
|
||||||
Environment.Exit(0);
|
Environment.Exit(0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ 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.Helpers;
|
using Iceshrimp.Backend.Core.Helpers;
|
||||||
|
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
|
||||||
using Iceshrimp.Backend.Core.Services;
|
using Iceshrimp.Backend.Core.Services;
|
||||||
using Microsoft.AspNetCore.Components.Endpoints;
|
using Microsoft.AspNetCore.Components.Endpoints;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@ -12,7 +13,7 @@ namespace Iceshrimp.Backend.Core.Middleware;
|
||||||
public class AuthenticationMiddleware(
|
public class AuthenticationMiddleware(
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
UserService userSvc,
|
UserService userSvc,
|
||||||
FlagService flags
|
MfmConverter mfmConverter
|
||||||
) : ConditionalMiddleware<AuthenticateAttribute>, IMiddlewareService
|
) : ConditionalMiddleware<AuthenticateAttribute>, IMiddlewareService
|
||||||
{
|
{
|
||||||
public static ServiceLifetime Lifetime => ServiceLifetime.Scoped;
|
public static ServiceLifetime Lifetime => ServiceLifetime.Scoped;
|
||||||
|
@ -78,9 +79,8 @@ public class AuthenticationMiddleware(
|
||||||
userSvc.UpdateOauthTokenMetadata(oauthToken);
|
userSvc.UpdateOauthTokenMetadata(oauthToken);
|
||||||
ctx.SetOauthToken(oauthToken);
|
ctx.SetOauthToken(oauthToken);
|
||||||
|
|
||||||
flags.SupportsHtmlFormatting.Value = oauthToken.SupportsHtmlFormatting;
|
mfmConverter.SupportsHtmlFormatting.Value = oauthToken.SupportsHtmlFormatting;
|
||||||
flags.SupportsInlineMedia.Value = oauthToken.SupportsInlineMedia;
|
mfmConverter.SupportsInlineMedia.Value = oauthToken.SupportsInlineMedia;
|
||||||
flags.IsPleroma.Value = oauthToken.IsPleroma;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -5,11 +5,10 @@ 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.Federation.Cryptography;
|
using Iceshrimp.Backend.Core.Federation.Cryptography;
|
||||||
|
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
|
||||||
using Iceshrimp.Backend.Core.Services;
|
using Iceshrimp.Backend.Core.Services;
|
||||||
using Iceshrimp.Utils.Common;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.Net.Http.Headers;
|
|
||||||
using static Iceshrimp.Backend.Core.Federation.ActivityPub.UserResolver;
|
using static Iceshrimp.Backend.Core.Federation.ActivityPub.UserResolver;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Middleware;
|
namespace Iceshrimp.Backend.Core.Middleware;
|
||||||
|
@ -24,7 +23,7 @@ public class AuthorizedFetchMiddleware(
|
||||||
ActivityPub.FederationControlService fedCtrlSvc,
|
ActivityPub.FederationControlService fedCtrlSvc,
|
||||||
ILogger<AuthorizedFetchMiddleware> logger,
|
ILogger<AuthorizedFetchMiddleware> logger,
|
||||||
IHostApplicationLifetime appLifetime,
|
IHostApplicationLifetime appLifetime,
|
||||||
FlagService flags
|
MfmConverter mfmConverter
|
||||||
) : ConditionalMiddleware<AuthorizedFetchAttribute>, IMiddlewareService
|
) : ConditionalMiddleware<AuthorizedFetchAttribute>, IMiddlewareService
|
||||||
{
|
{
|
||||||
public static ServiceLifetime Lifetime => ServiceLifetime.Scoped;
|
public static ServiceLifetime Lifetime => ServiceLifetime.Scoped;
|
||||||
|
@ -35,56 +34,28 @@ public class AuthorizedFetchMiddleware(
|
||||||
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
|
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
|
||||||
{
|
{
|
||||||
// Ensure we're rendering HTML markup (AsyncLocal)
|
// Ensure we're rendering HTML markup (AsyncLocal)
|
||||||
flags.SupportsHtmlFormatting.Value = true;
|
mfmConverter.SupportsHtmlFormatting.Value = true;
|
||||||
flags.SupportsInlineMedia.Value = true;
|
mfmConverter.SupportsInlineMedia.Value = true;
|
||||||
|
|
||||||
// Short-circuit fetches when signature validation is disabled
|
if (!config.Value.AuthorizedFetch)
|
||||||
if (config.Value is { AuthorizedFetch: false, ValidateRequestSignatures: false })
|
|
||||||
{
|
{
|
||||||
await next(ctx);
|
await next(ctx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short-circuit instance & relay actor fetches
|
|
||||||
_instanceActorUri ??= $"/users/{(await systemUserSvc.GetInstanceActorAsync()).Id}";
|
|
||||||
_relayActorUri ??= $"/users/{(await systemUserSvc.GetRelayActorAsync()).Id}";
|
|
||||||
if (ctx.Request.Path.Value == _instanceActorUri || ctx.Request.Path.Value == _relayActorUri)
|
|
||||||
{
|
|
||||||
await next(ctx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent authenticated responses from ending up in caches, and prevent unauthenticated responses
|
|
||||||
// from being returned for authenticated requests
|
|
||||||
if (ctx.Request.Headers.ContainsKey("Signature"))
|
|
||||||
ctx.Response.Headers.CacheControl = "private, no-store";
|
ctx.Response.Headers.CacheControl = "private, no-store";
|
||||||
else
|
|
||||||
ctx.Response.Headers.Append(HeaderNames.Vary, "Signature");
|
|
||||||
|
|
||||||
var res = await ValidateSignatureAsync(ctx);
|
|
||||||
|
|
||||||
// We want to reject blocked instances even if authorized fetch is disabled
|
|
||||||
if (res.TryGetError(out var error) && error is InstanceBlockedException)
|
|
||||||
throw error;
|
|
||||||
|
|
||||||
if (res.TryGetResult(out var user) && user.Value != null)
|
|
||||||
{
|
|
||||||
ctx.SetActor(user.Value);
|
|
||||||
}
|
|
||||||
else if (config.Value.AuthorizedFetch)
|
|
||||||
{
|
|
||||||
if (error != null) throw error;
|
|
||||||
throw new GracefulException(HttpStatusCode.Forbidden, "Request signature validation failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
await next(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Result<Optional<User>>> ValidateSignatureAsync(HttpContext ctx)
|
|
||||||
{
|
|
||||||
var request = ctx.Request;
|
var request = ctx.Request;
|
||||||
var ct = appLifetime.ApplicationStopping;
|
var ct = appLifetime.ApplicationStopping;
|
||||||
|
|
||||||
|
_instanceActorUri ??= $"/users/{(await systemUserSvc.GetInstanceActorAsync()).Id}";
|
||||||
|
_relayActorUri ??= $"/users/{(await systemUserSvc.GetRelayActorAsync()).Id}";
|
||||||
|
if (request.Path.Value == _instanceActorUri || request.Path.Value == _relayActorUri)
|
||||||
|
{
|
||||||
|
await next(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
UserPublickey? key = null;
|
UserPublickey? key = null;
|
||||||
var verified = false;
|
var verified = false;
|
||||||
|
|
||||||
|
@ -93,15 +64,17 @@ public class AuthorizedFetchMiddleware(
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!request.Headers.TryGetValue("signature", out var sigHeader))
|
if (!request.Headers.TryGetValue("signature", out var sigHeader))
|
||||||
return new GracefulException(HttpStatusCode.Unauthorized, "Request is missing the signature header");
|
throw new GracefulException(HttpStatusCode.Unauthorized, "Request is missing the signature header");
|
||||||
|
|
||||||
var sig = HttpSignature.Parse(sigHeader.ToString());
|
var sig = HttpSignature.Parse(sigHeader.ToString());
|
||||||
|
|
||||||
if (await fedCtrlSvc.ShouldBlockAsync(sig.KeyId))
|
if (await fedCtrlSvc.ShouldBlockAsync(sig.KeyId))
|
||||||
return new InstanceBlockedException();
|
throw new GracefulException(HttpStatusCode.Forbidden, "Forbidden", "Instance is blocked",
|
||||||
|
suppressLog: true);
|
||||||
|
|
||||||
// First, we check if we already have the key
|
// First, we check if we already have the key
|
||||||
key = await db.UserPublickeys.Include(p => p.User).FirstOrDefaultAsync(p => p.KeyId == sig.KeyId, ct);
|
key = await db.UserPublickeys.Include(p => p.User)
|
||||||
|
.FirstOrDefaultAsync(p => p.KeyId == sig.KeyId, ct);
|
||||||
|
|
||||||
// If we don't, we need to try to fetch it
|
// If we don't, we need to try to fetch it
|
||||||
if (key == null)
|
if (key == null)
|
||||||
|
@ -109,26 +82,31 @@ public class AuthorizedFetchMiddleware(
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var user = await userResolver.ResolveAsync(sig.KeyId, ResolveFlags.Uri).WaitAsync(ct);
|
var user = await userResolver.ResolveAsync(sig.KeyId, ResolveFlags.Uri).WaitAsync(ct);
|
||||||
key = await db.UserPublickeys.Include(p => p.User).FirstOrDefaultAsync(p => p.User == user, ct);
|
key = await db.UserPublickeys.Include(p => p.User)
|
||||||
|
.FirstOrDefaultAsync(p => p.User == user, ct);
|
||||||
|
|
||||||
// If the key is still null here, we have a data consistency issue and need to update the key manually
|
// If the key is still null here, we have a data consistency issue and need to update the key manually
|
||||||
key ??= await userSvc.UpdateUserPublicKeyAsync(user).WaitAsync(ct);
|
key ??= await userSvc.UpdateUserPublicKeyAsync(user).WaitAsync(ct);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
if (e is GracefulException) return e;
|
if (e is GracefulException) throw;
|
||||||
return new Exception($"Failed to fetch key of signature user ({sig.KeyId}) - {e.Message}");
|
throw new Exception($"Failed to fetch key of signature user ({sig.KeyId}) - {e.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we still don't have the key, something went wrong and we need to throw an exception
|
||||||
|
if (key == null) throw new Exception($"Failed to fetch key of signature user ({sig.KeyId})");
|
||||||
|
|
||||||
if (key.User.IsSuspended)
|
if (key.User.IsSuspended)
|
||||||
return GracefulException.Forbidden("User is suspended");
|
throw GracefulException.Forbidden("User is suspended");
|
||||||
if (key.User.IsLocalUser)
|
if (key.User.IsLocalUser)
|
||||||
return new Exception("Remote user must have a host");
|
throw new Exception("Remote user must have a host");
|
||||||
|
|
||||||
// We want to check both the user host & the keyId host (as account & web domain might be different)
|
// We want to check both the user host & the keyId host (as account & web domain might be different)
|
||||||
if (await fedCtrlSvc.ShouldBlockAsync(key.User.Host, key.KeyId))
|
if (await fedCtrlSvc.ShouldBlockAsync(key.User.Host, key.KeyId))
|
||||||
return new InstanceBlockedException();
|
throw new GracefulException(HttpStatusCode.Forbidden, "Forbidden", "Instance is blocked",
|
||||||
|
suppressLog: true);
|
||||||
|
|
||||||
List<string> headers = ["(request-target)", "host"];
|
List<string> headers = ["(request-target)", "host"];
|
||||||
|
|
||||||
|
@ -150,19 +128,18 @@ public class AuthorizedFetchMiddleware(
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
if (e is AuthFetchException afe) return GracefulException.Accepted(afe.Message);
|
if (e is AuthFetchException afe) throw GracefulException.Accepted(afe.Message);
|
||||||
if (e is GracefulException { SuppressLog: true }) return e;
|
if (e is GracefulException { SuppressLog: true }) throw;
|
||||||
logger.LogDebug("Error validating HTTP signature: {error}", e.Message);
|
logger.LogDebug("Error validating HTTP signature: {error}", e.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!verified || key == null)
|
if (!verified || key == null)
|
||||||
return new Optional<User>(null);
|
throw new GracefulException(HttpStatusCode.Forbidden, "Request signature validation failed");
|
||||||
|
|
||||||
return new Optional<User>(key.User);
|
ctx.SetActor(key.User);
|
||||||
|
|
||||||
|
await next(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class InstanceBlockedException() : GracefulException(HttpStatusCode.Forbidden, "Forbidden",
|
|
||||||
"Instance is blocked", suppressLog: true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AuthorizedFetchAttribute : Attribute;
|
public class AuthorizedFetchAttribute : Attribute;
|
||||||
|
|
|
@ -6,41 +6,14 @@ namespace Iceshrimp.Backend.Core.Services;
|
||||||
|
|
||||||
public class CronService(IServiceScopeFactory serviceScopeFactory) : BackgroundService
|
public class CronService(IServiceScopeFactory serviceScopeFactory) : BackgroundService
|
||||||
{
|
{
|
||||||
public CronTaskState[] Tasks { get; private set; } = [];
|
|
||||||
|
|
||||||
public async Task RunCronTaskAsync(ICronTask task, ICronTrigger trigger)
|
|
||||||
{
|
|
||||||
if (trigger.IsRunning) return;
|
|
||||||
trigger.IsRunning = true;
|
|
||||||
trigger.Error = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await using var scope = serviceScopeFactory.CreateAsyncScope();
|
|
||||||
await task.InvokeAsync(scope.ServiceProvider);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
trigger.Error = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
trigger.LastRun = DateTime.UtcNow;
|
|
||||||
trigger.IsRunning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Task ExecuteAsync(CancellationToken token)
|
protected override Task ExecuteAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
var tasks = PluginLoader.Assemblies
|
var tasks = PluginLoader
|
||||||
.Prepend(Assembly.GetExecutingAssembly())
|
.Assemblies.Prepend(Assembly.GetExecutingAssembly())
|
||||||
.SelectMany(AssemblyLoader.GetImplementationsOfInterface<ICronTask>)
|
.SelectMany(AssemblyLoader.GetImplementationsOfInterface<ICronTask>)
|
||||||
.OrderBy(p => p.AssemblyQualifiedName)
|
|
||||||
.ThenBy(p => p.Name)
|
|
||||||
.Select(p => Activator.CreateInstance(p) as ICronTask)
|
.Select(p => Activator.CreateInstance(p) as ICronTask)
|
||||||
.Where(p => p != null)
|
.Where(p => p != null)
|
||||||
.Cast<ICronTask>()
|
.Cast<ICronTask>();
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
List<CronTaskState> stateObjs = [];
|
|
||||||
|
|
||||||
foreach (var task in tasks)
|
foreach (var task in tasks)
|
||||||
{
|
{
|
||||||
|
@ -51,33 +24,24 @@ public class CronService(IServiceScopeFactory serviceScopeFactory) : BackgroundS
|
||||||
_ => throw new ArgumentOutOfRangeException()
|
_ => throw new ArgumentOutOfRangeException()
|
||||||
};
|
};
|
||||||
|
|
||||||
trigger.OnTrigger += async void (state) =>
|
trigger.OnTrigger += async void () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await RunCronTaskAsync(task, state);
|
await using var scope = serviceScopeFactory.CreateAsyncScope();
|
||||||
|
await task.InvokeAsync(scope.ServiceProvider);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// ignored (errors in the event handler crash the host process)
|
// ignored (errors in the event handler crash the host process)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
stateObjs.Add(new CronTaskState { Task = task, Trigger = trigger });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Tasks = stateObjs.ToArray();
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CronTaskState
|
|
||||||
{
|
|
||||||
public required ICronTask Task;
|
|
||||||
public required ICronTrigger Trigger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface ICronTask
|
public interface ICronTask
|
||||||
{
|
{
|
||||||
public TimeSpan Trigger { get; }
|
public TimeSpan Trigger { get; }
|
||||||
|
@ -91,59 +55,36 @@ public enum CronTaskType
|
||||||
Interval
|
Interval
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ICronTrigger
|
file interface ICronTrigger
|
||||||
{
|
{
|
||||||
public event Action<ICronTrigger>? OnTrigger;
|
public event Action? OnTrigger;
|
||||||
|
|
||||||
public DateTime NextTrigger { get; }
|
|
||||||
public DateTime? LastRun { get; set; }
|
|
||||||
public bool IsRunning { get; set; }
|
|
||||||
public Exception? Error { get; set; }
|
|
||||||
|
|
||||||
public TimeSpan UpdateNextTrigger();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DailyTrigger : ICronTrigger, IDisposable
|
file class DailyTrigger : ICronTrigger, IDisposable
|
||||||
{
|
{
|
||||||
public DailyTrigger(TimeSpan triggerTime, CancellationToken cancellationToken)
|
public DailyTrigger(TimeSpan triggerTime, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
TriggerTime = triggerTime;
|
TriggerTime = triggerTime;
|
||||||
CancellationToken = cancellationToken;
|
CancellationToken = cancellationToken;
|
||||||
NextTrigger = DateTime.UtcNow;
|
|
||||||
|
|
||||||
RunningTask = Task.Factory.StartNew(async () =>
|
RunningTask = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
while (!CancellationToken.IsCancellationRequested)
|
while (!CancellationToken.IsCancellationRequested)
|
||||||
{
|
|
||||||
var nextTrigger = UpdateNextTrigger();
|
|
||||||
await Task.Delay(nextTrigger, CancellationToken);
|
|
||||||
OnTrigger?.Invoke(this);
|
|
||||||
}
|
|
||||||
}, CancellationToken, TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
|
||||||
}
|
|
||||||
|
|
||||||
public TimeSpan UpdateNextTrigger()
|
|
||||||
{
|
{
|
||||||
var nextTrigger = DateTime.Today + TriggerTime - DateTime.Now;
|
var nextTrigger = DateTime.Today + TriggerTime - DateTime.Now;
|
||||||
if (nextTrigger < TimeSpan.Zero)
|
if (nextTrigger < TimeSpan.Zero)
|
||||||
nextTrigger = nextTrigger.Add(new TimeSpan(24, 0, 0));
|
nextTrigger = nextTrigger.Add(new TimeSpan(24, 0, 0));
|
||||||
|
await Task.Delay(nextTrigger, CancellationToken);
|
||||||
NextTrigger = DateTime.UtcNow + nextTrigger;
|
OnTrigger?.Invoke();
|
||||||
|
}
|
||||||
return nextTrigger;
|
}, CancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private TimeSpan TriggerTime { get; }
|
private TimeSpan TriggerTime { get; }
|
||||||
private CancellationToken CancellationToken { get; }
|
private CancellationToken CancellationToken { get; }
|
||||||
private Task RunningTask { get; set; }
|
private Task RunningTask { get; set; }
|
||||||
|
|
||||||
public event Action<ICronTrigger>? OnTrigger;
|
public event Action? OnTrigger;
|
||||||
|
|
||||||
public DateTime NextTrigger { get; set; }
|
|
||||||
public DateTime? LastRun { get; set; }
|
|
||||||
public bool IsRunning { get; set; }
|
|
||||||
|
|
||||||
public Exception? Error { get; set; } = null;
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
@ -155,49 +96,28 @@ public class DailyTrigger : ICronTrigger, IDisposable
|
||||||
~DailyTrigger() => Dispose();
|
~DailyTrigger() => Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class IntervalTrigger : ICronTrigger, IDisposable
|
file class IntervalTrigger : ICronTrigger, IDisposable
|
||||||
{
|
{
|
||||||
public IntervalTrigger(TimeSpan triggerInterval, CancellationToken cancellationToken)
|
public IntervalTrigger(TimeSpan triggerInterval, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
TriggerInterval = triggerInterval;
|
TriggerInterval = triggerInterval;
|
||||||
CancellationToken = cancellationToken;
|
CancellationToken = cancellationToken;
|
||||||
NextTrigger = DateTime.UtcNow + TriggerInterval;
|
|
||||||
|
|
||||||
RunningTask = Task.Factory.StartNew(async () =>
|
RunningTask = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
while (!CancellationToken.IsCancellationRequested)
|
while (!CancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
UpdateNextTrigger();
|
|
||||||
await Task.Delay(TriggerInterval, CancellationToken);
|
await Task.Delay(TriggerInterval, CancellationToken);
|
||||||
|
OnTrigger?.Invoke();
|
||||||
while (LastRun != null && LastRun + TriggerInterval + TimeSpan.FromMinutes(5) < DateTime.UtcNow)
|
|
||||||
{
|
|
||||||
NextTrigger = LastRun.Value + TriggerInterval;
|
|
||||||
await Task.Delay(NextTrigger - DateTime.UtcNow);
|
|
||||||
}
|
}
|
||||||
|
}, CancellationToken);
|
||||||
OnTrigger?.Invoke(this);
|
|
||||||
}
|
|
||||||
}, CancellationToken, TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
|
||||||
}
|
|
||||||
|
|
||||||
public TimeSpan UpdateNextTrigger()
|
|
||||||
{
|
|
||||||
NextTrigger = DateTime.UtcNow + TriggerInterval;
|
|
||||||
return TriggerInterval;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private TimeSpan TriggerInterval { get; }
|
private TimeSpan TriggerInterval { get; }
|
||||||
private CancellationToken CancellationToken { get; }
|
private CancellationToken CancellationToken { get; }
|
||||||
private Task RunningTask { get; set; }
|
private Task RunningTask { get; set; }
|
||||||
|
|
||||||
public event Action<ICronTrigger>? OnTrigger;
|
public event Action? OnTrigger;
|
||||||
|
|
||||||
public DateTime NextTrigger { get; set; }
|
|
||||||
public DateTime? LastRun { get; set; }
|
|
||||||
public bool IsRunning { get; set; }
|
|
||||||
|
|
||||||
public Exception? Error { get; set; } = null;
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|
|
@ -120,7 +120,7 @@ public class DriveService(
|
||||||
? storageConfig.Value.MaxCacheSizeBytes
|
? storageConfig.Value.MaxCacheSizeBytes
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
var stream = await input.GetSafeStreamOrNullAsync(maxLength, res.Content.Headers.ContentLength);
|
var stream = await GetSafeStreamOrNullAsync(input, maxLength, res.Content.Headers.ContentLength);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await StoreFileAsync(stream, user, request, skipImageProcessing);
|
return await StoreFileAsync(stream, user, request, skipImageProcessing);
|
||||||
|
@ -629,6 +629,37 @@ public class DriveService(
|
||||||
int GetTargetRes() => config.TargetRes ?? throw new Exception("TargetRes is required to encode images");
|
int GetTargetRes() => config.TargetRes ?? throw new Exception("TargetRes is required to encode images");
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// We can't trust the Content-Length header, and it might be null.
|
||||||
|
/// This makes sure that we only ever read up to maxLength into memory.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stream">The response content stream</param>
|
||||||
|
/// <param name="maxLength">The maximum length to buffer (null = unlimited)</param>
|
||||||
|
/// <param name="contentLength">The content length, if known</param>
|
||||||
|
/// <param name="token">A CancellationToken, if applicable</param>
|
||||||
|
/// <returns>Either a buffered MemoryStream, or Stream.Null</returns>
|
||||||
|
private static async Task<Stream> GetSafeStreamOrNullAsync(
|
||||||
|
Stream stream, long? maxLength, long? contentLength, CancellationToken token = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (maxLength is 0) return Stream.Null;
|
||||||
|
if (contentLength > maxLength) return Stream.Null;
|
||||||
|
|
||||||
|
MemoryStream buf = new();
|
||||||
|
if (contentLength < maxLength)
|
||||||
|
maxLength = contentLength.Value;
|
||||||
|
|
||||||
|
await stream.CopyToAsync(buf, maxLength, token);
|
||||||
|
if (maxLength == null || buf.Length <= maxLength)
|
||||||
|
{
|
||||||
|
buf.Seek(0, SeekOrigin.Begin);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
await buf.DisposeAsync();
|
||||||
|
return Stream.Null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DriveFileCreationRequest
|
public class DriveFileCreationRequest
|
||||||
|
|
|
@ -28,7 +28,7 @@ public partial class EmojiService(
|
||||||
});
|
});
|
||||||
|
|
||||||
public async Task<Emoji> CreateEmojiFromStreamAsync(
|
public async Task<Emoji> CreateEmojiFromStreamAsync(
|
||||||
Stream input, string fileName, string mimeType, List<string>? tags = null,
|
Stream input, string fileName, string mimeType, List<string>? aliases = null,
|
||||||
string? category = null
|
string? category = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
@ -51,7 +51,7 @@ public partial class EmojiService(
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
Name = name,
|
Name = name,
|
||||||
Tags = tags ?? [],
|
Aliases = aliases ?? [],
|
||||||
Category = category,
|
Category = category,
|
||||||
UpdatedAt = DateTime.UtcNow,
|
UpdatedAt = DateTime.UtcNow,
|
||||||
OriginalUrl = driveFile.Url,
|
OriginalUrl = driveFile.Url,
|
||||||
|
@ -118,39 +118,10 @@ public partial class EmojiService(
|
||||||
|
|
||||||
var resolved = await db.Emojis.Where(p => p.Host == host && emoji.Select(e => e.Name).Contains(p.Name))
|
var resolved = await db.Emojis.Where(p => p.Host == host && emoji.Select(e => e.Name).Contains(p.Name))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
var existing = emoji.Where(p => resolved.Any(i => i.Name == p.Name))
|
|
||||||
.ToDictionary(p => p, p => resolved.First(i => i.Name == p.Name));
|
|
||||||
|
|
||||||
foreach (var emojo in emoji)
|
//TODO: handle updated emoji
|
||||||
|
foreach (var emojo in emoji.Where(emojo => resolved.All(p => p.Name != emojo.Name)))
|
||||||
{
|
{
|
||||||
// Update emoji entry if modified
|
|
||||||
if (existing.TryGetValue(emojo, out var hit))
|
|
||||||
{
|
|
||||||
var url = emojo.Image?.Url?.Link;
|
|
||||||
if (emojo.Image == null || string.IsNullOrWhiteSpace(url))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (
|
|
||||||
hit.OriginalUrl != url
|
|
||||||
|| hit.RawPublicUrl != url
|
|
||||||
|| hit.Type != emojo.Image.MediaType
|
|
||||||
|| hit.Uri != emojo.Id
|
|
||||||
)
|
|
||||||
{
|
|
||||||
using (await KeyedLocker.LockAsync($"emoji:{host}:{emojo.Name}"))
|
|
||||||
{
|
|
||||||
await db.ReloadEntityAsync(hit);
|
|
||||||
hit.OriginalUrl = url;
|
|
||||||
hit.RawPublicUrl = url;
|
|
||||||
hit.Type = emojo.Image.MediaType;
|
|
||||||
hit.Uri = emojo.Id;
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
using (await KeyedLocker.LockAsync($"emoji:{host}:{emojo.Name}"))
|
using (await KeyedLocker.LockAsync($"emoji:{host}:{emojo.Name}"))
|
||||||
{
|
{
|
||||||
var dbEmojo = await db.Emojis.FirstOrDefaultAsync(p => p.Host == host && p.Name == emojo.Name);
|
var dbEmojo = await db.Emojis.FirstOrDefaultAsync(p => p.Host == host && p.Name == emojo.Name);
|
||||||
|
@ -253,24 +224,12 @@ public partial class EmojiService(
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Emoji?> UpdateLocalEmojiAsync(
|
public async Task<Emoji?> UpdateLocalEmojiAsync(
|
||||||
string id, string? name, List<string>? tags, string? category, string? license, bool? sensitive
|
string id, string? name, List<string>? aliases, string? category, string? license, bool? sensitive
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var emoji = await db.Emojis.FirstOrDefaultAsync(p => p.Id == id);
|
var emoji = await db.Emojis.FirstOrDefaultAsync(p => p.Id == id);
|
||||||
if (emoji == null) return null;
|
if (emoji == null) return null;
|
||||||
|
if (emoji.Host != null) return null;
|
||||||
if (emoji.Host != null)
|
|
||||||
{
|
|
||||||
// Only allow changing remote emoji sensitive status
|
|
||||||
if (!sensitive.HasValue)
|
|
||||||
throw GracefulException.BadRequest("Only sensitive can be updated on remote emojis");
|
|
||||||
|
|
||||||
emoji.Sensitive = sensitive.Value;
|
|
||||||
emoji.UpdatedAt = DateTime.UtcNow;
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return emoji;
|
|
||||||
}
|
|
||||||
|
|
||||||
emoji.UpdatedAt = DateTime.UtcNow;
|
emoji.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
@ -282,8 +241,8 @@ public partial class EmojiService(
|
||||||
emoji.Uri = emoji.GetPublicUri(config.Value);
|
emoji.Uri = emoji.GetPublicUri(config.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tags != null)
|
if (aliases != null)
|
||||||
emoji.Tags = tags.Select(p => p.Trim()).Where(p => !string.IsNullOrWhiteSpace(p)).ToList();
|
emoji.Aliases = aliases.Select(p => p.Trim()).Where(p => !string.IsNullOrWhiteSpace(p)).ToList();
|
||||||
|
|
||||||
// If category is provided but empty reset to null
|
// If category is provided but empty reset to null
|
||||||
if (category != null) emoji.Category = string.IsNullOrEmpty(category) ? null : category;
|
if (category != null) emoji.Category = string.IsNullOrEmpty(category) ? null : category;
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Services;
|
|
||||||
|
|
||||||
[UsedImplicitly]
|
|
||||||
public class FlagService : ISingletonService
|
|
||||||
{
|
|
||||||
public AsyncLocal<bool> SupportsHtmlFormatting { get; } = new();
|
|
||||||
public AsyncLocal<bool> SupportsInlineMedia { get; } = new();
|
|
||||||
public AsyncLocal<bool> IsPleroma { get; } = new();
|
|
||||||
}
|
|
|
@ -480,12 +480,8 @@ public class NoteService(
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var updateLastNoteTimestamp = create && (note.User.LastNoteAt == null || note.CreatedAt > note.User.LastNoteAt);
|
|
||||||
|
|
||||||
await db.Users.Where(p => p.Id == note.User.Id)
|
await db.Users.Where(p => p.Id == note.User.Id)
|
||||||
.ExecuteUpdateAsync(p => p
|
.ExecuteUpdateAsync(p => p.SetProperty(u => u.NotesCount, u => u.NotesCount + diff));
|
||||||
.SetProperty(u => u.NotesCount, u => u.NotesCount + diff)
|
|
||||||
.SetProperty(u => u.LastNoteAt, u => updateLastNoteTimestamp ? note.CreatedAt : u.LastNoteAt));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.Reply != null)
|
if (note.Reply != null)
|
||||||
|
@ -1028,7 +1024,7 @@ public class NoteService(
|
||||||
.ToList()
|
.ToList()
|
||||||
?? [];
|
?? [];
|
||||||
|
|
||||||
(text, htmlInlineMedia) = MfmConverter.FromHtml(note.Content, mentionData.Mentions, hashtags);
|
(text, htmlInlineMedia) = await MfmConverter.FromHtmlAsync(note.Content, mentionData.Mentions, hashtags);
|
||||||
}
|
}
|
||||||
|
|
||||||
var cw = note.Summary;
|
var cw = note.Summary;
|
||||||
|
@ -1128,7 +1124,7 @@ public class NoteService(
|
||||||
.ToList()
|
.ToList()
|
||||||
?? [];
|
?? [];
|
||||||
|
|
||||||
(text, htmlInlineMedia) = MfmConverter.FromHtml(note.Content, mentionData.Mentions, hashtags);
|
(text, htmlInlineMedia) = await MfmConverter.FromHtmlAsync(note.Content, mentionData.Mentions, hashtags);
|
||||||
}
|
}
|
||||||
|
|
||||||
var cw = note.Summary;
|
var cw = note.Summary;
|
||||||
|
|
|
@ -104,10 +104,7 @@ public class ObjectStorageService(IOptions<Config.StorageSection> config, HttpCl
|
||||||
|
|
||||||
public Uri GetFilePublicUrl(string filename)
|
public Uri GetFilePublicUrl(string filename)
|
||||||
{
|
{
|
||||||
var accessUrl = _accessUrl ?? throw new Exception("Invalid object storage access url");
|
var baseUri = new Uri(_accessUrl ?? throw new Exception("Invalid object storage access url"));
|
||||||
if (!accessUrl.EndsWith('/'))
|
|
||||||
accessUrl += '/';
|
|
||||||
var baseUri = new Uri(accessUrl);
|
|
||||||
return new Uri(baseUri, GetKeyWithPrefix(filename));
|
return new Uri(baseUri, GetKeyWithPrefix(filename));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,8 +57,8 @@ public class QueueService(
|
||||||
logger.LogInformation("Queue shutdown complete.");
|
logger.LogInformation("Queue shutdown complete.");
|
||||||
});
|
});
|
||||||
|
|
||||||
_ = Task.Factory.StartNew(ExecuteHealthchecksWorkerAsync, token, TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
_ = Task.Run(ExecuteHealthchecksWorkerAsync, token);
|
||||||
await Task.Factory.StartNew(ExecuteBackgroundWorkersAsync, tokenSource.Token, TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
await Task.Run(ExecuteBackgroundWorkersAsync, tokenSource.Token);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
using Iceshrimp.Backend.Core.Database;
|
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
|
||||||
using Iceshrimp.Backend.Core.Helpers;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Services;
|
|
||||||
|
|
||||||
public class ReportService(
|
|
||||||
ActivityPub.ActivityDeliverService deliverSvc,
|
|
||||||
ActivityPub.ActivityRenderer activityRenderer,
|
|
||||||
SystemUserService sysUserSvc,
|
|
||||||
DatabaseContext db
|
|
||||||
) : IScopedService
|
|
||||||
{
|
|
||||||
public async Task ForwardReportAsync(Report report, string? comment)
|
|
||||||
{
|
|
||||||
if (report.TargetUser.IsLocalUser)
|
|
||||||
throw new Exception("Refusing to forward report to local instance");
|
|
||||||
|
|
||||||
var actor = await sysUserSvc.GetInstanceActorAsync();
|
|
||||||
var activity = activityRenderer.RenderFlag(actor, report.TargetUser, report.Notes, comment ?? report.Comment);
|
|
||||||
|
|
||||||
var inbox = report.TargetUser.SharedInbox
|
|
||||||
?? report.TargetUser.Inbox
|
|
||||||
?? throw new Exception("Target user does not have inbox");
|
|
||||||
|
|
||||||
await deliverSvc.DeliverToAsync(activity, actor, inbox);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Report> CreateReportAsync(User reporter, User target, IEnumerable<Note> notes, string comment)
|
|
||||||
{
|
|
||||||
var report = new Report
|
|
||||||
{
|
|
||||||
Id = IdHelpers.GenerateSnowflakeId(),
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
TargetUser = target,
|
|
||||||
TargetUserHost = target.Host,
|
|
||||||
Reporter = reporter,
|
|
||||||
ReporterHost = reporter.Host,
|
|
||||||
Notes = notes.ToArray(),
|
|
||||||
Comment = comment
|
|
||||||
};
|
|
||||||
|
|
||||||
db.Add(report);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
await db.ReloadEntityRecursivelyAsync(report);
|
|
||||||
return report;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -21,17 +21,6 @@ public class StorageMaintenanceService(
|
||||||
{
|
{
|
||||||
public async Task MigrateLocalFilesAsync(bool purge)
|
public async Task MigrateLocalFilesAsync(bool purge)
|
||||||
{
|
{
|
||||||
try
|
|
||||||
{
|
|
||||||
await objectStorageSvc.VerifyCredentialsAsync();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
logger.LogCritical("Failed to initialize object storage: {message}", e.Message);
|
|
||||||
Environment.Exit(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var pathBase = options.Value.Local?.Path;
|
var pathBase = options.Value.Local?.Path;
|
||||||
var pathsToDelete = new ConcurrentBag<string>();
|
var pathsToDelete = new ConcurrentBag<string>();
|
||||||
var failed = new ConcurrentBag<string>();
|
var failed = new ConcurrentBag<string>();
|
||||||
|
|
|
@ -27,10 +27,11 @@ public class UserProfileMentionsResolver(
|
||||||
?? [];
|
?? [];
|
||||||
|
|
||||||
if (fields is not { Count: > 0 } && (actor.MkSummary ?? actor.Summary) == null) return ([], []);
|
if (fields is not { Count: > 0 } && (actor.MkSummary ?? actor.Summary) == null) return ([], []);
|
||||||
var parsedFields = fields.SelectMany<ASField, string?>(p => [p.Name, p.Value])
|
var parsedFields = await fields.SelectMany<ASField, string?>(p => [p.Name, p.Value])
|
||||||
.Select(MfmConverter.ExtractMentionsFromHtml);
|
.Select(async p => await MfmConverter.ExtractMentionsFromHtmlAsync(p))
|
||||||
|
.AwaitAllAsync();
|
||||||
|
|
||||||
var parsedBio = actor.MkSummary == null ? MfmConverter.ExtractMentionsFromHtml(actor.Summary) : [];
|
var parsedBio = actor.MkSummary == null ? await MfmConverter.ExtractMentionsFromHtmlAsync(actor.Summary) : [];
|
||||||
|
|
||||||
var userUris = parsedFields.Prepend(parsedBio).SelectMany(p => p).ToList();
|
var userUris = parsedFields.Prepend(parsedBio).SelectMany(p => p).ToList();
|
||||||
var mentionNodes = new List<MfmMentionNode>();
|
var mentionNodes = new List<MfmMentionNode>();
|
||||||
|
|
|
@ -148,12 +148,16 @@ public class UserService(
|
||||||
|
|
||||||
var emoji = await emojiSvc.ProcessEmojiAsync(actor.Tags?.OfType<ASEmoji>().ToList(), host);
|
var emoji = await emojiSvc.ProcessEmojiAsync(actor.Tags?.OfType<ASEmoji>().ToList(), host);
|
||||||
|
|
||||||
var fields = actor.Attachments?.OfType<ASField>()
|
var fields = actor.Attachments != null
|
||||||
|
? await actor.Attachments
|
||||||
|
.OfType<ASField>()
|
||||||
.Where(p => p is { Name: not null, Value: not null })
|
.Where(p => p is { Name: not null, Value: not null })
|
||||||
.Select(p => new UserProfile.Field
|
.Select(async p => new UserProfile.Field
|
||||||
{
|
{
|
||||||
Name = p.Name!, Value = MfmConverter.FromHtml(p.Value).Mfm
|
Name = p.Name!, Value = (await MfmConverter.FromHtmlAsync(p.Value)).Mfm
|
||||||
});
|
})
|
||||||
|
.AwaitAllAsync()
|
||||||
|
: null;
|
||||||
|
|
||||||
var pronouns = actor.Pronouns?.Values.ToDictionary(p => p.Key, p => p.Value ?? "");
|
var pronouns = actor.Pronouns?.Values.ToDictionary(p => p.Key, p => p.Value ?? "");
|
||||||
|
|
||||||
|
@ -166,7 +170,7 @@ public class UserService(
|
||||||
.ToList()
|
.ToList()
|
||||||
?? [];
|
?? [];
|
||||||
|
|
||||||
bio = MfmConverter.FromHtml(actor.Summary, hashtags: asHashtags).Mfm;
|
bio = (await MfmConverter.FromHtmlAsync(actor.Summary, hashtags: asHashtags)).Mfm;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tags = ResolveHashtags(MfmParser.Parse(bio), actor);
|
var tags = ResolveHashtags(MfmParser.Parse(bio), actor);
|
||||||
|
@ -314,12 +318,16 @@ public class UserService(
|
||||||
?? throw new
|
?? throw new
|
||||||
Exception("User host must not be null at this stage"));
|
Exception("User host must not be null at this stage"));
|
||||||
|
|
||||||
var fields = actor.Attachments?.OfType<ASField>()
|
var fields = actor.Attachments != null
|
||||||
|
? await actor.Attachments
|
||||||
|
.OfType<ASField>()
|
||||||
.Where(p => p is { Name: not null, Value: not null })
|
.Where(p => p is { Name: not null, Value: not null })
|
||||||
.Select(p => new UserProfile.Field
|
.Select(async p => new UserProfile.Field
|
||||||
{
|
{
|
||||||
Name = p.Name!, Value = MfmConverter.FromHtml(p.Value).Mfm
|
Name = p.Name!, Value = (await MfmConverter.FromHtmlAsync(p.Value)).Mfm
|
||||||
});
|
})
|
||||||
|
.AwaitAllAsync()
|
||||||
|
: null;
|
||||||
|
|
||||||
var pronouns = actor.Pronouns?.Values.ToDictionary(p => p.Key, p => p.Value ?? "");
|
var pronouns = actor.Pronouns?.Values.ToDictionary(p => p.Key, p => p.Value ?? "");
|
||||||
|
|
||||||
|
@ -340,7 +348,7 @@ public class UserService(
|
||||||
.ToList()
|
.ToList()
|
||||||
?? [];
|
?? [];
|
||||||
|
|
||||||
user.UserProfile.Description = MfmConverter.FromHtml(actor.Summary, hashtags: asHashtags).Mfm;
|
user.UserProfile.Description = (await MfmConverter.FromHtmlAsync(actor.Summary, hashtags: asHashtags)).Mfm;
|
||||||
}
|
}
|
||||||
|
|
||||||
//user.UserProfile.Birthday = TODO;
|
//user.UserProfile.Birthday = TODO;
|
||||||
|
@ -421,16 +429,15 @@ public class UserService(
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<User> CreateLocalUserAsync(string username, string password, string? invite, bool force = false)
|
public async Task<User> CreateLocalUserAsync(string username, string password, string? invite)
|
||||||
{
|
{
|
||||||
//TODO: invite system should allow multi-use invites & time limited invites
|
//TODO: invite system should allow multi-use invites & time limited invites
|
||||||
if (security.Value.Registrations == Enums.Registrations.Closed && !force)
|
if (security.Value.Registrations == Enums.Registrations.Closed)
|
||||||
throw new GracefulException(HttpStatusCode.Forbidden, "Registrations are disabled on this server");
|
throw new GracefulException(HttpStatusCode.Forbidden, "Registrations are disabled on this server");
|
||||||
if (security.Value.Registrations == Enums.Registrations.Invite && invite == null && !force)
|
if (security.Value.Registrations == Enums.Registrations.Invite && invite == null)
|
||||||
throw new GracefulException(HttpStatusCode.Forbidden, "Request is missing the invite code");
|
throw new GracefulException(HttpStatusCode.Forbidden, "Request is missing the invite code");
|
||||||
if (security.Value.Registrations == Enums.Registrations.Invite
|
if (security.Value.Registrations == Enums.Registrations.Invite
|
||||||
&& !await db.RegistrationInvites.AnyAsync(p => p.Code == invite)
|
&& !await db.RegistrationInvites.AnyAsync(p => p.Code == invite))
|
||||||
&& !force)
|
|
||||||
throw new GracefulException(HttpStatusCode.Forbidden, "The specified invite code is invalid");
|
throw new GracefulException(HttpStatusCode.Forbidden, "The specified invite code is invalid");
|
||||||
if (!Regex.IsMatch(username, @"^\w+$"))
|
if (!Regex.IsMatch(username, @"^\w+$"))
|
||||||
throw new GracefulException(HttpStatusCode.BadRequest, "Username must only contain letters and numbers");
|
throw new GracefulException(HttpStatusCode.BadRequest, "Username must only contain letters and numbers");
|
||||||
|
@ -465,7 +472,7 @@ public class UserService(
|
||||||
|
|
||||||
var usedUsername = new UsedUsername { CreatedAt = DateTime.UtcNow, Username = username.ToLowerInvariant() };
|
var usedUsername = new UsedUsername { CreatedAt = DateTime.UtcNow, Username = username.ToLowerInvariant() };
|
||||||
|
|
||||||
if (security.Value.Registrations == Enums.Registrations.Invite && !force)
|
if (security.Value.Registrations == Enums.Registrations.Invite)
|
||||||
{
|
{
|
||||||
var ticket = await db.RegistrationInvites.FirstOrDefaultAsync(p => p.Code == invite);
|
var ticket = await db.RegistrationInvites.FirstOrDefaultAsync(p => p.Code == invite);
|
||||||
if (ticket == null)
|
if (ticket == null)
|
||||||
|
@ -1124,17 +1131,21 @@ public class UserService(
|
||||||
{
|
{
|
||||||
var (mentions, splitDomainMapping) =
|
var (mentions, splitDomainMapping) =
|
||||||
await bgMentionsResolver.ResolveMentionsAsync(actor, bgUser.Host);
|
await bgMentionsResolver.ResolveMentionsAsync(actor, bgUser.Host);
|
||||||
var fields = actor.Attachments?.OfType<ASField>()
|
var fields = actor.Attachments != null
|
||||||
|
? await actor.Attachments
|
||||||
|
.OfType<ASField>()
|
||||||
.Where(p => p is { Name: not null, Value: not null })
|
.Where(p => p is { Name: not null, Value: not null })
|
||||||
.Select(p => new UserProfile.Field
|
.Select(async p => new UserProfile.Field
|
||||||
{
|
{
|
||||||
Name = p.Name!,
|
Name = p.Name!,
|
||||||
Value = MfmConverter.FromHtml(p.Value, mentions).Mfm
|
Value = (await MfmConverter.FromHtmlAsync(p.Value, mentions)).Mfm
|
||||||
});
|
})
|
||||||
|
.AwaitAllAsync()
|
||||||
|
: null;
|
||||||
|
|
||||||
var description = actor.MkSummary != null
|
var description = actor.MkSummary != null
|
||||||
? mentionsResolver.ResolveMentions(actor.MkSummary, bgUser.Host, mentions, splitDomainMapping)
|
? mentionsResolver.ResolveMentions(actor.MkSummary, bgUser.Host, mentions, splitDomainMapping)
|
||||||
: MfmConverter.FromHtml(actor.Summary, mentions).Mfm;
|
: (await MfmConverter.FromHtmlAsync(actor.Summary, mentions)).Mfm;
|
||||||
|
|
||||||
bgUser.UserProfile.Mentions = mentions;
|
bgUser.UserProfile.Mentions = mentions;
|
||||||
bgUser.UserProfile.Fields = fields?.ToArray() ?? [];
|
bgUser.UserProfile.Fields = fields?.ToArray() ?? [];
|
||||||
|
@ -1172,17 +1183,9 @@ public class UserService(
|
||||||
|
|
||||||
foreach (var userProfileField in user.UserProfile!.Fields)
|
foreach (var userProfileField in user.UserProfile!.Fields)
|
||||||
{
|
{
|
||||||
var url = userProfileField.Value;
|
|
||||||
if (url.StartsWith('[') && url.EndsWith(')'))
|
|
||||||
{
|
|
||||||
var idx = url.IndexOf("](", StringComparison.Ordinal);
|
|
||||||
if (idx != -1 && url.Length >= idx + "https://".Length)
|
|
||||||
url = url[(idx + 2)..^1];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!url.StartsWith("https://")
|
!userProfileField.Value.StartsWith("https://")
|
||||||
|| !Uri.TryCreate(url, UriKind.Absolute, out var uri)
|
|| !Uri.TryCreate(userProfileField.Value, UriKind.Absolute, out var uri)
|
||||||
|| uri is not { Scheme: "https" }
|
|| uri is not { Scheme: "https" }
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
@ -1192,14 +1195,13 @@ public class UserService(
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const int maxLength = 1_000_000;
|
|
||||||
var res = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
|
var res = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
res is not
|
res is not
|
||||||
{
|
{
|
||||||
IsSuccessStatusCode: true,
|
IsSuccessStatusCode: true,
|
||||||
Content.Headers: { ContentType.MediaType: "text/html", ContentLength: null or <= maxLength }
|
Content.Headers: { ContentType.MediaType: "text/html", ContentLength: <= 1_000_000 }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
@ -1210,18 +1212,11 @@ public class UserService(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentLength = res.Content.Headers.ContentLength;
|
var html = await res.Content.ReadAsStringAsync();
|
||||||
var stream = await res.Content.ReadAsStreamAsync()
|
var document = await new HtmlParser().ParseDocumentAsync(html);
|
||||||
.ContinueWithResult(p => p.GetSafeStreamOrNullAsync(maxLength, contentLength));
|
|
||||||
|
|
||||||
if (stream == Stream.Null) throw new Exception("Response size limit exceeded");
|
|
||||||
|
|
||||||
var document = await new HtmlParser().ParseDocumentAsync(stream);
|
|
||||||
var headLinks = document.Head?.Children.Where(el => el.NodeName.ToLower() == "link").ToList() ?? [];
|
|
||||||
|
|
||||||
userProfileField.IsVerified =
|
userProfileField.IsVerified =
|
||||||
headLinks.Concat(document.Links)
|
document.Links.Any(a => (a.GetAttribute("rel")?.Contains("me")
|
||||||
.Any(a => (a.GetAttribute("rel")?.Contains("me")
|
|
||||||
?? false)
|
?? false)
|
||||||
&& a.GetAttribute("href") == profileUrl
|
&& a.GetAttribute("href") == profileUrl
|
||||||
|| a.GetAttribute("href") == user.GetUriOrPublicUri(instance.Value));
|
|| a.GetAttribute("href") == user.GetUriOrPublicUri(instance.Value));
|
||||||
|
@ -1558,28 +1553,6 @@ public class UserService(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var mutes = db.Mutings
|
|
||||||
.Where(p => p.Mutee == source && p.ExpiresAt == null)
|
|
||||||
.Select(p => p.Muter)
|
|
||||||
.AsChunkedAsyncEnumerable(50, p => p.Id, p => p.PrecomputeRelationshipData(source));
|
|
||||||
|
|
||||||
await foreach (var muter in mutes)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (muter.Id == target.Id) continue;
|
|
||||||
|
|
||||||
// We need to transfer the precomputed properties to the target user for each muter so that the block method works correctly
|
|
||||||
target.PrecomputedIsMutedBy = muter.PrecomputedIsMuting;
|
|
||||||
await MuteUserAsync(muter, target, null);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
logger.LogWarning("Failed to process move ({sourceUri} -> {targetUri}) for muter {id}: {error}",
|
|
||||||
sourceUri, targetUri, muter.Id, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source.IsRemoteUser || target.IsRemoteUser) return;
|
if (source.IsRemoteUser || target.IsRemoteUser) return;
|
||||||
|
|
||||||
var following = db.Followings
|
var following = db.Followings
|
||||||
|
@ -1619,25 +1592,6 @@ public class UserService(
|
||||||
sourceUri, targetUri, blockee.Id, e);
|
sourceUri, targetUri, blockee.Id, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mutes = db.Mutings
|
|
||||||
.Where(p => p.Muter == source && p.ExpiresAt == null)
|
|
||||||
.Select(p => p.Mutee)
|
|
||||||
.AsChunkedAsyncEnumerable(50, p => p.Id, p => p.PrecomputeRelationshipData(source));
|
|
||||||
|
|
||||||
await foreach (var mutee in mutes)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (mutee.Id == target.Id) continue;
|
|
||||||
await MuteUserAsync(mutee, target, null);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
logger.LogWarning("Failed to process move ({sourceUri} -> {targetUri}) for mutee {id}: {error}",
|
|
||||||
sourceUri, targetUri, mutee.Id, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SuspendUserAsync(User user)
|
public async Task SuspendUserAsync(User user)
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
<PackageReference Include="EntityFrameworkCore.Projectables" Version="4.0.0" />
|
<PackageReference Include="EntityFrameworkCore.Projectables" Version="4.0.0" />
|
||||||
<PackageReference Include="FlexLabs.EntityFrameworkCore.Upsert" Version="8.1.2" />
|
<PackageReference Include="FlexLabs.EntityFrameworkCore.Upsert" Version="8.1.2" />
|
||||||
<PackageReference Include="Iceshrimp.Assets.Fonts" Version="1.0.0" />
|
<PackageReference Include="Iceshrimp.Assets.Fonts" Version="1.0.0" />
|
||||||
<PackageReference Include="Iceshrimp.EntityFrameworkCore.Extensions" Version="1.0.2" />
|
<PackageReference Include="Iceshrimp.EntityFrameworkCore.Extensions" Version="1.0.1" />
|
||||||
<PackageReference Include="Iceshrimp.ObjectStorage.S3" Version="0.34.3" />
|
<PackageReference Include="Iceshrimp.ObjectStorage.S3" Version="0.34.3" />
|
||||||
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
|
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
|
||||||
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0" />
|
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0" />
|
||||||
|
@ -45,9 +45,9 @@
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.3" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.3" />
|
||||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||||
<PackageReference Include="QRCoder" Version="1.6.0" />
|
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||||
<PackageReference Include="Scalar.AspNetCore" Version="2.1.0" />
|
<PackageReference Include="Scalar.AspNetCore" Version="2.0.15" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.5" />
|
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.5" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.8-iceshrimp" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7-iceshrimp" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||||
<PackageReference Include="System.IO.Hashing" Version="9.0.2" />
|
<PackageReference Include="System.IO.Hashing" Version="9.0.2" />
|
||||||
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
<PackageReference Include="Ulid" Version="1.3.4" />
|
<PackageReference Include="Ulid" Version="1.3.4" />
|
||||||
<PackageReference Include="Iceshrimp.Assets.Branding" Version="1.0.1" />
|
<PackageReference Include="Iceshrimp.Assets.Branding" Version="1.0.1" />
|
||||||
<PackageReference Include="Iceshrimp.AssemblyUtils" Version="1.0.3" />
|
<PackageReference Include="Iceshrimp.AssemblyUtils" Version="1.0.3" />
|
||||||
<PackageReference Include="Iceshrimp.MfmSharp" Version="1.2.20" />
|
<PackageReference Include="Iceshrimp.MfmSharp" Version="1.2.17" />
|
||||||
<PackageReference Include="Iceshrimp.Utils.Common" Version="1.2.1" />
|
<PackageReference Include="Iceshrimp.Utils.Common" Version="1.2.1" />
|
||||||
<PackageReference Include="Iceshrimp.MimeTypes" Version="1.0.1" />
|
<PackageReference Include="Iceshrimp.MimeTypes" Version="1.0.1" />
|
||||||
<PackageReference Include="Iceshrimp.WebPush" Version="2.1.0" />
|
<PackageReference Include="Iceshrimp.WebPush" Version="2.1.0" />
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
@page "/admin/tasks"
|
|
||||||
@using Iceshrimp.Backend.Components.Admin
|
|
||||||
@using Iceshrimp.Backend.Core.Extensions
|
|
||||||
@using Iceshrimp.Backend.Core.Services
|
|
||||||
@inherits AdminComponentBase
|
|
||||||
@inject CronService CronSvc
|
|
||||||
<AdminPageHeader Title="Cron tasks"/>
|
|
||||||
|
|
||||||
<table class="auto-table">
|
|
||||||
<thead>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Assembly</th>
|
|
||||||
<th>Schedule</th>
|
|
||||||
<th>Last run</th>
|
|
||||||
<th>Next run</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach (var task in CronSvc.Tasks)
|
|
||||||
{
|
|
||||||
var type = task.Task.GetType();
|
|
||||||
var schedule = task.Task.Type switch
|
|
||||||
{
|
|
||||||
CronTaskType.Daily when task.Task.Trigger == TimeSpan.Zero => "daily at midnight",
|
|
||||||
//
|
|
||||||
CronTaskType.Daily => $"daily at {((int)task.Task.Trigger.TotalHours).ToString().PadLeft(2, '0')}:{((int)task.Task.Trigger.TotalMinutes).ToString().PadLeft(2, '0')}",
|
|
||||||
CronTaskType.Interval => $"every {task.Task.Trigger.ToDisplayString(singleNumber: false)}",
|
|
||||||
_ => throw new ArgumentOutOfRangeException()
|
|
||||||
};
|
|
||||||
|
|
||||||
var last = task.Trigger.IsRunning
|
|
||||||
? "now"
|
|
||||||
: task.Trigger.LastRun.HasValue
|
|
||||||
? (DateTime.UtcNow - task.Trigger.LastRun.Value).ToDisplayString() + " ago"
|
|
||||||
: "never";
|
|
||||||
|
|
||||||
if (task.Trigger.LastRun.HasValue)
|
|
||||||
{
|
|
||||||
last += task.Trigger.Error is null ? " (OK)" : " (Error)";
|
|
||||||
}
|
|
||||||
|
|
||||||
var next = task.Trigger.IsRunning
|
|
||||||
? "now"
|
|
||||||
: "in " + (task.Trigger.NextTrigger - DateTime.UtcNow).ToDisplayString();
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>@type.Name</td>
|
|
||||||
<td>@type.Assembly.GetName().Name</td>
|
|
||||||
<td>@schedule</td>
|
|
||||||
@if (task.Trigger is { LastRun: not null, Error: { } e })
|
|
||||||
{
|
|
||||||
<td title="@e.ToString()">@last</td>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<td>@last</td>
|
|
||||||
}
|
|
||||||
<td>@next</td>
|
|
||||||
<td><a class="fake-link" onclick="runCronTask('@type.FullName', event.target)">Run now</a></td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
|
@ -102,7 +102,7 @@ else
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
@if (Offset is > 0)
|
@if (Offset is > 0)
|
||||||
{
|
{
|
||||||
<button role="link" data-target="/admin/users?@(_prevPageParams)" onclick="navigate(event)">
|
<button role="link" data-target="/admin/users?@(Remote ? "remote=true&" : "")offset=@(Math.Max(0, Offset.Value - 50))" onclick="navigate(event)">
|
||||||
❮ Previous page
|
❮ Previous page
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,7 @@ else
|
||||||
|
|
||||||
@if (_users.Length == 50)
|
@if (_users.Length == 50)
|
||||||
{
|
{
|
||||||
<button role="link" data-target="/admin/users?@(_nextPageParams)" onclick="navigate(event)">
|
<button role="link" data-target="/admin/users?@(Remote ? "remote=true&" : "")offset=@((Offset ?? 0) + 50)" onclick="navigate(event)">
|
||||||
Next page ❯
|
Next page ❯
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
@ -142,9 +142,6 @@ else
|
||||||
private User[] _users = [];
|
private User[] _users = [];
|
||||||
private int _count;
|
private int _count;
|
||||||
|
|
||||||
private string _prevPageParams = "";
|
|
||||||
private string _nextPageParams = "";
|
|
||||||
|
|
||||||
protected override async Task OnGet()
|
protected override async Task OnGet()
|
||||||
{
|
{
|
||||||
var query = Remote
|
var query = Remote
|
||||||
|
@ -157,11 +154,5 @@ else
|
||||||
|
|
||||||
_users = await query.Skip(Offset ?? 0).Take(50).ToArrayAsync();
|
_users = await query.Skip(Offset ?? 0).Take(50).ToArrayAsync();
|
||||||
_count = await query.CountAsync();
|
_count = await query.CountAsync();
|
||||||
|
|
||||||
if (Offset > 0)
|
|
||||||
_prevPageParams = (Query is { Length: > 0 } ? $"q={Query}&" : "") + (Remote ? "remote=true&" : "") + "offset=" + (Math.Max(0, Offset.Value - 50));
|
|
||||||
|
|
||||||
if (_users.Length == 50)
|
|
||||||
_nextPageParams = (Query is { Length: > 0 } ? $"q={Query}&" : "") + (Remote ? "remote=true&" : "") + "offset=" + ((Offset ?? 0) + 50);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -10,9 +10,9 @@
|
||||||
@section head
|
@section head
|
||||||
{
|
{
|
||||||
<meta name="twitter:card" content="summary">
|
<meta name="twitter:card" content="summary">
|
||||||
<meta property="og:title" content="Home - @Model.InstanceName">
|
<meta name="og:title" content="Home - @Model.InstanceName">
|
||||||
<meta property="og:description" content="@Model.InstanceDescription">
|
<meta name="og:description" content="@Model.InstanceDescription">
|
||||||
<meta property="og:site_name" content="@Model.InstanceName">
|
<meta name="og:site_name" content="@Model.InstanceName">
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
|
|
|
@ -123,11 +123,10 @@ else
|
||||||
}
|
}
|
||||||
|
|
||||||
<meta name="twitter:card" content="@cardType">
|
<meta name="twitter:card" content="@cardType">
|
||||||
<meta property="og:site_name" content="@_instanceName">
|
<meta name="og:site_name" content="@_instanceName">
|
||||||
<meta property="og:title" content="@title">
|
<meta name="og:title" content="@title">
|
||||||
<meta property="og:image" content="@previewImageUrl">
|
<meta name="og:image" content="@previewImageUrl">
|
||||||
<meta property="og:description" content="@description">
|
<meta name="og:description" content="@description">
|
||||||
<link rel="alternate" type="application/activity+json" href="@_note.Uri">
|
|
||||||
|
|
||||||
<VersionedLink rel="stylesheet" href="/css/public-preview.css"/>
|
<VersionedLink rel="stylesheet" href="/css/public-preview.css"/>
|
||||||
</HeadContent>
|
</HeadContent>
|
||||||
|
|
|
@ -51,14 +51,14 @@ public partial class NotePreview(
|
||||||
note.Renote?.GetPublicUriOrNull(config.Value) ??
|
note.Renote?.GetPublicUriOrNull(config.Value) ??
|
||||||
throw new Exception("Note is remote but has no uri");
|
throw new Exception("Note is remote but has no uri");
|
||||||
|
|
||||||
Redirect(target);
|
Context.Response.Redirect(target, permanent: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note is { User.Host: not null })
|
if (note is { User.Host: not null })
|
||||||
{
|
{
|
||||||
var target = note.Url ?? note.Uri ?? throw new Exception("Note is remote but has no uri");
|
var target = note.Url ?? note.Uri ?? throw new Exception("Note is remote but has no uri");
|
||||||
Redirect(target);
|
Context.Response.Redirect(target, permanent: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
@page "/openapi"
|
@page "/openapi"
|
||||||
|
@attribute [Route("/scalar")]
|
||||||
@using Microsoft.Extensions.Options
|
@using Microsoft.Extensions.Options
|
||||||
@using Swashbuckle.AspNetCore.SwaggerGen
|
@using Swashbuckle.AspNetCore.SwaggerGen
|
||||||
@inject IOptions<SwaggerGenOptions> Options;
|
@inject IOptions<SwaggerGenOptions> Options;
|
||||||
|
@ -17,7 +18,7 @@
|
||||||
<td>
|
<td>
|
||||||
<a href="/openapi/@(doc.Key).json">JSON</a> -
|
<a href="/openapi/@(doc.Key).json">JSON</a> -
|
||||||
<a href="/swagger/index.html?urls.primaryName=@(doc.Value.Title)">SwaggerUI</a> -
|
<a href="/swagger/index.html?urls.primaryName=@(doc.Value.Title)">SwaggerUI</a> -
|
||||||
<a href="/scalar/?api=@(doc.Key)">ScalarUI</a>
|
<a href="/scalar/@(doc.Key)">ScalarUI</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,14 +35,13 @@ else
|
||||||
<PageTitle>@@@_user.Username - @_instanceName</PageTitle>
|
<PageTitle>@@@_user.Username - @_instanceName</PageTitle>
|
||||||
<HeadContent>
|
<HeadContent>
|
||||||
<meta name="twitter:card" content="summary">
|
<meta name="twitter:card" content="summary">
|
||||||
<meta property="og:site_name" content="@_instanceName">
|
<meta name="og:site_name" content="@_instanceName">
|
||||||
<meta property="og:title" content="@@@_user.Username">
|
<meta name="og:title" content="@@@_user.Username">
|
||||||
@if (_user.Bio is { } bio)
|
@if (_user.Bio is { } bio)
|
||||||
{
|
{
|
||||||
<meta property="og:description" content="@bio">
|
<meta name="og:description" content="@bio">
|
||||||
}
|
}
|
||||||
<meta property="og:image" content="@_user.AvatarUrl">
|
<meta name="og:image" content="@_user.AvatarUrl">
|
||||||
<link rel="alternate" type="application/activity+json" href="@_user.Uri">
|
|
||||||
<VersionedLink rel="stylesheet" href="/css/public-preview.css"/>
|
<VersionedLink rel="stylesheet" href="/css/public-preview.css"/>
|
||||||
</HeadContent>
|
</HeadContent>
|
||||||
}
|
}
|
|
@ -54,7 +54,7 @@ public partial class UserPreview(
|
||||||
if (user is { IsRemoteUser: true })
|
if (user is { IsRemoteUser: true })
|
||||||
{
|
{
|
||||||
var target = user.UserProfile?.Url ?? user.Uri ?? throw new Exception("User is remote but has no uri");
|
var target = user.UserProfile?.Url ?? user.Uri ?? throw new Exception("User is remote but has no uri");
|
||||||
Redirect(target);
|
Context.Response.Redirect(target, permanent: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": false,
|
"launchBrowser": false,
|
||||||
"externalUrlConfiguration": true,
|
"externalUrlConfiguration": true,
|
||||||
"commandLineArgs": "--migrate-and-start",
|
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue