Compare commits
1 commit
dev
...
wip/sig-te
Author | SHA1 | Date | |
---|---|---|---|
![]() |
959384adf6 |
390 changed files with 3755 additions and 14057 deletions
|
@ -19,10 +19,10 @@ jobs:
|
||||||
- name: Print environment info
|
- name: Print environment info
|
||||||
run: dotnet --info
|
run: dotnet --info
|
||||||
- name: Build release artifacts
|
- name: Build release artifacts
|
||||||
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=$VERSION" VERBOSE=true DEP_VULN_WERROR=true
|
||||||
env:
|
env:
|
||||||
REPO: ${{ github.event.repository.name }}
|
REPO: ${{ github.event.repository.name }}
|
||||||
RELEASE_VERSION: ${{ github.ref_name }}
|
VERSION: ${{ github.ref_name }}
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/release-action@main
|
uses: actions/release-action@main
|
||||||
with:
|
with:
|
||||||
|
@ -37,8 +37,8 @@ jobs:
|
||||||
# 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:$GITHUB_REF_NAME -t $REPO:pre"
|
||||||
|
|
||||||
# The first section below can be safely removed once v2025.1 hits stable
|
# The first section below can be safely removed once v2024.1 hits stable
|
||||||
if [[ "$GITHUB_REF_NAME" == "v2025.1-beta"* ]]; then
|
if [[ "$GITHUB_REF_NAME" == "v2024.1-beta"* ]]; then
|
||||||
TAGS="$TAGS -t $REPO:latest"
|
TAGS="$TAGS -t $REPO:latest"
|
||||||
elif [[ "$GITHUB_REF_NAME" == *"-beta"* ]] || [[ "$GITHUB_REF_NAME" == *"-pre"* ]]; then
|
elif [[ "$GITHUB_REF_NAME" == *"-beta"* ]] || [[ "$GITHUB_REF_NAME" == *"-pre"* ]]; then
|
||||||
:
|
:
|
||||||
|
|
|
@ -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>
|
286
CHANGELOG.md
286
CHANGELOG.md
|
@ -1,289 +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
|
|
||||||
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.
|
|
||||||
|
|
||||||
### Blazor frontend
|
|
||||||
- Notifications no longer get randomly duplicated
|
|
||||||
- The moderation page now works correctly on mobile
|
|
||||||
- Style issues related to icons have been resolved
|
|
||||||
- The account dropdown now get positioned correctly when the page scroll position is not at the top
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
- Pure renote replies now get rejected no matter which method of creating them is used
|
|
||||||
- Security configuration changes now apply in real-time everywhere, resolving an issue where the frontend would not offer invite registration despite it being enabled
|
|
||||||
|
|
||||||
### Attribution
|
|
||||||
This release was made possible by project contributors: Laura Hausmann, Lilian & pancakes
|
|
||||||
|
|
||||||
## v2025.1-beta5
|
|
||||||
This release contains lots of new features & bug fixes. Upgrading is recommended for all server operators.
|
|
||||||
|
|
||||||
### Release notes
|
|
||||||
This release contains a **breaking change** - we now require PostgreSQL version 15 or higher. If you need assistance upgrading, please reach out to the [support chat](https://chat.iceshrimp.dev).
|
|
||||||
|
|
||||||
### Highlights
|
|
||||||
- The MFM parser has been completely rewritten, improving frontend performance by several orders of magnitude, as well as fixing countless bugs, slowdowns & edge cases.
|
|
||||||
- TOTP 2FA is now supported and can be configured in the user settings
|
|
||||||
- Instance rules can now be configured and displayed
|
|
||||||
- Links in user profile fields are now verified
|
|
||||||
- Full drive file management has been added
|
|
||||||
- Federated user pronouns have been added
|
|
||||||
- Remote media is now proxied by default
|
|
||||||
- The project and all in-house libraries now target .NET 9.0
|
|
||||||
|
|
||||||
### Blazor frontend
|
|
||||||
- Custom emoji in user bios, user fields, user display names, & note content warnings are now rendered correctly
|
|
||||||
- Display names & fields now only render their respective first line
|
|
||||||
- The note display now only breaks words break when necessary
|
|
||||||
- The font used on the frontend is now downloaded if it's not available on the client system
|
|
||||||
- The Iceshrimp.NET frontend can now be installed as a PWA (Offline support is not enabled yet)
|
|
||||||
- The frontend will automatically check for and notify about new versions
|
|
||||||
- Better emoji picker with categories and search support
|
|
||||||
- Follow button will no longer show up for your own profile
|
|
||||||
- User profiles now have badges that indicate if a user is following you, as well as badges for moderators, administrators, and automated accounts
|
|
||||||
- Notes by automated accounts are tagged as such
|
|
||||||
- Improved rendering of notifications
|
|
||||||
- Accounts that require follow approval are tagged appropriately
|
|
||||||
- Notes in the profile view can now be opened correctly
|
|
||||||
- The emoji picker now works correctly when composing a note
|
|
||||||
- Composing a reply no longer adds a mention for yourself
|
|
||||||
- The host part of local mentions is now hidden
|
|
||||||
- Alt text can now easily be viewed for note attachments
|
|
||||||
- More notification types are now supported, and feature appropriate icons and emoji
|
|
||||||
- A registration page has been added
|
|
||||||
- The login page has been reworked and now features an account selector for existing sessions
|
|
||||||
- TOTP 2FA enrollment and authentication are now possible
|
|
||||||
- Buttons that have a state now reflect their state better
|
|
||||||
- Default note visiblity is respected when composing new notes
|
|
||||||
- MfM rendering now supports many many more functions and should render most MfM art correctly (flip, font, x2/3/4, blur, rotate, crop, position, scale, fg, bg, fn, jelly, tada, jump, bounce, spin, shake, twitch, rainbow, fade, ruby, unixtime, center, small)
|
|
||||||
- All popover menus are now improved
|
|
||||||
- The attachment viewer now supports keyboard navigation and displays alt text
|
|
||||||
- Single character profiles can now be opened correctly
|
|
||||||
- When composing a note, attached files are now listed and have a preview
|
|
||||||
- Improved display of note reaction details
|
|
||||||
- User profile now has a menu for contextual actions
|
|
||||||
- Look of all buttons has been improved
|
|
||||||
- Full profile customization is now possible, including changing banners, profile pictures, tags, etc.
|
|
||||||
- The follow back button now renders correctly
|
|
||||||
- The note composer now has a preview of what your note will look like
|
|
||||||
- Note composer now features character count
|
|
||||||
- Posts can be submitted with ctrl/cmd + enter
|
|
||||||
- Virtual scroller was completely rewritten to be more performant
|
|
||||||
- Fetched note data is now cached
|
|
||||||
- You can now create rules that will be displayed on the registration page and the instances about page
|
|
||||||
- Support for setting profile avatar and banner alt text
|
|
||||||
- New better looking dialog system for prompts, notices, etc.
|
|
||||||
- Button to open/close all content warnings in a thread
|
|
||||||
- The cw button now shows how long the post behind the CW is
|
|
||||||
- Removed overscroll in places where it looks bad
|
|
||||||
- Added status indicator for notification and timeline streaming
|
|
||||||
- Refetch profile option for the profile page
|
|
||||||
- Drive management has been added, including folder support, upload, and deletion, and modification
|
|
||||||
- Added a dedicated pronoun field on the profile page
|
|
||||||
- Menus take up more of the screen on the mobile UI and are easier to navigate
|
|
||||||
- Management page for local and remote emoji (Upload, modification, cloning)
|
|
||||||
- Completely reworked default theme
|
|
||||||
- Style improvements to go with the new default theme
|
|
||||||
- Support for poll rendering and voting
|
|
||||||
- Improved loading spinners
|
|
||||||
- Menu to change accounts or log out
|
|
||||||
- Settings pages no longer exceed screen height unless needed
|
|
||||||
- Notification content is limited to a reasonable size
|
|
||||||
- Improved rendering of cw and reactions in indented notes
|
|
||||||
- Admin cookie persists unless you log out the admin account
|
|
||||||
- Fixed a crash in the attachment viewer on chrome
|
|
||||||
- Content warnings now correctly hide quotes
|
|
||||||
- Added indicator when attachments are uploading
|
|
||||||
- Disabled posting note while attachments are uploading or note is empty
|
|
||||||
- Blurred images are now easier to deblur
|
|
||||||
- Many z-index issues have been fixed
|
|
||||||
- Page title now reflects instance name and current page
|
|
||||||
|
|
||||||
### Razor (public preview, admin panel, queue dashboard, etc.)
|
|
||||||
- The admin dashboard now has a responsive navigation bar
|
|
||||||
- Constructor dependency injection is now used where applicable
|
|
||||||
- Static assets are now collected, compressed & fingerprinted at build time
|
|
||||||
- The favicon is now correctly set to the project logo
|
|
||||||
- The index page now displays the Iceshrimp project wordmark
|
|
||||||
- The page footer is now more responsive
|
|
||||||
- Emoji now have their name set as alt text
|
|
||||||
- The queue dashboard index page now has a "top delayed" section
|
|
||||||
- The page footer now shows a registration link when registrations are open or invite-only
|
|
||||||
- The generate invite button on the admin panel is now accessible to screen-reader users
|
|
||||||
- The federation management page of the admin panel now has a search box
|
|
||||||
- The admin panel now supports remote user management & user search
|
|
||||||
- Polls are now displayed in public preview
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
- Fork information in the version string is now handled correctly
|
|
||||||
- Version information is now only computed once
|
|
||||||
- Failed user resolutions no longer break the follow list import process
|
|
||||||
- Command line output referencing help pages now uses shortlinks, to prevent link rot
|
|
||||||
- Note backfilling now uses a stack instead of a queue
|
|
||||||
- MIME type & file extension are now being set correctly for converted images
|
|
||||||
- Locally originating create activities can now be fetched by their URI
|
|
||||||
- User responses now contain any emoji used in their display name or bio
|
|
||||||
- A DbContext race condition in UserRenderer has been fixed, resolving transient concurrency errors
|
|
||||||
- The search query parser now supports the has:media query
|
|
||||||
- User publickeys now have any extra whitespace removed before being added to the database
|
|
||||||
- Instance staff endpoints have been added
|
|
||||||
- User lookup error messages are now more specific
|
|
||||||
- User profile responses now include user roles, as well as the IsBot, IsCat and IsLocked fields
|
|
||||||
- Uploading files with long unicode names now works correctly
|
|
||||||
- Lock statements now use lock objects for improved performance
|
|
||||||
- GeneratedRegex partial methods have been converted to partial properties
|
|
||||||
- All params methods have been converted to take `IEnumerable<T>` as parameters
|
|
||||||
- The `dotnet ef database update` command now works as expected with multiplexing enabled
|
|
||||||
- An alternative OpenAPI UI - Scalar - has been added (accessible under `/scalar` & `/openapi`)
|
|
||||||
- Unauthenticated federation endpoints now cache their outputs for a short duration, easing database load during request bursts
|
|
||||||
- Release builds now use compiled EF models, reducing startup time by ~500ms
|
|
||||||
- The startup duration is now logged to console
|
|
||||||
- Entity model configuration has been moved into the respective entity classes
|
|
||||||
- The OpenAPI schema is now only generated once
|
|
||||||
- Usages of the `ConsumesHybrid` attribute have been replaced with `FromHybrid`
|
|
||||||
- `BlazorSsrHandoffMiddleware` now uses reflection instead of modifying the response
|
|
||||||
- A new exception verbosity option `Debug` has been added
|
|
||||||
- The error page title now contain the status code
|
|
||||||
- Middleware is now invoked conditionally, improving performance, simplifying stack traces and allowing plugins to add middleware to the stack
|
|
||||||
- Services are now runtime-discoverable, greatly improving readability
|
|
||||||
- Scoped services with request-specific properties have been converted to singletons using `AsyncLocal<T>`
|
|
||||||
- Unneeded compressed assets are no longer generated during build, improving build times
|
|
||||||
- The solution file now has virtual folders for build assets & project root files
|
|
||||||
- Version & web manifest endpoints have been added to support frontend PWA features
|
|
||||||
- Exceptions in StreamingConnectionAggregate no longer crash the backend
|
|
||||||
- Note creates & updates now get delivered to the author of note being replied to even if they're not mentioned
|
|
||||||
- Note recipients now get deduplicated
|
|
||||||
- Instance info endpoints have been added
|
|
||||||
- Support for note context collections has been added
|
|
||||||
- Reaction notifications now contain more information about the received reaction
|
|
||||||
- Note inline media is now supported using the `$[media <uri>]` MFM tag
|
|
||||||
- Session management endpoints have been added
|
|
||||||
- Line endings now get canonicalized during note/user ingest/update for improved frontend performance
|
|
||||||
- Empty & whitespace alt text now gets treated as no alt text
|
|
||||||
- User profile responses now contain the public URL of the user
|
|
||||||
- Endpoints related to user avatar, banner & display name have been added
|
|
||||||
- The user settings endpoints now allow for configuring the `isBot`, `isCat` and `speakAsCat` properties
|
|
||||||
- HTML markup tags are now deserialized to their corresponding MFM tag equivalent, instead of using symbol tags
|
|
||||||
- The note resolution lock now uses the fetched object `@id` property as its key
|
|
||||||
- Note lookups are now authenticated with the requesting user & don't attempt to redirect to inaccessible notes
|
|
||||||
- The batch emoji import endpoint is now excluded from the request size limit
|
|
||||||
- The emoji management endpoints now require moderator permissions instead of administrator ones
|
|
||||||
- Quotes without text no longer federate incorrectly to quote-aware implementations
|
|
||||||
- Notes from implementations sending HTML line breaks not followed by newline characters now get parsed correctly
|
|
||||||
- Quote blocks now aren't surrounded by extraneous line breaks
|
|
||||||
- The default renote visibility user setting can no longer be set to `specified`
|
|
||||||
- The user resolver now falls back to building the username/host tuple from the actor URI when it's not contained in the WebFinger response
|
|
||||||
- Reply backfill jobs now don't get scheduled for followers-only posts when authenticated user backfill is disabled
|
|
||||||
- The `w3id/identity-v1` JSON-LD context definition is now preloaded
|
|
||||||
- Outgoing unixtime MFM nodes now get converted to human-readable HTML
|
|
||||||
- Nodeinfo responses now return the configured instance name, description & admin contact email
|
|
||||||
- Support for backfilling user profiles has been added
|
|
||||||
- The exposed outbox collection is now functional
|
|
||||||
- Transient LD signature validation errors due to use of the wrong media type parser have been resolved
|
|
||||||
- Note refetches no longer wrongly mark notes as edited
|
|
||||||
- Fetching the relay actor now bypasses authorized fetch
|
|
||||||
- A startup error is now raised if the `ASPNETCORE_TEMP` is not writable
|
|
||||||
- Requests sent by suspended remote users are now rejected early during authorized fetch / inbox validation
|
|
||||||
- The unix socket permissions are now customizable
|
|
||||||
- The rewrite policy `CollapseWhitespace` was added
|
|
||||||
- Single emoji can now be given a name before uploading them
|
|
||||||
- The search query parser has been rewritten in C#, dropping the `FSharp.Core` dependency
|
|
||||||
- The UserResolver acct/uri mismatch message has been significantly improved
|
|
||||||
- Processed images now federate with the correct content type
|
|
||||||
- Negated search parameters now work with `match:words`
|
|
||||||
- The instance info response now contains the note length limit
|
|
||||||
- HTTP proxy configurations are now supported
|
|
||||||
- Hashtags are now handled more correctly, improving federation compatibility
|
|
||||||
- The home timeline heuristic now gets updated automatically for recently active users
|
|
||||||
- Hashtags now get the correct class set in when serialized to HTML
|
|
||||||
- Notes with `publishedAt`/`updatedAt` set to timestamps from the future will now get clamped to the current time
|
|
||||||
- The `Result<T>` helper type is now provided by `Iceshrimp.Utils.Common`
|
|
||||||
- User migration events now also transfer incoming and outgoing blocks to the new account
|
|
||||||
- The emoji table now correctly enforces unique names for local emoji (duplicates get fixed automatically, the newest entry is preserved)
|
|
||||||
- Like activities with `content` property now get correctly processed as reactions
|
|
||||||
- Deletion failures during media fixup are now ignored
|
|
||||||
- Avatar & banner alt text now federates bidirectionally, is returned in corresponding API responses & can be set
|
|
||||||
- `ExpressionExtensions` and `QueryableExtensions.AsChunkedAsyncEnumerable<T>` are now provided by `Iceshrimp.EntityFrameworkCore.Extensions`
|
|
||||||
- The license of assets included in the repository has been clarified to be `CC BY-SA 4.0`
|
|
||||||
- A refetch user endpoint has been added
|
|
||||||
- Remote emoji management endpoints have been added
|
|
||||||
- Polls can now be created, retrieved & voted on via the Web API
|
|
||||||
- Emoji media types now get populated & federated as appropriate
|
|
||||||
- Emoji entity names now get wrapped in colons for federation, resolving an interoperability issue with NodeBB
|
|
||||||
|
|
||||||
### Akkoma client API
|
|
||||||
- Local-only visibility is now respected
|
|
||||||
|
|
||||||
### Mastodon client API
|
|
||||||
- Admin scopes are now considered valid, allowing clients who request these to authenticate
|
|
||||||
- The confusing status context logic has been removed, matching -js & web api behavior
|
|
||||||
- The specified WebSocket protocol is now echoed back for streaming connections, fixing compatibility issues with some clients
|
|
||||||
- Attachment metadata is now returned when available
|
|
||||||
- Filter matches are now deduplicated, preventing duplicate filter match mesages
|
|
||||||
- The "reply inaccessible" marker now gets moved into the content warning (if any) and is more consistent
|
|
||||||
- Blockquotes now get rendered correctly when `supportsHtmlFormatting` is disabled
|
|
||||||
- Multiple accounts can now be fetched in one go via `/api/v1/accounts`
|
|
||||||
- Multiple statuses can now be fetched in one go via `/api/v1/statuses`
|
|
||||||
- The status response now correctly lists all hashtags
|
|
||||||
- The `/api/v1/accounts/{id}/statuses` endpoint no longer requires authentication, matching Mastodon's behavior
|
|
||||||
|
|
||||||
### Unit tests
|
|
||||||
- Tests now take less time to run due to higher parallelization
|
|
||||||
- The testing platform has been changed from `VSTest` to `Microsoft.Testing.Platform`
|
|
||||||
- The assertions library has been changed from `FluentAssertions` to `Iceshrimp.Assertions` due to a license change
|
|
||||||
|
|
||||||
### Build tasks
|
|
||||||
- Compressed razor class library assets now have corresponding static asset selector routes
|
|
||||||
- Pre-fingerprinted static assets collected from razor class libraries now get mapped correctly
|
|
||||||
|
|
||||||
### Miscellaneous
|
|
||||||
- The README has been updated
|
|
||||||
- The Dockerfile has been updated
|
|
||||||
- The security policy has been updated
|
|
||||||
- The OpenAPI documentation has been improved
|
|
||||||
|
|
||||||
### Attribution
|
|
||||||
This release was made possible by project contributors: blueb, Jeder, Kopper, Laura Hausmann, Lilian, notfire, pancakes & Tamara Schmitz
|
|
||||||
|
|
||||||
## v2024.1-beta4.security2
|
## v2024.1-beta4.security2
|
||||||
This is a security hotfix release. It's identical to v2024.1-beta4.security1, except for the security mitigations listed below. Upgrading is strongly recommended for all server operators.
|
This is a security hotfix release. It's identical to v2024.1-beta4.security1, except for the security mitigations listed below. Upgrading is strongly recommended for all server operators.
|
||||||
|
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
|
|
||||||
<!-- Version metadata -->
|
<!-- Version metadata -->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<VersionPrefix>2025.1</VersionPrefix>
|
<VersionPrefix>2024.1</VersionPrefix>
|
||||||
<VersionSuffix>beta5.patch2.security1</VersionSuffix>
|
<VersionSuffix>beta4.security2</VersionSuffix>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -12,10 +12,10 @@ WORKDIR /src
|
||||||
ARG BUILDPLATFORM
|
ARG BUILDPLATFORM
|
||||||
ARG AOT=false
|
ARG AOT=false
|
||||||
|
|
||||||
# copy csproj files & nuget config, then restore as distinct layers
|
# copy csproj/fsproj & nuget config, then restore as distinct layers
|
||||||
COPY NuGet.Config /src
|
COPY NuGet.Config /src
|
||||||
COPY Iceshrimp.Backend/*.csproj /src/Iceshrimp.Backend/
|
COPY Iceshrimp.Backend/*.csproj /src/Iceshrimp.Backend/
|
||||||
COPY Iceshrimp.Parsing/*.csproj /src/Iceshrimp.Parsing/
|
COPY Iceshrimp.Parsing/*.fsproj /src/Iceshrimp.Parsing/
|
||||||
COPY Iceshrimp.Frontend/*.csproj /src/Iceshrimp.Frontend/
|
COPY Iceshrimp.Frontend/*.csproj /src/Iceshrimp.Frontend/
|
||||||
COPY Iceshrimp.Shared/*.csproj /src/Iceshrimp.Shared/
|
COPY Iceshrimp.Shared/*.csproj /src/Iceshrimp.Shared/
|
||||||
COPY Iceshrimp.Build/*.csproj /src/Iceshrimp.Build/
|
COPY Iceshrimp.Build/*.csproj /src/Iceshrimp.Build/
|
||||||
|
|
|
@ -22,7 +22,7 @@ This document **MAY** alias JSON-LD namespace IRIs to their well-known aliases.
|
||||||
+ See [here](https://iceshrimp.dev/iceshrimp/Iceshrimp.NET/src/branch/dev/Iceshrimp.Backend/Core/Federation/ActivityStreams/LdHelpers.cs#L16-L24) and [here](https://iceshrimp.dev/iceshrimp/Iceshrimp.NET/src/branch/dev/Iceshrimp.Backend/Core/Federation/ActivityStreams/Contexts) to see all preloaded LD contexts we ship.
|
+ See [here](https://iceshrimp.dev/iceshrimp/Iceshrimp.NET/src/branch/dev/Iceshrimp.Backend/Core/Federation/ActivityStreams/LdHelpers.cs#L16-L24) and [here](https://iceshrimp.dev/iceshrimp/Iceshrimp.NET/src/branch/dev/Iceshrimp.Backend/Core/Federation/ActivityStreams/Contexts) to see all preloaded LD contexts we ship.
|
||||||
- Outgoing activities are compacted against our well-known LD context ([iceshrimp.json](https://iceshrimp.dev/iceshrimp/Iceshrimp.NET/src/branch/dev/Iceshrimp.Backend/Core/Federation/ActivityStreams/Contexts/iceshrimp.json)).
|
- Outgoing activities are compacted against our well-known LD context ([iceshrimp.json](https://iceshrimp.dev/iceshrimp/Iceshrimp.NET/src/branch/dev/Iceshrimp.Backend/Core/Federation/ActivityStreams/Contexts/iceshrimp.json)).
|
||||||
+ For compatibility with implementors that are not doing full LD processing, we force some attributes to be an array:
|
+ For compatibility with implementors that are not doing full LD processing, we force some attributes to be an array:
|
||||||
* `tag`, `attachment`, `to`, `cc`, `bcc`, `bto`, `alsoKnownAs` (all in the `https://www.w3.org/ns/activitystreams` namespace)
|
* `tag`, `attachment`, `to`, `cc`, `bcc`, `bto` (all in the `https://www.w3.org/ns/activitystreams` namespace)
|
||||||
+ For the same reason, we forcibly keep `https://www.w3.org/ns/activitystreams#Public` as the full IRI, instead of compacting it to `as:Public`.
|
+ For the same reason, we forcibly keep `https://www.w3.org/ns/activitystreams#Public` as the full IRI, instead of compacting it to `as:Public`.
|
||||||
+ We trim unused inline properties from the context. For technical reasons, unused namespace aliases are currently not trimmed, but this is subject to change.
|
+ We trim unused inline properties from the context. For technical reasons, unused namespace aliases are currently not trimmed, but this is subject to change.
|
||||||
- [WebFinger](https://webfinger.net/)
|
- [WebFinger](https://webfinger.net/)
|
||||||
|
@ -34,14 +34,12 @@ This document **MAY** alias JSON-LD namespace IRIs to their well-known aliases.
|
||||||
- We support WebFinger over `application/jrd+json` as well as `application/xrd+xml` (both incoming and outgoing).
|
- We support WebFinger over `application/jrd+json` as well as `application/xrd+xml` (both incoming and outgoing).
|
||||||
+ However, we do not ask for `xrd+xml` in our `Accept` header for outgoing WebFinger requests due to [compatibility issues](https://github.com/friendica/friendica/issues/14370) with Friendica.
|
+ However, we do not ask for `xrd+xml` in our `Accept` header for outgoing WebFinger requests due to [compatibility issues](https://github.com/friendica/friendica/issues/14370) with Friendica.
|
||||||
+ Responses **MUST** have their `Content-Type` set to `application/jrd+json`, `application/xrd+xml`, `application/json`, or `application/xml`.
|
+ Responses **MUST** have their `Content-Type` set to `application/jrd+json`, `application/xrd+xml`, `application/json`, or `application/xml`.
|
||||||
+ Responses **MUST** contain a link with the attributes `rel='self'` and `type='application/activity+json'`.
|
|
||||||
* `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` is treated interchangably with `application/activity+json`.
|
|
||||||
+ Responses **SHOULD** contain the `acct:` URI of the actor in the `subject` or `aliases` fields.
|
|
||||||
* If no such URI is found, we attempt to fetch the actor via ActivityPub and assemble the link from the actor's `preferredUsername` and `@id` host.
|
|
||||||
- We support host-meta over `application/jrd+json` as well as `application/xrd+xml` (both incoming and outgoing).
|
- We support host-meta over `application/jrd+json` as well as `application/xrd+xml` (both incoming and outgoing).
|
||||||
+ The json representation is also accessible under `/.well-known/host-meta.json`.
|
+ The json representation is also accessible under `/.well-known/host-meta.json`.
|
||||||
+ Implementors **SHOULD** advertise the WebFinger `Content-Type` in the `type` attribute of the WebFinger template in the host-meta response.
|
+ Implementors **SHOULD** advertise the WebFinger `Content-Type` in the `type` attribute of the WebFinger template in the host-meta response.
|
||||||
* However, since major implementors either omit the attribute, or incorrectly advertise `jrd+json` as `xrd+xml`, we presently ignore this property.
|
* However, since major implementors either omit the attribute, or incorrectly advertise `jrd+json` as `xrd+xml`, we presently ignore this property.
|
||||||
|
+ Implementors **MUST** return a link with the attributes `rel='self'` and `type='application/activity+json'` in the response.
|
||||||
|
* `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` is treated interchangably with `application/activity+json`.
|
||||||
- [HTTP Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures)
|
- [HTTP Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures)
|
||||||
- Incoming activities sent to the shared inbox or actor inbox **MUST** carry a valid HTTP signature, unless LD Signatures are explicitly enabled in the configuration, and the activity carries a valid LD signature.
|
- Incoming activities sent to the shared inbox or actor inbox **MUST** carry a valid HTTP signature, unless LD Signatures are explicitly enabled in the configuration, and the activity carries a valid LD signature.
|
||||||
- Incoming federation requests **MUST** carry a valid HTTP signature, unless authorized fetch is explicitly disabled in the configuration.
|
- Incoming federation requests **MUST** carry a valid HTTP signature, unless authorized fetch is explicitly disabled in the configuration.
|
||||||
|
|
|
@ -10,11 +10,9 @@
|
||||||
[
|
[
|
||||||
new("/admin", "Overview", Icons.ChartLine), // spacer for alignment
|
new("/admin", "Overview", Icons.ChartLine), // spacer for alignment
|
||||||
new("/admin/metadata", "Instance metadata", Icons.Info),
|
new("/admin/metadata", "Instance metadata", Icons.Info),
|
||||||
new("/admin/rules", "Rules", Icons.Scales),
|
|
||||||
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)
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -15,12 +15,13 @@
|
||||||
}
|
}
|
||||||
@if (OverflowsLg)
|
@if (OverflowsLg)
|
||||||
{
|
{
|
||||||
|
var offset = Links.Count - MaxItemsLg;
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
<a class="dropdown-button" tabindex="0">
|
<a class="dropdown-button" tabindex="0">
|
||||||
<Icon Name="Icons.DotsThree" Size="20pt"/>
|
<Icon Name="Icons.DotsThree" Size="20pt"/>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
@foreach (var link in Links[MaxItemsLg..])
|
@foreach (var link in Links[offset..])
|
||||||
{
|
{
|
||||||
<li>
|
<li>
|
||||||
<NavBarLink Link="link"/>
|
<NavBarLink Link="link"/>
|
||||||
|
@ -28,10 +29,7 @@
|
||||||
}
|
}
|
||||||
@if (Right is { Count: > 0 })
|
@if (Right is { Count: > 0 })
|
||||||
{
|
{
|
||||||
if (MaxItemsLg != Links.Count)
|
<li class="dropdown-spacer"></li>
|
||||||
{
|
|
||||||
<li class="dropdown-spacer"></li>
|
|
||||||
}
|
|
||||||
@foreach (var link in Right)
|
@foreach (var link in Right)
|
||||||
{
|
{
|
||||||
<li>
|
<li>
|
||||||
|
@ -73,12 +71,13 @@
|
||||||
}
|
}
|
||||||
@if (OverflowsMd)
|
@if (OverflowsMd)
|
||||||
{
|
{
|
||||||
|
var offset = Links.Count - MaxItemsMd;
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
<a class="dropdown-button" tabindex="0">
|
<a class="dropdown-button" tabindex="0">
|
||||||
<Icon Name="Icons.DotsThree" Size="20pt"/>
|
<Icon Name="Icons.DotsThree" Size="20pt"/>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
@foreach (var link in Links[MaxItemsMd..])
|
@foreach (var link in Links[offset..])
|
||||||
{
|
{
|
||||||
<li>
|
<li>
|
||||||
<NavBarLink Link="link"/>
|
<NavBarLink Link="link"/>
|
||||||
|
@ -86,10 +85,7 @@
|
||||||
}
|
}
|
||||||
@if (Right is { Count: > 0 })
|
@if (Right is { Count: > 0 })
|
||||||
{
|
{
|
||||||
if (MaxItemsMd != Links.Count)
|
<li class="dropdown-spacer"></li>
|
||||||
{
|
|
||||||
<li class="dropdown-spacer"></li>
|
|
||||||
}
|
|
||||||
@foreach (var link in Right)
|
@foreach (var link in Right)
|
||||||
{
|
{
|
||||||
<li>
|
<li>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
@using Iceshrimp.Assets.PhosphorIcons
|
@using Iceshrimp.Assets.PhosphorIcons
|
||||||
<NavLink href="@Link.Href" class="nav-link" Match="NavLinkMatch.AllExcludingQuery" target="@Target">
|
<NavLink href="@Link.Href" class="nav-link" Match="NavLinkMatch.All" target="@Target">
|
||||||
@if (Link.Icon != null)
|
@if (Link.Icon != null)
|
||||||
{
|
{
|
||||||
<Icon Name="Link.Icon"/>
|
<Icon Name="Link.Icon"/>
|
||||||
|
|
|
@ -1,244 +0,0 @@
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Globalization;
|
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
using Microsoft.AspNetCore.Components.Rendering;
|
|
||||||
using Microsoft.AspNetCore.Components.Routing;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Components.Generic;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A component that renders an anchor tag, automatically toggling its 'active'
|
|
||||||
/// class based on whether its 'href' matches the current URI.
|
|
||||||
/// </summary>
|
|
||||||
public class NavLink : ComponentBase, IDisposable
|
|
||||||
{
|
|
||||||
private const string DefaultActiveClass = "active";
|
|
||||||
|
|
||||||
private bool _isActive;
|
|
||||||
private string? _hrefAbsolute;
|
|
||||||
private string? _class;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the CSS class name applied to the NavLink when the
|
|
||||||
/// current route matches the NavLink href.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
public string? ActiveClass { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a collection of additional attributes that will be added to the generated
|
|
||||||
/// <c>a</c> element.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter(CaptureUnmatchedValues = true)]
|
|
||||||
public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the computed CSS class based on whether or not the link is active.
|
|
||||||
/// </summary>
|
|
||||||
protected string? CssClass { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the child content of the component.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
public RenderFragment? ChildContent { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value representing the URL matching behavior.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
public NavLinkMatch Match { get; set; }
|
|
||||||
|
|
||||||
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
|
||||||
// We'll consider re-rendering on each location change
|
|
||||||
NavigationManager.LocationChanged += OnLocationChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void OnParametersSet()
|
|
||||||
{
|
|
||||||
// Update computed state
|
|
||||||
string? href = null;
|
|
||||||
if (AdditionalAttributes != null && AdditionalAttributes.TryGetValue("href", out var obj))
|
|
||||||
{
|
|
||||||
href = Convert.ToString(obj, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
_hrefAbsolute = href == null ? null : NavigationManager.ToAbsoluteUri(href).AbsoluteUri;
|
|
||||||
_isActive = ShouldMatch(NavigationManager.Uri);
|
|
||||||
|
|
||||||
_class = null;
|
|
||||||
if (AdditionalAttributes != null && AdditionalAttributes.TryGetValue("class", out obj))
|
|
||||||
{
|
|
||||||
_class = Convert.ToString(obj, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateCssClass();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
// To avoid leaking memory, it's important to detach any event handlers in Dispose()
|
|
||||||
NavigationManager.LocationChanged -= OnLocationChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateCssClass()
|
|
||||||
{
|
|
||||||
CssClass = _isActive ? CombineWithSpace(_class, ActiveClass ?? DefaultActiveClass) : _class;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnLocationChanged(object? sender, LocationChangedEventArgs args)
|
|
||||||
{
|
|
||||||
// We could just re-render always, but for this component we know the
|
|
||||||
// only relevant state change is to the _isActive property.
|
|
||||||
var shouldBeActiveNow = ShouldMatch(args.Location);
|
|
||||||
if (shouldBeActiveNow != _isActive)
|
|
||||||
{
|
|
||||||
_isActive = shouldBeActiveNow;
|
|
||||||
UpdateCssClass();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ShouldMatch(string currentUriAbsolute)
|
|
||||||
{
|
|
||||||
if (_hrefAbsolute == null)
|
|
||||||
return false;
|
|
||||||
if (EqualsHrefExactlyOrIfTrailingSlashAdded(currentUriAbsolute))
|
|
||||||
return true;
|
|
||||||
if (Match == NavLinkMatch.AllExcludingQuery && EqualsHrefExcludingQuery(currentUriAbsolute))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return Match == NavLinkMatch.Prefix && IsStrictlyPrefixWithSeparator(currentUriAbsolute, _hrefAbsolute);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool EqualsHrefExactlyOrIfTrailingSlashAdded(string currentUriAbsolute)
|
|
||||||
{
|
|
||||||
if (string.Equals(currentUriAbsolute, _hrefAbsolute, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentUriAbsolute.Length == _hrefAbsolute!.Length - 1)
|
|
||||||
{
|
|
||||||
// Special case: highlight links to http://host/path/ even if you're
|
|
||||||
// at http://host/path (with no trailing slash)
|
|
||||||
//
|
|
||||||
// This is because the router accepts an absolute URI value of "same
|
|
||||||
// as base URI but without trailing slash" as equivalent to "base URI",
|
|
||||||
// which in turn is because it's common for servers to return the same page
|
|
||||||
// for http://host/vdir as they do for host://host/vdir/ as it's no
|
|
||||||
// good to display a blank page in that case.
|
|
||||||
if (_hrefAbsolute[^1] == '/'
|
|
||||||
&& _hrefAbsolute.StartsWith(currentUriAbsolute, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool EqualsHrefExcludingQuery(string currentUriAbsolute)
|
|
||||||
{
|
|
||||||
Debug.Assert(_hrefAbsolute != null);
|
|
||||||
|
|
||||||
currentUriAbsolute = currentUriAbsolute.Split('?')[0];
|
|
||||||
|
|
||||||
if (string.Equals(currentUriAbsolute, _hrefAbsolute, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentUriAbsolute.Length == _hrefAbsolute.Length - 1)
|
|
||||||
{
|
|
||||||
// Special case: highlight links to http://host/path/ even if you're
|
|
||||||
// at http://host/path (with no trailing slash)
|
|
||||||
//
|
|
||||||
// This is because the router accepts an absolute URI value of "same
|
|
||||||
// as base URI but without trailing slash" as equivalent to "base URI",
|
|
||||||
// which in turn is because it's common for servers to return the same page
|
|
||||||
// for http://host/vdir as they do for host://host/vdir/ as it's no
|
|
||||||
// good to display a blank page in that case.
|
|
||||||
if (_hrefAbsolute[^1] == '/'
|
|
||||||
&& _hrefAbsolute.StartsWith(currentUriAbsolute, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
|
||||||
{
|
|
||||||
builder.OpenElement(0, "a");
|
|
||||||
|
|
||||||
builder.AddMultipleAttributes(1, AdditionalAttributes);
|
|
||||||
builder.AddAttribute(2, "class", CssClass);
|
|
||||||
if (_isActive)
|
|
||||||
{
|
|
||||||
builder.AddAttribute(3, "aria-current", "page");
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.AddContent(4, ChildContent);
|
|
||||||
|
|
||||||
builder.CloseElement();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string CombineWithSpace(string? str1, string str2) => str1 == null ? str2 : $"{str1} {str2}";
|
|
||||||
|
|
||||||
private static bool IsStrictlyPrefixWithSeparator(string value, string prefix)
|
|
||||||
{
|
|
||||||
var prefixLength = prefix.Length;
|
|
||||||
if (value.Length > prefixLength)
|
|
||||||
{
|
|
||||||
return value.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& (
|
|
||||||
// Only match when there's a separator character either at the end of the
|
|
||||||
// prefix or right after it.
|
|
||||||
// Example: "/abc" is treated as a prefix of "/abc/def" but not "/abcdef"
|
|
||||||
// Example: "/abc/" is treated as a prefix of "/abc/def" but not "/abcdef"
|
|
||||||
prefixLength == 0
|
|
||||||
|| !IsUnreservedCharacter(prefix[prefixLength - 1])
|
|
||||||
|| !IsUnreservedCharacter(value[prefixLength])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsUnreservedCharacter(char c)
|
|
||||||
{
|
|
||||||
// Checks whether it is an unreserved character according to
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
|
|
||||||
// Those are characters that are allowed in a URI but do not have a reserved
|
|
||||||
// purpose (e.g. they do not separate the components of the URI)
|
|
||||||
return char.IsLetterOrDigit(c) || c == '-' || c == '.' || c == '_' || c == '~';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Modifies the URL matching behavior for a <see cref="T:Microsoft.AspNetCore.Components.Routing.NavLink" />.
|
|
||||||
/// </summary>
|
|
||||||
public enum NavLinkMatch
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Specifies that the <see cref="T:Microsoft.AspNetCore.Components.Routing.NavLink" /> should be active when it matches any prefix
|
|
||||||
/// of the current URL.
|
|
||||||
/// </summary>
|
|
||||||
Prefix,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Specifies that the <see cref="T:Microsoft.AspNetCore.Components.Routing.NavLink" /> should be active when it matches the entire
|
|
||||||
/// current URL.
|
|
||||||
/// </summary>
|
|
||||||
All,
|
|
||||||
AllExcludingQuery
|
|
||||||
}
|
|
|
@ -2,19 +2,16 @@ 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 Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Components.PublicPreview.Renderers;
|
namespace Iceshrimp.Backend.Components.PublicPreview.Renderers;
|
||||||
|
|
||||||
public readonly record struct MfmRenderData(MarkupString Html, List<MfmInlineMedia> InlineMedia);
|
public readonly record struct MfmRenderData(MarkupString Html, List<MfmInlineMedia> InlineMedia);
|
||||||
|
|
||||||
[UsedImplicitly]
|
public class MfmRenderer(MfmConverter converter) : ISingletonService
|
||||||
public class MfmRenderer(MfmConverter converter, FlagService flags) : 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 +20,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,7 +3,6 @@ using Iceshrimp.Backend.Core.Configuration;
|
||||||
using Iceshrimp.Backend.Core.Database;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
using Iceshrimp.Backend.Core.Services;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
@ -13,7 +12,6 @@ public class NoteRenderer(
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
UserRenderer userRenderer,
|
UserRenderer userRenderer,
|
||||||
MfmRenderer mfm,
|
MfmRenderer mfm,
|
||||||
MediaProxyService mediaProxy,
|
|
||||||
IOptions<Config.InstanceSection> instance,
|
IOptions<Config.InstanceSection> instance,
|
||||||
IOptionsSnapshot<Config.SecuritySection> security
|
IOptionsSnapshot<Config.SecuritySection> security
|
||||||
) : IScopedService
|
) : IScopedService
|
||||||
|
@ -28,33 +26,29 @@ public class NoteRenderer(
|
||||||
var emoji = await GetEmojiAsync(allNotes);
|
var emoji = await GetEmojiAsync(allNotes);
|
||||||
var users = await GetUsersAsync(allNotes);
|
var users = await GetUsersAsync(allNotes);
|
||||||
var attachments = await GetAttachmentsAsync(allNotes);
|
var attachments = await GetAttachmentsAsync(allNotes);
|
||||||
var polls = await GetPollsAsync(allNotes);
|
|
||||||
|
|
||||||
return Render(note, users, mentions, emoji, attachments, polls);
|
return await RenderAsync(note, users, mentions, emoji, attachments);
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
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
|
||||||
{
|
{
|
||||||
User = users.First(p => p.Id == note.User.Id),
|
User = users.First(p => p.Id == note.User.Id),
|
||||||
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(),
|
||||||
Poll = polls.GetValueOrDefault(note.Id),
|
CreatedAt = note.CreatedAt.ToDisplayStringTz(),
|
||||||
CreatedAt = note.CreatedAt.ToDisplayStringTz(),
|
UpdatedAt = note.UpdatedAt?.ToDisplayStringTz()
|
||||||
UpdatedAt = note.UpdatedAt?.ToDisplayStringTz()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
@ -108,7 +102,7 @@ public class NoteRenderer(
|
||||||
.Select(f => new PreviewAttachment
|
.Select(f => new PreviewAttachment
|
||||||
{
|
{
|
||||||
MimeType = f.Type,
|
MimeType = f.Type,
|
||||||
Url = mediaProxy.GetProxyUrl(f),
|
Url = f.AccessUrl,
|
||||||
Name = f.Name,
|
Name = f.Name,
|
||||||
Alt = f.Comment,
|
Alt = f.Comment,
|
||||||
Sensitive = f.IsSensitive
|
Sensitive = f.IsSensitive
|
||||||
|
@ -116,24 +110,6 @@ public class NoteRenderer(
|
||||||
.ToList());
|
.ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Dictionary<string, PreviewPoll>> GetPollsAsync(List<Note> notes)
|
|
||||||
{
|
|
||||||
if (notes is []) return new Dictionary<string, PreviewPoll>();
|
|
||||||
var ids = notes.Select(p => p.Id).ToList();
|
|
||||||
var polls = await db.Polls
|
|
||||||
.Where(p => ids.Contains(p.NoteId))
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return polls.ToDictionary(p => p.NoteId,
|
|
||||||
p => new PreviewPoll
|
|
||||||
{
|
|
||||||
ExpiresAt = p.ExpiresAt,
|
|
||||||
Multiple = p.Multiple,
|
|
||||||
Choices = p.Choices.Zip(p.Votes).Select(c => (c.First, c.Second)).ToList(),
|
|
||||||
VotersCount = p.VotersCount
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<PreviewNote>> RenderManyAsync(List<Note> notes)
|
public async Task<List<PreviewNote>> RenderManyAsync(List<Note> notes)
|
||||||
{
|
{
|
||||||
if (notes is []) return [];
|
if (notes is []) return [];
|
||||||
|
@ -142,7 +118,8 @@ public class NoteRenderer(
|
||||||
var mentions = await GetMentionsAsync(allNotes);
|
var mentions = await GetMentionsAsync(allNotes);
|
||||||
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);
|
return await notes.Select(p => RenderAsync(p, users, mentions, emoji, attachments))
|
||||||
return notes.Select(p => Render(p, users, mentions, emoji, attachments, polls)).ToList();
|
.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.AvatarUrl ?? user.IdenticonUrlPath,
|
||||||
BannerUrl = user.GetBannerUrl(instance.Value),
|
BannerUrl = user.BannerUrl,
|
||||||
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,11 +8,9 @@ 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;
|
||||||
public required PreviewPoll? Poll;
|
|
||||||
public required string CreatedAt;
|
public required string CreatedAt;
|
||||||
public required string? UpdatedAt;
|
public required string? UpdatedAt;
|
||||||
}
|
}
|
||||||
|
@ -25,11 +23,3 @@ public class PreviewAttachment
|
||||||
public required string? Alt;
|
public required string? Alt;
|
||||||
public required bool Sensitive;
|
public required bool Sensitive;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PreviewPoll
|
|
||||||
{
|
|
||||||
public required DateTime? ExpiresAt { get; set; }
|
|
||||||
public required bool Multiple { get; set; }
|
|
||||||
public required List<(string Value, int Votes)> Choices { get; set; }
|
|
||||||
public required int? VotersCount { get; set; }
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
|
|
@ -151,12 +151,7 @@ public class ActivityPubController(
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||||
public async Task<ActionResult<JObject>> GetUser(string id)
|
public async Task<ActionResult<JObject>> GetUser(string id)
|
||||||
{
|
{
|
||||||
var user = await db.Users
|
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id);
|
||||||
.IncludeCommonProperties()
|
|
||||||
.Include(p => p.Avatar)
|
|
||||||
.Include(p => p.Banner)
|
|
||||||
.FirstOrDefaultAsync(p => p.Id == id);
|
|
||||||
|
|
||||||
if (user == null) throw GracefulException.NotFound("User not found");
|
if (user == null) throw GracefulException.NotFound("User not found");
|
||||||
if (user.IsRemoteUser)
|
if (user.IsRemoteUser)
|
||||||
{
|
{
|
||||||
|
@ -287,8 +282,6 @@ public class ActivityPubController(
|
||||||
|
|
||||||
var user = await db.Users
|
var user = await db.Users
|
||||||
.IncludeCommonProperties()
|
.IncludeCommonProperties()
|
||||||
.Include(p => p.Avatar)
|
|
||||||
.Include(p => p.Banner)
|
|
||||||
.FirstOrDefaultAsync(p => p.UsernameLower == acct.ToLowerInvariant() && p.IsLocalUser);
|
.FirstOrDefaultAsync(p => p.UsernameLower == acct.ToLowerInvariant() && p.IsLocalUser);
|
||||||
|
|
||||||
if (user == null) throw GracefulException.NotFound("User not found");
|
if (user == null) throw GracefulException.NotFound("User not found");
|
||||||
|
@ -333,8 +326,8 @@ public class ActivityPubController(
|
||||||
var rendered = new ASEmoji
|
var rendered = new ASEmoji
|
||||||
{
|
{
|
||||||
Id = emoji.GetPublicUri(config.Value),
|
Id = emoji.GetPublicUri(config.Value),
|
||||||
Name = $":{emoji.Name}:",
|
Name = emoji.Name,
|
||||||
Image = new ASImage { Url = new ASLink(emoji.RawPublicUrl), MediaType = emoji.Type }
|
Image = new ASImage { Url = new ASLink(emoji.PublicUrl) }
|
||||||
};
|
};
|
||||||
|
|
||||||
return LdHelpers.Compact(rendered);
|
return LdHelpers.Compact(rendered);
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
|
@ -13,7 +12,6 @@ using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
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.Helpers;
|
|
||||||
using Microsoft.AspNetCore.Cors;
|
using Microsoft.AspNetCore.Cors;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
@ -109,6 +107,7 @@ public class AccountController(
|
||||||
var avatar = await driveSvc.StoreFileAsync(request.Avatar.OpenReadStream(), user, rq);
|
var avatar = await driveSvc.StoreFileAsync(request.Avatar.OpenReadStream(), user, rq);
|
||||||
user.Avatar = avatar;
|
user.Avatar = avatar;
|
||||||
user.AvatarBlurhash = avatar.Blurhash;
|
user.AvatarBlurhash = avatar.Blurhash;
|
||||||
|
user.AvatarUrl = avatar.AccessUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.Banner != null)
|
if (request.Banner != null)
|
||||||
|
@ -122,6 +121,7 @@ public class AccountController(
|
||||||
var banner = await driveSvc.StoreFileAsync(request.Banner.OpenReadStream(), user, rq);
|
var banner = await driveSvc.StoreFileAsync(request.Banner.OpenReadStream(), user, rq);
|
||||||
user.Banner = banner;
|
user.Banner = banner;
|
||||||
user.BannerBlurhash = banner.Blurhash;
|
user.BannerBlurhash = banner.Blurhash;
|
||||||
|
user.BannerUrl = banner.AccessUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
user = await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
user = await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
||||||
|
@ -139,6 +139,7 @@ public class AccountController(
|
||||||
var id = user.AvatarId;
|
var id = user.AvatarId;
|
||||||
|
|
||||||
user.AvatarId = null;
|
user.AvatarId = null;
|
||||||
|
user.AvatarUrl = null;
|
||||||
user.AvatarBlurhash = null;
|
user.AvatarBlurhash = null;
|
||||||
|
|
||||||
db.Update(user);
|
db.Update(user);
|
||||||
|
@ -160,6 +161,7 @@ public class AccountController(
|
||||||
var id = user.BannerId;
|
var id = user.BannerId;
|
||||||
|
|
||||||
user.BannerId = null;
|
user.BannerId = null;
|
||||||
|
user.BannerUrl = null;
|
||||||
user.BannerBlurhash = null;
|
user.BannerBlurhash = null;
|
||||||
|
|
||||||
db.Update(user);
|
db.Update(user);
|
||||||
|
@ -170,26 +172,6 @@ public class AccountController(
|
||||||
return await VerifyUserCredentials();
|
return await VerifyUserCredentials();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.Forbidden)]
|
|
||||||
public async Task<IEnumerable<AccountEntity>> GetManyUsers(
|
|
||||||
[FromQuery(Name = "id")] [MaxLength(40)]
|
|
||||||
List<string> ids
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var localUser = HttpContext.GetUser();
|
|
||||||
if (config.Value.PublicPreview == Enums.PublicPreview.Lockdown && localUser == null)
|
|
||||||
throw GracefulException.Forbidden("Public preview is disabled on this instance");
|
|
||||||
|
|
||||||
var query = db.Users.IncludeCommonProperties().Where(p => ids.Contains(p.Id));
|
|
||||||
|
|
||||||
if (config.Value.PublicPreview <= Enums.PublicPreview.Restricted && localUser == null)
|
|
||||||
query = query.Where(p => p.IsLocalUser);
|
|
||||||
|
|
||||||
return await userRenderer.RenderManyAsync(await query.ToArrayAsync(), localUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||||
|
@ -199,8 +181,8 @@ public class AccountController(
|
||||||
if (config.Value.PublicPreview == Enums.PublicPreview.Lockdown && localUser == null)
|
if (config.Value.PublicPreview == Enums.PublicPreview.Lockdown && localUser == null)
|
||||||
throw GracefulException.Forbidden("Public preview is disabled on this instance");
|
throw GracefulException.Forbidden("Public preview is disabled on this instance");
|
||||||
|
|
||||||
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id)
|
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id) ??
|
||||||
?? throw GracefulException.RecordNotFound();
|
throw GracefulException.RecordNotFound();
|
||||||
|
|
||||||
if (config.Value.PublicPreview <= Enums.PublicPreview.Restricted && user.IsRemoteUser && localUser == null)
|
if (config.Value.PublicPreview <= Enums.PublicPreview.Restricted && user.IsRemoteUser && localUser == null)
|
||||||
throw GracefulException.Forbidden("Public preview is disabled on this instance");
|
throw GracefulException.Forbidden("Public preview is disabled on this instance");
|
||||||
|
@ -222,8 +204,8 @@ public class AccountController(
|
||||||
var followee = await db.Users.IncludeCommonProperties()
|
var followee = await db.Users.IncludeCommonProperties()
|
||||||
.Where(p => p.Id == id)
|
.Where(p => p.Id == id)
|
||||||
.PrecomputeRelationshipData(user)
|
.PrecomputeRelationshipData(user)
|
||||||
.FirstOrDefaultAsync()
|
.FirstOrDefaultAsync() ??
|
||||||
?? throw GracefulException.RecordNotFound();
|
throw GracefulException.RecordNotFound();
|
||||||
|
|
||||||
if ((followee.PrecomputedIsBlockedBy ?? true) || (followee.PrecomputedIsBlocking ?? true))
|
if ((followee.PrecomputedIsBlockedBy ?? true) || (followee.PrecomputedIsBlocking ?? true))
|
||||||
throw GracefulException.Forbidden("This action is not allowed");
|
throw GracefulException.Forbidden("This action is not allowed");
|
||||||
|
@ -255,8 +237,8 @@ public class AccountController(
|
||||||
.Where(p => p.Id == id)
|
.Where(p => p.Id == id)
|
||||||
.IncludeCommonProperties()
|
.IncludeCommonProperties()
|
||||||
.PrecomputeRelationshipData(user)
|
.PrecomputeRelationshipData(user)
|
||||||
.FirstOrDefaultAsync()
|
.FirstOrDefaultAsync() ??
|
||||||
?? throw GracefulException.RecordNotFound();
|
throw GracefulException.RecordNotFound();
|
||||||
|
|
||||||
await userSvc.UnfollowUserAsync(user, followee);
|
await userSvc.UnfollowUserAsync(user, followee);
|
||||||
return RenderRelationship(followee);
|
return RenderRelationship(followee);
|
||||||
|
@ -276,8 +258,8 @@ public class AccountController(
|
||||||
.Where(p => p.FolloweeId == user.Id && p.FollowerId == id)
|
.Where(p => p.FolloweeId == user.Id && p.FollowerId == id)
|
||||||
.Select(p => p.Follower)
|
.Select(p => p.Follower)
|
||||||
.PrecomputeRelationshipData(user)
|
.PrecomputeRelationshipData(user)
|
||||||
.FirstOrDefaultAsync()
|
.FirstOrDefaultAsync() ??
|
||||||
?? throw GracefulException.RecordNotFound();
|
throw GracefulException.RecordNotFound();
|
||||||
|
|
||||||
await userSvc.RemoveFromFollowersAsync(user, follower);
|
await userSvc.RemoveFromFollowersAsync(user, follower);
|
||||||
return RenderRelationship(follower);
|
return RenderRelationship(follower);
|
||||||
|
@ -297,8 +279,8 @@ public class AccountController(
|
||||||
.Where(p => p.Id == id)
|
.Where(p => p.Id == id)
|
||||||
.IncludeCommonProperties()
|
.IncludeCommonProperties()
|
||||||
.PrecomputeRelationshipData(user)
|
.PrecomputeRelationshipData(user)
|
||||||
.FirstOrDefaultAsync()
|
.FirstOrDefaultAsync() ??
|
||||||
?? throw GracefulException.RecordNotFound();
|
throw GracefulException.RecordNotFound();
|
||||||
|
|
||||||
//TODO: handle notifications parameter
|
//TODO: handle notifications parameter
|
||||||
DateTime? expiration = request.Duration == 0 ? null : DateTime.UtcNow + TimeSpan.FromSeconds(request.Duration);
|
DateTime? expiration = request.Duration == 0 ? null : DateTime.UtcNow + TimeSpan.FromSeconds(request.Duration);
|
||||||
|
@ -320,8 +302,8 @@ public class AccountController(
|
||||||
.Where(p => p.Id == id)
|
.Where(p => p.Id == id)
|
||||||
.IncludeCommonProperties()
|
.IncludeCommonProperties()
|
||||||
.PrecomputeRelationshipData(user)
|
.PrecomputeRelationshipData(user)
|
||||||
.FirstOrDefaultAsync()
|
.FirstOrDefaultAsync() ??
|
||||||
?? throw GracefulException.RecordNotFound();
|
throw GracefulException.RecordNotFound();
|
||||||
|
|
||||||
await userSvc.UnmuteUserAsync(user, mutee);
|
await userSvc.UnmuteUserAsync(user, mutee);
|
||||||
return RenderRelationship(mutee);
|
return RenderRelationship(mutee);
|
||||||
|
@ -341,8 +323,8 @@ public class AccountController(
|
||||||
.Where(p => p.Id == id)
|
.Where(p => p.Id == id)
|
||||||
.IncludeCommonProperties()
|
.IncludeCommonProperties()
|
||||||
.PrecomputeRelationshipData(user)
|
.PrecomputeRelationshipData(user)
|
||||||
.FirstOrDefaultAsync()
|
.FirstOrDefaultAsync() ??
|
||||||
?? throw GracefulException.RecordNotFound();
|
throw GracefulException.RecordNotFound();
|
||||||
|
|
||||||
await userSvc.BlockUserAsync(user, blockee);
|
await userSvc.BlockUserAsync(user, blockee);
|
||||||
return RenderRelationship(blockee);
|
return RenderRelationship(blockee);
|
||||||
|
@ -362,8 +344,8 @@ public class AccountController(
|
||||||
.Where(p => p.Id == id)
|
.Where(p => p.Id == id)
|
||||||
.IncludeCommonProperties()
|
.IncludeCommonProperties()
|
||||||
.PrecomputeRelationshipData(user)
|
.PrecomputeRelationshipData(user)
|
||||||
.FirstOrDefaultAsync()
|
.FirstOrDefaultAsync() ??
|
||||||
?? throw GracefulException.RecordNotFound();
|
throw GracefulException.RecordNotFound();
|
||||||
|
|
||||||
await userSvc.UnblockUserAsync(user, blockee);
|
await userSvc.UnblockUserAsync(user, blockee);
|
||||||
return RenderRelationship(blockee);
|
return RenderRelationship(blockee);
|
||||||
|
@ -394,10 +376,7 @@ public class AccountController(
|
||||||
string id, AccountSchemas.AccountStatusesRequest request, MastodonPaginationQuery query
|
string id, AccountSchemas.AccountStatusesRequest request, MastodonPaginationQuery query
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUser();
|
var user = HttpContext.GetUserOrFail();
|
||||||
if (config.Value.PublicPreview == Enums.PublicPreview.Lockdown && user == null)
|
|
||||||
throw GracefulException.Forbidden("Public preview is disabled on this instance");
|
|
||||||
|
|
||||||
var account = await db.Users.FirstOrDefaultAsync(p => p.Id == id) ?? throw GracefulException.RecordNotFound();
|
var account = await db.Users.FirstOrDefaultAsync(p => p.Id == id) ?? throw GracefulException.RecordNotFound();
|
||||||
|
|
||||||
return await db.Notes
|
return await db.Notes
|
||||||
|
@ -424,8 +403,8 @@ public class AccountController(
|
||||||
|
|
||||||
var account = await db.Users
|
var account = await db.Users
|
||||||
.Include(p => p.UserProfile)
|
.Include(p => p.UserProfile)
|
||||||
.FirstOrDefaultAsync(p => p.Id == id)
|
.FirstOrDefaultAsync(p => p.Id == id) ??
|
||||||
?? throw GracefulException.RecordNotFound();
|
throw GracefulException.RecordNotFound();
|
||||||
|
|
||||||
if (config.Value.PublicPreview <= Enums.PublicPreview.Restricted && account.IsRemoteUser && user == null)
|
if (config.Value.PublicPreview <= Enums.PublicPreview.Restricted && account.IsRemoteUser && user == null)
|
||||||
throw GracefulException.Forbidden("Public preview is disabled on this instance");
|
throw GracefulException.Forbidden("Public preview is disabled on this instance");
|
||||||
|
@ -460,8 +439,8 @@ public class AccountController(
|
||||||
|
|
||||||
var account = await db.Users
|
var account = await db.Users
|
||||||
.Include(p => p.UserProfile)
|
.Include(p => p.UserProfile)
|
||||||
.FirstOrDefaultAsync(p => p.Id == id)
|
.FirstOrDefaultAsync(p => p.Id == id) ??
|
||||||
?? throw GracefulException.RecordNotFound();
|
throw GracefulException.RecordNotFound();
|
||||||
|
|
||||||
if (config.Value.PublicPreview <= Enums.PublicPreview.Restricted && account.IsRemoteUser && user == null)
|
if (config.Value.PublicPreview <= Enums.PublicPreview.Restricted && account.IsRemoteUser && user == null)
|
||||||
throw GracefulException.Forbidden("Public preview is disabled on this instance");
|
throw GracefulException.Forbidden("Public preview is disabled on this instance");
|
||||||
|
@ -490,8 +469,8 @@ public class AccountController(
|
||||||
{
|
{
|
||||||
_ = await db.Users
|
_ = await db.Users
|
||||||
.Include(p => p.UserProfile)
|
.Include(p => p.UserProfile)
|
||||||
.FirstOrDefaultAsync(p => p.Id == id)
|
.FirstOrDefaultAsync(p => p.Id == id) ??
|
||||||
?? throw GracefulException.RecordNotFound();
|
throw GracefulException.RecordNotFound();
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -615,8 +594,8 @@ public class AccountController(
|
||||||
.IncludeCommonProperties()
|
.IncludeCommonProperties()
|
||||||
.PrecomputeRelationshipData(user)
|
.PrecomputeRelationshipData(user)
|
||||||
.Select(u => RenderRelationship(u))
|
.Select(u => RenderRelationship(u))
|
||||||
.FirstOrDefaultAsync()
|
.FirstOrDefaultAsync() ??
|
||||||
?? throw GracefulException.RecordNotFound();
|
throw GracefulException.RecordNotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("/api/v1/follow_requests/{id}/reject")]
|
[HttpPost("/api/v1/follow_requests/{id}/reject")]
|
||||||
|
@ -638,8 +617,8 @@ public class AccountController(
|
||||||
.IncludeCommonProperties()
|
.IncludeCommonProperties()
|
||||||
.PrecomputeRelationshipData(user)
|
.PrecomputeRelationshipData(user)
|
||||||
.Select(u => RenderRelationship(u))
|
.Select(u => RenderRelationship(u))
|
||||||
.FirstOrDefaultAsync()
|
.FirstOrDefaultAsync() ??
|
||||||
?? throw GracefulException.RecordNotFound();
|
throw GracefulException.RecordNotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("lookup")]
|
[HttpGet("lookup")]
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -21,11 +21,7 @@ namespace Iceshrimp.Backend.Controllers.Mastodon;
|
||||||
[EnableCors("mastodon")]
|
[EnableCors("mastodon")]
|
||||||
[EnableRateLimiting("sliding")]
|
[EnableRateLimiting("sliding")]
|
||||||
[Produces(MediaTypeNames.Application.Json)]
|
[Produces(MediaTypeNames.Application.Json)]
|
||||||
public class InstanceController(
|
public class InstanceController(DatabaseContext db, MetaService meta) : ControllerBase
|
||||||
IOptions<Config.InstanceSection> instance,
|
|
||||||
DatabaseContext db,
|
|
||||||
MetaService meta
|
|
||||||
) : ControllerBase
|
|
||||||
{
|
{
|
||||||
[HttpGet("/api/v1/instance")]
|
[HttpGet("/api/v1/instance")]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
|
@ -46,8 +42,7 @@ public class InstanceController(
|
||||||
return new InstanceInfoV1Response(config.Value, instanceName, instanceDescription, adminContact)
|
return new InstanceInfoV1Response(config.Value, instanceName, instanceDescription, adminContact)
|
||||||
{
|
{
|
||||||
Stats = new InstanceStats(userCount, noteCount, instanceCount),
|
Stats = new InstanceStats(userCount, noteCount, instanceCount),
|
||||||
Pleroma = new PleromaInstanceExtensions { VapidPublicKey = vapidKey, Metadata = new InstanceMetadata() },
|
Pleroma = new PleromaInstanceExtensions { VapidPublicKey = vapidKey, Metadata = new InstanceMetadata() }
|
||||||
Rules = await GetRules()
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,10 +51,9 @@ public class InstanceController(
|
||||||
public async Task<InstanceInfoV2Response> GetInstanceInfoV2([FromServices] IOptionsSnapshot<Config> config)
|
public async Task<InstanceInfoV2Response> GetInstanceInfoV2([FromServices] IOptionsSnapshot<Config> config)
|
||||||
{
|
{
|
||||||
var cutoff = DateTime.UtcNow - TimeSpan.FromDays(30);
|
var cutoff = DateTime.UtcNow - TimeSpan.FromDays(30);
|
||||||
var activeMonth =
|
var activeMonth = await db.Users.LongCountAsync(p => p.IsLocalUser &&
|
||||||
await db.Users.LongCountAsync(p => p.IsLocalUser
|
!Constants.SystemUsers.Contains(p.UsernameLower) &&
|
||||||
&& !Constants.SystemUsers.Contains(p.UsernameLower)
|
p.LastActiveDate > cutoff);
|
||||||
&& p.LastActiveDate > cutoff);
|
|
||||||
|
|
||||||
var (instanceName, instanceDescription, adminContact) =
|
var (instanceName, instanceDescription, adminContact) =
|
||||||
await meta.GetManyAsync(MetaEntity.InstanceName, MetaEntity.InstanceDescription,
|
await meta.GetManyAsync(MetaEntity.InstanceName, MetaEntity.InstanceDescription,
|
||||||
|
@ -67,8 +61,7 @@ public class InstanceController(
|
||||||
|
|
||||||
return new InstanceInfoV2Response(config.Value, instanceName, instanceDescription, adminContact)
|
return new InstanceInfoV2Response(config.Value, instanceName, instanceDescription, adminContact)
|
||||||
{
|
{
|
||||||
Usage = new InstanceUsage { Users = new InstanceUsersUsage { ActiveMonth = activeMonth } },
|
Usage = new InstanceUsage { Users = new InstanceUsersUsage { ActiveMonth = activeMonth } }
|
||||||
Rules = await GetRules()
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,25 +74,14 @@ public class InstanceController(
|
||||||
{
|
{
|
||||||
Id = p.Id,
|
Id = p.Id,
|
||||||
Shortcode = p.Name,
|
Shortcode = p.Name,
|
||||||
Url = p.GetAccessUrl(instance.Value),
|
Url = p.PublicUrl,
|
||||||
StaticUrl = p.GetAccessUrl(instance.Value), //TODO
|
StaticUrl = p.PublicUrl, //TODO
|
||||||
VisibleInPicker = true,
|
VisibleInPicker = true,
|
||||||
Category = p.Category
|
Category = p.Category
|
||||||
})
|
})
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("/api/v1/instance/rules")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
public async Task<List<RuleEntity>> GetRules()
|
|
||||||
{
|
|
||||||
return await db.Rules
|
|
||||||
.OrderBy(p => p.Order)
|
|
||||||
.ThenBy(p => p.Id)
|
|
||||||
.Select(p => new RuleEntity { Id = p.Id, Text = p.Text, Hint = p.Description })
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("/api/v1/instance/translation_languages")]
|
[HttpGet("/api/v1/instance/translation_languages")]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
public Dictionary<string, IEnumerable<string>> GetTranslationLanguages() => new();
|
public Dictionary<string, IEnumerable<string>> GetTranslationLanguages() => new();
|
||||||
|
|
|
@ -23,11 +23,7 @@ namespace Iceshrimp.Backend.Controllers.Mastodon;
|
||||||
[EnableCors("mastodon")]
|
[EnableCors("mastodon")]
|
||||||
[EnableRateLimiting("sliding")]
|
[EnableRateLimiting("sliding")]
|
||||||
[Produces(MediaTypeNames.Application.Json)]
|
[Produces(MediaTypeNames.Application.Json)]
|
||||||
public class MediaController(
|
public class MediaController(DriveService driveSvc, DatabaseContext db) : ControllerBase
|
||||||
DriveService driveSvc,
|
|
||||||
DatabaseContext db,
|
|
||||||
AttachmentRenderer attachmentRenderer
|
|
||||||
) : ControllerBase
|
|
||||||
{
|
{
|
||||||
[MaxRequestSizeIsMaxUploadSize]
|
[MaxRequestSizeIsMaxUploadSize]
|
||||||
[HttpPost("/api/v1/media")]
|
[HttpPost("/api/v1/media")]
|
||||||
|
@ -44,7 +40,7 @@ public class MediaController(
|
||||||
MimeType = request.File.ContentType
|
MimeType = request.File.ContentType
|
||||||
};
|
};
|
||||||
var file = await driveSvc.StoreFileAsync(request.File.OpenReadStream(), user, rq);
|
var file = await driveSvc.StoreFileAsync(request.File.OpenReadStream(), user, rq);
|
||||||
return attachmentRenderer.Render(file);
|
return AttachmentRenderer.Render(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("/api/v1/media/{id}")]
|
[HttpPut("/api/v1/media/{id}")]
|
||||||
|
@ -55,12 +51,12 @@ public class MediaController(
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Id == id && p.User == user)
|
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Id == id && p.User == user) ??
|
||||||
?? throw GracefulException.RecordNotFound();
|
throw GracefulException.RecordNotFound();
|
||||||
file.Comment = request.Description;
|
file.Comment = request.Description;
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
return attachmentRenderer.Render(file);
|
return AttachmentRenderer.Render(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("/api/v1/media/{id}")]
|
[HttpGet("/api/v1/media/{id}")]
|
||||||
|
@ -69,10 +65,10 @@ public class MediaController(
|
||||||
public async Task<AttachmentEntity> GetAttachment(string id)
|
public async Task<AttachmentEntity> GetAttachment(string id)
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Id == id && p.User == user)
|
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Id == id && p.User == user) ??
|
||||||
?? throw GracefulException.RecordNotFound();
|
throw GracefulException.RecordNotFound();
|
||||||
|
|
||||||
return attachmentRenderer.Render(file);
|
return AttachmentRenderer.Render(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("/api/v2/media/{id}")]
|
[HttpPut("/api/v2/media/{id}")]
|
||||||
|
|
|
@ -1,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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +1,17 @@
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
|
||||||
using Iceshrimp.Backend.Core.Services;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
namespace Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
||||||
|
|
||||||
public class AttachmentRenderer(MediaProxyService mediaProxy) : ISingletonService
|
public static class AttachmentRenderer
|
||||||
{
|
{
|
||||||
public AttachmentEntity Render(DriveFile file, bool proxy = true) => new()
|
public static AttachmentEntity Render(DriveFile file) => new()
|
||||||
{
|
{
|
||||||
Id = file.Id,
|
Id = file.Id,
|
||||||
Type = AttachmentEntity.GetType(file.Type),
|
Type = AttachmentEntity.GetType(file.Type),
|
||||||
Url = proxy ? mediaProxy.GetProxyUrl(file) : file.RawAccessUrl,
|
Url = file.AccessUrl,
|
||||||
Blurhash = file.Blurhash,
|
Blurhash = file.Blurhash,
|
||||||
PreviewUrl = proxy ? mediaProxy.GetThumbnailProxyUrl(file) : file.RawThumbnailAccessUrl,
|
PreviewUrl = file.ThumbnailAccessUrl,
|
||||||
Description = file.Comment,
|
Description = file.Comment,
|
||||||
RemoteUrl = file.Uri,
|
RemoteUrl = file.Uri,
|
||||||
Sensitive = file.IsSensitive,
|
Sensitive = file.IsSensitive,
|
||||||
|
|
|
@ -19,9 +19,7 @@ public class NoteRenderer(
|
||||||
PollRenderer pollRenderer,
|
PollRenderer pollRenderer,
|
||||||
MfmConverter mfmConverter,
|
MfmConverter mfmConverter,
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
EmojiService emojiSvc,
|
EmojiService emojiSvc
|
||||||
AttachmentRenderer attachmentRenderer,
|
|
||||||
FlagService flags
|
|
||||||
) : IScopedService
|
) : IScopedService
|
||||||
{
|
{
|
||||||
private static readonly FilterResultEntity InaccessibleFilter = new()
|
private static readonly FilterResultEntity InaccessibleFilter = new()
|
||||||
|
@ -90,13 +88,6 @@ public class NoteRenderer(
|
||||||
? await GetReactionsAsync([note], user)
|
? await GetReactionsAsync([note], user)
|
||||||
: [..data.Reactions.Where(p => p.NoteId == note.Id)];
|
: [..data.Reactions.Where(p => p.NoteId == note.Id)];
|
||||||
|
|
||||||
var tags = note.Tags.Select(tag => new StatusTags
|
|
||||||
{
|
|
||||||
Name = tag,
|
|
||||||
Url = $"https://{config.Value.WebDomain}/tags/{tag}"
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var mentionedUsers = mentions.Select(p => new Note.MentionedUser
|
var mentionedUsers = mentions.Select(p => new Note.MentionedUser
|
||||||
{
|
{
|
||||||
Host = p.Host ?? config.Value.AccountDomain,
|
Host = p.Host ?? config.Value.AccountDomain,
|
||||||
|
@ -121,40 +112,13 @@ public class NoteRenderer(
|
||||||
_ => MfmInlineMedia.MediaType.Other
|
_ => MfmInlineMedia.MediaType.Other
|
||||||
}, p.RemoteUrl ?? p.Url, p.Description)).ToList();
|
}, p.RemoteUrl ?? p.Url, p.Description)).ToList();
|
||||||
|
|
||||||
var filters = data?.Filters ?? await GetFiltersAsync(user, filterContext);
|
|
||||||
|
|
||||||
List<FilterResultEntity> filterResult;
|
|
||||||
if (filters.Count > 0 && filterContext == null)
|
|
||||||
{
|
|
||||||
var filtered = FilterHelper.CheckFilters([note, note.Reply, note.Renote, note.Renote?.Renote], filters);
|
|
||||||
filterResult = GetFilterResult(filtered);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var filtered = FilterHelper.IsFiltered([note, note.Reply, note.Renote, note.Renote?.Renote], filters);
|
|
||||||
filterResult = GetFilterResult(filtered.HasValue ? [filtered.Value] : []);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((user?.UserSettings?.FilterInaccessible ?? false) && (replyInaccessible || quoteInaccessible))
|
|
||||||
filterResult.Insert(0, InaccessibleFilter);
|
|
||||||
|
|
||||||
var cw = note.Cw;
|
|
||||||
if (replyInaccessible && !string.IsNullOrEmpty(cw))
|
|
||||||
{
|
|
||||||
// prefix with lock emoji
|
|
||||||
cw = "RE: \ud83d\udd12, " + cw;
|
|
||||||
|
|
||||||
// prevent duplicating inaccessible marker in the body
|
|
||||||
replyInaccessible = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
string? content = null;
|
string? content = null;
|
||||||
if (data?.Source != true)
|
if (data?.Source != true)
|
||||||
{
|
{
|
||||||
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,19 +135,22 @@ 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
|
var filters = data?.Filters ?? await GetFiltersAsync(user, filterContext);
|
||||||
? "local"
|
|
||||||
: StatusEntity.EncodeVisibility(note.Visibility);
|
|
||||||
|
|
||||||
var pleromaExtensions = flags.IsPleroma.Value
|
List<FilterResultEntity> filterResult;
|
||||||
? new PleromaStatusExtensions
|
if (filters.Count > 0 && filterContext == null)
|
||||||
{
|
{
|
||||||
LocalOnly = note.LocalOnly,
|
var filtered = FilterHelper.CheckFilters([note, note.Reply, note.Renote, note.Renote?.Renote], filters);
|
||||||
Reactions = reactions,
|
filterResult = GetFilterResult(filtered);
|
||||||
ConversationId = note.ThreadId,
|
}
|
||||||
ThreadMuted = muted
|
else
|
||||||
}
|
{
|
||||||
: null;
|
var filtered = FilterHelper.IsFiltered([note, note.Reply, note.Renote, note.Renote?.Renote], filters);
|
||||||
|
filterResult = GetFilterResult(filtered.HasValue ? [filtered.Value] : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((user?.UserSettings?.FilterInaccessible ?? false) && (replyInaccessible || quoteInaccessible))
|
||||||
|
filterResult.Insert(0, InaccessibleFilter);
|
||||||
|
|
||||||
var res = new StatusEntity
|
var res = new StatusEntity
|
||||||
{
|
{
|
||||||
|
@ -208,8 +175,8 @@ public class NoteRenderer(
|
||||||
IsBookmarked = bookmarked,
|
IsBookmarked = bookmarked,
|
||||||
IsMuted = muted,
|
IsMuted = muted,
|
||||||
IsSensitive = sensitive,
|
IsSensitive = sensitive,
|
||||||
ContentWarning = cw ?? "",
|
ContentWarning = note.Cw ?? "",
|
||||||
Visibility = visibility,
|
Visibility = StatusEntity.EncodeVisibility(note.Visibility),
|
||||||
Content = content,
|
Content = content,
|
||||||
Text = text,
|
Text = text,
|
||||||
Mentions = mentions,
|
Mentions = mentions,
|
||||||
|
@ -218,9 +185,8 @@ public class NoteRenderer(
|
||||||
Emojis = noteEmoji,
|
Emojis = noteEmoji,
|
||||||
Poll = poll,
|
Poll = poll,
|
||||||
Reactions = reactions,
|
Reactions = reactions,
|
||||||
Tags = tags,
|
|
||||||
Filtered = filterResult,
|
Filtered = filterResult,
|
||||||
Pleroma = pleromaExtensions
|
Pleroma = new PleromaStatusExtensions { Reactions = reactions, ConversationId = note.ThreadId }
|
||||||
};
|
};
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
@ -258,7 +224,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
|
||||||
|
@ -320,7 +286,7 @@ public class NoteRenderer(
|
||||||
if (notes.Count == 0) return [];
|
if (notes.Count == 0) return [];
|
||||||
var ids = notes.SelectMany(n => n.FileIds).Distinct();
|
var ids = notes.SelectMany(n => n.FileIds).Distinct();
|
||||||
return await db.DriveFiles.Where(p => ids.Contains(p.Id))
|
return await db.DriveFiles.Where(p => ids.Contains(p.Id))
|
||||||
.Select(f => attachmentRenderer.Render(f, true))
|
.Select(f => AttachmentRenderer.Render(f))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -329,7 +295,7 @@ public class NoteRenderer(
|
||||||
var ids = fileIds.Distinct().ToList();
|
var ids = fileIds.Distinct().ToList();
|
||||||
if (ids.Count == 0) return [];
|
if (ids.Count == 0) return [];
|
||||||
return await db.DriveFiles.Where(p => ids.Contains(p.Id))
|
return await db.DriveFiles.Where(p => ids.Contains(p.Id))
|
||||||
.Select(f => attachmentRenderer.Render(f, true))
|
.Select(f => AttachmentRenderer.Render(f))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -378,8 +344,8 @@ public class NoteRenderer(
|
||||||
{
|
{
|
||||||
var hit = await emojiSvc.ResolveEmojiAsync(item.Name);
|
var hit = await emojiSvc.ResolveEmojiAsync(item.Name);
|
||||||
if (hit == null) continue;
|
if (hit == null) continue;
|
||||||
item.Url = hit.GetAccessUrl(config.Value);
|
item.Url = hit.PublicUrl;
|
||||||
item.StaticUrl = hit.GetAccessUrl(config.Value);
|
item.StaticUrl = hit.PublicUrl;
|
||||||
item.Name = item.Name.Trim(':');
|
item.Name = item.Name.Trim(':');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -446,8 +412,8 @@ public class NoteRenderer(
|
||||||
{
|
{
|
||||||
Id = p.Id,
|
Id = p.Id,
|
||||||
Shortcode = p.Name.Trim(':'),
|
Shortcode = p.Name.Trim(':'),
|
||||||
Url = p.GetAccessUrl(config.Value),
|
Url = p.PublicUrl,
|
||||||
StaticUrl = p.GetAccessUrl(config.Value), //TODO
|
StaticUrl = p.PublicUrl, //TODO
|
||||||
VisibleInPicker = true,
|
VisibleInPicker = true,
|
||||||
Category = p.Category
|
Category = p.Category
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,22 +1,17 @@
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||||
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
||||||
using Iceshrimp.Backend.Core.Configuration;
|
|
||||||
using Iceshrimp.Backend.Core.Database;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
using Iceshrimp.Backend.Core.Services;
|
using Iceshrimp.Backend.Core.Services;
|
||||||
using Iceshrimp.EntityFrameworkCore.Extensions;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
namespace Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
||||||
|
|
||||||
public class NotificationRenderer(
|
public class NotificationRenderer(
|
||||||
IOptions<Config.InstanceSection> instance,
|
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
NoteRenderer noteRenderer,
|
NoteRenderer noteRenderer,
|
||||||
UserRenderer userRenderer,
|
UserRenderer userRenderer
|
||||||
FlagService flags
|
|
||||||
) : IScopedService
|
) : IScopedService
|
||||||
{
|
{
|
||||||
public async Task<NotificationEntity> RenderAsync(
|
public async Task<NotificationEntity> RenderAsync(
|
||||||
|
@ -29,13 +24,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)
|
||||||
|
@ -50,7 +45,7 @@ public class NotificationRenderer(
|
||||||
var parts = notification.Reaction.Trim(':').Split('@');
|
var parts = notification.Reaction.Trim(':').Split('@');
|
||||||
emojiUrl = await db.Emojis
|
emojiUrl = await db.Emojis
|
||||||
.Where(e => e.Name == parts[0] && e.Host == (parts.Length > 1 ? parts[1] : null))
|
.Where(e => e.Name == parts[0] && e.Host == (parts.Length > 1 ? parts[1] : null))
|
||||||
.Select(e => e.GetAccessUrl(instance.Value))
|
.Select(e => e.PublicUrl)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,9 +59,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 +94,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));
|
||||||
Name = $":{e.Name}{(e.Host != null ? "@" + e.Host : "")}:",
|
|
||||||
Url = e.GetAccessUrl(instance.Value)
|
|
||||||
})
|
|
||||||
.ToDictionaryAsync(e => e.Name, e => e.Url);
|
|
||||||
|
|
||||||
var res = await notificationList
|
//TODO: can we somehow optimize this to do the dedupe database side?
|
||||||
.Select(p => RenderAsync(p, user, isPleroma, accounts, notes, emojiUrls))
|
var emojiUrls = await urlQ.Select(e => new
|
||||||
.AwaitAllAsync();
|
{
|
||||||
|
Name = $":{e.Name}{(e.Host != null ? "@" + e.Host : "")}:",
|
||||||
|
Url = e.PublicUrl
|
||||||
|
})
|
||||||
|
.ToArrayAsync()
|
||||||
|
.ContinueWithResult(res => res.DistinctBy(e => e.Name)
|
||||||
|
.ToDictionary(e => e.Name, e => e.Url));
|
||||||
|
|
||||||
return res;
|
return await notificationList
|
||||||
|
.Select(p => RenderAsync(p, user, isPleroma, accounts, notes, emojiUrls))
|
||||||
|
.AwaitAllAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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,117 +13,68 @@ 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";
|
||||||
|
|
||||||
public Task<AccountEntity> RenderAsync(User user, UserProfile? profile, User? localUser, bool source = false)
|
public async Task<AccountEntity> RenderAsync(
|
||||||
=> RenderAsync(user, profile, localUser, null, source);
|
User user, UserProfile? profile, User? localUser, IEnumerable<EmojiEntity>? emoji = null, bool source = false
|
||||||
|
|
||||||
private async Task<AccountEntity> RenderAsync(
|
|
||||||
User user, UserProfile? profile, User? localUser, UserRendererDto? data = null, bool source = false
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var acct = user.Username;
|
var acct = user.Username;
|
||||||
if (user.IsRemoteUser)
|
if (user.IsRemoteUser)
|
||||||
acct += $"@{user.Host}";
|
acct += $"@{user.Host}";
|
||||||
|
|
||||||
var profileEmoji = data?.Emoji.Where(p => user.Emojis.Contains(p.Id)).ToList() ?? await GetEmojiAsync([user]);
|
var profileEmoji = 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,
|
{
|
||||||
Value = (mfmConverter.ToHtml(p.Value, mentions, user.Host)).Html,
|
Name = p.Name,
|
||||||
VerifiedAt = p.IsVerified.HasValue && p.IsVerified.Value
|
Value = (await mfmConverter.ToHtmlAsync(p.Value, mentions, user.Host)).Html,
|
||||||
? DateTime.Now.ToStringIso8601Like()
|
VerifiedAt = p.IsVerified.HasValue && p.IsVerified.Value
|
||||||
: null
|
? DateTime.Now.ToStringIso8601Like()
|
||||||
});
|
: 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() ?? []
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
var avatarAlt = data?.AvatarAlt.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,
|
||||||
DisplayName = user.DisplayName ?? user.Username,
|
DisplayName = user.DisplayName ?? user.Username,
|
||||||
AvatarUrl = user.GetAvatarUrl(config.Value),
|
AvatarUrl = user.AvatarUrl ?? user.GetIdenticonUrlPng(config.Value),
|
||||||
Username = user.Username,
|
Username = user.Username,
|
||||||
Acct = acct,
|
Acct = acct,
|
||||||
FullyQualifiedName = $"{user.Username}@{user.Host ?? config.Value.AccountDomain}",
|
FullyQualifiedName = $"{user.Username}@{user.Host ?? config.Value.AccountDomain}",
|
||||||
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.AvatarUrl ?? user.GetIdenticonUrlPng(config.Value), //TODO
|
||||||
AvatarDescription = avatarAlt ?? "",
|
HeaderUrl = user.BannerUrl ?? _transparent,
|
||||||
HeaderUrl = user.GetBannerUrl(config.Value) ?? _transparent,
|
HeaderStaticUrl = user.BannerUrl ?? _transparent, //TODO
|
||||||
HeaderStaticUrl = user.GetBannerUrl(config.Value) ?? _transparent, //TODO
|
MovedToAccount = null, //TODO
|
||||||
HeaderDescription = bannerAlt ?? "",
|
|
||||||
MovedToAccount = null, //TODO
|
|
||||||
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
|
||||||
{
|
{
|
||||||
res.AvatarUrl = user.GetIdenticonUrl(config.Value);
|
res.AvatarUrl = user.GetIdenticonUrlPng(config.Value);
|
||||||
res.AvatarStaticUrl = user.GetIdenticonUrl(config.Value);
|
res.AvatarStaticUrl = user.GetIdenticonUrlPng(config.Value);
|
||||||
res.HeaderUrl = _transparent;
|
res.HeaderUrl = _transparent;
|
||||||
res.HeaderStaticUrl = _transparent;
|
res.HeaderStaticUrl = _transparent;
|
||||||
}
|
}
|
||||||
|
@ -138,9 +87,8 @@ public class UserRenderer(
|
||||||
Fields = fieldsSource,
|
Fields = fieldsSource,
|
||||||
Language = "",
|
Language = "",
|
||||||
Note = profile?.Description ?? "",
|
Note = profile?.Description ?? "",
|
||||||
Privacy =
|
Privacy = StatusEntity.EncodeVisibility(user.UserSettings?.DefaultNoteVisibility ??
|
||||||
StatusEntity.EncodeVisibility(user.UserSettings?.DefaultNoteVisibility
|
Note.NoteVisibility.Public),
|
||||||
?? Note.NoteVisibility.Public),
|
|
||||||
Sensitive = false,
|
Sensitive = false,
|
||||||
FollowRequestCount = await db.FollowRequests.CountAsync(p => p.Followee == user)
|
FollowRequestCount = await db.FollowRequests.CountAsync(p => p.Followee == user)
|
||||||
};
|
};
|
||||||
|
@ -160,63 +108,24 @@ public class UserRenderer(
|
||||||
{
|
{
|
||||||
Id = p.Id,
|
Id = p.Id,
|
||||||
Shortcode = p.Name,
|
Shortcode = p.Name,
|
||||||
Url = p.GetAccessUrl(config.Value),
|
Url = p.PublicUrl,
|
||||||
StaticUrl = p.GetAccessUrl(config.Value), //TODO
|
StaticUrl = p.PublicUrl, //TODO
|
||||||
VisibleInPicker = true,
|
VisibleInPicker = true,
|
||||||
Category = p.Category
|
Category = p.Category
|
||||||
})
|
})
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Dictionary<string, string?>> GetAvatarAltAsync(IEnumerable<User> users)
|
|
||||||
{
|
|
||||||
var ids = users.Select(p => p.Id).ToList();
|
|
||||||
return await db.Users
|
|
||||||
.Where(p => ids.Contains(p.Id))
|
|
||||||
.Include(p => p.Avatar)
|
|
||||||
.ToDictionaryAsync(p => p.Id, p => p.Avatar?.Comment);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Dictionary<string, string?>> GetBannerAltAsync(IEnumerable<User> users)
|
|
||||||
{
|
|
||||||
var ids = users.Select(p => p.Id).ToList();
|
|
||||||
return await db.Users
|
|
||||||
.Where(p => ids.Contains(p.Id))
|
|
||||||
.Include(p => p.Banner)
|
|
||||||
.ToDictionaryAsync(p => p.Id, p => p.Banner?.Comment);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<AccountEntity> RenderAsync(User user, User? localUser, List<EmojiEntity>? emoji = null)
|
public async Task<AccountEntity> RenderAsync(User user, User? localUser, List<EmojiEntity>? emoji = null)
|
||||||
{
|
{
|
||||||
var data = new UserRendererDto
|
return await RenderAsync(user, user.UserProfile, localUser, emoji);
|
||||||
{
|
|
||||||
Emoji = emoji ?? await GetEmojiAsync([user]),
|
|
||||||
AvatarAlt = await GetAvatarAltAsync([user]),
|
|
||||||
BannerAlt = await GetBannerAltAsync([user])
|
|
||||||
};
|
|
||||||
|
|
||||||
return await RenderAsync(user, user.UserProfile, localUser, data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<AccountEntity>> RenderManyAsync(IEnumerable<User> users, User? localUser)
|
public async Task<IEnumerable<AccountEntity>> RenderManyAsync(IEnumerable<User> users, User? localUser)
|
||||||
{
|
{
|
||||||
var userList = users.ToList();
|
var userList = users.ToList();
|
||||||
if (userList.Count == 0) return [];
|
if (userList.Count == 0) return [];
|
||||||
|
var emoji = await GetEmojiAsync(userList);
|
||||||
var data = new UserRendererDto
|
return await userList.Select(p => RenderAsync(p, localUser, emoji)).AwaitAllAsync();
|
||||||
{
|
|
||||||
Emoji = await GetEmojiAsync(userList),
|
|
||||||
AvatarAlt = await GetAvatarAltAsync(userList),
|
|
||||||
BannerAlt = await GetBannerAltAsync(userList)
|
|
||||||
};
|
|
||||||
|
|
||||||
return await userList.Select(p => RenderAsync(p, p.UserProfile, localUser, data)).AwaitAllAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class UserRendererDto
|
|
||||||
{
|
|
||||||
public required List<EmojiEntity> Emoji;
|
|
||||||
public required Dictionary<string, string?> AvatarAlt;
|
|
||||||
public required Dictionary<string, string?> BannerAlt;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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,40 +1,33 @@
|
||||||
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Shared.Helpers;
|
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||||
|
|
||||||
public class AccountEntity : IIdentifiable
|
public class AccountEntity : IEntity
|
||||||
{
|
{
|
||||||
[J("username")] public required string Username { get; set; }
|
[J("username")] public required string Username { get; set; }
|
||||||
[J("acct")] public required string Acct { get; set; }
|
[J("acct")] public required string Acct { get; set; }
|
||||||
[J("fqn")] public required string FullyQualifiedName { get; set; }
|
[J("fqn")] public required string FullyQualifiedName { get; set; }
|
||||||
[J("display_name")] public required string DisplayName { get; set; }
|
[J("display_name")] public required string DisplayName { get; set; }
|
||||||
[J("locked")] public required bool IsLocked { get; set; }
|
[J("locked")] public required bool IsLocked { get; set; }
|
||||||
[J("created_at")] public required string CreatedAt { get; set; }
|
[J("created_at")] public required string CreatedAt { get; set; }
|
||||||
[J("followers_count")] public required long FollowersCount { get; set; }
|
[J("followers_count")] public required long FollowersCount { get; set; }
|
||||||
[J("following_count")] public required long FollowingCount { get; set; }
|
[J("following_count")] public required long FollowingCount { get; set; }
|
||||||
[J("statuses_count")] public required long StatusesCount { get; set; }
|
[J("statuses_count")] public required long StatusesCount { get; set; }
|
||||||
[J("note")] public required string Note { get; set; }
|
[J("note")] public required string Note { get; set; }
|
||||||
[J("url")] public required string Url { get; set; }
|
[J("url")] public required string Url { get; set; }
|
||||||
[J("uri")] public required string Uri { get; set; }
|
[J("uri")] public required string Uri { get; set; }
|
||||||
[J("avatar")] public required string AvatarUrl { get; set; }
|
[J("avatar")] public required string AvatarUrl { get; set; }
|
||||||
[J("avatar_static")] public required string AvatarStaticUrl { get; set; }
|
[J("avatar_static")] public required string AvatarStaticUrl { get; set; }
|
||||||
[J("header")] public required string HeaderUrl { get; set; }
|
[J("header")] public required string HeaderUrl { get; set; }
|
||||||
[J("header_static")] public required string HeaderStaticUrl { get; set; }
|
[J("header_static")] public required string HeaderStaticUrl { get; set; }
|
||||||
[J("moved")] public required AccountEntity? MovedToAccount { get; set; }
|
[J("moved")] public required AccountEntity? MovedToAccount { get; set; }
|
||||||
[J("bot")] public required bool IsBot { get; set; }
|
[J("bot")] public required bool IsBot { get; set; }
|
||||||
[J("discoverable")] public required bool IsDiscoverable { get; set; }
|
[J("discoverable")] public required bool IsDiscoverable { get; set; }
|
||||||
[J("fields")] public required List<Field> Fields { get; set; }
|
[J("fields")] public required List<Field> Fields { get; set; }
|
||||||
[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("header_description")] public required string HeaderDescription { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Field
|
public class Field
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
using Iceshrimp.Shared.Helpers;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||||
|
|
||||||
public class ConversationEntity : IIdentifiable
|
public class ConversationEntity : IEntity
|
||||||
{
|
{
|
||||||
[J("unread")] public required bool Unread { get; set; }
|
[J("unread")] public required bool Unread { get; set; }
|
||||||
[J("accounts")] public required List<AccountEntity> Accounts { get; set; }
|
[J("accounts")] public required List<AccountEntity> Accounts { get; set; }
|
||||||
|
|
|
@ -1,25 +1,21 @@
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
||||||
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Backend.Core.Middleware;
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
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;
|
||||||
|
|
||||||
public class NotificationEntity : IIdentifiable
|
public class NotificationEntity : IEntity
|
||||||
{
|
{
|
||||||
[J("created_at")] public required string CreatedAt { get; set; }
|
[J("created_at")] public required string CreatedAt { get; set; }
|
||||||
[J("type")] public required string Type { get; set; }
|
[J("type")] public required string Type { get; set; }
|
||||||
[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("emoji")] public string? Emoji { get; set; }
|
[J("pleroma")] public required PleromaNotificationExtensions Pleroma { get; set; }
|
||||||
[J("emoji_url")] public string? EmojiUrl { get; set; }
|
[J("emoji")] public string? Emoji { 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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
using Iceshrimp.Shared.Helpers;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||||
|
|
||||||
public class PollEntity : IIdentifiable
|
public class PollEntity : IEntity
|
||||||
{
|
{
|
||||||
[J("expires_at")] public required string? ExpiresAt { get; set; }
|
[J("expires_at")] public required string? ExpiresAt { get; set; }
|
||||||
[J("expired")] public required bool Expired { get; set; }
|
[J("expired")] public required bool Expired { get; set; }
|
||||||
|
|
|
@ -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,11 +1,11 @@
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Iceshrimp.Shared.Helpers;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||||
using JI = System.Text.Json.Serialization.JsonIgnoreAttribute;
|
using JI = System.Text.Json.Serialization.JsonIgnoreAttribute;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||||
|
|
||||||
public class RelationshipEntity : IIdentifiable
|
public class RelationshipEntity : IEntity
|
||||||
{
|
{
|
||||||
[J("following")] public required bool Following { get; set; }
|
[J("following")] public required bool Following { get; set; }
|
||||||
[J("followed_by")] public required bool FollowedBy { get; set; }
|
[J("followed_by")] public required bool FollowedBy { 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; }
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
using Iceshrimp.Shared.Helpers;
|
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
|
||||||
|
|
||||||
public class RuleEntity : IIdentifiable
|
|
||||||
{
|
|
||||||
[J("id")] public required string Id { get; set; }
|
|
||||||
[J("text")] public required string Text { get; set; }
|
|
||||||
[J("hint")] public required string? Hint { get; set; }
|
|
||||||
}
|
|
|
@ -1,14 +1,14 @@
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
||||||
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
using Iceshrimp.Backend.Core.Middleware;
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
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 JI = System.Text.Json.Serialization.JsonIgnoreAttribute;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||||
|
|
||||||
public class StatusEntity : IIdentifiable, ICloneable
|
public class StatusEntity : IEntity, ICloneable
|
||||||
{
|
{
|
||||||
[JI] public string? MastoReplyUserId;
|
[JI] public string? MastoReplyUserId;
|
||||||
[J("text")] public required string? Text { get; set; }
|
[J("text")] public required string? Text { get; set; }
|
||||||
|
@ -46,18 +46,17 @@ public class StatusEntity : IIdentifiable, ICloneable
|
||||||
[J("media_attachments")] public required List<AttachmentEntity> Attachments { get; set; }
|
[J("media_attachments")] public required List<AttachmentEntity> Attachments { get; set; }
|
||||||
[J("emojis")] public required List<EmojiEntity> Emojis { get; set; }
|
[J("emojis")] public required List<EmojiEntity> Emojis { get; set; }
|
||||||
[J("reactions")] public required List<ReactionEntity> Reactions { get; set; }
|
[J("reactions")] public required List<ReactionEntity> Reactions { get; set; }
|
||||||
[J("tags")] public required List<StatusTags> Tags { get; set; }
|
|
||||||
|
|
||||||
[J("card")] public object? Card => null; //FIXME
|
[J("tags")] public object[] Tags => []; //FIXME
|
||||||
[J("application")] public object? Application => null; //FIXME
|
[J("card")] public object? Card => null; //FIXME
|
||||||
|
[J("application")] public object? Application => null; //FIXME
|
||||||
|
|
||||||
[J("language")] public string? Language => null; //FIXME
|
[J("language")] public string? Language => null; //FIXME
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
|
@ -108,9 +107,3 @@ public class StatusEdit
|
||||||
[J("media_attachments")] public required List<AttachmentEntity> Attachments { get; set; }
|
[J("media_attachments")] public required List<AttachmentEntity> Attachments { get; set; }
|
||||||
[J("emojis")] public required List<EmojiEntity> Emojis { get; set; }
|
[J("emojis")] public required List<EmojiEntity> Emojis { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StatusTags
|
|
||||||
{
|
|
||||||
[J("name")] public required string Name { get; set; }
|
|
||||||
[J("url")] public required string Url { get; set; }
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
|
||||||
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
||||||
using Iceshrimp.Backend.Core.Configuration;
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
|
@ -33,17 +32,15 @@ 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; }
|
||||||
|
|
||||||
[J("rules")] public required List<RuleEntity> Rules { get; set; }
|
|
||||||
|
|
||||||
//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 +95,5 @@ public class InstancePollConfiguration
|
||||||
|
|
||||||
public class InstanceReactionConfiguration
|
public class InstanceReactionConfiguration
|
||||||
{
|
{
|
||||||
[J("max_reactions")] public int MaxOptions => 100;
|
[J("max_reactions")] public int MaxOptions => 1;
|
||||||
}
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
|
||||||
using Iceshrimp.Backend.Core.Configuration;
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||||
|
@ -27,8 +26,6 @@ public class InstanceInfoV2Response(
|
||||||
|
|
||||||
[J("usage")] public required InstanceUsage Usage { get; set; }
|
[J("usage")] public required InstanceUsage Usage { get; set; }
|
||||||
|
|
||||||
[J("rules")] public required List<RuleEntity> Rules { get; set; }
|
|
||||||
|
|
||||||
//TODO: add the rest
|
//TODO: add the rest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,12 +36,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; } = "";
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,3 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using AsyncKeyedLock;
|
using AsyncKeyedLock;
|
||||||
|
@ -15,7 +14,6 @@ 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.MfmSharp;
|
using Iceshrimp.MfmSharp;
|
||||||
using Iceshrimp.Shared.Helpers;
|
|
||||||
using Microsoft.AspNetCore.Cors;
|
using Microsoft.AspNetCore.Cors;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
@ -72,32 +70,6 @@ public class StatusController(
|
||||||
return await noteRenderer.RenderAsync(note.EnforceRenoteReplyVisibility(), user);
|
return await noteRenderer.RenderAsync(note.EnforceRenoteReplyVisibility(), user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Authenticate("read:statuses")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.Forbidden)]
|
|
||||||
public async Task<IEnumerable<StatusEntity>> GetManyNotes([FromQuery(Name = "id")] [MaxLength(20)] List<string> ids)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUser();
|
|
||||||
if (security.Value.PublicPreview == Enums.PublicPreview.Lockdown && user == null)
|
|
||||||
throw GracefulException.Forbidden("Public preview is disabled on this instance");
|
|
||||||
|
|
||||||
var query = db.Notes.Where(p => ids.Contains(p.Id));
|
|
||||||
|
|
||||||
if (security.Value.PublicPreview <= Enums.PublicPreview.Restricted && user == null)
|
|
||||||
query = query.Where(p => p.User.IsLocalUser);
|
|
||||||
|
|
||||||
var notes = await query.IncludeCommonProperties()
|
|
||||||
.FilterHidden(user, db, false, false,
|
|
||||||
filterMentions: false)
|
|
||||||
.EnsureVisibleFor(user)
|
|
||||||
.PrecomputeVisibilities(user)
|
|
||||||
.ToArrayAsync();
|
|
||||||
|
|
||||||
return await noteRenderer.RenderManyAsync(notes.EnforceRenoteReplyVisibility(), user);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}/context")]
|
[HttpGet("{id}/context")]
|
||||||
[Authenticate("read:statuses")]
|
[Authenticate("read:statuses")]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
|
@ -437,17 +409,6 @@ public class StatusController(
|
||||||
};
|
};
|
||||||
|
|
||||||
newText = quoteUri != null ? parsed.SkipLast(1).Serialize() : parsed.Serialize();
|
newText = quoteUri != null ? parsed.SkipLast(1).Serialize() : parsed.Serialize();
|
||||||
|
|
||||||
if (
|
|
||||||
newText.AsSpan().Trim().Length == 0
|
|
||||||
&& request.Cw?.AsSpan().Trim().Length is null or 0
|
|
||||||
&& request.Poll is null or { Options.Count: 0 }
|
|
||||||
&& attachments?.Count is null or 0
|
|
||||||
)
|
|
||||||
{
|
|
||||||
quoteUri = null;
|
|
||||||
newText = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request is { Sensitive: true, MediaIds.Count: > 0 })
|
if (request is { Sensitive: true, MediaIds.Count: > 0 })
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,13 +3,11 @@ using System.Net.Mime;
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||||
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
|
||||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||||
using Iceshrimp.Backend.Core.Configuration;
|
|
||||||
using Iceshrimp.Backend.Core.Database;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Microsoft.AspNetCore.Cors;
|
using Microsoft.AspNetCore.Cors;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Pleroma;
|
namespace Iceshrimp.Backend.Controllers.Pleroma;
|
||||||
|
|
||||||
|
@ -17,7 +15,7 @@ namespace Iceshrimp.Backend.Controllers.Pleroma;
|
||||||
[EnableCors("mastodon")]
|
[EnableCors("mastodon")]
|
||||||
[EnableRateLimiting("sliding")]
|
[EnableRateLimiting("sliding")]
|
||||||
[Produces(MediaTypeNames.Application.Json)]
|
[Produces(MediaTypeNames.Application.Json)]
|
||||||
public class EmojiController(IOptions<Config.InstanceSection> instance, DatabaseContext db) : ControllerBase
|
public class EmojiController(DatabaseContext db) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet("/api/v1/pleroma/emoji")]
|
[HttpGet("/api/v1/pleroma/emoji")]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
|
@ -28,7 +26,7 @@ public class EmojiController(IOptions<Config.InstanceSection> instance, Database
|
||||||
.Select(p => KeyValuePair.Create(p.Name,
|
.Select(p => KeyValuePair.Create(p.Name,
|
||||||
new PleromaEmojiEntity
|
new PleromaEmojiEntity
|
||||||
{
|
{
|
||||||
ImageUrl = p.GetAccessUrl(instance.Value),
|
ImageUrl = p.PublicUrl,
|
||||||
Tags = new[] { p.Category ?? "" }
|
Tags = new[] { p.Category ?? "" }
|
||||||
}))
|
}))
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||||
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Backend.Core.Middleware;
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
using Iceshrimp.Shared.Helpers;
|
|
||||||
using Microsoft.AspNetCore.Http.Extensions;
|
using Microsoft.AspNetCore.Http.Extensions;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
@ -36,7 +36,7 @@ public class LinkPaginationAttribute(
|
||||||
|
|
||||||
var entities = context.HttpContext.GetPaginationData();
|
var entities = context.HttpContext.GetPaginationData();
|
||||||
if (entities == null && context.Result is ObjectResult { StatusCode: null or >= 200 and <= 299 } result)
|
if (entities == null && context.Result is ObjectResult { StatusCode: null or >= 200 and <= 299 } result)
|
||||||
entities = result.Value as IEnumerable<IIdentifiable>;
|
entities = result.Value as IEnumerable<IEntity>;
|
||||||
|
|
||||||
if (entities is null) return;
|
if (entities is null) return;
|
||||||
var ids = entities.Select(p => p.Id).ToList();
|
var ids = entities.Select(p => p.Id).ToList();
|
||||||
|
@ -89,15 +89,15 @@ public static class HttpContextExtensions
|
||||||
{
|
{
|
||||||
private const string Key = "link-pagination";
|
private const string Key = "link-pagination";
|
||||||
|
|
||||||
internal static void SetPaginationData(this HttpContext ctx, IEnumerable<IIdentifiable> entities)
|
internal static void SetPaginationData(this HttpContext ctx, IEnumerable<IEntity> entities)
|
||||||
{
|
{
|
||||||
ctx.Items.Add(Key, entities);
|
ctx.Items.Add(Key, entities);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<IIdentifiable>? GetPaginationData(this HttpContext ctx)
|
public static IEnumerable<IEntity>? GetPaginationData(this HttpContext ctx)
|
||||||
{
|
{
|
||||||
ctx.Items.TryGetValue(Key, out var entities);
|
ctx.Items.TryGetValue(Key, out var entities);
|
||||||
return entities as IEnumerable<IIdentifiable>;
|
return entities as IEnumerable<IEntity>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,6 @@ 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.Tasks;
|
using Iceshrimp.Backend.Core.Tasks;
|
||||||
using Iceshrimp.EntityFrameworkCore.Extensions;
|
|
||||||
using Iceshrimp.Shared.Configuration;
|
using Iceshrimp.Shared.Configuration;
|
||||||
using Iceshrimp.Shared.Schemas.Web;
|
using Iceshrimp.Shared.Schemas.Web;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
@ -298,24 +297,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();
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||||
using Iceshrimp.Backend.Core.Configuration;
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
using Iceshrimp.Backend.Core.Database;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
|
||||||
using Iceshrimp.Backend.Core.Helpers;
|
using Iceshrimp.Backend.Core.Helpers;
|
||||||
using Iceshrimp.Backend.Core.Middleware;
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
using Iceshrimp.Backend.Core.Queues;
|
using Iceshrimp.Backend.Core.Queues;
|
||||||
|
@ -13,7 +10,6 @@ using Iceshrimp.Backend.Core.Services;
|
||||||
using Iceshrimp.Shared.Schemas.Web;
|
using Iceshrimp.Shared.Schemas.Web;
|
||||||
using Microsoft.AspNetCore.Cors;
|
using Microsoft.AspNetCore.Cors;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
@ -27,91 +23,63 @@ public class DriveController(
|
||||||
IOptionsSnapshot<Config.StorageSection> options,
|
IOptionsSnapshot<Config.StorageSection> options,
|
||||||
ILogger<DriveController> logger,
|
ILogger<DriveController> logger,
|
||||||
DriveService driveSvc,
|
DriveService driveSvc,
|
||||||
QueueService queueSvc,
|
QueueService queueSvc
|
||||||
HttpClient httpClient
|
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
private const string CacheControl = "max-age=31536000, immutable";
|
|
||||||
|
|
||||||
[EnableCors("drive")]
|
[EnableCors("drive")]
|
||||||
[EnableRateLimiting("proxy")]
|
[HttpGet("/files/{accessKey}")]
|
||||||
[HttpGet("/files/{accessKey}/{version?}")]
|
[ProducesResults(HttpStatusCode.OK, HttpStatusCode.NoContent)]
|
||||||
[ProducesResults(HttpStatusCode.OK, HttpStatusCode.Redirect)]
|
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||||
public async Task<IActionResult> GetFileByAccessKey(string accessKey, string? version)
|
public async Task<IActionResult> GetFileByAccessKey(string accessKey)
|
||||||
{
|
{
|
||||||
return await GetFileByAccessKey(accessKey, version, null);
|
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.AccessKey == accessKey ||
|
||||||
}
|
p.PublicAccessKey == accessKey ||
|
||||||
|
p.ThumbnailAccessKey == accessKey);
|
||||||
[EnableCors("drive")]
|
if (file == null)
|
||||||
[EnableRateLimiting("proxy")]
|
|
||||||
[HttpGet("/media/emoji/{id}")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK, HttpStatusCode.Redirect)]
|
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
|
||||||
public async Task<IActionResult> GetEmojiById(string id)
|
|
||||||
{
|
|
||||||
var emoji = await db.Emojis.FirstOrDefaultAsync(p => p.Id == id)
|
|
||||||
?? throw GracefulException.NotFound("Emoji not found");
|
|
||||||
|
|
||||||
if (!options.Value.ProxyRemoteMedia || emoji.Host == null)
|
|
||||||
return Redirect(emoji.RawPublicUrl);
|
|
||||||
|
|
||||||
return await ProxyAsync(emoji.RawPublicUrl, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
[EnableCors("drive")]
|
|
||||||
[EnableRateLimiting("proxy")]
|
|
||||||
[HttpGet("/avatars/{userId}/{version}")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK, HttpStatusCode.Redirect)]
|
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
|
||||||
public async Task<IActionResult> GetAvatarByUserId(string userId, string? version)
|
|
||||||
{
|
|
||||||
var user = await db.Users.Include(p => p.Avatar).FirstOrDefaultAsync(p => p.Id == userId)
|
|
||||||
?? throw GracefulException.NotFound("User not found");
|
|
||||||
|
|
||||||
if (user.Avatar is null)
|
|
||||||
{
|
{
|
||||||
var stream = await IdenticonHelper.GetIdenticonAsync(user.Id);
|
Response.Headers.CacheControl = "max-age=86400";
|
||||||
Response.Headers.CacheControl = CacheControl;
|
throw GracefulException.NotFound("File not found");
|
||||||
return new InlineFileStreamResult(stream, "image/png", $"{user.Id}.png", false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.Value.ProxyRemoteMedia)
|
if (file.StoredInternal)
|
||||||
return Redirect(user.Avatar.RawThumbnailAccessUrl);
|
{
|
||||||
|
var pathBase = options.Value.Local?.Path;
|
||||||
|
if (string.IsNullOrWhiteSpace(pathBase))
|
||||||
|
{
|
||||||
|
logger.LogError("Failed to get file {accessKey} from local storage: path does not exist", accessKey);
|
||||||
|
throw GracefulException.NotFound("File not found");
|
||||||
|
}
|
||||||
|
|
||||||
return await GetFileByAccessKey(user.Avatar.AccessKey, "thumbnail", user.Avatar);
|
var path = Path.Join(pathBase, accessKey);
|
||||||
}
|
var stream = System.IO.File.OpenRead(path);
|
||||||
|
|
||||||
[EnableCors("drive")]
|
Response.Headers.CacheControl = "max-age=31536000, immutable";
|
||||||
[EnableRateLimiting("proxy")]
|
Response.Headers.XContentTypeOptions = "nosniff";
|
||||||
[HttpGet("/banners/{userId}/{version}")]
|
return Constants.BrowserSafeMimeTypes.Contains(file.Type)
|
||||||
[ProducesResults(HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Redirect)]
|
? new InlineFileStreamResult(stream, file.Type, file.Name, true)
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
: File(stream, file.Type, file.Name, true);
|
||||||
public async Task<IActionResult> GetBannerByUserId(string userId, string? version)
|
}
|
||||||
{
|
else
|
||||||
var user = await db.Users.Include(p => p.Banner).FirstOrDefaultAsync(p => p.Id == userId)
|
{
|
||||||
?? throw GracefulException.NotFound("User not found");
|
if (file.IsLink)
|
||||||
|
{
|
||||||
|
//TODO: handle remove media proxying
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
if (user.Banner is null)
|
var stream = await objectStorage.GetFileAsync(accessKey);
|
||||||
return NoContent();
|
if (stream == null)
|
||||||
|
{
|
||||||
|
logger.LogError("Failed to get file {accessKey} from object storage", accessKey);
|
||||||
|
throw GracefulException.NotFound("File not found");
|
||||||
|
}
|
||||||
|
|
||||||
if (!options.Value.ProxyRemoteMedia)
|
Response.Headers.CacheControl = "max-age=31536000, immutable";
|
||||||
return Redirect(user.Banner.RawThumbnailAccessUrl);
|
Response.Headers.XContentTypeOptions = "nosniff";
|
||||||
|
return Constants.BrowserSafeMimeTypes.Contains(file.Type)
|
||||||
return await GetFileByAccessKey(user.Banner.AccessKey, "thumbnail", user.Banner);
|
? new InlineFileStreamResult(stream, file.Type, file.Name, true)
|
||||||
}
|
: File(stream, file.Type, file.Name, true);
|
||||||
|
}
|
||||||
[EnableCors("drive")]
|
|
||||||
[HttpGet("/identicon/{userId}")]
|
|
||||||
[HttpGet("/identicon/{userId}.png")]
|
|
||||||
[Produces(MediaTypeNames.Image.Png)]
|
|
||||||
[ProducesResults(HttpStatusCode.OK, HttpStatusCode.Redirect)]
|
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
|
||||||
public async Task<IActionResult> GetIdenticonByUserId(string userId)
|
|
||||||
{
|
|
||||||
var stream = await IdenticonHelper.GetIdenticonAsync(userId);
|
|
||||||
Response.Headers.CacheControl = CacheControl;
|
|
||||||
return new InlineFileStreamResult(stream, "image/png", $"{userId}.png", false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
@ -120,15 +88,14 @@ public class DriveController(
|
||||||
[Produces(MediaTypeNames.Application.Json)]
|
[Produces(MediaTypeNames.Application.Json)]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
[MaxRequestSizeIsMaxUploadSize]
|
[MaxRequestSizeIsMaxUploadSize]
|
||||||
public async Task<DriveFileResponse> UploadFile(IFormFile file, [FromQuery] string? folderId)
|
public async Task<DriveFileResponse> UploadFile(IFormFile file)
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
var request = new DriveFileCreationRequest
|
var request = new DriveFileCreationRequest
|
||||||
{
|
{
|
||||||
Filename = file.FileName,
|
Filename = file.FileName,
|
||||||
MimeType = file.ContentType,
|
MimeType = file.ContentType,
|
||||||
IsSensitive = false,
|
IsSensitive = false
|
||||||
FolderId = folderId
|
|
||||||
};
|
};
|
||||||
var res = await driveSvc.StoreFileAsync(file.OpenReadStream(), user, request);
|
var res = await driveSvc.StoreFileAsync(file.OpenReadStream(), user, request);
|
||||||
return await GetFileById(res.Id);
|
return await GetFileById(res.Id);
|
||||||
|
@ -143,23 +110,18 @@ public class DriveController(
|
||||||
public async Task<DriveFileResponse> GetFileById(string id)
|
public async Task<DriveFileResponse> GetFileById(string id)
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
var file = await db.DriveFiles
|
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.User == user && p.Id == id) ??
|
||||||
.Include(p => p.UserAvatar)
|
throw GracefulException.NotFound("File not found");
|
||||||
.Include(p => p.UserBanner)
|
|
||||||
.FirstOrDefaultAsync(p => p.User == user && p.Id == id)
|
|
||||||
?? throw GracefulException.NotFound("File not found");
|
|
||||||
|
|
||||||
return new DriveFileResponse
|
return new DriveFileResponse
|
||||||
{
|
{
|
||||||
Id = file.Id,
|
Id = file.Id,
|
||||||
Url = file.RawAccessUrl,
|
Url = file.AccessUrl,
|
||||||
ThumbnailUrl = file.RawThumbnailAccessUrl,
|
ThumbnailUrl = file.ThumbnailAccessUrl,
|
||||||
Filename = file.Name,
|
Filename = file.Name,
|
||||||
ContentType = file.Type,
|
ContentType = file.Type,
|
||||||
Description = file.Comment,
|
Description = file.Comment,
|
||||||
Sensitive = file.IsSensitive,
|
Sensitive = file.IsSensitive
|
||||||
IsAvatar = file.UserAvatar != null,
|
|
||||||
IsBanner = file.UserBanner != null
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,23 +134,18 @@ public class DriveController(
|
||||||
public async Task<DriveFileResponse> GetFileByHash(string sha256)
|
public async Task<DriveFileResponse> GetFileByHash(string sha256)
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
var file = await db.DriveFiles
|
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.User == user && p.Sha256 == sha256) ??
|
||||||
.Include(p => p.UserAvatar)
|
throw GracefulException.NotFound("File not found");
|
||||||
.Include(p => p.UserBanner)
|
|
||||||
.FirstOrDefaultAsync(p => p.User == user && p.Sha256 == sha256)
|
|
||||||
?? throw GracefulException.NotFound("File not found");
|
|
||||||
|
|
||||||
return new DriveFileResponse
|
return new DriveFileResponse
|
||||||
{
|
{
|
||||||
Id = file.Id,
|
Id = file.Id,
|
||||||
Url = file.RawAccessUrl,
|
Url = file.AccessUrl,
|
||||||
ThumbnailUrl = file.RawThumbnailAccessUrl,
|
ThumbnailUrl = file.ThumbnailAccessUrl,
|
||||||
Filename = file.Name,
|
Filename = file.Name,
|
||||||
ContentType = file.Type,
|
ContentType = file.Type,
|
||||||
Description = file.Comment,
|
Description = file.Comment,
|
||||||
Sensitive = file.IsSensitive,
|
Sensitive = file.IsSensitive
|
||||||
IsAvatar = file.UserAvatar != null,
|
|
||||||
IsBanner = file.UserBanner != null
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,8 +159,8 @@ public class DriveController(
|
||||||
public async Task<DriveFileResponse> UpdateFile(string id, UpdateDriveFileRequest request)
|
public async Task<DriveFileResponse> UpdateFile(string id, UpdateDriveFileRequest request)
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.User == user && p.Id == id)
|
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.User == user && p.Id == id) ??
|
||||||
?? throw GracefulException.NotFound("File not found");
|
throw GracefulException.NotFound("File not found");
|
||||||
|
|
||||||
file.Name = request.Filename ?? file.Name;
|
file.Name = request.Filename ?? file.Name;
|
||||||
file.IsSensitive = request.Sensitive ?? file.IsSensitive;
|
file.IsSensitive = request.Sensitive ?? file.IsSensitive;
|
||||||
|
@ -221,8 +178,8 @@ public class DriveController(
|
||||||
public async Task<IActionResult> DeleteFile(string id)
|
public async Task<IActionResult> DeleteFile(string id)
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.User == user && p.Id == id)
|
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.User == user && p.Id == id) ??
|
||||||
?? throw GracefulException.NotFound("File not found");
|
throw GracefulException.NotFound("File not found");
|
||||||
|
|
||||||
if (await db.Users.AnyAsync(p => p.Avatar == file || p.Banner == file))
|
if (await db.Users.AnyAsync(p => p.Avatar == file || p.Banner == file))
|
||||||
throw GracefulException.UnprocessableEntity("Refusing to delete file: used in banner or avatar");
|
throw GracefulException.UnprocessableEntity("Refusing to delete file: used in banner or avatar");
|
||||||
|
@ -236,318 +193,4 @@ public class DriveController(
|
||||||
|
|
||||||
return StatusCode(StatusCodes.Status202Accepted);
|
return StatusCode(StatusCodes.Status202Accepted);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/move")]
|
|
||||||
[Authenticate]
|
|
||||||
[Authorize]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
|
||||||
public async Task<DriveFileResponse> UpdateFileParent(string id, DriveMoveRequest request)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
|
|
||||||
var file = await db.DriveFiles
|
|
||||||
.FirstOrDefaultAsync(p => p.Id == id && p.UserId == user.Id)
|
|
||||||
?? throw GracefulException.RecordNotFound();
|
|
||||||
|
|
||||||
if (request.FolderId != null)
|
|
||||||
{
|
|
||||||
var parent = await db.DriveFolders
|
|
||||||
.FirstOrDefaultAsync(p => p.Id == request.FolderId && p.UserId == user.Id);
|
|
||||||
if (parent == null)
|
|
||||||
throw GracefulException.NotFound("The new parent folder doesn't exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
file.FolderId = request.FolderId;
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return await GetFileById(file.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("folder")]
|
|
||||||
[Authenticate]
|
|
||||||
[Authorize]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
public async Task<DriveFolderResponse> GetRootFolder()
|
|
||||||
{
|
|
||||||
return await GetFolder(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("folder")]
|
|
||||||
[Authenticate]
|
|
||||||
[Authorize]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound, HttpStatusCode.Conflict)]
|
|
||||||
[SuppressMessage("Performance", "CA1862", Justification = "string.Equals() cannot be used in DbSet LINQ operations")]
|
|
||||||
public async Task<DriveFolderResponse> CreateFolder(DriveFolderRequest request)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(request.Name))
|
|
||||||
throw GracefulException.BadRequest("Folder name cannot be empty");
|
|
||||||
|
|
||||||
var existing = await db.DriveFolders
|
|
||||||
.AnyAsync(p => p.Name.ToLower() == request.Name.Trim().ToLower()
|
|
||||||
&& p.ParentId == request.ParentId
|
|
||||||
&& p.UserId == user.Id);
|
|
||||||
if (existing)
|
|
||||||
throw GracefulException.Conflict("A folder with this name already exists");
|
|
||||||
|
|
||||||
var driveFolder = await driveSvc.CreateFolderAsync(user, request.Name.Trim(), request.ParentId);
|
|
||||||
|
|
||||||
return new DriveFolderResponse
|
|
||||||
{
|
|
||||||
Id = driveFolder.Id,
|
|
||||||
Name = driveFolder.Name,
|
|
||||||
ParentId = driveFolder.ParentId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("folder/{id}")]
|
|
||||||
[Authenticate]
|
|
||||||
[Authorize]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
|
||||||
public async Task<DriveFolderResponse> GetFolder(string? id)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
|
|
||||||
var folder = id != null
|
|
||||||
? await db.DriveFolders.FirstOrDefaultAsync(p => p.Id == id && p.UserId == user.Id)
|
|
||||||
?? throw GracefulException.RecordNotFound()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
var driveFiles = await db.DriveFiles
|
|
||||||
.Where(p => p.FolderId == id && p.UserId == user.Id)
|
|
||||||
.OrderByDescending(p => p.CreatedAt)
|
|
||||||
.ThenBy(p => p.Id)
|
|
||||||
.Select(p => new DriveFileResponse
|
|
||||||
{
|
|
||||||
Id = p.Id,
|
|
||||||
Url = p.RawAccessUrl,
|
|
||||||
ThumbnailUrl = p.RawThumbnailAccessUrl,
|
|
||||||
Filename = p.Name,
|
|
||||||
ContentType = p.Type,
|
|
||||||
Sensitive = p.IsSensitive,
|
|
||||||
Description = p.Comment,
|
|
||||||
IsAvatar = p.UserAvatar != null,
|
|
||||||
IsBanner = p.UserBanner != null
|
|
||||||
})
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var driveFolders = await db.DriveFolders
|
|
||||||
.Where(p => p.ParentId == id && p.UserId == user.Id)
|
|
||||||
.OrderBy(p => p.Name)
|
|
||||||
.ThenBy(p => p.Id)
|
|
||||||
.Select(p => new DriveFolderResponse
|
|
||||||
{
|
|
||||||
Id = p.Id, Name = p.Name, ParentId = p.ParentId
|
|
||||||
})
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return new DriveFolderResponse
|
|
||||||
{
|
|
||||||
Id = folder?.Id,
|
|
||||||
Name = folder?.Name,
|
|
||||||
ParentId = folder?.ParentId,
|
|
||||||
Files = driveFiles,
|
|
||||||
Folders = driveFolders
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("folder/{id}")]
|
|
||||||
[Authenticate]
|
|
||||||
[Authorize]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound, HttpStatusCode.Conflict)]
|
|
||||||
[SuppressMessage("Performance", "CA1862", Justification = "string.Equals() cannot be used in DbSet LINQ operations")]
|
|
||||||
public async Task<DriveFolderResponse> UpdateFolder(string id, [FromHybrid] string name)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
|
||||||
throw GracefulException.BadRequest("Name must not be empty");
|
|
||||||
|
|
||||||
var folder = await db.DriveFolders
|
|
||||||
.FirstOrDefaultAsync(p => p.Id == id && p.UserId == user.Id)
|
|
||||||
?? throw GracefulException.RecordNotFound();
|
|
||||||
|
|
||||||
var existing = await db.DriveFolders
|
|
||||||
.AnyAsync(p => p.Name.ToLower() == name.Trim().ToLower()
|
|
||||||
&& p.ParentId == folder.ParentId
|
|
||||||
&& p.UserId == user.Id);
|
|
||||||
if (existing)
|
|
||||||
throw GracefulException.Conflict("A folder with this name already exists");
|
|
||||||
|
|
||||||
folder.Name = name.Trim();
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return new DriveFolderResponse { Id = folder.Id, Name = folder.Name, ParentId = folder.ParentId };
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("folder/{id}")]
|
|
||||||
[Authenticate]
|
|
||||||
[Authorize]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound, HttpStatusCode.Conflict)]
|
|
||||||
public async Task DeleteFolder(string id)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(id))
|
|
||||||
throw GracefulException.BadRequest("Cannot delete root folder");
|
|
||||||
|
|
||||||
var folder = await db.DriveFolders
|
|
||||||
.FirstOrDefaultAsync(p => p.Id == id && p.UserId == user.Id)
|
|
||||||
?? throw GracefulException.RecordNotFound();
|
|
||||||
|
|
||||||
var driveFiles = await db.DriveFiles
|
|
||||||
.CountAsync(p => p.FolderId == id);
|
|
||||||
var driveFolders = await db.DriveFolders
|
|
||||||
.CountAsync(p => p.ParentId == id);
|
|
||||||
|
|
||||||
if (driveFiles != 0 || driveFolders != 0)
|
|
||||||
throw GracefulException.Conflict("Cannot delete a non-empty folder");
|
|
||||||
|
|
||||||
db.Remove(folder);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("folder/{id}/move")]
|
|
||||||
[Authenticate]
|
|
||||||
[Authorize]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound, HttpStatusCode.Conflict)]
|
|
||||||
[SuppressMessage("Performance", "CA1862", Justification = "string.Equals() cannot be used in DbSet LINQ operations")]
|
|
||||||
public async Task<DriveFolderResponse> UpdateFolderParent(string id, DriveMoveRequest request)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
|
|
||||||
if (request.FolderId == id)
|
|
||||||
throw GracefulException.BadRequest("Cannot move a folder into itself");
|
|
||||||
|
|
||||||
var folder = await db.DriveFolders
|
|
||||||
.FirstOrDefaultAsync(p => p.Id == id && p.UserId == user.Id)
|
|
||||||
?? throw GracefulException.RecordNotFound();
|
|
||||||
|
|
||||||
if (request.FolderId != null)
|
|
||||||
{
|
|
||||||
var parent = await db.DriveFolders
|
|
||||||
.FirstOrDefaultAsync(p => p.Id == request.FolderId && p.UserId == user.Id);
|
|
||||||
if (parent == null)
|
|
||||||
throw GracefulException.NotFound("The new parent folder doesn't exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
var existing = await db.DriveFolders
|
|
||||||
.AnyAsync(p => p.Name.ToLower() == folder.Name.ToLower()
|
|
||||||
&& p.ParentId == request.FolderId
|
|
||||||
&& p.UserId == user.Id);
|
|
||||||
if (existing)
|
|
||||||
throw GracefulException.Conflict("A folder with this name already exists in the new parent folder");
|
|
||||||
|
|
||||||
folder.ParentId = request.FolderId;
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return new DriveFolderResponse { Id = folder.Id, Name = folder.Name, ParentId = folder.ParentId, };
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<IActionResult> GetFileByAccessKey(string accessKey, string? version, DriveFile? file)
|
|
||||||
{
|
|
||||||
file ??= await db.DriveFiles.FirstOrDefaultAsync(p => p.AccessKey == accessKey
|
|
||||||
|| p.PublicAccessKey == accessKey
|
|
||||||
|| p.ThumbnailAccessKey == accessKey);
|
|
||||||
if (file == null)
|
|
||||||
{
|
|
||||||
Response.Headers.CacheControl = "max-age=86400";
|
|
||||||
throw GracefulException.NotFound("File not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.IsLink)
|
|
||||||
{
|
|
||||||
var fetchUrl = version is "thumbnail"
|
|
||||||
? file.RawThumbnailAccessUrl
|
|
||||||
: file.RawAccessUrl;
|
|
||||||
|
|
||||||
if (!options.Value.ProxyRemoteMedia)
|
|
||||||
return Redirect(fetchUrl);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var filename = file.AccessKey == accessKey || file.Name.EndsWith(".webp")
|
|
||||||
? file.Name
|
|
||||||
: $"{file.Name}.webp";
|
|
||||||
|
|
||||||
return await ProxyAsync(fetchUrl, file.Type, filename);
|
|
||||||
}
|
|
||||||
catch (Exception e) when (e is not GracefulException)
|
|
||||||
{
|
|
||||||
throw GracefulException.BadGateway($"Failed to proxy request: {e.Message}", suppressLog: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.StoredInternal)
|
|
||||||
{
|
|
||||||
var pathBase = options.Value.Local?.Path;
|
|
||||||
if (string.IsNullOrWhiteSpace(pathBase))
|
|
||||||
{
|
|
||||||
logger.LogError("Failed to get file {accessKey} from local storage: path does not exist", accessKey);
|
|
||||||
throw GracefulException.NotFound("File not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
var path = Path.Join(pathBase, accessKey);
|
|
||||||
var stream = System.IO.File.OpenRead(path);
|
|
||||||
|
|
||||||
Response.Headers.CacheControl = CacheControl;
|
|
||||||
Response.Headers.XContentTypeOptions = "nosniff";
|
|
||||||
|
|
||||||
return Constants.BrowserSafeMimeTypes.Contains(file.Type)
|
|
||||||
? new InlineFileStreamResult(stream, file.Type, file.Name, true)
|
|
||||||
: File(stream, file.Type, file.Name, true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var stream = await objectStorage.GetFileAsync(accessKey);
|
|
||||||
if (stream == null)
|
|
||||||
{
|
|
||||||
logger.LogError("Failed to get file {accessKey} from object storage", accessKey);
|
|
||||||
throw GracefulException.NotFound("File not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
Response.Headers.CacheControl = CacheControl;
|
|
||||||
Response.Headers.XContentTypeOptions = "nosniff";
|
|
||||||
|
|
||||||
return Constants.BrowserSafeMimeTypes.Contains(file.Type)
|
|
||||||
? new InlineFileStreamResult(stream, file.Type, file.Name, true)
|
|
||||||
: File(stream, file.Type, file.Name, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<IActionResult> ProxyAsync(string url, string? expectedMediaType, string? filename)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// @formatter:off
|
|
||||||
var res = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
|
|
||||||
if (!res.IsSuccessStatusCode)
|
|
||||||
throw GracefulException.BadGateway($"Failed to proxy request: response status was {res.StatusCode}", suppressLog: true);
|
|
||||||
if (res.Content.Headers.ContentType?.MediaType is not { } mediaType)
|
|
||||||
throw GracefulException.BadGateway("Failed to proxy request: remote didn't return Content-Type");
|
|
||||||
if (expectedMediaType != null && mediaType != expectedMediaType && !Constants.BrowserSafeMimeTypes.Contains(mediaType))
|
|
||||||
throw GracefulException.BadGateway("Failed to proxy request: content type mismatch", suppressLog: true);
|
|
||||||
// @formatter:on
|
|
||||||
|
|
||||||
Response.Headers.CacheControl = CacheControl;
|
|
||||||
Response.Headers.XContentTypeOptions = "nosniff";
|
|
||||||
|
|
||||||
var stream = await res.Content.ReadAsStreamAsync();
|
|
||||||
|
|
||||||
return Constants.BrowserSafeMimeTypes.Contains(mediaType)
|
|
||||||
? new InlineFileStreamResult(stream, mediaType, filename, true)
|
|
||||||
: File(stream, mediaType, filename, true);
|
|
||||||
}
|
|
||||||
catch (Exception e) when (e is not GracefulException)
|
|
||||||
{
|
|
||||||
throw GracefulException.BadGateway($"Failed to proxy request: {e.Message}", suppressLog: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,18 +1,13 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||||
using Iceshrimp.Backend.Controllers.Shared.Schemas;
|
|
||||||
using Iceshrimp.Backend.Core.Configuration;
|
|
||||||
using Iceshrimp.Backend.Core.Database;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
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.Helpers;
|
|
||||||
using Iceshrimp.Shared.Schemas.Web;
|
using Iceshrimp.Shared.Schemas.Web;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Web;
|
namespace Iceshrimp.Backend.Controllers.Web;
|
||||||
|
|
||||||
|
@ -23,7 +18,6 @@ namespace Iceshrimp.Backend.Controllers.Web;
|
||||||
[Route("/api/iceshrimp/emoji")]
|
[Route("/api/iceshrimp/emoji")]
|
||||||
[Produces(MediaTypeNames.Application.Json)]
|
[Produces(MediaTypeNames.Application.Json)]
|
||||||
public class EmojiController(
|
public class EmojiController(
|
||||||
IOptions<Config.InstanceSection> instance,
|
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
EmojiService emojiSvc,
|
EmojiService emojiSvc,
|
||||||
EmojiImportService emojiImportSvc
|
EmojiImportService emojiImportSvc
|
||||||
|
@ -40,102 +34,31 @@ 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.PublicUrl,
|
||||||
License = p.License,
|
License = p.License,
|
||||||
Sensitive = p.Sensitive
|
Sensitive = p.Sensitive
|
||||||
})
|
})
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("remote")]
|
|
||||||
[Authorize("role:moderator")]
|
|
||||||
[RestPagination(100, 500)]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
public async Task<PaginationWrapper<List<EmojiResponse>>> GetRemoteEmoji(
|
|
||||||
[FromQuery] string? name, [FromQuery] string? host, PaginationQuery pq
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var res = await db.Emojis
|
|
||||||
.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
|
|
||||||
{
|
|
||||||
Id = p.Id,
|
|
||||||
Name = p.Name,
|
|
||||||
Uri = p.Uri,
|
|
||||||
Tags = p.Tags,
|
|
||||||
Category = p.Host,
|
|
||||||
PublicUrl = p.GetAccessUrl(instance.Value),
|
|
||||||
License = p.License,
|
|
||||||
Sensitive = p.Sensitive
|
|
||||||
})
|
|
||||||
.Paginate(pq, ControllerContext)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return HttpContext.CreatePaginationWrapper(pq, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("remote/{host}")]
|
|
||||||
[Authorize("role:moderator")]
|
|
||||||
[RestPagination(100, 500)]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
public async Task<PaginationWrapper<List<EmojiResponse>>> GetRemoteEmojiByHost(string host, PaginationQuery pq)
|
|
||||||
{
|
|
||||||
var res = await db.Emojis
|
|
||||||
.Where(p => p.Host == host)
|
|
||||||
.Select(p => new EmojiResponse
|
|
||||||
{
|
|
||||||
Id = p.Id,
|
|
||||||
Name = p.Name,
|
|
||||||
Uri = p.Uri,
|
|
||||||
Tags = p.Tags,
|
|
||||||
Category = p.Host,
|
|
||||||
PublicUrl = p.GetAccessUrl(instance.Value),
|
|
||||||
License = p.License,
|
|
||||||
Sensitive = p.Sensitive
|
|
||||||
})
|
|
||||||
.Paginate(pq, ControllerContext)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return HttpContext.CreatePaginationWrapper(pq, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("remote/hosts")]
|
|
||||||
[Authorize("role:moderator")]
|
|
||||||
[LinkPagination(20, 250)]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
public async Task<IEnumerable<EntityWrapper<string>>> GetEmojiHostsAsync(PaginationQuery pq)
|
|
||||||
{
|
|
||||||
pq.MinId ??= "";
|
|
||||||
var res = await db.Emojis.Where(p => p.Host != null)
|
|
||||||
.Select(p => new EntityWrapper<string> { Entity = p.Host!, Id = p.Host! })
|
|
||||||
.Distinct()
|
|
||||||
.Paginate(pq, ControllerContext)
|
|
||||||
.ToListAsync()
|
|
||||||
.ContinueWithResult(p => p.NotNull());
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||||
public async Task<EmojiResponse> GetEmoji(string id)
|
public async Task<EmojiResponse> GetEmoji(string id)
|
||||||
{
|
{
|
||||||
var emoji = await db.Emojis.FirstOrDefaultAsync(p => p.Id == id)
|
var emoji = await db.Emojis.FirstOrDefaultAsync(p => p.Id == id) ??
|
||||||
?? throw GracefulException.NotFound("Emoji not found");
|
throw GracefulException.NotFound("Emoji not found");
|
||||||
|
|
||||||
return new EmojiResponse
|
return new EmojiResponse
|
||||||
{
|
{
|
||||||
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.PublicUrl,
|
||||||
License = emoji.License,
|
License = emoji.License,
|
||||||
Sensitive = emoji.Sensitive
|
Sensitive = emoji.Sensitive
|
||||||
};
|
};
|
||||||
|
@ -145,19 +68,18 @@ public class EmojiController(
|
||||||
[Authorize("role:moderator")]
|
[Authorize("role:moderator")]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
[ProducesErrors(HttpStatusCode.Conflict)]
|
[ProducesErrors(HttpStatusCode.Conflict)]
|
||||||
public async Task<EmojiResponse> UploadEmoji(IFormFile file, [FromQuery] string name)
|
public async Task<EmojiResponse> UploadEmoji(IFormFile file)
|
||||||
{
|
{
|
||||||
var ext = Path.HasExtension(file.FileName) ? Path.GetExtension(file.FileName) : "";
|
var emoji = await emojiSvc.CreateEmojiFromStreamAsync(file.OpenReadStream(), file.FileName, file.ContentType);
|
||||||
var emoji = await emojiSvc.CreateEmojiFromStreamAsync(file.OpenReadStream(), name + ext, file.ContentType);
|
|
||||||
|
|
||||||
return new EmojiResponse
|
return new EmojiResponse
|
||||||
{
|
{
|
||||||
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.PublicUrl,
|
||||||
License = null,
|
License = null,
|
||||||
Sensitive = false
|
Sensitive = false
|
||||||
};
|
};
|
||||||
|
@ -181,9 +103,9 @@ 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.PublicUrl,
|
||||||
License = null,
|
License = null,
|
||||||
Sensitive = cloned.Sensitive
|
Sensitive = cloned.Sensitive
|
||||||
};
|
};
|
||||||
|
@ -191,7 +113,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,21 +126,21 @@ 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");
|
||||||
|
|
||||||
return new EmojiResponse
|
return new EmojiResponse
|
||||||
{
|
{
|
||||||
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.PublicUrl,
|
||||||
License = emoji.License,
|
License = emoji.License,
|
||||||
Sensitive = emoji.Sensitive
|
Sensitive = emoji.Sensitive
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,15 +1,27 @@
|
||||||
using System.IO.Hashing;
|
using System.IO.Hashing;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Mime;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||||
|
using Microsoft.AspNetCore.Cors;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.Drawing.Processing;
|
using SixLabors.ImageSharp.Drawing.Processing;
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
using SixLabors.ImageSharp.Processing;
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Helpers;
|
namespace Iceshrimp.Backend.Controllers.Web;
|
||||||
|
|
||||||
public static class IdenticonHelper
|
[ApiController]
|
||||||
|
[EnableCors("drive")]
|
||||||
|
[Route("/identicon/{id}")]
|
||||||
|
[Route("/identicon/{id}.png")]
|
||||||
|
[Produces(MediaTypeNames.Image.Png)]
|
||||||
|
public class IdenticonController : ControllerBase
|
||||||
{
|
{
|
||||||
public static async Task<Stream> GetIdenticonAsync(string id)
|
[HttpGet]
|
||||||
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
|
public async Task GetIdenticon(string id)
|
||||||
{
|
{
|
||||||
using var image = new Image<Rgb24>(Size, Size);
|
using var image = new Image<Rgb24>(Size, Size);
|
||||||
|
|
||||||
|
@ -62,10 +74,9 @@ public static class IdenticonHelper
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var stream = new MemoryStream();
|
Response.Headers.CacheControl = "max-age=31536000, immutable";
|
||||||
await image.SaveAsPngAsync(stream);
|
Response.Headers.ContentType = "image/png";
|
||||||
stream.Seek(0, SeekOrigin.Begin);
|
await image.SaveAsPngAsync(Response.Body);
|
||||||
return stream;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Color definitions & Constants
|
#region Color definitions & Constants
|
|
@ -10,7 +10,6 @@ using Iceshrimp.Backend.Core.Services;
|
||||||
using Iceshrimp.Shared.Schemas.Web;
|
using Iceshrimp.Shared.Schemas.Web;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Web;
|
namespace Iceshrimp.Backend.Controllers.Web;
|
||||||
|
@ -23,94 +22,23 @@ public class InstanceController(
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
UserRenderer userRenderer,
|
UserRenderer userRenderer,
|
||||||
IOptions<Config.InstanceSection> instanceConfig,
|
IOptions<Config.InstanceSection> instanceConfig,
|
||||||
IOptionsSnapshot<Config.SecuritySection> securityConfig,
|
IOptions<Config.SecuritySection> securityConfig,
|
||||||
MetaService meta,
|
MetaService meta
|
||||||
InstanceService instanceSvc
|
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
public async Task<InstanceResponse> GetInfo()
|
public async Task<InstanceResponse> GetInfo()
|
||||||
{
|
{
|
||||||
var limits = new Limitations { NoteLength = instanceConfig.Value.CharacterLimit };
|
|
||||||
|
|
||||||
return new InstanceResponse
|
return new InstanceResponse
|
||||||
{
|
{
|
||||||
AccountDomain = instanceConfig.Value.AccountDomain,
|
AccountDomain = instanceConfig.Value.AccountDomain,
|
||||||
WebDomain = instanceConfig.Value.WebDomain,
|
WebDomain = instanceConfig.Value.WebDomain,
|
||||||
Registration = (Registrations)securityConfig.Value.Registrations,
|
Registration = (Registrations)securityConfig.Value.Registrations,
|
||||||
Name = await meta.GetAsync(MetaEntity.InstanceName),
|
Name = await meta.GetAsync(MetaEntity.InstanceName)
|
||||||
Limits = limits
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("rules")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
public async Task<List<RuleResponse>> GetRules()
|
|
||||||
{
|
|
||||||
return await db.Rules
|
|
||||||
.OrderBy(p => p.Order)
|
|
||||||
.ThenBy(p => p.Id)
|
|
||||||
.Select(p => new RuleResponse { Id = p.Id, Text = p.Text, Description = p.Description })
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("rules")]
|
|
||||||
[Authenticate]
|
|
||||||
[Authorize("role:admin")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
public async Task<RuleResponse> CreateRule(RuleCreateRequest request)
|
|
||||||
{
|
|
||||||
var rule = await instanceSvc.CreateRuleAsync(request.Text.Trim(), request.Description?.Trim());
|
|
||||||
|
|
||||||
return new RuleResponse { Id = rule.Id, Text = rule.Text, Description = rule.Description };
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPatch("rules/{id}")]
|
|
||||||
[Authenticate]
|
|
||||||
[Authorize("role:admin")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
|
||||||
public async Task<RuleResponse> UpdateRule(string id, RuleUpdateRequest request)
|
|
||||||
{
|
|
||||||
var rule = await db.Rules.FirstOrDefaultAsync(p => p.Id == id)
|
|
||||||
?? throw GracefulException.RecordNotFound();
|
|
||||||
|
|
||||||
var order = request.Order ?? 0;
|
|
||||||
var text = request.Text?.Trim() ?? rule.Text;
|
|
||||||
var description = request.Description != null
|
|
||||||
? string.IsNullOrWhiteSpace(request.Description)
|
|
||||||
? null
|
|
||||||
: request.Description.Trim()
|
|
||||||
: rule.Description;
|
|
||||||
|
|
||||||
var res = await instanceSvc.UpdateRuleAsync(rule, order, text, description);
|
|
||||||
|
|
||||||
return new RuleResponse { Id = res.Id, Text = res.Text, Description = res.Description };
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("rules/{id}")]
|
|
||||||
[Authenticate]
|
|
||||||
[Authorize("role:admin")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
|
||||||
public async Task DeleteRule(string id)
|
|
||||||
{
|
|
||||||
var rule = await db.Rules.FirstOrDefaultAsync(p => p.Id == id)
|
|
||||||
?? throw GracefulException.RecordNotFound();
|
|
||||||
|
|
||||||
var rules = await db.Rules
|
|
||||||
.Where(p => p.Order > rule.Order)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
db.Remove(rule);
|
|
||||||
|
|
||||||
foreach (var r in rules)
|
|
||||||
r.Order -= 1;
|
|
||||||
db.UpdateRange(rules);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("staff")]
|
[HttpGet("staff")]
|
||||||
[Authenticate]
|
[Authenticate]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -29,9 +29,7 @@ public class NoteController(
|
||||||
NoteRenderer noteRenderer,
|
NoteRenderer noteRenderer,
|
||||||
UserRenderer userRenderer,
|
UserRenderer userRenderer,
|
||||||
CacheService cache,
|
CacheService cache,
|
||||||
BiteService biteSvc,
|
BiteService biteSvc
|
||||||
PollService pollSvc,
|
|
||||||
ReportService reportSvc
|
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
|
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
|
||||||
|
@ -159,10 +157,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();
|
||||||
|
@ -504,64 +500,6 @@ public class NoteController(
|
||||||
await db.NoteThreadMutings.Where(p => p.User == user && p.ThreadId == target).ExecuteDeleteAsync();
|
await db.NoteThreadMutings.Where(p => p.User == user && p.ThreadId == target).ExecuteDeleteAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/vote")]
|
|
||||||
[Authenticate]
|
|
||||||
[Authorize]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
|
|
||||||
public async Task<NotePollSchema> AddPollVote(string id, NotePollRequest request)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
|
|
||||||
request.Choices.RemoveAll(p => p < 0);
|
|
||||||
if (request.Choices.Count == 0)
|
|
||||||
throw GracefulException.BadRequest("At least one vote must be included");
|
|
||||||
|
|
||||||
var target = await db.Notes
|
|
||||||
.IncludeCommonProperties()
|
|
||||||
.Include(p => p.Poll)
|
|
||||||
.Where(p => p.Id == id && p.Poll != null)
|
|
||||||
.EnsureVisibleFor(user)
|
|
||||||
.FirstOrDefaultAsync()
|
|
||||||
?? throw GracefulException.NotFound("Poll not found");
|
|
||||||
|
|
||||||
if (target.Poll!.ExpiresAt != null && target.Poll.ExpiresAt < DateTime.UtcNow)
|
|
||||||
throw GracefulException.NotFound("Poll has expired");
|
|
||||||
|
|
||||||
var voted = await db.PollVotes
|
|
||||||
.AnyAsync(p => p.NoteId == id && p.UserId == user.Id);
|
|
||||||
if (voted)
|
|
||||||
throw GracefulException.BadRequest("You have already voted");
|
|
||||||
|
|
||||||
if (!target.Poll.Multiple)
|
|
||||||
request.Choices.RemoveRange(1, request.Choices.Count - 1);
|
|
||||||
|
|
||||||
List<PollVote> votes = [];
|
|
||||||
foreach (var choice in request.Choices)
|
|
||||||
{
|
|
||||||
var vote = new PollVote
|
|
||||||
{
|
|
||||||
Id = IdHelpers.GenerateSnowflakeId(),
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
UserId = user.Id,
|
|
||||||
NoteId = id,
|
|
||||||
Choice = choice
|
|
||||||
};
|
|
||||||
await db.AddAsync(vote);
|
|
||||||
votes.Add(vote);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
foreach (var vote in votes)
|
|
||||||
await pollSvc.RegisterPollVoteAsync(vote, target.Poll, target);
|
|
||||||
|
|
||||||
await db.ReloadEntityAsync(target.Poll);
|
|
||||||
|
|
||||||
var res = await GetNote(id);
|
|
||||||
return res.Poll!;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authenticate]
|
[Authenticate]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
@ -617,18 +555,6 @@ public class NoteController(
|
||||||
? await db.DriveFiles.Where(p => request.MediaIds.Contains(p.Id)).ToListAsync()
|
? await db.DriveFiles.Where(p => request.MediaIds.Contains(p.Id)).ToListAsync()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var minPollExpire = DateTime.UtcNow.AddMinutes(5);
|
|
||||||
var poll = request.Poll != null
|
|
||||||
? new Poll
|
|
||||||
{
|
|
||||||
ExpiresAt = request.Poll.ExpiresAt != null
|
|
||||||
? request.Poll.ExpiresAt <= minPollExpire ? minPollExpire : request.Poll.ExpiresAt
|
|
||||||
: null,
|
|
||||||
Multiple = request.Poll.Multiple,
|
|
||||||
Choices = request.Poll.Choices,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
|
|
||||||
var note = await noteSvc.CreateNoteAsync(new NoteService.NoteCreationData
|
var note = await noteSvc.CreateNoteAsync(new NoteService.NoteCreationData
|
||||||
{
|
{
|
||||||
User = user,
|
User = user,
|
||||||
|
@ -637,8 +563,7 @@ public class NoteController(
|
||||||
Cw = request.Cw,
|
Cw = request.Cw,
|
||||||
Reply = reply,
|
Reply = reply,
|
||||||
Renote = renote,
|
Renote = renote,
|
||||||
Attachments = attachments,
|
Attachments = attachments
|
||||||
Poll = poll
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (request.IdempotencyKey != null)
|
if (request.IdempotencyKey != null)
|
||||||
|
@ -646,18 +571,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);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,14 +1,12 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||||
using Iceshrimp.Backend.Core.Database;
|
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
using Iceshrimp.Backend.Core.Middleware;
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
using Iceshrimp.Backend.Core.Services;
|
using Iceshrimp.Backend.Core.Services;
|
||||||
using Iceshrimp.Shared.Schemas.Web;
|
using Iceshrimp.Shared.Schemas.Web;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Web;
|
namespace Iceshrimp.Backend.Controllers.Web;
|
||||||
|
|
||||||
|
@ -18,11 +16,7 @@ namespace Iceshrimp.Backend.Controllers.Web;
|
||||||
[EnableRateLimiting("sliding")]
|
[EnableRateLimiting("sliding")]
|
||||||
[Route("/api/iceshrimp/profile")]
|
[Route("/api/iceshrimp/profile")]
|
||||||
[Produces(MediaTypeNames.Application.Json)]
|
[Produces(MediaTypeNames.Application.Json)]
|
||||||
public class ProfileController(
|
public class ProfileController(UserService userSvc, DriveService driveSvc) : ControllerBase
|
||||||
UserService userSvc,
|
|
||||||
DriveService driveSvc,
|
|
||||||
DatabaseContext db
|
|
||||||
) : ControllerBase
|
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
|
@ -43,8 +37,7 @@ public class ProfileController(
|
||||||
DisplayName = user.DisplayName ?? "",
|
DisplayName = user.DisplayName ?? "",
|
||||||
IsBot = user.IsBot,
|
IsBot = user.IsBot,
|
||||||
IsCat = user.IsCat,
|
IsCat = user.IsCat,
|
||||||
SpeakAsCat = user.SpeakAsCat,
|
SpeakAsCat = user.SpeakAsCat
|
||||||
Pronouns = profile.Pronouns ?? []
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +69,6 @@ public class ProfileController(
|
||||||
profile.Birthday = birthday;
|
profile.Birthday = birthday;
|
||||||
profile.Fields = fields.ToArray();
|
profile.Fields = fields.ToArray();
|
||||||
profile.FFVisibility = (UserProfile.UserProfileFFVisibility)newProfile.FFVisibility;
|
profile.FFVisibility = (UserProfile.UserProfileFFVisibility)newProfile.FFVisibility;
|
||||||
profile.Pronouns = newProfile.Pronouns;
|
|
||||||
|
|
||||||
user.DisplayName = string.IsNullOrWhiteSpace(newProfile.DisplayName) ? null : newProfile.DisplayName.Trim();
|
user.DisplayName = string.IsNullOrWhiteSpace(newProfile.DisplayName) ? null : newProfile.DisplayName.Trim();
|
||||||
|
|
||||||
|
@ -84,28 +76,6 @@ public class ProfileController(
|
||||||
user.IsCat = newProfile.IsCat;
|
user.IsCat = newProfile.IsCat;
|
||||||
user.SpeakAsCat = newProfile is { SpeakAsCat: true, IsCat: true };
|
user.SpeakAsCat = newProfile is { SpeakAsCat: true, IsCat: true };
|
||||||
|
|
||||||
if (newProfile.AvatarAlt != null)
|
|
||||||
{
|
|
||||||
var avatar = await db.DriveFiles
|
|
||||||
.FirstOrDefaultAsync(p => p.UserId == user.Id && p.UserAvatar != null);
|
|
||||||
|
|
||||||
if (avatar != null)
|
|
||||||
{
|
|
||||||
user.Avatar = avatar;
|
|
||||||
user.Avatar.Comment = string.IsNullOrWhiteSpace(newProfile.AvatarAlt) ? null : newProfile.AvatarAlt.Trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (newProfile.BannerAlt != null)
|
|
||||||
{
|
|
||||||
var banner = await db.DriveFiles
|
|
||||||
.FirstOrDefaultAsync(p => p.UserId == user.Id && p.UserBanner != null);
|
|
||||||
|
|
||||||
if (banner != null)
|
|
||||||
{
|
|
||||||
user.Banner = banner;
|
|
||||||
user.Banner.Comment = string.IsNullOrWhiteSpace(newProfile.BannerAlt) ? null : newProfile.BannerAlt.Trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var prevAvatarId = user.AvatarId;
|
var prevAvatarId = user.AvatarId;
|
||||||
var prevBannerId = user.BannerId;
|
var prevBannerId = user.BannerId;
|
||||||
await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
||||||
|
@ -113,35 +83,16 @@ public class ProfileController(
|
||||||
|
|
||||||
[HttpGet("avatar")]
|
[HttpGet("avatar")]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
public string GetAvatarUrl()
|
||||||
public async Task<DriveFileResponse> GetAvatar()
|
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
|
return user.AvatarUrl ?? "";
|
||||||
var file = await db.DriveFiles
|
|
||||||
.Include(p => p.UserAvatar)
|
|
||||||
.Include(p => p.UserBanner)
|
|
||||||
.FirstOrDefaultAsync(p => p.UserId == user.Id && p.UserAvatar != null)
|
|
||||||
?? throw GracefulException.RecordNotFound();
|
|
||||||
|
|
||||||
return new DriveFileResponse
|
|
||||||
{
|
|
||||||
Id = file.Id,
|
|
||||||
Url = file.RawAccessUrl,
|
|
||||||
ThumbnailUrl = file.RawThumbnailAccessUrl,
|
|
||||||
Filename = file.Name,
|
|
||||||
ContentType = file.Type,
|
|
||||||
Sensitive = file.IsSensitive,
|
|
||||||
Description = file.Comment,
|
|
||||||
IsAvatar = file.UserAvatar != null,
|
|
||||||
IsBanner = file.UserBanner != null
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("avatar")]
|
[HttpPost("avatar")]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
[ProducesErrors(HttpStatusCode.BadRequest)]
|
[ProducesErrors(HttpStatusCode.BadRequest)]
|
||||||
public async Task UpdateAvatar(IFormFile file, [FromQuery] string? altText)
|
public async Task UpdateAvatar(IFormFile file)
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
|
|
||||||
|
@ -155,21 +106,21 @@ public class ProfileController(
|
||||||
{
|
{
|
||||||
Filename = file.FileName,
|
Filename = file.FileName,
|
||||||
IsSensitive = false,
|
IsSensitive = false,
|
||||||
MimeType = file.ContentType,
|
MimeType = file.ContentType
|
||||||
Comment = altText
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var avatar = await driveSvc.StoreFileAsync(file.OpenReadStream(), user, rq);
|
var avatar = await driveSvc.StoreFileAsync(file.OpenReadStream(), user, rq);
|
||||||
|
|
||||||
user.Avatar = avatar;
|
user.Avatar = avatar;
|
||||||
user.AvatarId = avatar.Id;
|
|
||||||
user.AvatarBlurhash = avatar.Blurhash;
|
user.AvatarBlurhash = avatar.Blurhash;
|
||||||
|
user.AvatarUrl = avatar.AccessUrl;
|
||||||
|
|
||||||
await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("avatar")]
|
[HttpDelete("avatar")]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||||
public async Task DeleteAvatar()
|
public async Task DeleteAvatar()
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
|
@ -177,45 +128,28 @@ public class ProfileController(
|
||||||
var prevAvatarId = user.AvatarId;
|
var prevAvatarId = user.AvatarId;
|
||||||
var prevBannerId = user.BannerId;
|
var prevBannerId = user.BannerId;
|
||||||
|
|
||||||
if (prevAvatarId == null) return;
|
if (prevAvatarId == null)
|
||||||
|
throw GracefulException.NotFound("You do not have an avatar");
|
||||||
|
|
||||||
user.Avatar = null;
|
user.Avatar = null;
|
||||||
user.AvatarBlurhash = null;
|
user.AvatarBlurhash = null;
|
||||||
|
user.AvatarUrl = null;
|
||||||
|
|
||||||
await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("banner")]
|
[HttpGet("banner")]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
public string GetBannerUrl()
|
||||||
public async Task<DriveFileResponse> GetBanner()
|
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
|
return user.BannerUrl ?? "";
|
||||||
var file = await db.DriveFiles
|
|
||||||
.Include(p => p.UserAvatar)
|
|
||||||
.Include(p => p.UserBanner)
|
|
||||||
.FirstOrDefaultAsync(p => p.UserId == user.Id && p.UserBanner != null)
|
|
||||||
?? throw GracefulException.RecordNotFound();
|
|
||||||
|
|
||||||
return new DriveFileResponse
|
|
||||||
{
|
|
||||||
Id = file.Id,
|
|
||||||
Url = file.RawAccessUrl,
|
|
||||||
ThumbnailUrl = file.RawThumbnailAccessUrl,
|
|
||||||
Filename = file.Name,
|
|
||||||
ContentType = file.Type,
|
|
||||||
Sensitive = file.IsSensitive,
|
|
||||||
Description = file.Comment,
|
|
||||||
IsAvatar = file.UserAvatar != null,
|
|
||||||
IsBanner = file.UserBanner != null
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("banner")]
|
[HttpPost("banner")]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
[ProducesErrors(HttpStatusCode.BadRequest)]
|
[ProducesErrors(HttpStatusCode.BadRequest)]
|
||||||
public async Task UpdateBanner(IFormFile file, [FromQuery] string? altText)
|
public async Task UpdateBanner(IFormFile file)
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
|
|
||||||
|
@ -229,21 +163,21 @@ public class ProfileController(
|
||||||
{
|
{
|
||||||
Filename = file.FileName,
|
Filename = file.FileName,
|
||||||
IsSensitive = false,
|
IsSensitive = false,
|
||||||
MimeType = file.ContentType,
|
MimeType = file.ContentType
|
||||||
Comment = altText
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var banner = await driveSvc.StoreFileAsync(file.OpenReadStream(), user, rq);
|
var banner = await driveSvc.StoreFileAsync(file.OpenReadStream(), user, rq);
|
||||||
|
|
||||||
user.Banner = banner;
|
user.Banner = banner;
|
||||||
user.BannerId = banner.Id;
|
|
||||||
user.BannerBlurhash = banner.Blurhash;
|
user.BannerBlurhash = banner.Blurhash;
|
||||||
|
user.BannerUrl = banner.AccessUrl;
|
||||||
|
|
||||||
await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("banner")]
|
[HttpDelete("banner")]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||||
public async Task DeleteBanner()
|
public async Task DeleteBanner()
|
||||||
{
|
{
|
||||||
var user = HttpContext.GetUserOrFail();
|
var user = HttpContext.GetUserOrFail();
|
||||||
|
@ -251,10 +185,12 @@ public class ProfileController(
|
||||||
var prevAvatarId = user.AvatarId;
|
var prevAvatarId = user.AvatarId;
|
||||||
var prevBannerId = user.BannerId;
|
var prevBannerId = user.BannerId;
|
||||||
|
|
||||||
if (prevBannerId == null) return;
|
if (prevBannerId == null)
|
||||||
|
throw GracefulException.NotFound("You do not have a banner");
|
||||||
|
|
||||||
user.Banner = null;
|
user.Banner = null;
|
||||||
user.BannerBlurhash = null;
|
user.BannerBlurhash = null;
|
||||||
|
user.BannerUrl = null;
|
||||||
|
|
||||||
await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ public class NoteRenderer(
|
||||||
UserRenderer userRenderer,
|
UserRenderer userRenderer,
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
EmojiService emojiSvc,
|
EmojiService emojiSvc,
|
||||||
MediaProxyService mediaProxy,
|
|
||||||
IOptions<Config.InstanceSection> config
|
IOptions<Config.InstanceSection> config
|
||||||
) : IScopedService
|
) : IScopedService
|
||||||
{
|
{
|
||||||
|
@ -38,7 +37,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,10 +75,9 @@ 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);
|
|
||||||
|
|
||||||
return new NoteResponse
|
return new NoteResponse
|
||||||
{
|
{
|
||||||
|
@ -91,7 +88,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(),
|
||||||
|
@ -99,8 +95,7 @@ public class NoteRenderer(
|
||||||
Renotes = note.RenoteCount,
|
Renotes = note.RenoteCount,
|
||||||
Replies = note.RepliesCount,
|
Replies = note.RepliesCount,
|
||||||
Liked = liked,
|
Liked = liked,
|
||||||
Emoji = emoji,
|
Emoji = emoji
|
||||||
Poll = poll
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,13 +114,12 @@ public class NoteRenderer(
|
||||||
return files.Select(p => new NoteAttachment
|
return files.Select(p => new NoteAttachment
|
||||||
{
|
{
|
||||||
Id = p.Id,
|
Id = p.Id,
|
||||||
Url = mediaProxy.GetProxyUrl(p),
|
Url = p.AccessUrl,
|
||||||
ThumbnailUrl = mediaProxy.GetThumbnailProxyUrl(p),
|
ThumbnailUrl = p.ThumbnailAccessUrl,
|
||||||
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,15 +135,13 @@ 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,
|
||||||
})
|
})
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
@ -157,7 +149,7 @@ public class NoteRenderer(
|
||||||
{
|
{
|
||||||
var hit = await emojiSvc.ResolveEmojiAsync(item.Name);
|
var hit = await emojiSvc.ResolveEmojiAsync(item.Name);
|
||||||
if (hit == null) continue;
|
if (hit == null) continue;
|
||||||
item.Url = hit.GetAccessUrl(config.Value);
|
item.Url = hit.PublicUrl;
|
||||||
item.Sensitive = hit.Sensitive;
|
item.Sensitive = hit.Sensitive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,47 +191,15 @@ 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.PublicUrl,
|
||||||
License = p.License,
|
License = p.License,
|
||||||
Sensitive = p.Sensitive
|
Sensitive = p.Sensitive
|
||||||
})
|
})
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<NotePollSchema>> GetPollsAsync(IEnumerable<Note> notes, User? user)
|
|
||||||
{
|
|
||||||
var polls = await db.Polls
|
|
||||||
.Where(p => notes.Contains(p.Note))
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var votes = user != null
|
|
||||||
? await db.PollVotes
|
|
||||||
.Where(p => notes.Contains(p.Note) && p.UserId == user.Id)
|
|
||||||
.ToListAsync()
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return polls
|
|
||||||
.Select(p => new NotePollSchema
|
|
||||||
{
|
|
||||||
NoteId = p.NoteId,
|
|
||||||
ExpiresAt = p.ExpiresAt,
|
|
||||||
Multiple = p.Multiple,
|
|
||||||
Choices = p.Choices.Zip(p.Votes)
|
|
||||||
.Select((c, i) => new NotePollChoice
|
|
||||||
{
|
|
||||||
Value = c.First,
|
|
||||||
Votes = c.Second,
|
|
||||||
Voted = user != null
|
|
||||||
&& votes.Any(v => v.NoteId == p.NoteId && v.Choice == i)
|
|
||||||
})
|
|
||||||
.ToList(),
|
|
||||||
VotersCount = p.VotersCount
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<NoteResponse>> RenderManyAsync(
|
public async Task<IEnumerable<NoteResponse>> RenderManyAsync(
|
||||||
IEnumerable<Note> notes, User? user, Filter.FilterContext? filterContext = null
|
IEnumerable<Note> notes, User? user, Filter.FilterContext? filterContext = null
|
||||||
)
|
)
|
||||||
|
@ -254,8 +214,7 @@ public class NoteRenderer(
|
||||||
Reactions = await GetReactionsAsync(allNotes, user),
|
Reactions = await GetReactionsAsync(allNotes, user),
|
||||||
Filters = await GetFiltersAsync(user, filterContext),
|
Filters = await GetFiltersAsync(user, filterContext),
|
||||||
LikedNotes = await GetLikedNotesAsync(allNotes, user),
|
LikedNotes = await GetLikedNotesAsync(allNotes, user),
|
||||||
Emoji = await GetEmojiAsync(allNotes),
|
Emoji = await GetEmojiAsync(allNotes)
|
||||||
Polls = await GetPollsAsync(allNotes, user)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return await notesList.Select(p => RenderOne(p, user, filterContext, data)).AwaitAllAsync();
|
return await notesList.Select(p => RenderOne(p, user, filterContext, data)).AwaitAllAsync();
|
||||||
|
@ -269,6 +228,5 @@ public class NoteRenderer(
|
||||||
public List<string>? LikedNotes;
|
public List<string>? LikedNotes;
|
||||||
public List<NoteReactionSchema>? Reactions;
|
public List<NoteReactionSchema>? Reactions;
|
||||||
public List<UserResponse>? Users;
|
public List<UserResponse>? Users;
|
||||||
public List<NotePollSchema>? Polls;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,41 +1,33 @@
|
||||||
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 static Iceshrimp.Shared.Schemas.Web.NotificationResponse;
|
using static Iceshrimp.Shared.Schemas.Web.NotificationResponse;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Web.Renderers;
|
namespace Iceshrimp.Backend.Controllers.Web.Renderers;
|
||||||
|
|
||||||
public class NotificationRenderer(
|
public class NotificationRenderer(UserRenderer userRenderer, NoteRenderer noteRenderer, EmojiService emojiSvc) : IScopedService
|
||||||
IOptions<Config.InstanceSection> instance,
|
|
||||||
UserRenderer userRenderer,
|
|
||||||
NoteRenderer noteRenderer,
|
|
||||||
DatabaseContext db
|
|
||||||
) : IScopedService
|
|
||||||
{
|
{
|
||||||
private static NotificationResponse Render(Notification notification, NotificationRendererDto data)
|
private static NotificationResponse Render(Notification notification, NotificationRendererDto data)
|
||||||
{
|
{
|
||||||
var user = notification.Notifier != null
|
var user = notification.Notifier != null
|
||||||
? data.Users?.First(p => p.Id == notification.Notifier.Id)
|
? data.Users?.First(p => p.Id == notification.Notifier.Id) ??
|
||||||
?? throw new Exception("DTO didn't contain the notifier")
|
throw new Exception("DTO didn't contain the notifier")
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var note = notification.Note != null
|
var note = notification.Note != null
|
||||||
? data.Notes?.First(p => p.Id == notification.Note.Id) ?? throw new Exception("DTO didn't contain the note")
|
? data.Notes?.First(p => p.Id == notification.Note.Id) ??
|
||||||
|
throw new Exception("DTO didn't contain the note")
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var bite = notification.Bite != null
|
var bite = notification.Bite != null
|
||||||
? data.Bites?.First(p => p.Id == notification.Bite.Id) ?? throw new Exception("DTO didn't contain the bite")
|
? data.Bites?.First(p => p.Id == notification.Bite.Id) ??
|
||||||
|
throw new Exception("DTO didn't contain the bite")
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var reaction = notification.Reaction != null
|
var reaction = notification.Reaction != null
|
||||||
? data.Reactions?.First(p => p.Name == notification.Reaction)
|
? data.Reactions?.First(p => p.Name == notification.Reaction) ??
|
||||||
?? throw new Exception("DTO didn't contain the reaction")
|
throw new Exception("DTO didn't contain the reaction")
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return new NotificationResponse
|
return new NotificationResponse
|
||||||
|
@ -57,9 +49,9 @@ public class NotificationRenderer(
|
||||||
{
|
{
|
||||||
var data = new NotificationRendererDto
|
var data = new NotificationRendererDto
|
||||||
{
|
{
|
||||||
Users = await GetUsersAsync([notification]),
|
Users = await GetUsersAsync([notification]),
|
||||||
Notes = await GetNotesAsync([notification], localUser),
|
Notes = await GetNotesAsync([notification], localUser),
|
||||||
Bites = GetBites([notification]),
|
Bites = GetBites([notification]),
|
||||||
Reactions = await GetReactionsAsync([notification])
|
Reactions = await GetReactionsAsync([notification])
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -109,55 +101,15 @@ public class NotificationRenderer(
|
||||||
{
|
{
|
||||||
var reactions = notifications.Select(p => p.Reaction).NotNull().ToList();
|
var reactions = notifications.Select(p => p.Reaction).NotNull().ToList();
|
||||||
|
|
||||||
var emojis = reactions.Where(p => !EmojiService.IsCustomEmoji(p))
|
var emojis = reactions.Where(p => !EmojiService.IsCustomEmoji(p)).Select(p => new ReactionResponse { Name = p, Url = null, Sensitive = false }).ToList();
|
||||||
.Select(p => new ReactionResponse
|
var custom = reactions.Where(EmojiService.IsCustomEmoji).ToArray();
|
||||||
{
|
|
||||||
Name = p,
|
|
||||||
Url = null,
|
|
||||||
Sensitive = false
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
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 = s, Url = emoji.PublicUrl, Sensitive = emoji.Sensitive }
|
||||||
{
|
: new ReactionResponse { Name = s, Url = null, Sensitive = false };
|
||||||
Name = name,
|
|
||||||
Url = emoji.Url,
|
|
||||||
Sensitive = emoji.Sensitive
|
|
||||||
}
|
|
||||||
: new ReactionResponse
|
|
||||||
{
|
|
||||||
Name = name,
|
|
||||||
Url = null,
|
|
||||||
Sensitive = false
|
|
||||||
};
|
|
||||||
|
|
||||||
emojis.Add(reaction);
|
emojis.Add(reaction);
|
||||||
}
|
}
|
||||||
|
@ -172,9 +124,9 @@ public class NotificationRenderer(
|
||||||
var notificationsList = notifications.ToList();
|
var notificationsList = notifications.ToList();
|
||||||
var data = new NotificationRendererDto
|
var data = new NotificationRendererDto
|
||||||
{
|
{
|
||||||
Users = await GetUsersAsync(notificationsList),
|
Users = await GetUsersAsync(notificationsList),
|
||||||
Notes = await GetNotesAsync(notificationsList, user),
|
Notes = await GetNotesAsync(notificationsList, user),
|
||||||
Bites = GetBites(notificationsList),
|
Bites = GetBites(notificationsList),
|
||||||
Reactions = await GetReactionsAsync(notificationsList)
|
Reactions = await GetReactionsAsync(notificationsList)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -183,9 +135,9 @@ public class NotificationRenderer(
|
||||||
|
|
||||||
private class NotificationRendererDto
|
private class NotificationRendererDto
|
||||||
{
|
{
|
||||||
public List<NoteResponse>? Notes;
|
public List<NoteResponse>? Notes;
|
||||||
public List<UserResponse>? Users;
|
public List<UserResponse>? Users;
|
||||||
public List<BiteResponse>? Bites;
|
public List<BiteResponse>? Bites;
|
||||||
public List<ReactionResponse>? Reactions;
|
public List<ReactionResponse>? Reactions;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -55,7 +55,7 @@ public class UserProfileRenderer(DatabaseContext db, IOptions<Config.InstanceSec
|
||||||
? Role.Moderator
|
? Role.Moderator
|
||||||
: Role.None;
|
: Role.None;
|
||||||
|
|
||||||
var url = user.Host != null ? user.UserProfile?.Url ?? user.Uri : user.GetPublicUrl(instance.Value);
|
var url = user.Uri ?? user.GetPublicUrl(instance.Value.WebDomain);
|
||||||
|
|
||||||
return new UserProfileResponse
|
return new UserProfileResponse
|
||||||
{
|
{
|
||||||
|
@ -69,9 +69,7 @@ public class UserProfileRenderer(DatabaseContext db, IOptions<Config.InstanceSec
|
||||||
Relations = relations,
|
Relations = relations,
|
||||||
Role = role,
|
Role = role,
|
||||||
IsLocked = user.IsLocked,
|
IsLocked = user.IsLocked,
|
||||||
Url = url,
|
Url = url
|
||||||
Lang = user.UserProfile?.Lang,
|
|
||||||
Pronouns = user.UserProfile?.Pronouns
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,19 +21,14 @@ public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseConte
|
||||||
if (!data.Emojis.TryGetValue(user.Id, out var emoji))
|
if (!data.Emojis.TryGetValue(user.Id, out var emoji))
|
||||||
throw new Exception("DTO didn't contain emoji for user");
|
throw new Exception("DTO didn't contain emoji for user");
|
||||||
|
|
||||||
var avatarAlt = data.AvatarAlt.GetValueOrDefault(user.Id);
|
|
||||||
var bannerAlt = data.BannerAlt.GetValueOrDefault(user.Id);
|
|
||||||
|
|
||||||
return new UserResponse
|
return new UserResponse
|
||||||
{
|
{
|
||||||
Id = user.Id,
|
Id = user.Id,
|
||||||
Username = user.Username,
|
Username = user.Username,
|
||||||
Host = user.Host,
|
Host = user.Host,
|
||||||
DisplayName = user.DisplayName,
|
DisplayName = user.DisplayName,
|
||||||
AvatarUrl = user.GetAvatarUrl(config.Value),
|
AvatarUrl = user.AvatarUrl ?? user.GetIdenticonUrl(config.Value),
|
||||||
AvatarAlt = avatarAlt,
|
BannerUrl = user.BannerUrl,
|
||||||
BannerUrl = user.GetBannerUrl(config.Value),
|
|
||||||
BannerAlt = bannerAlt,
|
|
||||||
InstanceName = instanceName,
|
InstanceName = instanceName,
|
||||||
InstanceIconUrl = instanceIcon,
|
InstanceIconUrl = instanceIcon,
|
||||||
Emojis = emoji,
|
Emojis = emoji,
|
||||||
|
@ -47,15 +42,7 @@ public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseConte
|
||||||
{
|
{
|
||||||
var instanceData = await GetInstanceDataAsync([user]);
|
var instanceData = await GetInstanceDataAsync([user]);
|
||||||
var emojis = await GetEmojisAsync([user]);
|
var emojis = await GetEmojisAsync([user]);
|
||||||
var avatarAlt = await GetAvatarAltAsync([user]);
|
var data = new UserRendererDto { Emojis = emojis, InstanceData = instanceData };
|
||||||
var bannerAlt = await GetBannerAltAsync([user]);
|
|
||||||
var data = new UserRendererDto
|
|
||||||
{
|
|
||||||
Emojis = emojis,
|
|
||||||
InstanceData = instanceData,
|
|
||||||
AvatarAlt = avatarAlt,
|
|
||||||
BannerAlt = bannerAlt
|
|
||||||
};
|
|
||||||
|
|
||||||
return Render(user, data);
|
return Render(user, data);
|
||||||
}
|
}
|
||||||
|
@ -66,33 +53,12 @@ public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseConte
|
||||||
return await db.Instances.Where(p => hosts.Contains(p.Host)).ToListAsync();
|
return await db.Instances.Where(p => hosts.Contains(p.Host)).ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Dictionary<string, string?>> GetAvatarAltAsync(IEnumerable<User> users)
|
|
||||||
{
|
|
||||||
var ids = users.Select(p => p.Id).ToList();
|
|
||||||
return await db.Users
|
|
||||||
.Where(p => ids.Contains(p.Id))
|
|
||||||
.Include(p => p.Avatar)
|
|
||||||
.ToDictionaryAsync(p => p.Id, p => p.Avatar?.Comment);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Dictionary<string, string?>> GetBannerAltAsync(IEnumerable<User> users)
|
|
||||||
{
|
|
||||||
var ids = users.Select(p => p.Id).ToList();
|
|
||||||
return await db.Users
|
|
||||||
.Where(p => ids.Contains(p.Id))
|
|
||||||
.Include(p => p.Banner)
|
|
||||||
.ToDictionaryAsync(p => p.Id, p => p.Banner?.Comment);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<UserResponse>> RenderManyAsync(IEnumerable<User> users)
|
public async Task<IEnumerable<UserResponse>> RenderManyAsync(IEnumerable<User> users)
|
||||||
{
|
{
|
||||||
var userList = users.ToList();
|
var userList = users.ToList();
|
||||||
var data = new UserRendererDto
|
var data = new UserRendererDto
|
||||||
{
|
{
|
||||||
InstanceData = await GetInstanceDataAsync(userList),
|
InstanceData = await GetInstanceDataAsync(userList), Emojis = await GetEmojisAsync(userList)
|
||||||
Emojis = await GetEmojisAsync(userList),
|
|
||||||
AvatarAlt = await GetAvatarAltAsync(userList),
|
|
||||||
BannerAlt = await GetBannerAltAsync(userList)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return userList.Select(p => Render(p, data));
|
return userList.Select(p => Render(p, data));
|
||||||
|
@ -110,9 +76,9 @@ 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.PublicUrl,
|
||||||
License = p.License,
|
License = p.License,
|
||||||
Sensitive = p.Sensitive
|
Sensitive = p.Sensitive
|
||||||
})
|
})
|
||||||
|
@ -125,7 +91,5 @@ public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseConte
|
||||||
{
|
{
|
||||||
public required List<Instance> InstanceData;
|
public required List<Instance> InstanceData;
|
||||||
public required Dictionary<string, List<EmojiResponse>> Emojis;
|
public required Dictionary<string, List<EmojiResponse>> Emojis;
|
||||||
public required Dictionary<string, string?> AvatarAlt;
|
|
||||||
public required Dictionary<string, string?> BannerAlt;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -57,65 +57,4 @@ public class SessionController(DatabaseContext db) : ControllerBase
|
||||||
db.Remove(session);
|
db.Remove(session);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("mastodon")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
public async Task<List<MastodonSessionResponse>> GetMastodonSessions(int page = 0)
|
|
||||||
{
|
|
||||||
const int pageSize = 20;
|
|
||||||
|
|
||||||
return await db.OauthTokens
|
|
||||||
.Include(p => p.App)
|
|
||||||
.Where(p => p.User == HttpContext.GetUserOrFail())
|
|
||||||
.OrderByDescending(p => p.LastActiveDate ?? p.CreatedAt)
|
|
||||||
.Skip(page * pageSize)
|
|
||||||
.Take(pageSize)
|
|
||||||
.Select(p => new MastodonSessionResponse
|
|
||||||
{
|
|
||||||
Id = p.Id,
|
|
||||||
Active = p.Active,
|
|
||||||
CreatedAt = p.CreatedAt,
|
|
||||||
LastActive = p.LastActiveDate,
|
|
||||||
App = p.App.Name,
|
|
||||||
Scopes = p.Scopes,
|
|
||||||
Flags = new MastodonSessionFlags
|
|
||||||
{
|
|
||||||
SupportsHtmlFormatting = p.SupportsHtmlFormatting,
|
|
||||||
AutoDetectQuotes = p.AutoDetectQuotes,
|
|
||||||
IsPleroma = p.IsPleroma,
|
|
||||||
SupportsInlineMedia = p.SupportsInlineMedia
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPatch("mastodon/{id}")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
|
|
||||||
public async Task UpdateMastodonSession(string id, [FromBody] MastodonSessionFlags flags)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
var token = await db.OauthTokens.FirstOrDefaultAsync(p => p.Id == id && p.User == user)
|
|
||||||
?? throw GracefulException.NotFound("Session not found");
|
|
||||||
|
|
||||||
token.SupportsHtmlFormatting = flags.SupportsHtmlFormatting;
|
|
||||||
token.AutoDetectQuotes = flags.AutoDetectQuotes;
|
|
||||||
token.IsPleroma = flags.IsPleroma;
|
|
||||||
token.SupportsInlineMedia = flags.SupportsInlineMedia;
|
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("mastodon/{id}")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
|
|
||||||
public async Task TerminateMastodonSession(string id)
|
|
||||||
{
|
|
||||||
var user = HttpContext.GetUserOrFail();
|
|
||||||
var token = await db.OauthTokens.FirstOrDefaultAsync(p => p.Id == id && p.User == user)
|
|
||||||
?? throw GracefulException.NotFound("Session not found");
|
|
||||||
|
|
||||||
db.Remove(token);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,70 +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")]
|
|
||||||
[Authenticate]
|
|
||||||
[Authorize]
|
|
||||||
[EnableRateLimiting("strict")]
|
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
|
||||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
|
||||||
public async Task<UserResponse> RefetchUser(string id)
|
|
||||||
{
|
|
||||||
var user = await db.Users.IncludeCommonProperties()
|
|
||||||
.FirstOrDefaultAsync(p => p.Id == id && p.Host != null && p.Uri != null)
|
|
||||||
?? throw GracefulException.NotFound("User not found");
|
|
||||||
|
|
||||||
await userSvc.UpdateUserAsync(user, force: true);
|
|
||||||
|
|
||||||
await db.ReloadEntityRecursivelyAsync(user);
|
|
||||||
|
|
||||||
return await userRenderer.RenderOne(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/remove_from_followers")]
|
[HttpPost("{id}/remove_from_followers")]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
|
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
|
||||||
|
|
|
@ -17,7 +17,6 @@ public sealed class Config
|
||||||
public required InstanceSection Instance { get; init; } = new();
|
public required InstanceSection Instance { get; init; } = new();
|
||||||
public required DatabaseSection Database { get; init; } = new();
|
public required DatabaseSection Database { get; init; } = new();
|
||||||
public required SecuritySection Security { get; init; } = new();
|
public required SecuritySection Security { get; init; } = new();
|
||||||
public required NetworkSection Network { get; init; } = new();
|
|
||||||
public required StorageSection Storage { get; init; } = new();
|
public required StorageSection Storage { get; init; } = new();
|
||||||
public required PerformanceSection Performance { get; init; } = new();
|
public required PerformanceSection Performance { get; init; } = new();
|
||||||
public required QueueSection Queue { get; init; } = new();
|
public required QueueSection Queue { get; init; } = new();
|
||||||
|
@ -34,14 +33,13 @@ public sealed class Config
|
||||||
public string Version => _versionInfo.Version;
|
public string Version => _versionInfo.Version;
|
||||||
public string UserAgent => $"Iceshrimp.NET/{Version} (+https://{WebDomain}/)";
|
public string UserAgent => $"Iceshrimp.NET/{Version} (+https://{WebDomain}/)";
|
||||||
|
|
||||||
[Range(1, 65535)] public int ListenPort { get; init; } = 3000;
|
[Range(1, 65535)] public int ListenPort { get; init; } = 3000;
|
||||||
[Required] public string ListenHost { get; init; } = "localhost";
|
[Required] public string ListenHost { get; init; } = "localhost";
|
||||||
public string? ListenSocket { get; init; }
|
public string? ListenSocket { get; init; }
|
||||||
public string ListenSocketPerms { get; init; } = "660";
|
[Required] public string WebDomain { get; init; } = null!;
|
||||||
[Required] public string WebDomain { get; init; } = null!;
|
[Required] public string AccountDomain { get; init; } = null!;
|
||||||
[Required] public string AccountDomain { get; init; } = null!;
|
[Range(1, 100000)] public int CharacterLimit { get; init; } = 8192;
|
||||||
[Range(1, 100000)] public int CharacterLimit { get; init; } = 8192;
|
public string? RedirectIndexTo { get; init; }
|
||||||
public string? RedirectIndexTo { get; init; }
|
|
||||||
|
|
||||||
public string? AdditionalDomains
|
public string? AdditionalDomains
|
||||||
{
|
{
|
||||||
|
@ -54,26 +52,18 @@ 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;
|
public bool AllowLocalIPv6 { get; init; } = false;
|
||||||
public bool AllowLocalIPv6 { get; init; } = false;
|
public bool AllowLocalIPv4 { get; init; } = false;
|
||||||
public bool AllowLocalIPv4 { get; init; } = false;
|
public ExceptionVerbosity ExceptionVerbosity { get; init; } = ExceptionVerbosity.Basic;
|
||||||
public ExceptionVerbosity ExceptionVerbosity { get; init; } = ExceptionVerbosity.Basic;
|
public Enums.Registrations Registrations { get; init; } = Enums.Registrations.Closed;
|
||||||
public Enums.Registrations Registrations { get; init; } = Enums.Registrations.Closed;
|
public Enums.FederationMode FederationMode { get; init; } = Enums.FederationMode.BlockList;
|
||||||
public Enums.FederationMode FederationMode { get; init; } = Enums.FederationMode.BlockList;
|
public Enums.ItemVisibility ExposeFederationList { get; init; } = Enums.ItemVisibility.Registered;
|
||||||
public Enums.ItemVisibility ExposeFederationList { get; init; } = Enums.ItemVisibility.Registered;
|
public Enums.ItemVisibility ExposeBlockReasons { get; init; } = Enums.ItemVisibility.Registered;
|
||||||
public Enums.ItemVisibility ExposeBlockReasons { get; init; } = Enums.ItemVisibility.Registered;
|
public Enums.PublicPreview PublicPreview { get; init; } = Enums.PublicPreview.Public;
|
||||||
public Enums.PublicPreview PublicPreview { get; init; } = Enums.PublicPreview.Public;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class NetworkSection
|
|
||||||
{
|
|
||||||
public string? HttpProxy { get; init; } = null;
|
|
||||||
public string? HttpProxyUser { get; init; } = null!;
|
|
||||||
public string? HttpProxyPass { get; init; } = null!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class DatabaseSection
|
public sealed class DatabaseSection
|
||||||
|
@ -94,10 +84,9 @@ public sealed class Config
|
||||||
public readonly long? MaxUploadSizeBytes;
|
public readonly long? MaxUploadSizeBytes;
|
||||||
public readonly TimeSpan? MediaRetentionTimeSpan;
|
public readonly TimeSpan? MediaRetentionTimeSpan;
|
||||||
|
|
||||||
public bool CleanAvatars { get; init; } = false;
|
public bool CleanAvatars = false;
|
||||||
public bool CleanBanners { get; init; } = false;
|
public bool CleanBanners = false;
|
||||||
public bool ProxyRemoteMedia { get; init; } = true;
|
public Enums.FileStorage Provider { get; init; } = Enums.FileStorage.Local;
|
||||||
public Enums.FileStorage Provider { get; init; } = Enums.FileStorage.Local;
|
|
||||||
|
|
||||||
[Obsolete("This property is for backwards compatibility only, use StorageSection.Provider instead", true)]
|
[Obsolete("This property is for backwards compatibility only, use StorageSection.Provider instead", true)]
|
||||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||||
|
|
|
@ -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!;
|
||||||
|
@ -65,7 +65,6 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options)
|
||||||
public virtual DbSet<RegistryItem> RegistryItems { get; init; } = null!;
|
public virtual DbSet<RegistryItem> RegistryItems { get; init; } = null!;
|
||||||
public virtual DbSet<Relay> Relays { get; init; } = null!;
|
public virtual DbSet<Relay> Relays { get; init; } = null!;
|
||||||
public virtual DbSet<RenoteMuting> RenoteMutings { get; init; } = null!;
|
public virtual DbSet<RenoteMuting> RenoteMutings { get; init; } = null!;
|
||||||
public virtual DbSet<Rule> Rules { get; set; }
|
|
||||||
public virtual DbSet<Session> Sessions { get; init; } = null!;
|
public virtual DbSet<Session> Sessions { get; init; } = null!;
|
||||||
public virtual DbSet<SwSubscription> SwSubscriptions { get; init; } = null!;
|
public virtual DbSet<SwSubscription> SwSubscriptions { get; init; } = null!;
|
||||||
public virtual DbSet<PushSubscription> PushSubscriptions { get; init; } = null!;
|
public virtual DbSet<PushSubscription> PushSubscriptions { get; init; } = null!;
|
||||||
|
@ -92,7 +91,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)
|
||||||
|
@ -101,15 +99,14 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options)
|
||||||
{
|
{
|
||||||
ConnectionStringBuilder =
|
ConnectionStringBuilder =
|
||||||
{
|
{
|
||||||
Host = config.Host,
|
Host = config.Host,
|
||||||
Port = config.Port,
|
Port = config.Port,
|
||||||
Username = config.Username,
|
Username = config.Username,
|
||||||
Password = config.Password,
|
Password = config.Password,
|
||||||
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"
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
12
Iceshrimp.Backend/Core/Database/IEntity.cs
Normal file
12
Iceshrimp.Backend/Core/Database/IEntity.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
namespace Iceshrimp.Backend.Core.Database;
|
||||||
|
|
||||||
|
public interface IEntity
|
||||||
|
{
|
||||||
|
public string Id { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EntityWrapper<T> : IEntity
|
||||||
|
{
|
||||||
|
public required T Entity { get; init; }
|
||||||
|
public required string Id { get; init; }
|
||||||
|
}
|
|
@ -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.0")
|
||||||
.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")
|
||||||
|
@ -681,7 +747,6 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
.HasColumnName("id");
|
.HasColumnName("id");
|
||||||
|
|
||||||
b.Property<string>("AccessKey")
|
b.Property<string>("AccessKey")
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
.HasMaxLength(256)
|
||||||
.HasColumnType("character varying(256)")
|
.HasColumnType("character varying(256)")
|
||||||
.HasColumnName("accessKey");
|
.HasColumnName("accessKey");
|
||||||
|
@ -912,6 +977,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)")
|
||||||
|
@ -944,7 +1016,7 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
.HasColumnType("character varying(512)")
|
.HasColumnType("character varying(512)")
|
||||||
.HasColumnName("originalUrl");
|
.HasColumnName("originalUrl");
|
||||||
|
|
||||||
b.Property<string>("RawPublicUrl")
|
b.Property<string>("PublicUrl")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasMaxLength(512)
|
.HasMaxLength(512)
|
||||||
|
@ -956,13 +1028,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)")
|
||||||
|
@ -991,18 +1056,6 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
b.HasIndex("Name", "Host")
|
b.HasIndex("Name", "Host")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
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,111 +3867,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 =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("character varying(32)")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasMaxLength(512)
|
|
||||||
.HasColumnType("character varying(512)")
|
|
||||||
.HasColumnName("description");
|
|
||||||
|
|
||||||
b.Property<int>("Order")
|
|
||||||
.HasColumnType("integer")
|
|
||||||
.HasColumnName("order");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)")
|
|
||||||
.HasColumnName("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("rule");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Session", b =>
|
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Session", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
|
@ -4050,6 +3998,12 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
.HasColumnName("avatarId")
|
.HasColumnName("avatarId")
|
||||||
.HasComment("The ID of avatar DriveFile.");
|
.HasComment("The ID of avatar DriveFile.");
|
||||||
|
|
||||||
|
b.Property<string>("AvatarUrl")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)")
|
||||||
|
.HasColumnName("avatarUrl")
|
||||||
|
.HasComment("The URL of the avatar DriveFile");
|
||||||
|
|
||||||
b.Property<string>("BannerBlurhash")
|
b.Property<string>("BannerBlurhash")
|
||||||
.HasMaxLength(128)
|
.HasMaxLength(128)
|
||||||
.HasColumnType("character varying(128)")
|
.HasColumnType("character varying(128)")
|
||||||
|
@ -4062,6 +4016,12 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
.HasColumnName("bannerId")
|
.HasColumnName("bannerId")
|
||||||
.HasComment("The ID of banner DriveFile.");
|
.HasComment("The ID of banner DriveFile.");
|
||||||
|
|
||||||
|
b.Property<string>("BannerUrl")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)")
|
||||||
|
.HasColumnName("bannerUrl")
|
||||||
|
.HasComment("The URL of the banner DriveFile");
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("createdAt")
|
.HasColumnName("createdAt")
|
||||||
|
@ -4202,10 +4162,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)")
|
||||||
|
@ -4682,10 +4638,6 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
.HasColumnType("character varying(32)")
|
.HasColumnType("character varying(32)")
|
||||||
.HasColumnName("pinnedPageId");
|
.HasColumnName("pinnedPageId");
|
||||||
|
|
||||||
b.Property<Dictionary<string, string>>("Pronouns")
|
|
||||||
.HasColumnType("jsonb")
|
|
||||||
.HasColumnName("pronouns");
|
|
||||||
|
|
||||||
b.Property<string>("Url")
|
b.Property<string>("Url")
|
||||||
.HasMaxLength(512)
|
.HasMaxLength(512)
|
||||||
.HasColumnType("character varying(512)")
|
.HasColumnType("character varying(512)")
|
||||||
|
@ -4944,19 +4896,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 +5694,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 +5904,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,47 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Database.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
[DbContext(typeof(DatabaseContext))]
|
|
||||||
[Migration("20241123224718_MakeDriveFileAccessKeyNonOptional")]
|
|
||||||
public partial class MakeDriveFileAccessKeyNonOptional : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.Sql("""
|
|
||||||
UPDATE "drive_file" SET "accessKey" = gen_random_uuid() WHERE "accessKey" IS NULL;
|
|
||||||
""");
|
|
||||||
|
|
||||||
migrationBuilder.AlterColumn<string>(
|
|
||||||
name: "accessKey",
|
|
||||||
table: "drive_file",
|
|
||||||
type: "character varying(256)",
|
|
||||||
maxLength: 256,
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: "",
|
|
||||||
oldClrType: typeof(string),
|
|
||||||
oldType: "character varying(256)",
|
|
||||||
oldMaxLength: 256,
|
|
||||||
oldNullable: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AlterColumn<string>(
|
|
||||||
name: "accessKey",
|
|
||||||
table: "drive_file",
|
|
||||||
type: "character varying(256)",
|
|
||||||
maxLength: 256,
|
|
||||||
nullable: true,
|
|
||||||
oldClrType: typeof(string),
|
|
||||||
oldType: "character varying(256)",
|
|
||||||
oldMaxLength: 256);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Database.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
[DbContext(typeof(DatabaseContext))]
|
|
||||||
[Migration("20250106074951_Rules")]
|
|
||||||
public partial class Rules : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "rule",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
|
||||||
order = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
text = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
|
||||||
description = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_rule", x => x.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "rule");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Database.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
[DbContext(typeof(DatabaseContext))]
|
|
||||||
// The migration should be called "20250109095535_RemoveUserAvatarBannerUrlColumns" instead,
|
|
||||||
// but the typo was only spotted after the migration had already been deployed by several users.
|
|
||||||
[Migration("20250109095535_RemoteUserAvatarBannerUrlColumns")]
|
|
||||||
public partial class RemoteUserAvatarBannerUrlColumns : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "avatarUrl",
|
|
||||||
table: "user");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "bannerUrl",
|
|
||||||
table: "user");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "avatarUrl",
|
|
||||||
table: "user",
|
|
||||||
type: "character varying(512)",
|
|
||||||
maxLength: 512,
|
|
||||||
nullable: true,
|
|
||||||
comment: "The URL of the avatar DriveFile");
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "bannerUrl",
|
|
||||||
table: "user",
|
|
||||||
type: "character varying(512)",
|
|
||||||
maxLength: 512,
|
|
||||||
nullable: true,
|
|
||||||
comment: "The URL of the banner DriveFile");
|
|
||||||
|
|
||||||
migrationBuilder.Sql("""
|
|
||||||
UPDATE "user" SET "avatarUrl" = (SELECT COALESCE("webpublicUrl", "url") FROM "drive_file" WHERE "id" = "user"."avatarId");
|
|
||||||
UPDATE "user" SET "bannerUrl" = (SELECT COALESCE("webpublicUrl", "url") FROM "drive_file" WHERE "id" = "user"."bannerId");
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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("20250121203942_FixupEmojiUniqueIndex")]
|
|
||||||
public partial class FixupEmojiUniqueIndex : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropIndex(
|
|
||||||
name: "IX_emoji_name_host",
|
|
||||||
table: "emoji");
|
|
||||||
|
|
||||||
// Clean up duplicates, preserving the newest one
|
|
||||||
migrationBuilder.Sql("""
|
|
||||||
DELETE FROM "emoji" e USING "emoji" e2 WHERE e."host" IS NULL AND e."name" = e2."name" AND e2."host" IS NULL AND e."id" < e2."id";
|
|
||||||
""");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_emoji_name_host",
|
|
||||||
table: "emoji",
|
|
||||||
columns: new[] { "name", "host" },
|
|
||||||
unique: true)
|
|
||||||
.Annotation("Npgsql:NullsDistinct", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropIndex(
|
|
||||||
name: "IX_emoji_name_host",
|
|
||||||
table: "emoji");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_emoji_name_host",
|
|
||||||
table: "emoji",
|
|
||||||
columns: new[] { "name", "host" },
|
|
||||||
unique: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Database.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
[DbContext(typeof(DatabaseContext))]
|
|
||||||
[Migration("20250127070810_Pronouns")]
|
|
||||||
public partial class Pronouns : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddColumn<Dictionary<string, string>>(
|
|
||||||
name: "pronouns",
|
|
||||||
table: "user_profile",
|
|
||||||
type: "jsonb",
|
|
||||||
nullable: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "pronouns",
|
|
||||||
table: "user_profile");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Database.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
[DbContext(typeof(DatabaseContext))]
|
|
||||||
[Migration("20250217182259_FixupEmojiAliases")]
|
|
||||||
public partial class FixupEmojiAliases : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.Sql("""
|
|
||||||
UPDATE "emoji" SET "aliases" = array_remove("aliases", '') WHERE '' = ANY("aliases");
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Database.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
[DbContext(typeof(DatabaseContext))]
|
|
||||||
[Migration("20250217192617_FixupEmojiType")]
|
|
||||||
public partial class FixupEmojiType : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.Sql("""
|
|
||||||
UPDATE "emoji"
|
|
||||||
SET "type" =
|
|
||||||
(SELECT COALESCE("drive_file"."webpublicType", "drive_file"."type")
|
|
||||||
FROM "drive_file"
|
|
||||||
WHERE "drive_file"."userHost" IS NULL
|
|
||||||
AND ("drive_file"."webpublicUrl" = "emoji"."publicUrl"
|
|
||||||
OR "drive_file"."url" = "emoji"."publicUrl"
|
|
||||||
)
|
|
||||||
LIMIT 1
|
|
||||||
)
|
|
||||||
WHERE "host" IS NULL
|
|
||||||
AND EXISTS
|
|
||||||
(SELECT 1
|
|
||||||
FROM "drive_file"
|
|
||||||
WHERE "drive_file"."userHost" IS NULL
|
|
||||||
AND ("drive_file"."webpublicUrl" = "emoji"."publicUrl"
|
|
||||||
OR "drive_file"."url" = "emoji"."publicUrl"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder) { }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using Iceshrimp.Shared.Helpers;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||||
|
@ -21,7 +20,7 @@ namespace Iceshrimp.Backend.Core.Database.Tables;
|
||||||
[Index(nameof(AccessKey))]
|
[Index(nameof(AccessKey))]
|
||||||
[Index(nameof(Uri))]
|
[Index(nameof(Uri))]
|
||||||
[Index(nameof(ThumbnailAccessKey))]
|
[Index(nameof(ThumbnailAccessKey))]
|
||||||
public class DriveFile : IIdentifiable
|
public class DriveFile : IEntity
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The created date of the DriveFile.
|
/// The created date of the DriveFile.
|
||||||
|
@ -107,7 +106,7 @@ public class DriveFile : IIdentifiable
|
||||||
|
|
||||||
[Column("accessKey")]
|
[Column("accessKey")]
|
||||||
[StringLength(256)]
|
[StringLength(256)]
|
||||||
public string AccessKey { get; set; } = null!;
|
public string? AccessKey { get; set; }
|
||||||
|
|
||||||
[Column("thumbnailAccessKey")]
|
[Column("thumbnailAccessKey")]
|
||||||
[StringLength(256)]
|
[StringLength(256)]
|
||||||
|
@ -190,8 +189,8 @@ public class DriveFile : IIdentifiable
|
||||||
[InverseProperty(nameof(Tables.User.Banner))]
|
[InverseProperty(nameof(Tables.User.Banner))]
|
||||||
public virtual User? UserBanner { get; set; }
|
public virtual User? UserBanner { get; set; }
|
||||||
|
|
||||||
[NotMapped] public string RawAccessUrl => PublicUrl ?? Url;
|
[NotMapped] public string AccessUrl => PublicUrl ?? Url;
|
||||||
[NotMapped] public string RawThumbnailAccessUrl => ThumbnailUrl ?? PublicUrl ?? Url;
|
[NotMapped] public string ThumbnailAccessUrl => ThumbnailUrl ?? PublicUrl ?? Url;
|
||||||
|
|
||||||
[Key]
|
[Key]
|
||||||
[Column("id")]
|
[Column("id")]
|
||||||
|
|
|
@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
namespace Iceshrimp.Backend.Core.Database.Tables;
|
namespace Iceshrimp.Backend.Core.Database.Tables;
|
||||||
|
|
||||||
[Table("emoji")]
|
[Table("emoji")]
|
||||||
|
[Index(nameof(Name), nameof(Host), IsUnique = true)]
|
||||||
[Index(nameof(Host))]
|
[Index(nameof(Host))]
|
||||||
[Index(nameof(Name))]
|
[Index(nameof(Name))]
|
||||||
public class Emoji
|
public class Emoji
|
||||||
|
@ -30,8 +31,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)]
|
||||||
|
@ -39,7 +40,7 @@ public class Emoji
|
||||||
|
|
||||||
[Column("publicUrl")]
|
[Column("publicUrl")]
|
||||||
[StringLength(512)]
|
[StringLength(512)]
|
||||||
public string RawPublicUrl { get; set; } = null!;
|
public string PublicUrl { get; set; } = null!;
|
||||||
|
|
||||||
[Column("license")]
|
[Column("license")]
|
||||||
[StringLength(1024)]
|
[StringLength(1024)]
|
||||||
|
@ -68,27 +69,14 @@ public class Emoji
|
||||||
? $"https://{config.WebDomain}/emoji/{Name}"
|
? $"https://{config.WebDomain}/emoji/{Name}"
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
public string GetAccessUrl(Config.InstanceSection config)
|
|
||||||
=> $"https://{config.WebDomain}/media/emoji/{Id}";
|
|
||||||
|
|
||||||
private class EntityTypeConfiguration : IEntityTypeConfiguration<Emoji>
|
private class EntityTypeConfiguration : IEntityTypeConfiguration<Emoji>
|
||||||
{
|
{
|
||||||
public void Configure(EntityTypeBuilder<Emoji> entity)
|
public void Configure(EntityTypeBuilder<Emoji> entity)
|
||||||
{
|
{
|
||||||
entity.Property(e => e.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.PublicUrl).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
|
|
||||||
entity.HasIndex(nameof(Name), nameof(Host)).IsUnique().AreNullsDistinct(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using Iceshrimp.Shared.Helpers;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
@ -10,7 +9,7 @@ namespace Iceshrimp.Backend.Core.Database.Tables;
|
||||||
[Index(nameof(FolloweeId))]
|
[Index(nameof(FolloweeId))]
|
||||||
[Index(nameof(FollowerId))]
|
[Index(nameof(FollowerId))]
|
||||||
[Index(nameof(FollowerId), nameof(FolloweeId), IsUnique = true)]
|
[Index(nameof(FollowerId), nameof(FolloweeId), IsUnique = true)]
|
||||||
public class FollowRequest : IIdentifiable
|
public class FollowRequest : IEntity
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The created date of the FollowRequest.
|
/// The created date of the FollowRequest.
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using Iceshrimp.Shared.Helpers;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
@ -8,7 +7,7 @@ namespace Iceshrimp.Backend.Core.Database.Tables;
|
||||||
|
|
||||||
[Table("hashtag")]
|
[Table("hashtag")]
|
||||||
[Index(nameof(Name), IsUnique = true)]
|
[Index(nameof(Name), IsUnique = true)]
|
||||||
public class Hashtag : IIdentifiable
|
public class Hashtag : IEntity
|
||||||
{
|
{
|
||||||
[Column("name")] [StringLength(128)] public string Name { get; set; } = null!;
|
[Column("name")] [StringLength(128)] public string Name { get; set; } = null!;
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis;
|
||||||
using EntityFrameworkCore.Projectables;
|
using EntityFrameworkCore.Projectables;
|
||||||
using Iceshrimp.Backend.Core.Configuration;
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
using Iceshrimp.Backend.Core.Helpers;
|
using Iceshrimp.Backend.Core.Helpers;
|
||||||
using Iceshrimp.Shared.Helpers;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
using NpgsqlTypes;
|
using NpgsqlTypes;
|
||||||
|
@ -33,7 +32,7 @@ namespace Iceshrimp.Backend.Core.Database.Tables;
|
||||||
[Index(nameof(Visibility))]
|
[Index(nameof(Visibility))]
|
||||||
[Index(nameof(ReplyUri))]
|
[Index(nameof(ReplyUri))]
|
||||||
[Index(nameof(RenoteUri))]
|
[Index(nameof(RenoteUri))]
|
||||||
public class Note : IIdentifiable
|
public class Note : IEntity
|
||||||
{
|
{
|
||||||
[PgName("note_visibility_enum")]
|
[PgName("note_visibility_enum")]
|
||||||
public enum NoteVisibility
|
public enum NoteVisibility
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using Iceshrimp.Shared.Helpers;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
@ -10,7 +9,7 @@ namespace Iceshrimp.Backend.Core.Database.Tables;
|
||||||
[Index(nameof(UserId))]
|
[Index(nameof(UserId))]
|
||||||
[Index(nameof(NoteId))]
|
[Index(nameof(NoteId))]
|
||||||
[Index(nameof(UserId), nameof(NoteId), IsUnique = true)]
|
[Index(nameof(UserId), nameof(NoteId), IsUnique = true)]
|
||||||
public class NoteLike : IIdentifiable
|
public class NoteLike : IEntity
|
||||||
{
|
{
|
||||||
[Key]
|
[Key]
|
||||||
[Column("id")]
|
[Column("id")]
|
||||||
|
|
|
@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using EntityFrameworkCore.Projectables;
|
using EntityFrameworkCore.Projectables;
|
||||||
using Iceshrimp.Backend.Core.Configuration;
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
using Iceshrimp.Shared.Helpers;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
@ -11,7 +10,7 @@ namespace Iceshrimp.Backend.Core.Database.Tables;
|
||||||
|
|
||||||
[Table("note_thread")]
|
[Table("note_thread")]
|
||||||
[Index(nameof(Uri), IsUnique = true)]
|
[Index(nameof(Uri), IsUnique = true)]
|
||||||
public class NoteThread : IIdentifiable
|
public class NoteThread : IEntity
|
||||||
{
|
{
|
||||||
[Column("id")]
|
[Column("id")]
|
||||||
[StringLength(256)]
|
[StringLength(256)]
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using Iceshrimp.Shared.Helpers;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
using NpgsqlTypes;
|
using NpgsqlTypes;
|
||||||
|
@ -16,7 +15,7 @@ namespace Iceshrimp.Backend.Core.Database.Tables;
|
||||||
[Index(nameof(AppAccessTokenId))]
|
[Index(nameof(AppAccessTokenId))]
|
||||||
[Index(nameof(MastoId))]
|
[Index(nameof(MastoId))]
|
||||||
[Index(nameof(NoteId))]
|
[Index(nameof(NoteId))]
|
||||||
public class Notification : IIdentifiable
|
public class Notification : IEntity
|
||||||
{
|
{
|
||||||
[PgName("notification_type_enum")]
|
[PgName("notification_type_enum")]
|
||||||
public enum NotificationType
|
public enum NotificationType
|
||||||
|
|
|
@ -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!;
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Database.Tables;
|
|
||||||
|
|
||||||
[Table("rule")]
|
|
||||||
public class Rule
|
|
||||||
{
|
|
||||||
[Key]
|
|
||||||
[Column("id")]
|
|
||||||
[StringLength(32)]
|
|
||||||
public string Id { get; set; } = null!;
|
|
||||||
|
|
||||||
[Column("order")]
|
|
||||||
public int Order { get; set; }
|
|
||||||
|
|
||||||
[Column("text")]
|
|
||||||
[StringLength(128)]
|
|
||||||
public string Text { get; set; } = null!;
|
|
||||||
|
|
||||||
[Column("description")]
|
|
||||||
[StringLength(512)]
|
|
||||||
public string? Description { get; set; }
|
|
||||||
}
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using Iceshrimp.Shared.Helpers;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
@ -8,7 +7,7 @@ namespace Iceshrimp.Backend.Core.Database.Tables;
|
||||||
|
|
||||||
[Table("session")]
|
[Table("session")]
|
||||||
[Index(nameof(Token))]
|
[Index(nameof(Token))]
|
||||||
public class Session : IIdentifiable
|
public class Session : IEntity
|
||||||
{
|
{
|
||||||
[Key]
|
[Key]
|
||||||
[Column("id")]
|
[Column("id")]
|
||||||
|
|
|
@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using EntityFrameworkCore.Projectables;
|
using EntityFrameworkCore.Projectables;
|
||||||
using Iceshrimp.Backend.Core.Configuration;
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
using Iceshrimp.Backend.Core.Helpers;
|
using Iceshrimp.Backend.Core.Helpers;
|
||||||
using Iceshrimp.Shared.Helpers;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
@ -24,7 +23,7 @@ namespace Iceshrimp.Backend.Core.Database.Tables;
|
||||||
[Index(nameof(AvatarId), IsUnique = true)]
|
[Index(nameof(AvatarId), IsUnique = true)]
|
||||||
[Index(nameof(BannerId), IsUnique = true)]
|
[Index(nameof(BannerId), IsUnique = true)]
|
||||||
[Index(nameof(IsSuspended))]
|
[Index(nameof(IsSuspended))]
|
||||||
public class User : IIdentifiable
|
public class User : IEntity
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The created date of the User.
|
/// The created date of the User.
|
||||||
|
@ -41,7 +40,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]
|
||||||
|
@ -244,6 +242,13 @@ public class User : IIdentifiable
|
||||||
[Column("speakAsCat")]
|
[Column("speakAsCat")]
|
||||||
public bool SpeakAsCat { get; set; }
|
public bool SpeakAsCat { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The URL of the avatar DriveFile
|
||||||
|
/// </summary>
|
||||||
|
[Column("avatarUrl")]
|
||||||
|
[StringLength(512)]
|
||||||
|
public string? AvatarUrl { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The blurhash of the avatar DriveFile
|
/// The blurhash of the avatar DriveFile
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -251,6 +256,13 @@ public class User : IIdentifiable
|
||||||
[StringLength(128)]
|
[StringLength(128)]
|
||||||
public string? AvatarBlurhash { get; set; }
|
public string? AvatarBlurhash { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The URL of the banner DriveFile
|
||||||
|
/// </summary>
|
||||||
|
[Column("bannerUrl")]
|
||||||
|
[StringLength(512)]
|
||||||
|
public string? BannerUrl { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The blurhash of the banner DriveFile
|
/// The blurhash of the banner DriveFile
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -260,14 +272,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>();
|
||||||
|
@ -575,12 +587,12 @@ public class User : IIdentifiable
|
||||||
|
|
||||||
[Projectable]
|
[Projectable]
|
||||||
public bool HasInteractedWith(Note note) =>
|
public bool HasInteractedWith(Note note) =>
|
||||||
HasLiked(note)
|
HasLiked(note) ||
|
||||||
|| HasReacted(note)
|
HasReacted(note) ||
|
||||||
|| HasBookmarked(note)
|
HasBookmarked(note) ||
|
||||||
|| HasReplied(note)
|
HasReplied(note) ||
|
||||||
|| HasRenoted(note)
|
HasRenoted(note) ||
|
||||||
|| HasVoted(note);
|
HasVoted(note);
|
||||||
|
|
||||||
[Projectable]
|
[Projectable]
|
||||||
public bool ProhibitInteractionWith(User user) => IsBlocking(user) || IsBlockedBy(user);
|
public bool ProhibitInteractionWith(User user) => IsBlocking(user) || IsBlockedBy(user);
|
||||||
|
@ -611,10 +623,11 @@ public class User : IIdentifiable
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetPublicUrl(Config.InstanceSection config) => GetPublicUrl(config.WebDomain);
|
public string GetPublicUrl(Config.InstanceSection config) => GetPublicUrl(config.WebDomain);
|
||||||
public string GetPublicUri(Config.InstanceSection config) => GetPublicUri(config.WebDomain);
|
public string GetPublicUri(Config.InstanceSection config) => GetPublicUri(config.WebDomain);
|
||||||
public string GetUriOrPublicUri(Config.InstanceSection config) => GetUriOrPublicUri(config.WebDomain);
|
public string GetUriOrPublicUri(Config.InstanceSection config) => GetUriOrPublicUri(config.WebDomain);
|
||||||
public string GetIdenticonUrl(Config.InstanceSection config) => GetIdenticonUrl(config.WebDomain);
|
public string GetIdenticonUrl(Config.InstanceSection config) => GetIdenticonUrl(config.WebDomain);
|
||||||
|
public string GetIdenticonUrlPng(Config.InstanceSection config) => GetIdenticonUrl(config.WebDomain) + ".png";
|
||||||
|
|
||||||
public string GetPublicUri(string webDomain) => Host == null
|
public string GetPublicUri(string webDomain) => Host == null
|
||||||
? $"https://{webDomain}/users/{Id}"
|
? $"https://{webDomain}/users/{Id}"
|
||||||
|
@ -630,12 +643,6 @@ public class User : IIdentifiable
|
||||||
|
|
||||||
public string GetIdenticonUrl(string webDomain) => $"https://{webDomain}{IdenticonUrlPath}";
|
public string GetIdenticonUrl(string webDomain) => $"https://{webDomain}{IdenticonUrlPath}";
|
||||||
|
|
||||||
public string GetAvatarUrl(Config.InstanceSection config)
|
|
||||||
=> $"https://{config.WebDomain}/avatars/{Id}/{AvatarId ?? "identicon"}";
|
|
||||||
|
|
||||||
public string? GetBannerUrl(Config.InstanceSection config)
|
|
||||||
=> BannerId != null ? $"https://{config.WebDomain}/banners/{Id}/{BannerId}" : null;
|
|
||||||
|
|
||||||
private class EntityTypeConfiguration : IEntityTypeConfiguration<User>
|
private class EntityTypeConfiguration : IEntityTypeConfiguration<User>
|
||||||
{
|
{
|
||||||
public void Configure(EntityTypeBuilder<User> entity)
|
public void Configure(EntityTypeBuilder<User> entity)
|
||||||
|
@ -643,8 +650,10 @@ public class User : IIdentifiable
|
||||||
entity.Property(e => e.AlsoKnownAs).HasComment("URIs the user is known as too");
|
entity.Property(e => e.AlsoKnownAs).HasComment("URIs the user is known as too");
|
||||||
entity.Property(e => e.AvatarBlurhash).HasComment("The blurhash of the avatar DriveFile");
|
entity.Property(e => e.AvatarBlurhash).HasComment("The blurhash of the avatar DriveFile");
|
||||||
entity.Property(e => e.AvatarId).HasComment("The ID of avatar DriveFile.");
|
entity.Property(e => e.AvatarId).HasComment("The ID of avatar DriveFile.");
|
||||||
|
entity.Property(e => e.AvatarUrl).HasComment("The URL of the avatar DriveFile");
|
||||||
entity.Property(e => e.BannerBlurhash).HasComment("The blurhash of the banner DriveFile");
|
entity.Property(e => e.BannerBlurhash).HasComment("The blurhash of the banner DriveFile");
|
||||||
entity.Property(e => e.BannerId).HasComment("The ID of banner DriveFile.");
|
entity.Property(e => e.BannerId).HasComment("The ID of banner DriveFile.");
|
||||||
|
entity.Property(e => e.BannerUrl).HasComment("The URL of the banner DriveFile");
|
||||||
entity.Property(e => e.CreatedAt).HasComment("The created date of the User.");
|
entity.Property(e => e.CreatedAt).HasComment("The created date of the User.");
|
||||||
entity.Property(e => e.DriveCapacityOverrideMb).HasComment("Overrides user drive capacity limit");
|
entity.Property(e => e.DriveCapacityOverrideMb).HasComment("Overrides user drive capacity limit");
|
||||||
entity.Property(e => e.Emojis).HasDefaultValueSql("'{}'::character varying[]");
|
entity.Property(e => e.Emojis).HasDefaultValueSql("'{}'::character varying[]");
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using Iceshrimp.Shared.Helpers;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
@ -10,7 +9,7 @@ namespace Iceshrimp.Backend.Core.Database.Tables;
|
||||||
[Index(nameof(UserListId))]
|
[Index(nameof(UserListId))]
|
||||||
[Index(nameof(UserId), nameof(UserListId), IsUnique = true)]
|
[Index(nameof(UserId), nameof(UserListId), IsUnique = true)]
|
||||||
[Index(nameof(UserId))]
|
[Index(nameof(UserId))]
|
||||||
public class UserListMember : IIdentifiable
|
public class UserListMember : IEntity
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The created date of the UserListMember.
|
/// The created date of the UserListMember.
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue