Compare commits

..

77 Commits

Author SHA1 Message Date
Claire
26e524836d Merge pull request #3140 from ClearlyClaire/glitch-soc/backports-4.4
Merge upstream changes up to v4.4.2 into stable-4.4
2025-07-23 18:39:12 +02:00
Claire
cfc4bb1dc0 Fix links in posts always having noreferrer in glitch flavor (#3135)
Fixes #3128
2025-07-23 18:20:21 +02:00
diondiondion
d7099b1b38 [Glitch] refactor: Disable useDrag hook when main menu is not openable
Port a842b14c84 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 18:20:08 +02:00
diondiondion
69fb382424 [Glitch] fix: Add lang attribute to current composer language in alt text modal
Port 138746bdcc to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 18:19:55 +02:00
diondiondion
47d469ec5e [Glitch] fix: Fix quote posts styling on notifications page
Port 3771f9e04b to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 18:19:42 +02:00
diondiondion
4d3e2efb69 [Glitch] fix: Improve Dropdown component accessibility
Port 82a6ff091f to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 18:19:08 +02:00
diondiondion
92fc7a30dc [Glitch] fix: Fix selected item in poll select menus is unreadable in Firefox
Port 558b9c90a6 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 18:16:49 +02:00
diondiondion
4311369ab8 [Glitch] refactor: Only remove pointer-events when necessary
Port 74fc4dbacf to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 18:16:34 +02:00
diondiondion
18653ce15d [Glitch] fix: Improve a11y of custom select menus in notifications settings
Port faffb73cbd to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 18:16:13 +02:00
Echo
71a35d3953 [Glitch] Make bio hashtags open the local page instead of the remote instance
Port 853a0c466e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 18:15:44 +02:00
Claire
e69c1479a8 Merge commit '77d2cdb30230ae6292bd247f6c6f97d00bd38084' into glitch-soc/backports-4.4 2025-07-23 18:10:05 +02:00
github-actions[bot]
77d2cdb302 New Crowdin Translations for stable-4.4 (automated) (#35477)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-07-23 16:28:50 +02:00
David Roetzel
c727197760 Combine two items 2025-07-23 16:08:43 +02:00
David Roetzel
d6859c9658 Update CHANGELOG.md
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 16:08:43 +02:00
David Roetzel
7a9e98f4d6 Update CHANGELOG.md
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 16:08:43 +02:00
David Roetzel
7924a27ae7 Update CHANGELOG.md
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 16:08:43 +02:00
David Roetzel
d664b9d8ff Update "Security" section...
...to account for multiple updates that have been added since.
2025-07-23 16:08:43 +02:00
David Roetzel
4558cfadd8 Update dependency thor 2025-07-23 16:08:43 +02:00
David Roetzel
713965467d Update dependency axios 2025-07-23 16:08:43 +02:00
David Roetzel
aec6d0f807 Bump version to v4.4.2 2025-07-23 16:08:43 +02:00
diondiondion
e103815d2d Don't require JSDoc params & return in TS (#35426) 2025-07-23 16:08:43 +02:00
renovate[bot]
d73b9fba90 chore(deps): update dependency nokogiri to v1.18.9 (#35433)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-23 16:08:43 +02:00
diondiondion
a89d11bc08 refactor: Disable useDrag hook when main menu is not openable (#35414) 2025-07-23 16:08:43 +02:00
diondiondion
a250928934 fix: Add lang attribute to current composer language in alt text modal (#35412) 2025-07-23 16:08:43 +02:00
diondiondion
1d1b17b04b fix: Fix quote posts styling on notifications page (#35411) 2025-07-23 16:08:43 +02:00
diondiondion
2aff51013c fix: Improve a11y of custom select menus in notifications settings (#35403) 2025-07-23 16:08:43 +02:00
diondiondion
8c3c1faaec fix: Fix selected item in poll select menus is unreadable in Firefox (#35402) 2025-07-23 16:08:43 +02:00
diondiondion
a2888f1bb2 refactor: Only remove pointer-events when necessary (#35390) 2025-07-23 16:08:43 +02:00
diondiondion
77fe044f03 Update age limit wording (#35387) 2025-07-23 16:08:43 +02:00
Claire
da0cc0f5b9 Fix support for quote verification in implicit status updates (#35384) 2025-07-23 16:08:43 +02:00
Claire
ee83f3a8b9 Always give local quote of remote posts a quote request URI (#35383) 2025-07-23 16:08:43 +02:00
Claire
7ae78b1032 Refactor ActivityPub::Activity::Accept and ActivityPub::Activity::Reject specs (#35382) 2025-07-23 16:08:43 +02:00
Claire
c4b7c3bdda Fix quoteAuthorization type in JSON-LD context (#35380) 2025-07-23 16:08:43 +02:00
diondiondion
a79dbf8334 fix: Improve Dropdown component accessibility (#35373) 2025-07-23 16:08:43 +02:00
Claire
ef6f5f9357 Fix quote attributes missing from Mastodon's context (#35354) 2025-07-23 16:08:43 +02:00
Echo
f65f6ad6f1 Make bio hashtags open the local page instead of the remote instance (#35349) 2025-07-23 16:08:43 +02:00
Claire
c0e242cb73 Fix styling of external log-in button (#35320) 2025-07-23 16:08:43 +02:00
Claire
6c1cc5b25a Merge pull request #3126 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to 609a40181e
2025-07-09 17:48:59 +02:00
Claire
ec6f93ae45 Merge commit '609a40181e0f3f505707d196985dfb78ab3b3f88' into glitch-soc/merge-4.4 2025-07-09 17:31:28 +02:00
Claire
609a40181e Bump version to v4.4.1 2025-07-09 17:16:57 +02:00
Claire
93ce44d21d Fix nearly every sub-directory being crawled as part of Vite build (#35323) 2025-07-09 17:16:57 +02:00
David Roetzel
fb3ff194b5 Relax error restriction in initializer (#35321) 2025-07-09 17:16:57 +02:00
Claire
81b363b338 Fix replying from media modal or pop-in-player tagging user @undefined (#35317) 2025-07-09 17:16:57 +02:00
Claire
1151b05c2d Fix support for special characters in various environment variables (#35314)
Co-authored-by: Matt Jankowski <matt@jankowski.online>
2025-07-09 17:16:57 +02:00
Matt Jankowski
f96743fcfb Use if_exists: true when removing duplicate indexes (#35309) 2025-07-09 17:16:57 +02:00
Claire
7a51ad7ebd Merge pull request #3121 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to 69e14246b8
2025-07-08 16:30:51 +02:00
Claire
06a46e77b8 Merge commit '69e14246b838443985a541e97327494b8d2fdffb' into glitch-soc/merge-4.4 2025-07-08 16:15:29 +02:00
Claire
69e14246b8 Fix 4.4 container images not being marked as latest (#35294) 2025-07-08 16:07:41 +02:00
Claire
174370dec2 Merge pull request #3120 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to c1794fb948
2025-07-08 16:06:31 +02:00
Claire
18e08bf493 Merge commit 'c1794fb948dfb13865214bce8a90e88da44d4ff6' into glitch-soc/merge-4.4 2025-07-08 15:51:09 +02:00
Claire
c1794fb948 Bump version to v4.4.0 (#35268) 2025-07-08 15:25:26 +02:00
Claire
061c966ab3 Merge pull request #3119 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to 333a17a478
2025-07-08 14:02:48 +02:00
diondiondion
326f6bc12a [Glitch] fix: Fix can't skip search field by tabbing
Port 2dcededcf0 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-08 13:14:27 +02:00
Claire
bd442485d0 Merge commit '333a17a478f0ddcee4115a50f01077cb1dc5c22e' into glitch-soc/merge-4.4 2025-07-08 13:13:07 +02:00
David Roetzel
333a17a478 Better error response to malformed headers (#35278) 2025-07-08 11:45:24 +02:00
github-actions[bot]
388e09e1a3 New Crowdin Translations for stable-4.4 (automated) (#35288)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-07-08 11:22:46 +02:00
diondiondion
2dcededcf0 fix: Fix can't skip search field by tabbing (#35281) 2025-07-07 17:48:13 +02:00
github-actions[bot]
2db8a328cd New Crowdin Translations (automated) (#35269)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-07-07 17:48:13 +02:00
Matt Jankowski
b4a950c2fc Remove unused scopes in Account model (#35276) 2025-07-07 17:48:13 +02:00
Claire
194645aada Add ability to manually trigger i18n uploads (#35279) 2025-07-07 15:40:54 +02:00
Claire
48aaecec7b Merge pull request #3118 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to 0c5ce23ae4
2025-07-04 18:27:47 +02:00
diondiondion
6cac651ff2 [Glitch] fix: Remove focus highlight when status is clicked in light mode
Port 921af5d27d to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-04 17:58:26 +02:00
Claire
385dd5ea37 Merge commit '0c5ce23ae496af26b96aaab742800af93f552f44' into glitch-soc/merge-4.4 2025-07-04 17:56:10 +02:00
Claire
0c5ce23ae4 Fix incorrect name in scheduler configuration (#35263) 2025-07-04 15:10:17 +02:00
github-actions[bot]
cb937a920e New Crowdin Translations (automated) (#35261)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-07-04 15:10:17 +02:00
David Roetzel
7051458467 Raise better exception on FASP error responses (#35262) 2025-07-04 15:10:17 +02:00
Matt Jankowski
025abf7325 Fix intermittent failure of TOS model spec from effective date collision (#35244) 2025-07-04 15:10:17 +02:00
Matt Jankowski
28373a9c88 Use ActiveModel::Attributes in admin/status_batch_action (#35255) 2025-07-04 15:10:17 +02:00
Claire
42884d8727 Fix error handling for blank actions in account moderation action form (#35246) 2025-07-04 15:10:17 +02:00
github-actions[bot]
000ff9c05f New Crowdin Translations (automated) (#35250)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-07-04 15:10:17 +02:00
diondiondion
921af5d27d fix: Remove focus highlight when status is clicked in light mode (#35251) 2025-07-04 15:10:17 +02:00
Matt Jankowski
878e1e65eb Use ActiveModel::Attributes for admin/account_action boolean values (#35247) 2025-07-04 15:10:17 +02:00
Matt Jankowski
06f5f270cc Use Account#targeted_reports association where needed (#35249) 2025-07-04 15:10:17 +02:00
Matt Jankowski
961c22a6fd Add coverage for TOS interstitial interruption flow of web app controller concern (#35235) 2025-07-04 15:10:17 +02:00
github-actions[bot]
07b4fa55c8 New Crowdin Translations (automated) (#35238)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-07-04 15:10:17 +02:00
Matt Jankowski
041bce9ed6 Add coverage for valid_locale_or_nil languages helper method (#34866) 2025-07-04 15:10:17 +02:00
Claire
d7a08d81b6 Fix error on log-in from old users requiring ToS interstitial when said ToS has been removed (#35233) 2025-07-04 15:10:17 +02:00
1272 changed files with 13555 additions and 43569 deletions

View File

@@ -5,7 +5,6 @@
.gitattributes .gitattributes
.gitignore .gitignore
.github .github
.vscode
public/system public/system
public/assets public/assets
public/packs public/packs
@@ -21,7 +20,6 @@ postgres14
redis redis
elasticsearch elasticsearch
chart chart
storybook-static
.yarn/ .yarn/
!.yarn/patches !.yarn/patches
!.yarn/plugins !.yarn/plugins

View File

@@ -321,6 +321,9 @@ SESSION_RETENTION_PERIOD=31556952
# Fetch All Replies Behavior # Fetch All Replies Behavior
# -------------------------- # --------------------------
# When a user expands a post (DetailedStatus view), fetch all of its replies
# (default: false)
FETCH_REPLIES_ENABLED=false
# Period to wait between fetching replies (in minutes) # Period to wait between fetching replies (in minutes)
FETCH_REPLIES_COOLDOWN_MINUTES=15 FETCH_REPLIES_COOLDOWN_MINUTES=15

View File

@@ -6,7 +6,6 @@
':labels(dependencies)', ':labels(dependencies)',
':prConcurrentLimitNone', // Remove limit for open PRs at any time. ':prConcurrentLimitNone', // Remove limit for open PRs at any time.
':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour. ':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour.
':enableVulnerabilityAlertsWithLabel(security)',
], ],
rebaseWhen: 'conflicted', rebaseWhen: 'conflicted',
minimumReleaseAge: '3', // Wait 3 days after the package has been published before upgrading it minimumReleaseAge: '3', // Wait 3 days after the package has been published before upgrading it
@@ -24,6 +23,7 @@
matchManagers: ['npm'], matchManagers: ['npm'],
matchPackageNames: [ matchPackageNames: [
'tesseract.js', // Requires code changes 'tesseract.js', // Requires code changes
'react-hotkeys', // Requires code changes
// react-router: Requires manual upgrade // react-router: Requires manual upgrade
'history', 'history',
@@ -94,19 +94,6 @@
matchUpdateTypes: ['patch', 'minor'], matchUpdateTypes: ['patch', 'minor'],
groupName: 'eslint (non-major)', groupName: 'eslint (non-major)',
}, },
{
// Group all Storybook-related packages in the same PR
matchManagers: ['npm'],
matchPackageNames: [
'chromatic',
'storybook',
'@storybook/*',
'msw',
'msw-storybook-addon',
],
matchUpdateTypes: ['patch', 'minor'],
groupName: 'storybook (non-major)',
},
{ {
// Group actions/*-artifact in the same PR // Group actions/*-artifact in the same PR
matchManagers: ['github-actions'], matchManagers: ['github-actions'],
@@ -155,12 +142,6 @@
matchUpdateTypes: ['patch', 'minor'], matchUpdateTypes: ['patch', 'minor'],
groupName: 'opentelemetry-ruby (non-major)', groupName: 'opentelemetry-ruby (non-major)',
}, },
{
// Group Playwright Ruby & JS deps in the same PR, as they need to be in sync
matchManagers: ['bundler', 'npm'],
matchPackageNames: ['playwright-ruby-client', 'playwright'],
groupName: 'Playwright',
},
// Add labels depending on package manager // Add labels depending on package manager
{ matchManagers: ['npm', 'nvm'], addLabels: ['javascript'] }, { matchManagers: ['npm', 'nvm'], addLabels: ['javascript'] },
{ matchManagers: ['bundler', 'ruby-version'], addLabels: ['ruby'] }, { matchManagers: ['bundler', 'ruby-version'], addLabels: ['ruby'] },

View File

@@ -20,7 +20,7 @@ jobs:
# Only tag with latest when ran against the latest stable branch # Only tag with latest when ran against the latest stable branch
# This needs to be updated after each minor version release # This needs to be updated after each minor version release
flavor: | flavor: |
latest=${{ startsWith(github.ref, 'refs/tags/v4.3.') }} latest=${{ startsWith(github.ref, 'refs/tags/v4.4.') }}
tags: | tags: |
type=pep440,pattern={{raw}} type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}} type=pep440,pattern=v{{major}}.{{minor}}
@@ -37,7 +37,7 @@ jobs:
# Only tag with latest when ran against the latest stable branch # Only tag with latest when ran against the latest stable branch
# This needs to be updated after each minor version release # This needs to be updated after each minor version release
flavor: | flavor: |
latest=${{ startsWith(github.ref, 'refs/tags/v4.3.') }} latest=${{ startsWith(github.ref, 'refs/tags/v4.4.') }}
tags: | tags: |
type=pep440,pattern={{raw}} type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}} type=pep440,pattern=v{{major}}.{{minor}}

View File

@@ -25,8 +25,8 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
language: ['actions', 'javascript', 'ruby'] language: ['javascript', 'ruby']
# CodeQL supports [ 'actions', 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps: steps:

View File

@@ -9,7 +9,7 @@ permissions:
jobs: jobs:
download-translations-stable: download-translations-stable:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'glitch-soc/mastodon' if: github.repository == 'mastodon/mastodon'
steps: steps:
- name: Checkout - name: Checkout
@@ -51,7 +51,7 @@ jobs:
# Create or update the pull request # Create or update the pull request
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v7.0.8 uses: peter-evans/create-pull-request@v7.0.6
with: with:
commit-message: 'New Crowdin translations' commit-message: 'New Crowdin translations'
title: 'New Crowdin Translations for ${{ github.base_ref || github.ref_name }} (automated)' title: 'New Crowdin Translations for ${{ github.base_ref || github.ref_name }} (automated)'

2
.nvmrc
View File

@@ -1 +1 @@
22.20 22.17

View File

@@ -1,21 +1,17 @@
--- ---
Metrics/AbcSize: Metrics/AbcSize:
Enabled: false Exclude:
- lib/mastodon/cli/*.rb
Metrics/BlockLength: Metrics/BlockLength:
Enabled: false Enabled: false
Metrics/BlockNesting:
Enabled: false
Metrics/ClassLength: Metrics/ClassLength:
Enabled: false Enabled: false
Metrics/CollectionLiteralLength:
Enabled: false
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Enabled: false Exclude:
- lib/mastodon/cli/*.rb
Metrics/MethodLength: Metrics/MethodLength:
Enabled: false Enabled: false
@@ -24,7 +20,4 @@ Metrics/ModuleLength:
Enabled: false Enabled: false
Metrics/ParameterLists: Metrics/ParameterLists:
Enabled: false CountKeywordArgs: false
Metrics/PerceivedComplexity:
Enabled: false

View File

@@ -1,11 +1,32 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp` # `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.80.2. # using RuboCop version 1.77.0.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again. # versions of RuboCop, may require this file to be generated again.
Lint/NonLocalExitFromIterator:
Exclude:
- 'app/helpers/json_ld_helper.rb'
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize:
Max: 90
# Configuration parameters: CountBlocks, CountModifierForms, Max.
Metrics/BlockNesting:
Exclude:
- 'lib/tasks/mastodon.rake'
# Configuration parameters: AllowedMethods, AllowedPatterns.
Metrics/CyclomaticComplexity:
Max: 25
# Configuration parameters: AllowedMethods, AllowedPatterns.
Metrics/PerceivedComplexity:
Max: 27
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowedVars, DefaultToNil. # Configuration parameters: AllowedVars, DefaultToNil.
Style/FetchEnvVar: Style/FetchEnvVar:

View File

@@ -1 +1 @@
3.4.7 3.4.4

View File

@@ -1,5 +1,3 @@
import { resolve } from 'node:path';
import type { StorybookConfig } from '@storybook/react-vite'; import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = { const config: StorybookConfig = {
@@ -28,12 +26,6 @@ const config: StorybookConfig = {
'oops.png', 'oops.png',
].map((path) => ({ from: `../public/${path}`, to: `/${path}` })), ].map((path) => ({ from: `../public/${path}`, to: `/${path}` })),
], ],
viteFinal(config) {
// For an unknown reason, Storybook does not use the root
// from the Vite config so we need to set it manually.
config.root = resolve(__dirname, '../app/javascript');
return config;
},
}; };
export default config; export default config;

View File

@@ -1,2 +0,0 @@
<html class="no-reduce-motion">
</html>

View File

@@ -12,14 +12,13 @@ import { initialize, mswLoader } from 'msw-storybook-addon';
import { action } from 'storybook/actions'; import { action } from 'storybook/actions';
import type { LocaleData } from '@/mastodon/locales'; import type { LocaleData } from '@/mastodon/locales';
import { reducerWithInitialState } from '@/mastodon/reducers'; import { reducerWithInitialState, rootReducer } from '@/mastodon/reducers';
import { defaultMiddleware } from '@/mastodon/store/store'; import { defaultMiddleware } from '@/mastodon/store/store';
import { mockHandlers, unhandledRequestHandler } from '@/testing/api'; import { mockHandlers, unhandledRequestHandler } from '@/testing/api';
// If you want to run the dark theme during development, // If you want to run the dark theme during development,
// you can change the below to `/application.scss` // you can change the below to `/application.scss`
import '../app/javascript/styles/mastodon-light.scss'; import '../app/javascript/styles/mastodon-light.scss';
import './styles.css';
const localeFiles = import.meta.glob('@/mastodon/locales/*.json', { const localeFiles = import.meta.glob('@/mastodon/locales/*.json', {
query: { as: 'json' }, query: { as: 'json' },
@@ -50,23 +49,12 @@ const preview: Preview = {
locale: 'en', locale: 'en',
}, },
decorators: [ decorators: [
(Story, { parameters, globals, args }) => { (Story, { parameters }) => {
// Get the locale from the global toolbar
// and merge it with any parameters or args state.
const { locale } = globals as { locale: string };
const { state = {} } = parameters; const { state = {} } = parameters;
const { state: argsState = {} } = args; let reducer = rootReducer;
if (typeof state === 'object' && state) {
const reducer = reducerWithInitialState( reducer = reducerWithInitialState(state as Record<string, unknown>);
{ }
meta: {
locale,
},
},
state as Record<string, unknown>,
argsState as Record<string, unknown>,
);
const store = configureStore({ const store = configureStore({
reducer, reducer,
middleware(getDefaultMiddleware) { middleware(getDefaultMiddleware) {

View File

@@ -7,8 +7,8 @@
* - Please do NOT modify this file. * - Please do NOT modify this file.
*/ */
const PACKAGE_VERSION = '2.11.3' const PACKAGE_VERSION = '2.10.2'
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set() const activeClientIds = new Set()
@@ -71,6 +71,11 @@ addEventListener('message', async function (event) {
break break
} }
case 'MOCK_DEACTIVATE': {
activeClientIds.delete(clientId)
break
}
case 'CLIENT_CLOSED': { case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId) activeClientIds.delete(clientId)
@@ -89,8 +94,6 @@ addEventListener('message', async function (event) {
}) })
addEventListener('fetch', function (event) { addEventListener('fetch', function (event) {
const requestInterceptedAt = Date.now()
// Bypass navigation requests. // Bypass navigation requests.
if (event.request.mode === 'navigate') { if (event.request.mode === 'navigate') {
return return
@@ -107,29 +110,23 @@ addEventListener('fetch', function (event) {
// Bypass all requests when there are no active clients. // Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests // Prevents the self-unregistered worked from handling requests
// after it's been terminated (still remains active until the next reload). // after it's been deleted (still remains active until the next reload).
if (activeClientIds.size === 0) { if (activeClientIds.size === 0) {
return return
} }
const requestId = crypto.randomUUID() const requestId = crypto.randomUUID()
event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) event.respondWith(handleRequest(event, requestId))
}) })
/** /**
* @param {FetchEvent} event * @param {FetchEvent} event
* @param {string} requestId * @param {string} requestId
* @param {number} requestInterceptedAt
*/ */
async function handleRequest(event, requestId, requestInterceptedAt) { async function handleRequest(event, requestId) {
const client = await resolveMainClient(event) const client = await resolveMainClient(event)
const requestCloneForEvents = event.request.clone() const requestCloneForEvents = event.request.clone()
const response = await getResponse( const response = await getResponse(event, client, requestId)
event,
client,
requestId,
requestInterceptedAt,
)
// Send back the response clone for the "response:*" life-cycle events. // Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise // Ensure MSW is active and ready to handle the message, otherwise
@@ -207,7 +204,7 @@ async function resolveMainClient(event) {
* @param {string} requestId * @param {string} requestId
* @returns {Promise<Response>} * @returns {Promise<Response>}
*/ */
async function getResponse(event, client, requestId, requestInterceptedAt) { async function getResponse(event, client, requestId) {
// Clone the request because it might've been already used // Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client). // (i.e. its body has been read and sent to the client).
const requestClone = event.request.clone() const requestClone = event.request.clone()
@@ -258,7 +255,6 @@ async function getResponse(event, client, requestId, requestInterceptedAt) {
type: 'REQUEST', type: 'REQUEST',
payload: { payload: {
id: requestId, id: requestId,
interceptedAt: requestInterceptedAt,
...serializedRequest, ...serializedRequest,
}, },
}, },

View File

@@ -1,8 +0,0 @@
a {
color: inherit;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}

View File

@@ -2,180 +2,6 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [4.5.0] - UNRELEASED
### Added
- **Add support for allowing and authoring quotes** (#35355, #35578, #35614, #35618, #35624, #35626, #35652, #35629, #35665, #35653, #35670, #35677, #35690, #35697, #35689, #35699, #35700, #35701, #35709, #35714, #35713, #35715, #35725, #35749, #35769, #35780, #35762, #35804, #35808, #35805, #35819, #35824, #35828, #35822, #35835, #35865, #35860, #35832, #35891, #35894, #35895, #35820, #35917, #35924, #35925, #35914, #35930, #35941, #35939, #35948, #35955, #35967, #35990, #35991, #35975, #35971, #36002, #35986, #36031, #36034, #36038, #36054, #36052, #36055, #36065, #36068, #36083, #36087, #36080, #36091, #36090, #36118, #36119, #36128, #36094, #36129, #36138, #36132, #36151, #36158, #36171, #36194, #36220, #36169, #36130, #36249, #36153, #36299, #36291, #36301, #36315, #36317, #36364, #36383, #36381, #36459, #36464, and #36461 by @ChaosExAnima, @ClearlyClaire, @Lycolia, @diondiondion, and @tribela)\
This includes a revamp of the composer interface.\
See https://blog.joinmastodon.org/2025/09/introducing-quote-posts/ for a user-centric overview of the feature, and https://docs.joinmastodon.org/client/quotes/ for API documentation.
- **Add support for fetching and refreshing replies to the web UI** (#35210, #35496, #35575, #35500, #35577, #35602, #35603, #35654, #36141, #36237, #36172, #36256, #36271, #36334, #36382, and #36239 by @ClearlyClaire, @Gargron, and @diondiondion)
- **Add ability to block words in usernames** (#35407, #35655, and #35806 by @ClearlyClaire and @Gargron)
- Add support for displaying link previews for Admin UI (#35958 by @ThisIsMissEm)
- Add support for dynamic viewport height (#36272 by @e1berd)
- Add support for numeric-based URIs for new local accounts (#32724, #36304, #36316, and #36365 by @ClearlyClaire)
- Add Traditional Mongolian to posting languages (#36196 by @shimon1024)
- Add example post with manual quote approval policy to `dev:populate_sample_data` (#36099 by @ClearlyClaire)
- Add server-side support for handling posts with a quote policy allowing followers to quote (#36093 and #36127 by @ClearlyClaire)
- Add schema.org markup to SEO-enabled posts (#36075 by @Gargron)
- Add migration to fill unset default quote policy based on default post privacy (#36041 by @ClearlyClaire)
- Add support for exposing conversation context for new public conversations according to FEP-7888 (#35959 and #36064 by @ClearlyClaire and @jesseplusplus)
- Add digest re-check before removing followers in synchronization mechanism (#34273 by @ClearlyClaire)
- Add “Posting defaults” setting page, moving existing settings from “Other” (#35896, #36033, #35966, #35969, and #36084 by @ClearlyClaire and @diondiondion)
- Add support for displaying Valkey version on admin dashboard (#35785 by @ykzts)
- Add delivery failure tracking and handling to FASP jobs (#35625, #35628, and #35723 by @oneiros)
- Add example of quote post with a preview card to development sample data (#35616 by @ClearlyClaire)
- Add second set of blocked text that applies to accounts regardless of account age for spam-blocking (#35563 by @ClearlyClaire)
- Add experimental feature to select custom emoji rendering (#35229, #35282, #35253, #35424, #35473, #35483, #35505, #35568, #35605, #35659, #35664, #35739, #35985, #36051, #36071, #36137, #36165, #36248, #36262, #36275, #36293, #36341, #36342, #36366, #36377, #36378, #36385, #36393, #36397, #36403, #36413, #36410, #36454, and #36402 by @ChaosExAnima and @braddunbar)\
This also completely reworks the processing and rendering of emojis and server-rendered HTML in statuses and other places.
### Changed
- Change confirmation dialogs for follow button actions “unfollow”, “unblock”, and “withdraw request” (#36289 by @diondiondion)
- Change “Follow” button labels (#36264 by @diondiondion)
- Change display of content warnings in Admin UI (#35935 by @ThisIsMissEm)
- Change index on `follows` table to improve performance of some queries (#36374 by @ClearlyClaire)
- Change links to accounts in settings and moderation views to link to local view unless account is suspended (#36340 by @diondiondion)
- Change redirection for denied registration from web app to sign-in page with error message (#36384 by @ClearlyClaire)
- Change `timeline_preview` setting into four more granular settings (#36338 and #36467 by @ClearlyClaire)
- Change wording and design of interaction dialog to simplify it (#36124 by @diondiondion)
- Change dropdown menus to allow disabled items to be focused (#36078 by @diondiondion)
- Change modal background colours in light mode (#36069 by @diondiondion)
- Change “Posting defaults” settings page to enforce `nobody` quote policy for `private` default visibility (#36040 by @ClearlyClaire)
- Change description of “Quiet public” (#36032 by @ClearlyClaire)
- Change “Boost with original visibility” to “Share again with your followers” (#36035 by @ClearlyClaire)
- Change handling of push subscriptions to automatically delete invalid ones on delivery (#35987 by @ThisIsMissEm)
- Change design of quote posts in web UI (#35584 and #35834 by @ClearlyClaire and @Gargron)
- Change auditable accounts to be sorted by username in admin action logs interface (#35272 by @breadtk)
- Change order of translation restoration and service credit on post card (#33619 by @colindean)
- Change position of add more to be inside table toolbar on reports (#35963 by @ThisIsMissEm)
### Fixed
- Fix rendering of poll options in status history modal (#35633 by @ThisIsMissEm)
- Fix “mute” button being displayed to unauthenticated visitors in hashtag dropdown (#36353 by @mkljczk)
- Fix overflow handling of `.more-from-author` (#36310 by @edent)
- Fix unfortunate action button wrapping in admin area (#36247 by @diondiondion)
- Fix translate button width in Safari (#36164 and #36216 by @diondiondion)
- Fix login page linking to other pages within OAuth authorization flow (#36115 by @Gargron)
- Fix stale search results being displayed in Web UI while new query is in progress (#36053 by @ChaosExAnima)
- Fix YouTube iframe not being able to start at a defined time (#26584 by @BrunoViveiros)
- Fix banned text being able to be circumvented via unicode (#35978 by @Gargron)
- Fix batch table toolbar displaying under status media (#35962 by @ThisIsMissEm)
- Fix incorrect RSS feed MIME type in gzip_types directive (#35562 by @iioflow)
- Fix 404 error after deleting status from detail view (#35800) (#35881 by @crafkaz)
- Fix feeds keyboard navigation issues (#35853, #35864, and #36267 by @braddunbar and @diondiondion)
- Fix layout shift caused by “Who to follow” widget (#35861 by @diondiondion)
- Fix Vagrantfile (#35765 by @ClearlyClaire)
- Fix reply indicator displaying wrong avatar in rare cases (#35756 by @ClearlyClaire)
- Fix `Chewy::UndefinedUpdateStrategy` in `dev:populate_sample_data` task when Elasticsearch is enabled (#35615 by @ClearlyClaire)
- Fix unnecessary account note addition for already-muted moved-to users (#35566 by @mjankowski)
- Fix seeded admin user creation failing on specific configurations (#35565 by @oneiros)
- Fix media modal images in Web UI having redundant `title` attribute (#35468 by @mayank99)
- Fix inconsistent default privacy post setting when unset in settings (#35422 by @oneiros)
- Fix glitchy status keyboard navigation (#35455 and #35504 by @diondiondion)
- Fix post being submitted when pressing “Enter” in the CW field (#35445 by @diondiondion)
## [4.4.7] - 2025-10-15
### Fixed
- Fix forwarder being called with `nil` status when quote post is soft-deleted (#36463 by @ClearlyClaire)
- Fix moderation warning e-mails that include posts (#36462 by @ClearlyClaire)
- Fix allow_referrer_origin typo (#36460 by @ShadowJonathan)
## [4.4.6] - 2025-10-13
### Security
- Update dependencies `rack` and `uri`
- Fix streaming server connection not being closed on user suspension (by @ThisIsMissEm, [GHSA-r2fh-jr9c-9pxh](https://github.com/mastodon/mastodon/security/advisories/GHSA-r2fh-jr9c-9pxh))
- Fix password change through admin CLI not invalidating existing sessions and access tokens (by @ThisIsMissEm, [GHSA-f3q3-rmf7-9655](https://github.com/mastodon/mastodon/security/advisories/GHSA-f3q3-rmf7-9655))
- Fix streaming server allowing access to public timelines even without the `read` or `read:statuses` OAuth scopes (by @ThisIsMissEm, [GHSA-7gwh-mw97-qjgp](https://github.com/mastodon/mastodon/security/advisories/GHSA-7gwh-mw97-qjgp))
### Added
- Add support for processing quotes of deleted posts signaled through a `Tombstone` (#36381 by @ClearlyClaire)
### Fixed
- Fix quote post state sometimes not being updated through streaming server (#36408 by @ClearlyClaire)
- Fix inconsistent “pending tags” count on admin dashboard (#36404 by @mjankowski)
- Fix JSON payload being potentially mutated when processing interaction policies (#36392 by @ClearlyClaire)
- Fix quotes not being displayed in email notifications (#36379 by @diondiondion)
- Fix redirect to external object when URL is missing or malformed (#36347 by @ClearlyClaire)
- Fix quotes not being displayed in the featured carousel (#36335 by @diondiondion)
## [4.4.5] - 2025-09-23
### Security
- Update dependencies
### Added
- Add support for `has:quote` in search (#36217 by @ClearlyClaire)
### Changed
- Change quoted posts from silenced accounts to use a click-through rather than being hidden (#36166 and #36167 by @ClearlyClaire)
### Fixed
- Fix processing of out-of-order `Update` as implicit updates (#36190 by @ClearlyClaire)
- Fix getting `Create` and `Update` out of order (#36176 by @ClearlyClaire)
- Fix quotes with Content Warnings but no text being shown without Content Warnings (#36150 by @ClearlyClaire)
## [4.4.4] - 2025-09-16
### Security
- Update dependencies
### Fixed
- Fix missing memoization in `Web::PushNotificationWorker` (#36085 by @ClearlyClaire)
- Fix unresponsive areas around GIFV modals in some cases (#36059 by @ClearlyClaire)
- Fix missing `beforeUnload` confirmation when a poll is being authored (#36030 by @ClearlyClaire)
- Fix processing of remote edited statuses with new media and no text (#35970 by @unfokus)
- Fix polls not being displayed in moderation interface (#35644 and #35933 by @ThisIsMissEm)
- Fix WebUI handling of deleted quoted posts (#35909 and #35918 by @ClearlyClaire and @diondiondion)
- Fix “Edit” and “Delete & Redraft” on a poll not inserting empty option (#35892 by @ClearlyClaire)
- Fix loading of some compatibility CSS on some configurations (#35876 by @shleeable)
- Fix HttpLog not being enabled with `RAILS_LOG_LEVEL=debug` (#35833 by @mjankowski)
- Fix self-destruct scheduler behavior on some Redis setups (#35823 by @ClearlyClaire)
- Fix `tootctl admin create` not bypassing reserved username checks (#35779 by @ClearlyClaire)
- Fix interaction policy changes in implicit updates not being saved (#35751 by @ClearlyClaire)
- Fix quote revocation not being streamed (#35710 by @ClearlyClaire)
- Fix export of large user archives by enabling Zip64 (#35850 by @ClearlyClaire)
### Changed
- Change labels for quote policy settings (#35893 by @ClearlyClaire)
- Change standalone “Share” page to redirect to web interface after posting (#35763 by @ChaosExAnima)
## [4.4.3] - 2025-08-05
### Security
- Update dependencies
- Fix incorrect rate-limit handling [GHSA-84ch-6436-c7mg](https://github.com/mastodon/mastodon/security/advisories/GHSA-84ch-6436-c7mg)
### Fixed
- Fix race condition caused by ActiveRecord query cache in `Create` critical path (#35662 by @ClearlyClaire)
- Fix race condition caused by quote post processing (#35657 by @ClearlyClaire)
- Fix WebUI crashing for accounts with `null` URL (#35651 by @ClearlyClaire)
- Fix friends-of-friends recommendations suggesting already-requested accounts (#35604 by @ClearlyClaire)
- Fix synchronous recursive fetching of deeply-nested quoted posts (#35600 by @ClearlyClaire)
- Fix “Expand this post” link including user `@undefined` (#35478 by @ClearlyClaire)
### Changed
- Change `StatusReachFinder` to consider quotes as well as reblogs (#35601 by @ClearlyClaire)
- Add restrictions on which quote posts can trend (#35507 by @ClearlyClaire)
- Change quote verification to not bypass authorization flow for mentions (#35528 by @ClearlyClaire)
## [4.4.2] - 2025-07-23 ## [4.4.2] - 2025-07-23
### Security ### Security
@@ -735,6 +561,7 @@ The following changelog entries focus on changes visible to users, administrator
You can now separately filter or drop notifications from people you don't follow, people who don't follow you, accounts created within the past 30 days, as well as unsolicited private mentions, and accounts limited by the moderation.\ You can now separately filter or drop notifications from people you don't follow, people who don't follow you, accounts created within the past 30 days, as well as unsolicited private mentions, and accounts limited by the moderation.\
Instead of being outright dropped, notifications that you chose to filter are put in a separate Filtered notifications box that you can review separately without it clogging your main notifications.\ Instead of being outright dropped, notifications that you chose to filter are put in a separate Filtered notifications box that you can review separately without it clogging your main notifications.\
This adds the following REST API endpoints: This adds the following REST API endpoints:
- `GET /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#get-policy - `GET /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#get-policy
- `PATCH /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#update-the-filtering-policy-for-notifications - `PATCH /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#update-the-filtering-policy-for-notifications
- `GET /api/v1/notifications/requests`: https://docs.joinmastodon.org/methods/notifications/#get-requests - `GET /api/v1/notifications/requests`: https://docs.joinmastodon.org/methods/notifications/#get-requests
@@ -746,6 +573,7 @@ The following changelog entries focus on changes visible to users, administrator
- `GET /api/v1/notifications/requests/merged`: https://docs.joinmastodon.org/methods/notifications/#requests-merged - `GET /api/v1/notifications/requests/merged`: https://docs.joinmastodon.org/methods/notifications/#requests-merged
In addition, accepting one or more notification requests generates a new streaming event: In addition, accepting one or more notification requests generates a new streaming event:
- `notifications_merged`: an event of this type indicates accepted notification requests have finished merging, and the notifications list should be refreshed - `notifications_merged`: an event of this type indicates accepted notification requests have finished merging, and the notifications list should be refreshed
- **Add notifications of severed relationships** (#27511, #29665, #29668, #29670, #29700, #29714, #29712, and #29731 by @ClearlyClaire and @Gargron)\ - **Add notifications of severed relationships** (#27511, #29665, #29668, #29670, #29700, #29714, #29712, and #29731 by @ClearlyClaire and @Gargron)\

View File

@@ -1,4 +1,4 @@
# syntax=docker/dockerfile:1.18 # syntax=docker/dockerfile:1.12
# This file is designed for production server deployment, not local development work # This file is designed for production server deployment, not local development work
# For a containerized local dev environment, see: https://github.com/mastodon/mastodon/blob/main/docs/DEVELOPMENT.md#docker # For a containerized local dev environment, see: https://github.com/mastodon/mastodon/blob/main/docs/DEVELOPMENT.md#docker
@@ -13,15 +13,15 @@ ARG BASE_REGISTRY="docker.io"
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"] # Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"]
# renovate: datasource=docker depName=docker.io/ruby # renovate: datasource=docker depName=docker.io/ruby
ARG RUBY_VERSION="3.4.7" ARG RUBY_VERSION="3.4.4"
# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] # # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
# renovate: datasource=node-version depName=node # renovate: datasource=node-version depName=node
ARG NODE_MAJOR_VERSION="22" ARG NODE_MAJOR_VERSION="22"
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="trixie"] # Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
ARG DEBIAN_VERSION="trixie" ARG DEBIAN_VERSION="bookworm"
# Node.js image to use for base image based on combined variables (ex: 20-trixie-slim) # Node.js image to use for base image based on combined variables (ex: 20-bookworm-slim)
FROM ${BASE_REGISTRY}/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim AS node FROM ${BASE_REGISTRY}/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim AS node
# Ruby image to use for base image based on combined variables (ex: 3.4.x-slim-trixie) # Ruby image to use for base image based on combined variables (ex: 3.4.x-slim-bookworm)
FROM ${BASE_REGISTRY}/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby FROM ${BASE_REGISTRY}/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA # Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
@@ -96,6 +96,9 @@ RUN \
# Set /opt/mastodon as working directory # Set /opt/mastodon as working directory
WORKDIR /opt/mastodon WORKDIR /opt/mastodon
# Add backport repository for some specific packages where we need the latest version
RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list
# hadolint ignore=DL3008,DL3005 # hadolint ignore=DL3008,DL3005
RUN \ RUN \
# Mount Apt cache and lib directories from Docker buildx caches # Mount Apt cache and lib directories from Docker buildx caches
@@ -158,11 +161,11 @@ RUN \
libexif-dev \ libexif-dev \
libexpat1-dev \ libexpat1-dev \
libgirepository1.0-dev \ libgirepository1.0-dev \
libheif-dev \ libheif-dev/bookworm-backports \
libhwy-dev \
libimagequant-dev \ libimagequant-dev \
libjpeg62-turbo-dev \ libjpeg62-turbo-dev \
liblcms2-dev \ liblcms2-dev \
liborc-dev \
libspng-dev \ libspng-dev \
libtiff-dev \ libtiff-dev \
libwebp-dev \ libwebp-dev \
@@ -183,7 +186,7 @@ FROM build AS libvips
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"] # libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips # renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
ARG VIPS_VERSION=8.17.2 ARG VIPS_VERSION=8.17.0
# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"] # libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"]
ARG VIPS_URL=https://github.com/libvips/libvips/releases/download ARG VIPS_URL=https://github.com/libvips/libvips/releases/download
@@ -206,7 +209,7 @@ FROM build AS ffmpeg
# ffmpeg version to compile, change with [--build-arg FFMPEG_VERSION="7.0.x"] # ffmpeg version to compile, change with [--build-arg FFMPEG_VERSION="7.0.x"]
# renovate: datasource=repology depName=ffmpeg packageName=openpkg_current/ffmpeg # renovate: datasource=repology depName=ffmpeg packageName=openpkg_current/ffmpeg
ARG FFMPEG_VERSION=8.0 ARG FFMPEG_VERSION=7.1
# ffmpeg download URL, change with [--build-arg FFMPEG_URL="https://ffmpeg.org/releases"] # ffmpeg download URL, change with [--build-arg FFMPEG_URL="https://ffmpeg.org/releases"]
ARG FFMPEG_URL=https://ffmpeg.org/releases ARG FFMPEG_URL=https://ffmpeg.org/releases
@@ -324,28 +327,28 @@ RUN \
# Apt update install non-dev versions of necessary components # Apt update install non-dev versions of necessary components
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
libexpat1 \ libexpat1 \
libglib2.0-0t64 \ libglib2.0-0 \
libicu76 \ libicu72 \
libidn12 \ libidn12 \
libpq5 \ libpq5 \
libreadline8t64 \ libreadline8 \
libssl3t64 \ libssl3 \
libyaml-0-2 \ libyaml-0-2 \
# libvips components # libvips components
libcgif0 \ libcgif0 \
libexif12 \ libexif12 \
libheif1 \ libheif1/bookworm-backports \
libhwy1t64 \
libimagequant0 \ libimagequant0 \
libjpeg62-turbo \ libjpeg62-turbo \
liblcms2-2 \ liblcms2-2 \
liborc-0.4-0 \
libspng0 \ libspng0 \
libtiff6 \ libtiff6 \
libwebp7 \ libwebp7 \
libwebpdemux2 \ libwebpdemux2 \
libwebpmux3 \ libwebpmux3 \
# ffmpeg components # ffmpeg components
libdav1d7 \ libdav1d6 \
libmp3lame0 \ libmp3lame0 \
libopencore-amrnb0 \ libopencore-amrnb0 \
libopencore-amrwb0 \ libopencore-amrwb0 \
@@ -355,9 +358,9 @@ RUN \
libvorbis0a \ libvorbis0a \
libvorbisenc2 \ libvorbisenc2 \
libvorbisfile3 \ libvorbisfile3 \
libvpx9 \ libvpx7 \
libx264-164 \ libx264-164 \
libx265-215 \ libx265-199 \
; ;
# Copy Mastodon sources into final layer # Copy Mastodon sources into final layer

50
Gemfile
View File

@@ -4,12 +4,12 @@ source 'https://rubygems.org'
ruby '>= 3.2.0', '< 3.5.0' ruby '>= 3.2.0', '< 3.5.0'
gem 'propshaft' gem 'propshaft'
gem 'puma', '~> 7.0' gem 'puma', '~> 6.3'
gem 'rails', '~> 8.0' gem 'rails', '~> 8.0'
gem 'thor', '~> 1.2' gem 'thor', '~> 1.2'
gem 'dotenv' gem 'dotenv'
gem 'haml-rails', '~>3.0' gem 'haml-rails', '~>2.0'
gem 'pg', '~> 1.5' gem 'pg', '~> 1.5'
gem 'pghero' gem 'pghero'
@@ -62,7 +62,7 @@ gem 'inline_svg'
gem 'irb', '~> 1.8' gem 'irb', '~> 1.8'
gem 'kaminari', '~> 1.2' gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
gem 'linzer', '~> 0.7.7' gem 'linzer', '~> 0.7.2'
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'mime-types', '~> 3.7.0', require: 'mime/types/columnar' gem 'mime-types', '~> 3.7.0', require: 'mime/types/columnar'
gem 'mutex_m' gem 'mutex_m'
@@ -82,13 +82,13 @@ gem 'rqrcode', '~> 3.0'
gem 'ruby-progressbar', '~> 1.13' gem 'ruby-progressbar', '~> 1.13'
gem 'sanitize', '~> 7.0' gem 'sanitize', '~> 7.0'
gem 'scenic', '~> 1.7' gem 'scenic', '~> 1.7'
gem 'sidekiq', '< 9' gem 'sidekiq', '< 8'
gem 'sidekiq-bulk', '~> 0.2.0' gem 'sidekiq-bulk', '~> 0.2.0'
gem 'sidekiq-scheduler', '~> 6.0' gem 'sidekiq-scheduler', '~> 5.0'
gem 'sidekiq-unique-jobs', '> 8' gem 'sidekiq-unique-jobs', '> 8'
gem 'simple_form', '~> 5.2' gem 'simple_form', '~> 5.2'
gem 'simple-navigation', '~> 4.4' gem 'simple-navigation', '~> 4.4'
gem 'stoplight' gem 'stoplight', '~> 4.1'
gem 'strong_migrations' gem 'strong_migrations'
gem 'tty-prompt', '~> 0.23', require: false gem 'tty-prompt', '~> 0.23', require: false
gem 'twitter-text', '~> 3.1.0' gem 'twitter-text', '~> 3.1.0'
@@ -102,23 +102,23 @@ gem 'rdf-normalize', '~> 0.5'
gem 'prometheus_exporter', '~> 2.2', require: false gem 'prometheus_exporter', '~> 2.2', require: false
gem 'opentelemetry-api', '~> 1.7.0' gem 'opentelemetry-api', '~> 1.5.0'
group :opentelemetry do group :opentelemetry do
gem 'opentelemetry-exporter-otlp', '~> 0.31.0', require: false gem 'opentelemetry-exporter-otlp', '~> 0.30.0', require: false
gem 'opentelemetry-instrumentation-active_job', '~> 0.9.0', require: false gem 'opentelemetry-instrumentation-active_job', '~> 0.8.0', require: false
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.23.0', require: false gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.22.0', require: false
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.23.0', require: false gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.22.0', require: false
gem 'opentelemetry-instrumentation-excon', '~> 0.25.0', require: false gem 'opentelemetry-instrumentation-excon', '~> 0.23.0', require: false
gem 'opentelemetry-instrumentation-faraday', '~> 0.29.0', require: false gem 'opentelemetry-instrumentation-faraday', '~> 0.27.0', require: false
gem 'opentelemetry-instrumentation-http', '~> 0.26.0', require: false gem 'opentelemetry-instrumentation-http', '~> 0.25.0', require: false
gem 'opentelemetry-instrumentation-http_client', '~> 0.25.0', require: false gem 'opentelemetry-instrumentation-http_client', '~> 0.23.0', require: false
gem 'opentelemetry-instrumentation-net_http', '~> 0.25.0', require: false gem 'opentelemetry-instrumentation-net_http', '~> 0.23.0', require: false
gem 'opentelemetry-instrumentation-pg', '~> 0.31.0', require: false gem 'opentelemetry-instrumentation-pg', '~> 0.30.0', require: false
gem 'opentelemetry-instrumentation-rack', '~> 0.28.0', require: false gem 'opentelemetry-instrumentation-rack', '~> 0.26.0', require: false
gem 'opentelemetry-instrumentation-rails', '~> 0.38.0', require: false gem 'opentelemetry-instrumentation-rails', '~> 0.36.0', require: false
gem 'opentelemetry-instrumentation-redis', '~> 0.27.0', require: false gem 'opentelemetry-instrumentation-redis', '~> 0.26.0', require: false
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.27.0', require: false gem 'opentelemetry-instrumentation-sidekiq', '~> 0.26.0', require: false
gem 'opentelemetry-sdk', '~> 1.4', require: false gem 'opentelemetry-sdk', '~> 1.4', require: false
end end
@@ -138,7 +138,6 @@ group :test do
# Browser integration testing # Browser integration testing
gem 'capybara', '~> 3.39' gem 'capybara', '~> 3.39'
gem 'capybara-playwright-driver' gem 'capybara-playwright-driver'
gem 'playwright-ruby-client', '1.55.0', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package
# Used to reset the database between system tests # Used to reset the database between system tests
gem 'database_cleaner-active_record' gem 'database_cleaner-active_record'
@@ -147,7 +146,7 @@ group :test do
gem 'climate_control' gem 'climate_control'
# Validate schemas in specs # Validate schemas in specs
gem 'json-schema', '~> 6.0' gem 'json-schema', '~> 5.0'
# Test harness fo rack components # Test harness fo rack components
gem 'rack-test', '~> 2.1' gem 'rack-test', '~> 2.1'
@@ -160,9 +159,6 @@ group :test do
# Stub web requests for specs # Stub web requests for specs
gem 'webmock', '~> 3.18' gem 'webmock', '~> 3.18'
# Websocket driver for testing integration between rails/sidekiq and streaming
gem 'websocket-driver', '~> 0.8', require: false
end end
group :development do group :development do
@@ -227,7 +223,7 @@ gem 'connection_pool', require: false
gem 'xorcist', '~> 1.1' gem 'xorcist', '~> 1.1'
gem 'net-http', '~> 0.6.0' gem 'net-http', '~> 0.6.0'
gem 'rubyzip', '~> 3.0' gem 'rubyzip', '~> 2.3'
gem 'hcaptcha', '~> 7.1' gem 'hcaptcha', '~> 7.1'

View File

@@ -10,29 +10,29 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (8.0.3) actioncable (8.0.2)
actionpack (= 8.0.3) actionpack (= 8.0.2)
activesupport (= 8.0.3) activesupport (= 8.0.2)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (8.0.3) actionmailbox (8.0.2)
actionpack (= 8.0.3) actionpack (= 8.0.2)
activejob (= 8.0.3) activejob (= 8.0.2)
activerecord (= 8.0.3) activerecord (= 8.0.2)
activestorage (= 8.0.3) activestorage (= 8.0.2)
activesupport (= 8.0.3) activesupport (= 8.0.2)
mail (>= 2.8.0) mail (>= 2.8.0)
actionmailer (8.0.3) actionmailer (8.0.2)
actionpack (= 8.0.3) actionpack (= 8.0.2)
actionview (= 8.0.3) actionview (= 8.0.2)
activejob (= 8.0.3) activejob (= 8.0.2)
activesupport (= 8.0.3) activesupport (= 8.0.2)
mail (>= 2.8.0) mail (>= 2.8.0)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (8.0.3) actionpack (8.0.2)
actionview (= 8.0.3) actionview (= 8.0.2)
activesupport (= 8.0.3) activesupport (= 8.0.2)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
rack (>= 2.2.4) rack (>= 2.2.4)
rack-session (>= 1.0.1) rack-session (>= 1.0.1)
@@ -40,15 +40,15 @@ GEM
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
useragent (~> 0.16) useragent (~> 0.16)
actiontext (8.0.3) actiontext (8.0.2)
actionpack (= 8.0.3) actionpack (= 8.0.2)
activerecord (= 8.0.3) activerecord (= 8.0.2)
activestorage (= 8.0.3) activestorage (= 8.0.2)
activesupport (= 8.0.3) activesupport (= 8.0.2)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (8.0.3) actionview (8.0.2)
activesupport (= 8.0.3) activesupport (= 8.0.2)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
@@ -58,22 +58,22 @@ GEM
activemodel (>= 4.1) activemodel (>= 4.1)
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (8.0.3) activejob (8.0.2)
activesupport (= 8.0.3) activesupport (= 8.0.2)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (8.0.3) activemodel (8.0.2)
activesupport (= 8.0.3) activesupport (= 8.0.2)
activerecord (8.0.3) activerecord (8.0.2)
activemodel (= 8.0.3) activemodel (= 8.0.2)
activesupport (= 8.0.3) activesupport (= 8.0.2)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (8.0.3) activestorage (8.0.2)
actionpack (= 8.0.3) actionpack (= 8.0.2)
activejob (= 8.0.3) activejob (= 8.0.2)
activerecord (= 8.0.3) activerecord (= 8.0.2)
activesupport (= 8.0.3) activesupport (= 8.0.2)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (8.0.3) activesupport (8.0.2)
base64 base64
benchmark (>= 0.3) benchmark (>= 0.3)
bigdecimal bigdecimal
@@ -90,13 +90,13 @@ GEM
public_suffix (>= 2.0.2, < 7.0) public_suffix (>= 2.0.2, < 7.0)
aes_key_wrap (1.1.0) aes_key_wrap (1.1.0)
android_key_attestation (0.3.0) android_key_attestation (0.3.0)
annotaterb (4.19.0) annotaterb (4.16.0)
activerecord (>= 6.0.0) activerecord (>= 6.0.0)
activesupport (>= 6.0.0) activesupport (>= 6.0.0)
ast (2.4.3) ast (2.4.3)
attr_required (1.0.2) attr_required (1.0.2)
aws-eventstream (1.4.0) aws-eventstream (1.3.2)
aws-partitions (1.1168.0) aws-partitions (1.1103.0)
aws-sdk-core (3.215.1) aws-sdk-core (3.215.1)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0) aws-partitions (~> 1, >= 1.992.0)
@@ -109,9 +109,9 @@ GEM
aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1) aws-sigv4 (1.11.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
azure-blob (0.5.9.1) azure-blob (0.5.8)
rexml rexml
base64 (0.3.0) base64 (0.3.0)
bcp47_spec (0.2.1) bcp47_spec (0.2.1)
@@ -121,7 +121,7 @@ GEM
erubi (>= 1.0.0) erubi (>= 1.0.0)
rack (>= 0.9.0) rack (>= 0.9.0)
rouge (>= 1.0.0) rouge (>= 1.0.0)
bigdecimal (3.3.1) bigdecimal (3.2.2)
bindata (2.5.1) bindata (2.5.1)
binding_of_caller (1.0.1) binding_of_caller (1.0.1)
debug_inspector (>= 1.2.0) debug_inspector (>= 1.2.0)
@@ -144,13 +144,13 @@ GEM
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
capybara-playwright-driver (0.5.7) capybara-playwright-driver (0.5.6)
addressable addressable
capybara capybara
playwright-ruby-client (>= 1.16.0) playwright-ruby-client (>= 1.16.0)
case_transform (0.2) case_transform (0.2)
activesupport activesupport
cbor (0.5.10.1) cbor (0.5.9.8)
cgi (0.4.2) cgi (0.4.2)
charlock_holmes (0.7.9) charlock_holmes (0.7.9)
chewy (7.6.0) chewy (7.6.0)
@@ -164,7 +164,7 @@ GEM
cocoon (1.2.15) cocoon (1.2.15)
color_diff (0.1) color_diff (0.1)
concurrent-ruby (1.3.5) concurrent-ruby (1.3.5)
connection_pool (2.5.4) connection_pool (2.5.3)
cose (1.3.1) cose (1.3.1)
cbor (~> 0.5.9) cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0) openssl-signature_algorithm (~> 1.0)
@@ -175,9 +175,9 @@ GEM
css_parser (1.21.1) css_parser (1.21.1)
addressable addressable
csv (3.3.5) csv (3.3.5)
database_cleaner-active_record (2.2.2) database_cleaner-active_record (2.2.1)
activerecord (>= 5.a) activerecord (>= 5.a)
database_cleaner-core (~> 2.0) database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1) database_cleaner-core (2.0.1)
date (3.4.1) date (3.4.1)
debug (1.11.0) debug (1.11.0)
@@ -207,7 +207,7 @@ GEM
railties (>= 5) railties (>= 5)
dotenv (3.1.8) dotenv (3.1.8)
drb (2.2.3) drb (2.2.3)
dry-cli (1.3.0) dry-cli (1.2.0)
elasticsearch (7.17.11) elasticsearch (7.17.11)
elasticsearch-api (= 7.17.11) elasticsearch-api (= 7.17.11)
elasticsearch-transport (= 7.17.11) elasticsearch-transport (= 7.17.11)
@@ -224,24 +224,24 @@ GEM
mail (~> 2.7) mail (~> 2.7)
email_validator (2.2.4) email_validator (2.2.4)
activemodel activemodel
erb (5.0.2) erb (5.0.1)
erubi (1.13.1) erubi (1.13.1)
et-orbi (1.4.0) et-orbi (1.2.11)
tzinfo tzinfo
excon (1.3.0) excon (1.2.5)
logger logger
fabrication (3.0.0) fabrication (3.0.0)
faker (3.5.2) faker (3.5.1)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (2.14.0) faraday (2.13.1)
faraday-net_http (>= 2.0, < 3.5) faraday-net_http (>= 2.0, < 3.5)
json json
logger logger
faraday-follow_redirects (0.4.0) faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3) faraday (>= 1, < 3)
faraday-httpclient (2.0.2) faraday-httpclient (2.0.2)
httpclient (>= 2.2) httpclient (>= 2.2)
faraday-net_http (3.4.1) faraday-net_http (3.4.0)
net-http (>= 0.5.0) net-http (>= 0.5.0)
fast_blank (1.0.1) fast_blank (1.0.1)
fastimage (2.4.0) fastimage (2.4.0)
@@ -266,43 +266,42 @@ GEM
fog-openstack (1.1.5) fog-openstack (1.1.5)
fog-core (~> 2.1) fog-core (~> 2.1)
fog-json (>= 1.0) fog-json (>= 1.0)
formatador (1.2.1) formatador (1.1.0)
reline
forwardable (1.3.3) forwardable (1.3.3)
fugit (1.12.0) fugit (1.11.1)
et-orbi (~> 1.4) et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4) raabro (~> 1.4)
globalid (1.3.0) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
google-protobuf (4.32.1) google-protobuf (4.31.0)
bigdecimal bigdecimal
rake (>= 13) rake (>= 13)
googleapis-common-protos-types (1.22.0) googleapis-common-protos-types (1.20.0)
google-protobuf (~> 4.26) google-protobuf (>= 3.18, < 5.a)
haml (6.3.0) haml (6.3.0)
temple (>= 0.8.2) temple (>= 0.8.2)
thor thor
tilt tilt
haml-rails (3.0.0) haml-rails (2.1.0)
actionpack (>= 5.1) actionpack (>= 5.1)
activesupport (>= 5.1) activesupport (>= 5.1)
haml (>= 4.0.6) haml (>= 4.0.6)
railties (>= 5.1) railties (>= 5.1)
haml_lint (0.66.0) haml_lint (0.64.0)
haml (>= 5.0) haml (>= 5.0)
parallel (~> 1.10) parallel (~> 1.10)
rainbow rainbow
rubocop (>= 1.0) rubocop (>= 1.0)
sysexits (~> 1.1) sysexits (~> 1.1)
hashdiff (1.2.1) hashdiff (1.1.2)
hashie (5.0.0) hashie (5.0.0)
hcaptcha (7.1.0) hcaptcha (7.1.0)
json json
highline (3.1.2) highline (3.1.2)
reline reline
hiredis (0.6.3) hiredis (0.6.3)
hiredis-client (0.26.1) hiredis-client (0.24.0)
redis-client (= 0.26.1) redis-client (= 0.24.0)
hkdf (0.3.0) hkdf (0.3.0)
htmlentities (4.3.4) htmlentities (4.3.4)
http (5.3.1) http (5.3.1)
@@ -310,13 +309,13 @@ GEM
http-cookie (~> 1.0) http-cookie (~> 1.0)
http-form_data (~> 2.2) http-form_data (~> 2.2)
llhttp-ffi (~> 0.5.0) llhttp-ffi (~> 0.5.0)
http-cookie (1.1.0) http-cookie (1.0.8)
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (2.3.0) http-form_data (2.3.0)
http_accept_language (2.1.1) http_accept_language (2.1.1)
httpclient (2.9.0) httpclient (2.9.0)
mutex_m mutex_m
httplog (1.7.3) httplog (1.7.0)
rack (>= 2.0) rack (>= 2.0)
rainbow (>= 2.0.0) rainbow (>= 2.0.0)
i18n (1.14.7) i18n (1.14.7)
@@ -336,7 +335,7 @@ GEM
inline_svg (1.10.0) inline_svg (1.10.0)
activesupport (>= 3.0) activesupport (>= 3.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
io-console (0.8.1) io-console (0.8.0)
irb (1.15.2) irb (1.15.2)
pp (>= 0.6.0) pp (>= 0.6.0)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
@@ -346,9 +345,9 @@ GEM
azure-blob (~> 0.5.2) azure-blob (~> 0.5.2)
hashie (~> 5.0) hashie (~> 5.0)
jmespath (1.6.2) jmespath (1.6.2)
json (2.15.1) json (2.12.2)
json-canonicalization (1.0.0) json-canonicalization (1.0.0)
json-jwt (1.17.0) json-jwt (1.16.7)
activesupport (>= 4.2) activesupport (>= 4.2)
aes_key_wrap aes_key_wrap
base64 base64
@@ -363,14 +362,14 @@ GEM
rack (>= 2.2, < 4) rack (>= 2.2, < 4)
rdf (~> 3.3) rdf (~> 3.3)
rexml (~> 3.2) rexml (~> 3.2)
json-ld-preloaded (3.3.2) json-ld-preloaded (3.3.1)
json-ld (~> 3.3) json-ld (~> 3.3)
rdf (~> 3.3) rdf (~> 3.3)
json-schema (6.0.0) json-schema (5.1.1)
addressable (~> 2.8) addressable (~> 2.8)
bigdecimal (~> 3.1) bigdecimal (~> 3.1)
jsonapi-renderer (0.2.2) jsonapi-renderer (0.2.2)
jwt (2.10.2) jwt (2.10.1)
base64 base64
kaminari (1.2.2) kaminari (1.2.2)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
@@ -404,7 +403,7 @@ GEM
rexml rexml
link_header (0.0.8) link_header (0.0.8)
lint_roller (1.1.0) lint_roller (1.1.0)
linzer (0.7.7) linzer (0.7.3)
cgi (~> 0.4.2) cgi (~> 0.4.2)
forwardable (~> 1.3, >= 1.3.3) forwardable (~> 1.3, >= 1.3.3)
logger (~> 1.7, >= 1.7.0) logger (~> 1.7, >= 1.7.0)
@@ -434,26 +433,24 @@ GEM
marcel (1.0.4) marcel (1.0.4)
mario-redis-lock (1.2.1) mario-redis-lock (1.2.1)
redis (>= 3.0.5) redis (>= 3.0.5)
matrix (0.4.3) matrix (0.4.2)
memory_profiler (1.1.0) memory_profiler (1.1.0)
mime-types (3.7.0) mime-types (3.7.0)
logger logger
mime-types-data (~> 3.2025, >= 3.2025.0507) mime-types-data (~> 3.2025, >= 3.2025.0507)
mime-types-data (3.2025.0924) mime-types-data (3.2025.0514)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.9) mini_portile2 (2.8.9)
minitest (5.25.5) minitest (5.25.5)
msgpack (1.8.0) msgpack (1.8.0)
multi_json (1.17.0) multi_json (1.15.0)
mutex_m (0.3.0) mutex_m (0.3.0)
net-http (0.6.0) net-http (0.6.0)
uri uri
net-imap (0.5.12) net-imap (0.5.8)
date date
net-protocol net-protocol
net-ldap (0.20.0) net-ldap (0.19.0)
base64
ostruct
net-pop (0.1.2) net-pop (0.1.2)
net-protocol net-protocol
net-protocol (0.2.2) net-protocol (0.2.2)
@@ -461,18 +458,17 @@ GEM
net-smtp (0.5.1) net-smtp (0.5.1)
net-protocol net-protocol
nio4r (2.7.4) nio4r (2.7.4)
nokogiri (1.18.10) nokogiri (1.18.9)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
oj (3.16.11) oj (3.16.11)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
ostruct (>= 0.2) ostruct (>= 0.2)
omniauth (2.1.4) omniauth (2.1.3)
hashie (>= 3.4.6) hashie (>= 3.4.6)
logger
rack (>= 2.2.3) rack (>= 2.2.3)
rack-protection rack-protection
omniauth-cas (3.0.2) omniauth-cas (3.0.1)
addressable (~> 2.8) addressable (~> 2.8)
nokogiri (~> 1.12) nokogiri (~> 1.12)
omniauth (~> 2.1) omniauth (~> 2.1)
@@ -498,101 +494,126 @@ GEM
tzinfo tzinfo
validate_url validate_url
webfinger (~> 2.0) webfinger (~> 2.0)
openssl (3.3.1) openssl (3.3.0)
openssl-signature_algorithm (1.3.0) openssl-signature_algorithm (1.3.0)
openssl (> 2.0) openssl (> 2.0)
opentelemetry-api (1.7.0) opentelemetry-api (1.5.0)
opentelemetry-common (0.23.0) opentelemetry-common (0.22.0)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-exporter-otlp (0.31.0) opentelemetry-exporter-otlp (0.30.0)
google-protobuf (>= 3.18) google-protobuf (>= 3.18)
googleapis-common-protos-types (~> 1.3) googleapis-common-protos-types (~> 1.3)
opentelemetry-api (~> 1.1) opentelemetry-api (~> 1.1)
opentelemetry-common (~> 0.20) opentelemetry-common (~> 0.20)
opentelemetry-sdk (~> 1.2) opentelemetry-sdk (~> 1.2)
opentelemetry-semantic_conventions opentelemetry-semantic_conventions
opentelemetry-helpers-sql (0.2.0) opentelemetry-helpers-sql (0.1.1)
opentelemetry-api (~> 1.7) opentelemetry-api (~> 1.0)
opentelemetry-helpers-sql-obfuscation (0.3.0) opentelemetry-helpers-sql-obfuscation (0.3.0)
opentelemetry-common (~> 0.21) opentelemetry-common (~> 0.21)
opentelemetry-instrumentation-action_mailer (0.5.0) opentelemetry-instrumentation-action_mailer (0.4.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.7) opentelemetry-instrumentation-active_support (~> 0.7)
opentelemetry-instrumentation-action_pack (0.14.1) opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-action_pack (0.12.1)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-rack (~> 0.21) opentelemetry-instrumentation-rack (~> 0.21)
opentelemetry-instrumentation-action_view (0.10.0) opentelemetry-instrumentation-action_view (0.9.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.7) opentelemetry-instrumentation-active_support (~> 0.7)
opentelemetry-instrumentation-active_job (0.9.2) opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-base (~> 0.24) opentelemetry-instrumentation-active_job (0.8.0)
opentelemetry-instrumentation-active_model_serializers (0.23.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-active_model_serializers (0.22.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (>= 0.7.0) opentelemetry-instrumentation-active_support (>= 0.7.0)
opentelemetry-instrumentation-active_record (0.10.1) opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-base (~> 0.24) opentelemetry-instrumentation-active_record (0.9.0)
opentelemetry-instrumentation-active_storage (0.2.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-active_storage (0.1.1)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.7) opentelemetry-instrumentation-active_support (~> 0.7)
opentelemetry-instrumentation-active_support (0.9.1) opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-base (~> 0.24) opentelemetry-instrumentation-active_support (0.8.0)
opentelemetry-instrumentation-base (0.24.0) opentelemetry-api (~> 1.0)
opentelemetry-api (~> 1.7) opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-base (0.23.0)
opentelemetry-api (~> 1.0)
opentelemetry-common (~> 0.21) opentelemetry-common (~> 0.21)
opentelemetry-registry (~> 0.1) opentelemetry-registry (~> 0.1)
opentelemetry-instrumentation-concurrent_ruby (0.23.1) opentelemetry-instrumentation-concurrent_ruby (0.22.0)
opentelemetry-instrumentation-base (~> 0.24) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-excon (0.25.2) opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-base (~> 0.24) opentelemetry-instrumentation-excon (0.23.0)
opentelemetry-instrumentation-faraday (0.29.1) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.24) opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-http (0.26.1) opentelemetry-instrumentation-faraday (0.27.0)
opentelemetry-instrumentation-base (~> 0.24) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-http_client (0.25.1) opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-base (~> 0.24) opentelemetry-instrumentation-http (0.25.0)
opentelemetry-instrumentation-net_http (0.25.1) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.24) opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-pg (0.31.1) opentelemetry-instrumentation-http_client (0.23.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-net_http (0.23.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-pg (0.30.1)
opentelemetry-api (~> 1.0)
opentelemetry-helpers-sql opentelemetry-helpers-sql
opentelemetry-helpers-sql-obfuscation opentelemetry-helpers-sql-obfuscation
opentelemetry-instrumentation-base (~> 0.24) opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-rack (0.28.2) opentelemetry-instrumentation-rack (0.26.0)
opentelemetry-instrumentation-base (~> 0.24) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-rails (0.38.0) opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-action_mailer (~> 0.4) opentelemetry-instrumentation-rails (0.36.0)
opentelemetry-instrumentation-action_pack (~> 0.13) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-action_view (~> 0.9) opentelemetry-instrumentation-action_mailer (~> 0.4.0)
opentelemetry-instrumentation-active_job (~> 0.8) opentelemetry-instrumentation-action_pack (~> 0.12.0)
opentelemetry-instrumentation-active_record (~> 0.9) opentelemetry-instrumentation-action_view (~> 0.9.0)
opentelemetry-instrumentation-active_storage (~> 0.1) opentelemetry-instrumentation-active_job (~> 0.8.0)
opentelemetry-instrumentation-active_support (~> 0.8) opentelemetry-instrumentation-active_record (~> 0.9.0)
opentelemetry-instrumentation-concurrent_ruby (~> 0.22) opentelemetry-instrumentation-active_storage (~> 0.1.0)
opentelemetry-instrumentation-redis (0.27.1) opentelemetry-instrumentation-active_support (~> 0.8.0)
opentelemetry-instrumentation-base (~> 0.24) opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-sidekiq (0.27.1) opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0)
opentelemetry-instrumentation-base (~> 0.24) opentelemetry-instrumentation-redis (0.26.1)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-sidekiq (0.26.1)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-registry (0.4.0) opentelemetry-registry (0.4.0)
opentelemetry-api (~> 1.1) opentelemetry-api (~> 1.1)
opentelemetry-sdk (1.10.0) opentelemetry-sdk (1.8.0)
opentelemetry-api (~> 1.1) opentelemetry-api (~> 1.1)
opentelemetry-common (~> 0.20) opentelemetry-common (~> 0.20)
opentelemetry-registry (~> 0.2) opentelemetry-registry (~> 0.2)
opentelemetry-semantic_conventions opentelemetry-semantic_conventions
opentelemetry-semantic_conventions (1.36.0) opentelemetry-semantic_conventions (1.11.0)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
ostruct (0.6.3) ostruct (0.6.1)
ox (2.14.23) ox (2.14.23)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
parallel (1.27.0) parallel (1.27.0)
parser (3.3.9.0) parser (3.3.8.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
parslet (2.0.0) parslet (2.0.0)
pastel (0.8.0) pastel (0.8.0)
tty-color (~> 0.5) tty-color (~> 0.5)
pg (1.6.2) pg (1.5.9)
pghero (3.7.0) pghero (3.7.0)
activerecord (>= 7.1) activerecord (>= 7.1)
playwright-ruby-client (1.55.0) playwright-ruby-client (1.52.0)
concurrent-ruby (>= 1.1.6) concurrent-ruby (>= 1.1.6)
mime-types (>= 3.0) mime-types (>= 3.0)
pp (0.6.3) pp (0.6.2)
prettyprint prettyprint
premailer (1.27.0) premailer (1.27.0)
addressable addressable
@@ -603,24 +624,25 @@ GEM
net-smtp net-smtp
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
prettyprint (0.2.0) prettyprint (0.2.0)
prism (1.5.1) prism (1.4.0)
prometheus_exporter (2.3.0) prometheus_exporter (2.2.0)
webrick webrick
propshaft (1.3.1) propshaft (1.1.0)
actionpack (>= 7.0.0) actionpack (>= 7.0.0)
activesupport (>= 7.0.0) activesupport (>= 7.0.0)
rack rack
railties (>= 7.0.0)
psych (5.2.6) psych (5.2.6)
date date
stringio stringio
public_suffix (6.0.2) public_suffix (6.0.2)
puma (7.0.4) puma (6.6.0)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.5.2) pundit (2.5.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.8.1) racc (1.8.1)
rack (3.2.3) rack (3.1.16)
rack-attack (6.7.0) rack-attack (6.7.0)
rack (>= 1.0, < 4) rack (>= 1.0, < 4)
rack-cors (3.0.0) rack-cors (3.0.0)
@@ -646,81 +668,76 @@ GEM
rack (>= 1.3) rack (>= 1.3)
rackup (2.2.1) rackup (2.2.1)
rack (>= 3) rack (>= 3)
rails (8.0.3) rails (8.0.2)
actioncable (= 8.0.3) actioncable (= 8.0.2)
actionmailbox (= 8.0.3) actionmailbox (= 8.0.2)
actionmailer (= 8.0.3) actionmailer (= 8.0.2)
actionpack (= 8.0.3) actionpack (= 8.0.2)
actiontext (= 8.0.3) actiontext (= 8.0.2)
actionview (= 8.0.3) actionview (= 8.0.2)
activejob (= 8.0.3) activejob (= 8.0.2)
activemodel (= 8.0.3) activemodel (= 8.0.2)
activerecord (= 8.0.3) activerecord (= 8.0.2)
activestorage (= 8.0.3) activestorage (= 8.0.2)
activesupport (= 8.0.3) activesupport (= 8.0.2)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 8.0.3) railties (= 8.0.2)
rails-dom-testing (2.3.0) rails-dom-testing (2.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.6.2) rails-html-sanitizer (1.6.2)
loofah (~> 2.21) loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails-i18n (8.0.2) rails-i18n (8.0.1)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9) railties (>= 8.0.0, < 9)
railties (8.0.3) railties (8.0.2)
actionpack (= 8.0.3) actionpack (= 8.0.2)
activesupport (= 8.0.3) activesupport (= 8.0.2)
irb (~> 1.13) irb (~> 1.13)
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0, >= 1.2.2) thor (~> 1.0, >= 1.2.2)
tsort (>= 0.2)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.3.0) rake (13.3.0)
rdf (3.3.4) rdf (3.3.2)
bcp47_spec (~> 0.2) bcp47_spec (~> 0.2)
bigdecimal (~> 3.1, >= 3.1.5) bigdecimal (~> 3.1, >= 3.1.5)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
logger (~> 1.5)
ostruct (~> 0.6)
readline (~> 0.0)
rdf-normalize (0.7.0) rdf-normalize (0.7.0)
rdf (~> 3.3) rdf (~> 3.3)
rdoc (6.15.0) rdoc (6.14.1)
erb erb
psych (>= 4.0.0) psych (>= 4.0.0)
tsort
readline (0.0.4)
reline
redcarpet (3.6.1) redcarpet (3.6.1)
redis (4.8.1) redis (4.8.1)
redis-client (0.26.1) redis-client (0.24.0)
connection_pool connection_pool
regexp_parser (2.11.3) redlock (1.3.2)
reline (0.6.2) redis (>= 3.0.0, < 6.0)
regexp_parser (2.10.0)
reline (0.6.1)
io-console (~> 0.5) io-console (~> 0.5)
request_store (1.7.0) request_store (1.7.0)
rack (>= 1.4) rack (>= 1.4)
responders (3.1.1) responders (3.1.1)
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
rexml (3.4.4) rexml (3.4.1)
rotp (6.3.0) rotp (6.3.0)
rouge (4.6.1) rouge (4.5.2)
rpam2 (4.0.2) rpam2 (4.0.2)
rqrcode (3.1.0) rqrcode (3.1.0)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode_core (~> 2.0) rqrcode_core (~> 2.0)
rqrcode_core (2.0.0) rqrcode_core (2.0.0)
rspec (3.13.1) rspec (3.13.0)
rspec-core (~> 3.13.0) rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0) rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0) rspec-mocks (~> 3.13.0)
rspec-core (3.13.5) rspec-core (3.13.4)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-expectations (3.13.5) rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
@@ -730,7 +747,7 @@ GEM
rspec-mocks (3.13.5) rspec-mocks (3.13.5)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-rails (8.0.2) rspec-rails (8.0.1)
actionpack (>= 7.2) actionpack (>= 7.2)
activesupport (>= 7.2) activesupport (>= 7.2)
railties (>= 7.2) railties (>= 7.2)
@@ -738,13 +755,13 @@ GEM
rspec-expectations (~> 3.13) rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13) rspec-mocks (~> 3.13)
rspec-support (~> 3.13) rspec-support (~> 3.13)
rspec-sidekiq (5.2.0) rspec-sidekiq (5.1.0)
rspec-core (~> 3.0) rspec-core (~> 3.0)
rspec-expectations (~> 3.0) rspec-expectations (~> 3.0)
rspec-mocks (~> 3.0) rspec-mocks (~> 3.0)
sidekiq (>= 5, < 9) sidekiq (>= 5, < 9)
rspec-support (3.13.6) rspec-support (3.13.4)
rubocop (1.81.1) rubocop (1.77.0)
json (~> 2.3) json (~> 2.3)
language_server-protocol (~> 3.17.0.2) language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0) lint_roller (~> 1.1.0)
@@ -752,10 +769,10 @@ GEM
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0) regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.47.1, < 2.0) rubocop-ast (>= 1.45.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0) unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.47.1) rubocop-ast (1.45.1)
parser (>= 3.3.7.2) parser (>= 3.3.7.2)
prism (~> 1.4) prism (~> 1.4)
rubocop-capybara (2.22.1) rubocop-capybara (2.22.1)
@@ -764,17 +781,17 @@ GEM
rubocop-i18n (3.2.3) rubocop-i18n (3.2.3)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rubocop (>= 1.72.1) rubocop (>= 1.72.1)
rubocop-performance (1.26.0) rubocop-performance (1.25.0)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0) rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.44.0, < 2.0) rubocop-ast (>= 1.38.0, < 2.0)
rubocop-rails (2.33.4) rubocop-rails (2.32.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.75.0, < 2.0) rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.44.0, < 2.0) rubocop-ast (>= 1.44.0, < 2.0)
rubocop-rspec (3.7.0) rubocop-rspec (3.6.0)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1) rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec_rails (2.31.0) rubocop-rspec_rails (2.31.0)
@@ -784,37 +801,38 @@ GEM
ruby-prof (1.7.2) ruby-prof (1.7.2)
base64 base64
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-saml (1.18.1) ruby-saml (1.18.0)
nokogiri (>= 1.13.10) nokogiri (>= 1.13.10)
rexml rexml
ruby-vips (2.2.5) ruby-vips (2.2.4)
ffi (~> 1.12) ffi (~> 1.12)
logger logger
rubyzip (3.1.1) rubyzip (2.4.1)
rufus-scheduler (3.9.2) rufus-scheduler (3.9.2)
fugit (~> 1.1, >= 1.11.1) fugit (~> 1.1, >= 1.11.1)
safety_net_attestation (0.5.0) safety_net_attestation (0.4.0)
jwt (>= 2.0, < 4.0) jwt (~> 2.0)
sanitize (7.0.0) sanitize (7.0.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.16.8) nokogiri (>= 1.16.8)
scenic (1.9.0) scenic (1.8.0)
activerecord (>= 4.0.0) activerecord (>= 4.0.0)
railties (>= 4.0.0) railties (>= 4.0.0)
securerandom (0.4.1) securerandom (0.4.1)
shoulda-matchers (6.5.0) shoulda-matchers (6.5.0)
activesupport (>= 5.2.0) activesupport (>= 5.2.0)
sidekiq (8.0.8) sidekiq (7.3.9)
connection_pool (>= 2.5.0) base64
json (>= 2.9.0) connection_pool (>= 2.3.0)
logger (>= 1.6.2) logger
rack (>= 3.1.0) rack (>= 2.2.4)
redis-client (>= 0.23.2) redis-client (>= 0.22.2)
sidekiq-bulk (0.2.0) sidekiq-bulk (0.2.0)
sidekiq sidekiq
sidekiq-scheduler (6.0.1) sidekiq-scheduler (5.0.6)
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 7.3, < 9) sidekiq (>= 6, < 8)
tilt (>= 1.4.0, < 3)
sidekiq-unique-jobs (8.0.11) sidekiq-unique-jobs (8.0.11)
concurrent-ruby (~> 1.0, >= 1.0.5) concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 7.0.0, < 9.0.0) sidekiq (>= 7.0.0, < 9.0.0)
@@ -828,16 +846,16 @@ GEM
docile (~> 1.1) docile (~> 1.1)
simplecov-html (~> 0.11) simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1) simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.2) simplecov-html (0.13.1)
simplecov-lcov (0.9.0) simplecov-lcov (0.8.0)
simplecov_json_formatter (0.1.4) simplecov_json_formatter (0.1.4)
stackprof (0.2.27) stackprof (0.2.27)
starry (0.2.0) starry (0.2.0)
base64 base64
stoplight (5.3.8) stoplight (4.1.1)
zeitwerk redlock (~> 1.0)
stringio (3.1.7) stringio (3.1.7)
strong_migrations (2.5.1) strong_migrations (2.4.0)
activerecord (>= 7.1) activerecord (>= 7.1)
swd (2.0.3) swd (2.0.3)
activesupport (>= 3) activesupport (>= 3)
@@ -845,20 +863,19 @@ GEM
faraday (~> 2.0) faraday (~> 2.0)
faraday-follow_redirects faraday-follow_redirects
sysexits (1.2.0) sysexits (1.2.0)
temple (0.10.4) temple (0.10.3)
terminal-table (4.0.0) terminal-table (4.0.0)
unicode-display_width (>= 1.1.1, < 4) unicode-display_width (>= 1.1.1, < 4)
terrapin (1.1.1) terrapin (1.1.0)
climate_control climate_control
test-prof (1.4.4) test-prof (1.4.4)
thor (1.4.0) thor (1.4.0)
tilt (2.6.1) tilt (2.6.0)
timeout (0.4.3) timeout (0.4.3)
tpm-key_attestation (0.14.1) tpm-key_attestation (0.14.1)
bindata (~> 2.4) bindata (~> 2.4)
openssl (> 2.0) openssl (> 2.0)
openssl-signature_algorithm (~> 1.0) openssl-signature_algorithm (~> 1.0)
tsort (0.2.0)
tty-color (0.6.0) tty-color (0.6.0)
tty-cursor (0.7.1) tty-cursor (0.7.1)
tty-prompt (0.23.1) tty-prompt (0.23.1)
@@ -879,10 +896,10 @@ GEM
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.9.1) unf_ext (0.0.9.1)
unicode-display_width (3.2.0) unicode-display_width (3.1.4)
unicode-emoji (~> 4.1) unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.1.0) unicode-emoji (4.0.4)
uri (1.0.4) uri (1.0.3)
useragent (0.16.11) useragent (0.16.11)
validate_url (1.0.15) validate_url (1.0.15)
activemodel (>= 3.0.0) activemodel (>= 3.0.0)
@@ -898,13 +915,13 @@ GEM
zeitwerk (~> 2.2) zeitwerk (~> 2.2)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
webauthn (3.4.2) webauthn (3.4.1)
android_key_attestation (~> 0.3.0) android_key_attestation (~> 0.3.0)
bindata (~> 2.4) bindata (~> 2.4)
cbor (~> 0.5.9) cbor (~> 0.5.9)
cose (~> 1.1) cose (~> 1.1)
openssl (>= 2.2) openssl (>= 2.2)
safety_net_attestation (~> 0.5.0) safety_net_attestation (~> 0.4.0)
tpm-key_attestation (~> 0.14.0) tpm-key_attestation (~> 0.14.0)
webfinger (2.1.3) webfinger (2.1.3)
activesupport activesupport
@@ -915,7 +932,7 @@ GEM
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.1) webrick (1.9.1)
websocket-driver (0.8.0) websocket-driver (0.7.7)
base64 base64
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
@@ -968,7 +985,7 @@ DEPENDENCIES
flatware-rspec flatware-rspec
fog-core (<= 2.6.0) fog-core (<= 2.6.0)
fog-openstack (~> 1.0) fog-openstack (~> 1.0)
haml-rails (~> 3.0) haml-rails (~> 2.0)
haml_lint haml_lint
hcaptcha (~> 7.1) hcaptcha (~> 7.1)
hiredis (~> 0.6) hiredis (~> 0.6)
@@ -985,13 +1002,13 @@ DEPENDENCIES
jd-paperclip-azure (~> 3.0) jd-paperclip-azure (~> 3.0)
json-ld json-ld
json-ld-preloaded (~> 3.2) json-ld-preloaded (~> 3.2)
json-schema (~> 6.0) json-schema (~> 5.0)
kaminari (~> 1.2) kaminari (~> 1.2)
kt-paperclip (~> 7.2) kt-paperclip (~> 7.2)
letter_opener (~> 1.8) letter_opener (~> 1.8)
letter_opener_web (~> 3.0) letter_opener_web (~> 3.0)
link_header (~> 0.0) link_header (~> 0.0)
linzer (~> 0.7.7) linzer (~> 0.7.2)
lograge (~> 0.12) lograge (~> 0.12)
mail (~> 2.8) mail (~> 2.8)
mario-redis-lock (~> 1.2) mario-redis-lock (~> 1.2)
@@ -1007,32 +1024,31 @@ DEPENDENCIES
omniauth-rails_csrf_protection (~> 1.0) omniauth-rails_csrf_protection (~> 1.0)
omniauth-saml (~> 2.0) omniauth-saml (~> 2.0)
omniauth_openid_connect (~> 0.8.0) omniauth_openid_connect (~> 0.8.0)
opentelemetry-api (~> 1.7.0) opentelemetry-api (~> 1.5.0)
opentelemetry-exporter-otlp (~> 0.31.0) opentelemetry-exporter-otlp (~> 0.30.0)
opentelemetry-instrumentation-active_job (~> 0.9.0) opentelemetry-instrumentation-active_job (~> 0.8.0)
opentelemetry-instrumentation-active_model_serializers (~> 0.23.0) opentelemetry-instrumentation-active_model_serializers (~> 0.22.0)
opentelemetry-instrumentation-concurrent_ruby (~> 0.23.0) opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0)
opentelemetry-instrumentation-excon (~> 0.25.0) opentelemetry-instrumentation-excon (~> 0.23.0)
opentelemetry-instrumentation-faraday (~> 0.29.0) opentelemetry-instrumentation-faraday (~> 0.27.0)
opentelemetry-instrumentation-http (~> 0.26.0) opentelemetry-instrumentation-http (~> 0.25.0)
opentelemetry-instrumentation-http_client (~> 0.25.0) opentelemetry-instrumentation-http_client (~> 0.23.0)
opentelemetry-instrumentation-net_http (~> 0.25.0) opentelemetry-instrumentation-net_http (~> 0.23.0)
opentelemetry-instrumentation-pg (~> 0.31.0) opentelemetry-instrumentation-pg (~> 0.30.0)
opentelemetry-instrumentation-rack (~> 0.28.0) opentelemetry-instrumentation-rack (~> 0.26.0)
opentelemetry-instrumentation-rails (~> 0.38.0) opentelemetry-instrumentation-rails (~> 0.36.0)
opentelemetry-instrumentation-redis (~> 0.27.0) opentelemetry-instrumentation-redis (~> 0.26.0)
opentelemetry-instrumentation-sidekiq (~> 0.27.0) opentelemetry-instrumentation-sidekiq (~> 0.26.0)
opentelemetry-sdk (~> 1.4) opentelemetry-sdk (~> 1.4)
ox (~> 2.14) ox (~> 2.14)
parslet parslet
pg (~> 1.5) pg (~> 1.5)
pghero pghero
playwright-ruby-client (= 1.55.0)
premailer-rails premailer-rails
prometheus_exporter (~> 2.2) prometheus_exporter (~> 2.2)
propshaft propshaft
public_suffix (~> 6.0) public_suffix (~> 6.0)
puma (~> 7.0) puma (~> 6.3)
pundit (~> 2.3) pundit (~> 2.3)
rack-attack (~> 6.6) rack-attack (~> 6.6)
rack-cors rack-cors
@@ -1056,20 +1072,20 @@ DEPENDENCIES
ruby-prof ruby-prof
ruby-progressbar (~> 1.13) ruby-progressbar (~> 1.13)
ruby-vips (~> 2.2) ruby-vips (~> 2.2)
rubyzip (~> 3.0) rubyzip (~> 2.3)
sanitize (~> 7.0) sanitize (~> 7.0)
scenic (~> 1.7) scenic (~> 1.7)
shoulda-matchers shoulda-matchers
sidekiq (< 9) sidekiq (< 8)
sidekiq-bulk (~> 0.2.0) sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 6.0) sidekiq-scheduler (~> 5.0)
sidekiq-unique-jobs (> 8) sidekiq-unique-jobs (> 8)
simple-navigation (~> 4.4) simple-navigation (~> 4.4)
simple_form (~> 5.2) simple_form (~> 5.2)
simplecov (~> 0.22) simplecov (~> 0.22)
simplecov-lcov (~> 0.8) simplecov-lcov (~> 0.8)
stackprof stackprof
stoplight stoplight (~> 4.1)
strong_migrations strong_migrations
test-prof test-prof
thor (~> 1.2) thor (~> 1.2)
@@ -1080,11 +1096,10 @@ DEPENDENCIES
webauthn (~> 3.0) webauthn (~> 3.0)
webmock (~> 3.18) webmock (~> 3.18)
webpush! webpush!
websocket-driver (~> 0.8)
xorcist (~> 1.1) xorcist (~> 1.1)
RUBY VERSION RUBY VERSION
ruby 3.4.1p0 ruby 3.4.1p0
BUNDLED WITH BUNDLED WITH
2.7.2 2.6.9

View File

@@ -33,71 +33,71 @@ Mastodon Glitch Edition is a fork of [Mastodon](https://github.com/mastodon/mast
<img src="https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg" alt="Crowdin" /></a> <img src="https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg" alt="Crowdin" /></a>
</p> </p>
Mastodon is a **free, open-source social network server** based on [ActivityPub](https://www.w3.org/TR/activitypub/) where users can follow friends and discover new ones. On Mastodon, users can publish anything they want: links, pictures, text, and video. All Mastodon servers are interoperable as a federated network (users on one server can seamlessly communicate with users from another one, including non-Mastodon software that implements ActivityPub!) Mastodon is a **free, open-source social network server** based on ActivityPub where users can follow friends and discover new ones. On Mastodon, users can publish anything they want: links, pictures, text, and video. All Mastodon servers are interoperable as a federated network (users on one server can seamlessly communicate with users from another one, including non-Mastodon software that implements ActivityPub!)
## Navigation ## Navigation
- [Project homepage 🐘](https://joinmastodon.org) - [Project homepage 🐘](https://joinmastodon.org)
- [Donate to support development 🎁](https://joinmastodon.org/sponsors#donate) - [Support the development via Patreon][patreon]
- [View sponsors](https://joinmastodon.org/sponsors) - [View sponsors](https://joinmastodon.org/sponsors)
- [Blog 📰](https://blog.joinmastodon.org) - [Blog](https://blog.joinmastodon.org)
- [Documentation 📚](https://docs.joinmastodon.org) - [Documentation](https://docs.joinmastodon.org)
- [Official container image 🚢](https://github.com/mastodon/mastodon/pkgs/container/mastodon) - [Roadmap](https://joinmastodon.org/roadmap)
- [Official Docker image](https://github.com/mastodon/mastodon/pkgs/container/mastodon)
- [Browse Mastodon servers](https://joinmastodon.org/communities)
- [Browse Mastodon apps](https://joinmastodon.org/apps)
[patreon]: https://www.patreon.com/mastodon
## Features ## Features
<img src="./app/javascript/images/elephant_ui_working.svg?raw=true" align="right" width="30%" /> <img src="/app/javascript/images/elephant_ui_working.svg?raw=true" align="right" width="30%" />
**Part of the Fediverse. Based on open standards, with no vendor lock-in.** - the network goes beyond just Mastodon; anything that implements ActivityPub is part of a broader social network known as [the Fediverse](https://jointhefediverse.net/). You can follow and interact with users on other servers (including those running different software), and they can follow you back. **No vendor lock-in: Fully interoperable with any conforming platform** - It doesn't have to be Mastodon; whatever implements ActivityPub is part of the social network! [Learn more](https://blog.joinmastodon.org/2018/06/why-activitypub-is-the-future/)
**Real-time, chronological timeline updates** - updates of people you're following appear in real-time in the UI. **Real-time, chronological timeline updates** - updates of people you're following appear in real-time in the UI via WebSockets. There's a firehose view as well!
**Media attachments** - upload and view images and videos attached to the updates. Videos with no audio track are treated like animated GIFs; normal videos loop continuously. **Media attachments like images and short videos** - upload and view images and WebM/MP4 videos attached to the updates. Videos with no audio track are treated like GIFs; normal videos loop continuously!
**Safety and moderation tools** - Mastodon includes private posts, locked accounts, phrase filtering, muting, blocking, and many other features, along with a reporting and moderation system. **Safety and moderation tools** - Mastodon includes private posts, locked accounts, phrase filtering, muting, blocking, and all sorts of other features, along with a reporting and moderation system. [Learn more](https://blog.joinmastodon.org/2018/07/cage-the-mastodon/)
**OAuth2 and a straightforward REST API** - Mastodon acts as an OAuth2 provider, and third party apps can use the REST and Streaming APIs. This results in a [rich app ecosystem](https://joinmastodon.org/apps) with a variety of choices! **OAuth2 and a straightforward REST API** - Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Streaming APIs. This results in a rich app ecosystem with a lot of choices!
## Deployment ## Deployment
### Tech stack ### Tech stack
- [Ruby on Rails](https://github.com/rails/rails) powers the REST API and other web pages. - **Ruby on Rails** powers the REST API and other web pages
- [PostgreSQL](https://www.postgresql.org/) is the main database. - **React.js** and **Redux** are used for the dynamic parts of the interface
- [Redis](https://redis.io/) and [Sidekiq](https://sidekiq.org/) are used for caching and queueing. - **Node.js** powers the streaming API
- [Node.js](https://nodejs.org/) powers the streaming API.
- [React.js](https://reactjs.org/) and [Redux](https://redux.js.org/) are used for the dynamic parts of the interface.
- [BrowserStack](https://www.browserstack.com/) supports testing on real devices and browsers. (This project is tested with BrowserStack)
- [Chromatic](https://www.chromatic.com/) provides visual regression testing. (This project is tested with Chromatic)
### Requirements ### Requirements
- **Ruby** 3.2+
- **PostgreSQL** 13+ - **PostgreSQL** 13+
- **Redis** 7.0+ - **Redis** 6.2+
- **Ruby** 3.2+
- **Node.js** 20+ - **Node.js** 20+
This repository includes deployment configurations for **Docker and docker-compose**, as well as for other environments like Heroku and Scalingo. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). A [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the main documentation. The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, and **Scalingo**. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation.
## Contributing ## Contributing
Mastodon is **free, open-source software** licensed under **AGPLv3**. We welcome contributions and help from anyone who wants to improve the project. Mastodon is **free, open-source software** licensed under **AGPLv3**.
You should read the overall [CONTRIBUTING](https://github.com/mastodon/.github/blob/main/CONTRIBUTING.md) guide, which covers our development processes. You can open issues for bugs you've found or features you think are missing. You
can also submit pull requests to this repository or translations via Crowdin. To
get started, look at the [CONTRIBUTING] and [DEVELOPMENT] guides. For changes
accepted into Mastodon, you can request to be paid through our [OpenCollective].
You should also read and understand the [CODE OF CONDUCT](https://github.com/mastodon/.github/blob/main/CODE_OF_CONDUCT.md) that enables us to maintain a welcoming and inclusive community. Collaboration begins with mutual respect and understanding. **IRC channel**: #mastodon on [`irc.libera.chat`](https://libera.chat)
You can learn about setting up a development environment in the [DEVELOPMENT](docs/DEVELOPMENT.md) documentation. ## License
If you would like to help with translations 🌐 you can do so on [Crowdin](https://crowdin.com/project/mastodon).
## LICENSE
Copyright (c) 2016-2025 Eugen Rochko (+ [`mastodon authors`](AUTHORS.md)) Copyright (c) 2016-2025 Eugen Rochko (+ [`mastodon authors`](AUTHORS.md))
Licensed under GNU Affero General Public License as stated in the [LICENSE](LICENSE): Licensed under GNU Affero General Public License as stated in the [LICENSE](LICENSE):
```text ```
Copyright (c) 2016-2025 Eugen Rochko & other Mastodon contributors Copyright (c) 2016-2025 Eugen Rochko & other Mastodon contributors
This program is free software: you can redistribute it and/or modify it under This program is free software: you can redistribute it and/or modify it under
@@ -113,3 +113,7 @@ details.
You should have received a copy of the GNU Affero General Public License along You should have received a copy of the GNU Affero General Public License along
with this program. If not, see https://www.gnu.org/licenses/ with this program. If not, see https://www.gnu.org/licenses/
``` ```
[CONTRIBUTING]: CONTRIBUTING.md
[DEVELOPMENT]: docs/DEVELOPMENT.md
[OpenCollective]: https://opencollective.com/mastodon

3
Vagrantfile vendored
View File

@@ -54,7 +54,6 @@ sudo apt-get install \
pkg-config \ pkg-config \
protobuf-compiler \ protobuf-compiler \
zlib1g-dev \ zlib1g-dev \
libvips42t64 \
-y -y
# Install rvm # Install rvm
@@ -135,7 +134,7 @@ VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "bento/ubuntu-24.04" config.vm.box = "ubuntu/focal64"
config.vm.provider :virtualbox do |vb| config.vm.provider :virtualbox do |vb|
vb.name = "mastodon" vb.name = "mastodon"

View File

@@ -71,10 +71,6 @@ class AccountsController < ApplicationController
params[:username] params[:username]
end end
def account_id_param
params[:id]
end
def skip_temporary_suspension_response? def skip_temporary_suspension_response?
request.format == :json request.format == :json
end end

View File

@@ -1,82 +0,0 @@
# frozen_string_literal: true
class ActivityPub::ContextsController < ActivityPub::BaseController
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_conversation
before_action :set_items
DESCENDANTS_LIMIT = 60
def show
expires_in 3.minutes, public: public_fetch_mode?
render_with_cache json: context_presenter, serializer: ActivityPub::ContextSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
def items
expires_in 3.minutes, public: public_fetch_mode?
render_with_cache json: items_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
private
def account_required?
false
end
def set_conversation
account_id, status_id = params[:id].split('-')
@conversation = Conversation.local.find_by(parent_account_id: account_id, parent_status_id: status_id)
end
def set_items
@items = @conversation.statuses.distributable_visibility.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
end
def context_presenter
first_page = ActivityPub::CollectionPresenter.new(
id: items_context_url(@conversation, page_params),
type: :unordered,
part_of: items_context_url(@conversation),
next: next_page,
items: @items.map { |status| status.local? ? ActivityPub::TagManager.instance.uri_for(status) : status.uri }
)
ActivityPub::ContextPresenter.from_conversation(@conversation).tap do |presenter|
presenter.first = first_page
end
end
def items_collection_presenter
page = ActivityPub::CollectionPresenter.new(
id: items_context_url(@conversation, page_params),
type: :unordered,
part_of: items_context_url(@conversation),
next: next_page,
items: @items.map { |status| status.local? ? ActivityPub::TagManager.instance.uri_for(status) : status.uri }
)
return page if page_requested?
ActivityPub::CollectionPresenter.new(
id: items_context_url(@conversation),
type: :unordered,
first: page
)
end
def page_requested?
truthy_param?(:page)
end
def next_page
return nil if @items.size < DESCENDANTS_LIMIT
items_context_url(@conversation, page: true, min_id: @items.last.id)
end
def page_params
params.permit(:page, :min_id)
end
end

View File

@@ -28,7 +28,7 @@ class ActivityPub::LikesController < ActivityPub::BaseController
def likes_collection_presenter def likes_collection_presenter
ActivityPub::CollectionPresenter.new( ActivityPub::CollectionPresenter.new(
id: ActivityPub::TagManager.instance.likes_uri_for(@status), id: account_status_likes_url(@account, @status),
type: :unordered, type: :unordered,
size: @status.favourites_count size: @status.favourites_count
) )

View File

@@ -73,8 +73,6 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
end end
def set_account def set_account
return super if params[:account_username].present? || params[:account_id].present? @account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative
@account = Account.representative
end end
end end

View File

@@ -1,30 +0,0 @@
# frozen_string_literal: true
class ActivityPub::QuoteAuthorizationsController < ActivityPub::BaseController
include Authorization
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_quote_authorization
def show
expires_in 30.seconds, public: true if @quote.status.distributable? && public_fetch_mode?
render json: @quote, serializer: ActivityPub::QuoteAuthorizationSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
private
def pundit_user
signed_request_account
end
def set_quote_authorization
@quote = Quote.accepted.where(quoted_account: @account).find(params[:id])
return not_found unless @quote.status.present? && @quote.quoted_status.present?
authorize @quote.status, :show?
rescue Mastodon::NotPermittedError
not_found
end
end

View File

@@ -37,7 +37,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
def replies_collection_presenter def replies_collection_presenter
page = ActivityPub::CollectionPresenter.new( page = ActivityPub::CollectionPresenter.new(
id: ActivityPub::TagManager.instance.replies_uri_for(@status, page_params), id: account_status_replies_url(@account, @status, page_params),
type: :unordered, type: :unordered,
part_of: account_status_replies_url(@account, @status), part_of: account_status_replies_url(@account, @status),
next: next_page, next: next_page,
@@ -47,7 +47,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
return page if page_requested? return page if page_requested?
ActivityPub::CollectionPresenter.new( ActivityPub::CollectionPresenter.new(
id: ActivityPub::TagManager.instance.replies_uri_for(@status), id: account_status_replies_url(@account, @status),
type: :unordered, type: :unordered,
first: page first: page
) )
@@ -66,7 +66,8 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
# Only consider remote accounts # Only consider remote accounts
return nil if @replies.size < DESCENDANTS_LIMIT return nil if @replies.size < DESCENDANTS_LIMIT
ActivityPub::TagManager.instance.replies_uri_for( account_status_replies_url(
@account,
@status, @status,
page: true, page: true,
min_id: @replies&.last&.id, min_id: @replies&.last&.id,
@@ -76,7 +77,8 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
# For now, we're serving only self-replies, but next page might be other accounts # For now, we're serving only self-replies, but next page might be other accounts
next_only_other_accounts = @replies&.last&.account_id != @account.id || @replies.size < DESCENDANTS_LIMIT next_only_other_accounts = @replies&.last&.account_id != @account.id || @replies.size < DESCENDANTS_LIMIT
ActivityPub::TagManager.instance.replies_uri_for( account_status_replies_url(
@account,
@status, @status,
page: true, page: true,
min_id: next_only_other_accounts ? nil : @replies&.last&.id, min_id: next_only_other_accounts ? nil : @replies&.last&.id,

View File

@@ -28,7 +28,7 @@ class ActivityPub::SharesController < ActivityPub::BaseController
def shares_collection_presenter def shares_collection_presenter
ActivityPub::CollectionPresenter.new( ActivityPub::CollectionPresenter.new(
id: ActivityPub::TagManager.instance.shares_uri_for(@status), id: account_status_shares_url(@account, @status),
type: :unordered, type: :unordered,
size: @status.reblogs_count size: @status.reblogs_count
) )

View File

@@ -16,14 +16,11 @@ module Admin
def batch def batch
authorize :account, :index? authorize :account, :index?
@form = Form::AccountBatch.new( @form = Form::AccountBatch.new(form_account_batch_params)
form_account_batch_params.merge( @form.current_account = current_account
action: action_from_button, @form.action = action_from_button
current_account:, @form.select_all_matching = params[:select_all_matching]
query: filtered_accounts, @form.query = filtered_accounts
select_all_matching: params[:select_all_matching]
)
)
@form.save @form.save
rescue ActionController::ParameterMissing rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected') flash[:alert] = I18n.t('admin.accounts.no_account_selected')

View File

@@ -6,7 +6,7 @@ module Admin
def index def index
authorize :audit_log, :index? authorize :audit_log, :index?
@auditable_accounts = Account.auditable.select(:id, :username).order(username: :asc) @auditable_accounts = Account.auditable.select(:id, :username)
end end
private private

View File

@@ -19,13 +19,15 @@ module Admin
log_action :resend, @user log_action :resend, @user
redirect_to admin_accounts_path, notice: t('admin.accounts.resend_confirmation.success') flash[:notice] = I18n.t('admin.accounts.resend_confirmation.success')
redirect_to admin_accounts_path
end end
private private
def redirect_confirmed_user def redirect_confirmed_user
redirect_to admin_accounts_path, flash: { error: t('admin.accounts.resend_confirmation.already_confirmed') } flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed')
redirect_to admin_accounts_path
end end
def user_confirmed? def user_confirmed?

View File

@@ -9,16 +9,10 @@ module Admin
@pending_appeals_count = Appeal.pending.async_count @pending_appeals_count = Appeal.pending.async_count
@pending_reports_count = Report.unresolved.async_count @pending_reports_count = Report.unresolved.async_count
@pending_tags_count = pending_tags.async_count @pending_tags_count = Tag.pending_review.async_count
@pending_users_count = User.pending.async_count @pending_users_count = User.pending.async_count
@system_checks = Admin::SystemCheck.perform(current_user) @system_checks = Admin::SystemCheck.perform(current_user)
@time_period = (29.days.ago.to_date...Time.now.utc.to_date) @time_period = (29.days.ago.to_date...Time.now.utc.to_date)
end end
private
def pending_tags
::Trends::TagFilter.new(status: :pending_review).results
end
end end
end end

View File

@@ -18,7 +18,7 @@ class Admin::Disputes::AppealsController < Admin::BaseController
end end
def reject def reject
authorize @appeal, :reject? authorize @appeal, :approve?
log_action :reject, @appeal log_action :reject, @appeal
@appeal.reject!(current_account) @appeal.reject!(current_account)
UserMailer.appeal_rejected(@appeal.account.user, @appeal).deliver_later UserMailer.appeal_rejected(@appeal.account.user, @appeal).deliver_later

View File

@@ -36,7 +36,7 @@ module Admin
end end
def edit def edit
authorize :domain_block, :update? authorize :domain_block, :create?
end end
def create def create
@@ -129,7 +129,7 @@ module Admin
end end
def requires_confirmation? def requires_confirmation?
@domain_block.valid? && (@domain_block.new_record? || @domain_block.severity_changed?) && @domain_block.suspend? && !params[:confirm] @domain_block.valid? && (@domain_block.new_record? || @domain_block.severity_changed?) && @domain_block.severity.to_s == 'suspend' && !params[:confirm]
end end
end end
end end

View File

@@ -49,8 +49,8 @@ module Admin
def export_data def export_data
CSV.generate(headers: export_headers, write_headers: true) do |content| CSV.generate(headers: export_headers, write_headers: true) do |content|
DomainAllow.allowed_domains.each do |domain| DomainAllow.allowed_domains.each do |instance|
content << [domain] content << [instance.domain]
end end
end end
end end

View File

@@ -13,9 +13,27 @@ class Admin::Reports::ActionsController < Admin::BaseController
case action_from_button case action_from_button
when 'delete', 'mark_as_sensitive' when 'delete', 'mark_as_sensitive'
Admin::StatusBatchAction.new(status_batch_action_params).save! status_batch_action = Admin::StatusBatchAction.new(
type: action_from_button,
status_ids: @report.status_ids,
current_account: current_account,
report_id: @report.id,
send_email_notification: !@report.spam?,
text: params[:text]
)
status_batch_action.save!
when 'silence', 'suspend' when 'silence', 'suspend'
Admin::AccountAction.new(account_action_params).save! account_action = Admin::AccountAction.new(
type: action_from_button,
report_id: @report.id,
target_account: @report.target_account,
current_account: current_account,
send_email_notification: !@report.spam?,
text: params[:text]
)
account_action.save!
else else
return redirect_to admin_report_path(@report), alert: I18n.t('admin.reports.unknown_action_msg', action: action_from_button) return redirect_to admin_report_path(@report), alert: I18n.t('admin.reports.unknown_action_msg', action: action_from_button)
end end
@@ -25,26 +43,6 @@ class Admin::Reports::ActionsController < Admin::BaseController
private private
def status_batch_action_params
shared_params
.merge(status_ids: @report.status_ids)
end
def account_action_params
shared_params
.merge(target_account: @report.target_account)
end
def shared_params
{
current_account: current_account,
report_id: @report.id,
send_email_notification: !@report.spam?,
text: params[:text],
type: action_from_button,
}
end
def set_report def set_report
@report = Report.find(params[:report_id]) @report = Report.find(params[:report_id])
end end

View File

@@ -14,7 +14,8 @@ module Admin
@admin_settings = Form::AdminSettings.new(settings_params) @admin_settings = Form::AdminSettings.new(settings_params)
if @admin_settings.save if @admin_settings.save
redirect_to after_update_redirect_path, notice: t('generic.changes_saved_msg') flash[:notice] = I18n.t('generic.changes_saved_msg')
redirect_to after_update_redirect_path
else else
render :show render :show
end end

View File

@@ -5,7 +5,6 @@ module Admin
before_action :set_tag, except: [:index] before_action :set_tag, except: [:index]
PER_PAGE = 20 PER_PAGE = 20
PERIOD_DAYS = 6.days
def index def index
authorize :tag, :index? authorize :tag, :index?
@@ -16,7 +15,7 @@ module Admin
def show def show
authorize @tag, :show? authorize @tag, :show?
@time_period = report_range @time_period = (6.days.ago.to_date...Time.now.utc.to_date)
end end
def update def update
@@ -25,7 +24,7 @@ module Admin
if @tag.update(tag_params.merge(reviewed_at: Time.now.utc)) if @tag.update(tag_params.merge(reviewed_at: Time.now.utc))
redirect_to admin_tag_path(@tag.id), notice: I18n.t('admin.tags.updated_msg') redirect_to admin_tag_path(@tag.id), notice: I18n.t('admin.tags.updated_msg')
else else
@time_period = report_range @time_period = (6.days.ago.to_date...Time.now.utc.to_date)
render :show render :show
end end
@@ -37,10 +36,6 @@ module Admin
@tag = Tag.find(params[:id]) @tag = Tag.find(params[:id])
end end
def report_range
(PERIOD_DAYS.ago.to_date...Time.now.utc.to_date)
end
def tag_params def tag_params
params params
.expect(tag: [:name, :display_name, :trendable, :usable, :listable]) .expect(tag: [:name, :display_name, :trendable, :usable, :listable])

View File

@@ -1,77 +0,0 @@
# frozen_string_literal: true
class Admin::UsernameBlocksController < Admin::BaseController
before_action :set_username_block, only: [:edit, :update]
def index
authorize :username_block, :index?
@username_blocks = UsernameBlock.order(username: :asc).page(params[:page])
@form = Form::UsernameBlockBatch.new
end
def batch
authorize :username_block, :index?
@form = Form::UsernameBlockBatch.new(form_username_block_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.username_blocks.no_username_block_selected')
rescue Mastodon::NotPermittedError
flash[:alert] = I18n.t('admin.username_blocks.not_permitted')
ensure
redirect_to admin_username_blocks_path
end
def new
authorize :username_block, :create?
@username_block = UsernameBlock.new(exact: true)
end
def edit
authorize @username_block, :update?
end
def create
authorize :username_block, :create?
@username_block = UsernameBlock.new(resource_params)
if @username_block.save
log_action :create, @username_block
redirect_to admin_username_blocks_path, notice: I18n.t('admin.username_blocks.created_msg')
else
render :new
end
end
def update
authorize @username_block, :update?
if @username_block.update(resource_params)
log_action :update, @username_block
redirect_to admin_username_blocks_path, notice: I18n.t('admin.username_blocks.updated_msg')
else
render :new
end
end
private
def set_username_block
@username_block = UsernameBlock.find(params[:id])
end
def form_username_block_batch_params
params
.expect(form_username_block_batch: [username_block_ids: []])
end
def resource_params
params
.expect(username_block: [:username, :comparison, :allow_with_approval])
end
def action_from_button
'delete' if params[:delete]
end
end

View File

@@ -48,7 +48,6 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
default_privacy: source_params.fetch(:privacy, @account.user.setting_default_privacy), default_privacy: source_params.fetch(:privacy, @account.user.setting_default_privacy),
default_sensitive: source_params.fetch(:sensitive, @account.user.setting_default_sensitive), default_sensitive: source_params.fetch(:sensitive, @account.user.setting_default_sensitive),
default_language: source_params.fetch(:language, @account.user.setting_default_language), default_language: source_params.fetch(:language, @account.user.setting_default_language),
default_quote_policy: source_params.fetch(:quote_policy, @account.user.setting_default_quote_policy),
}, },
} }
end end

View File

@@ -2,7 +2,6 @@
class Api::V1::Admin::TagsController < Api::BaseController class Api::V1::Admin::TagsController < Api::BaseController
include Authorization include Authorization
before_action -> { authorize_if_got_token! :'admin:read' }, only: [:index, :show] before_action -> { authorize_if_got_token! :'admin:read' }, only: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:write' }, only: :update before_action -> { authorize_if_got_token! :'admin:write' }, only: :update

View File

@@ -7,7 +7,6 @@ class Api::V1::InvitesController < Api::BaseController
skip_around_action :set_locale skip_around_action :set_locale
before_action :set_invite before_action :set_invite
before_action :check_valid_usage!
before_action :check_enabled_registrations! before_action :check_enabled_registrations!
# Override `current_user` to avoid reading session cookies # Override `current_user` to avoid reading session cookies
@@ -23,11 +22,9 @@ class Api::V1::InvitesController < Api::BaseController
@invite = Invite.find_by!(code: params[:invite_code]) @invite = Invite.find_by!(code: params[:invite_code])
end end
def check_valid_usage!
render json: { error: I18n.t('invites.invalid') }, status: 401 unless @invite.valid_for_use?
end
def check_enabled_registrations! def check_enabled_registrations!
return render json: { error: I18n.t('invites.invalid') }, status: 401 unless @invite.valid_for_use?
raise Mastodon::NotPermittedError unless allowed_registration?(request.remote_ip, @invite) raise Mastodon::NotPermittedError unless allowed_registration?(request.remote_ip, @invite)
end end
end end

View File

@@ -16,7 +16,16 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
def create def create
with_redis_lock("push_subscription:#{current_user.id}") do with_redis_lock("push_subscription:#{current_user.id}") do
destroy_web_push_subscriptions! destroy_web_push_subscriptions!
@push_subscription = Web::PushSubscription.create!(web_push_subscription_params)
@push_subscription = Web::PushSubscription.create!(
endpoint: subscription_params[:endpoint],
key_p256dh: subscription_params[:keys][:p256dh],
key_auth: subscription_params[:keys][:auth],
standard: subscription_params[:standard] || false,
data: data_params,
user_id: current_user.id,
access_token_id: doorkeeper_token.id
)
end end
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
@@ -46,18 +55,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
not_found if @push_subscription.nil? not_found if @push_subscription.nil?
end end
def web_push_subscription_params
{
access_token_id: doorkeeper_token.id,
data: data_params,
endpoint: subscription_params[:endpoint],
key_auth: subscription_params[:keys][:auth],
key_p256dh: subscription_params[:keys][:p256dh],
standard: subscription_params[:standard] || false,
user_id: current_user.id,
}
end
def subscription_params def subscription_params
params.expect(subscription: [:endpoint, :standard, keys: [:auth, :p256dh]]) params.expect(subscription: [:endpoint, :standard, keys: [:auth, :p256dh]])
end end

View File

@@ -1,28 +0,0 @@
# frozen_string_literal: true
class Api::V1::Statuses::InteractionPoliciesController < Api::V1::Statuses::BaseController
include Api::InteractionPoliciesConcern
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
def update
authorize @status, :update?
@status.update!(quote_approval_policy: quote_approval_policy)
broadcast_updates! if @status.quote_approval_policy_previously_changed?
render json: @status, serializer: REST::StatusSerializer
end
private
def status_params
params.permit(:quote_approval_policy)
end
def broadcast_updates!
DistributionWorker.perform_async(@status.id, { 'update' => true, 'skip_notifications' => true })
ActivityPub::StatusUpdateDistributionWorker.perform_async(@status.id, { 'updated_at' => Time.now.utc.iso8601 })
end
end

View File

@@ -1,74 +0,0 @@
# frozen_string_literal: true
class Api::V1::Statuses::QuotesController < Api::V1::Statuses::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: :revoke
before_action :set_statuses, only: :index
before_action :set_quote, only: :revoke
after_action :insert_pagination_headers, only: :index
def index
cache_if_unauthenticated!
render json: @statuses, each_serializer: REST::StatusSerializer
end
def revoke
authorize @quote, :revoke?
RevokeQuoteService.new.call(@quote)
render json: @quote.status, serializer: REST::StatusSerializer
end
private
def set_quote
@quote = @status.quotes.find_by!(status_id: params[:id])
end
def set_statuses
scope = default_statuses
scope = scope.not_excluded_by_account(current_account) unless current_account.nil?
@statuses = scope.merge(paginated_quotes).to_a
# Store next page info before filtering
@records_continue = @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
@pagination_since_id = @statuses.first.quote.id unless @statuses.empty?
@pagination_max_id = @statuses.last.quote.id if @records_continue
if current_account&.id != @status.account_id
domains = @statuses.filter_map(&:account_domain).uniq
account_ids = @statuses.map(&:account_id).uniq
relations = current_account&.relations_map(account_ids, domains) || {}
@statuses.reject! { |status| StatusFilter.new(status, current_account, relations).filtered? }
end
end
def default_statuses
Status.includes(:quote).references(:quote)
end
def paginated_quotes
@status.quotes.accepted.paginate_by_max_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
end
def next_path
api_v1_status_quotes_url pagination_params(max_id: pagination_max_id) if records_continue?
end
def prev_path
api_v1_status_quotes_url pagination_params(since_id: pagination_since_id) unless @statuses.empty?
end
attr_reader :pagination_max_id, :pagination_since_id
def records_continue?
@records_continue
end
end

View File

@@ -2,8 +2,6 @@
class Api::V1::StatusesController < Api::BaseController class Api::V1::StatusesController < Api::BaseController
include Authorization include Authorization
include AsyncRefreshesConcern
include Api::InteractionPoliciesConcern
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy] before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy] before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
@@ -11,7 +9,6 @@ class Api::V1::StatusesController < Api::BaseController
before_action :set_statuses, only: [:index] before_action :set_statuses, only: [:index]
before_action :set_status, only: [:show, :context] before_action :set_status, only: [:show, :context]
before_action :set_thread, only: [:create] before_action :set_thread, only: [:create]
before_action :set_quoted_status, only: [:create]
before_action :check_statuses_limit, only: [:index] before_action :check_statuses_limit, only: [:index]
override_rate_limit_headers :create, family: :statuses override_rate_limit_headers :create, family: :statuses
@@ -60,21 +57,9 @@ class Api::V1::StatusesController < Api::BaseController
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants) @context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
statuses = [@status] + @context.ancestors + @context.descendants statuses = [@status] + @context.ancestors + @context.descendants
refresh_key = "context:#{@status.id}:refresh"
async_refresh = AsyncRefresh.new(refresh_key)
if async_refresh.running?
add_async_refresh_header(async_refresh)
elsif !current_account.nil? && @status.should_fetch_replies?
add_async_refresh_header(AsyncRefresh.create(refresh_key))
WorkerBatch.new.within do |batch|
batch.connect(refresh_key, threshold: 1.0)
ActivityPub::FetchAllRepliesWorker.perform_async(@status.id, { 'batch_id' => batch.id })
end
end
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id) render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
ActivityPub::FetchAllRepliesWorker.perform_async(@status.id) if !current_account.nil? && @status.should_fetch_replies?
end end
def create def create
@@ -82,8 +67,6 @@ class Api::V1::StatusesController < Api::BaseController
current_user.account, current_user.account,
text: status_params[:status], text: status_params[:status],
thread: @thread, thread: @thread,
quoted_status: @quoted_status,
quote_approval_policy: quote_approval_policy,
media_ids: status_params[:media_ids], media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive], sensitive: status_params[:sensitive],
spoiler_text: status_params[:spoiler_text], spoiler_text: status_params[:spoiler_text],
@@ -117,7 +100,6 @@ class Api::V1::StatusesController < Api::BaseController
language: status_params[:language], language: status_params[:language],
spoiler_text: status_params[:spoiler_text], spoiler_text: status_params[:spoiler_text],
poll: status_params[:poll], poll: status_params[:poll],
quote_approval_policy: quote_approval_policy,
content_type: status_params[:content_type] content_type: status_params[:content_type]
) )
@@ -158,14 +140,6 @@ class Api::V1::StatusesController < Api::BaseController
render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404 render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404
end end
def set_quoted_status
@quoted_status = Status.find(status_params[:quoted_status_id]) if status_params[:quoted_status_id].present?
authorize(@quoted_status, :quote?) if @quoted_status.present?
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
# TODO: distinguish between non-existing and non-quotable posts
render json: { error: I18n.t('statuses.errors.quoted_status_not_found') }, status: 404
end
def check_statuses_limit def check_statuses_limit
raise(Mastodon::ValidationError) if status_ids.size > DEFAULT_STATUSES_LIMIT raise(Mastodon::ValidationError) if status_ids.size > DEFAULT_STATUSES_LIMIT
end end
@@ -182,8 +156,6 @@ class Api::V1::StatusesController < Api::BaseController
params.permit( params.permit(
:status, :status,
:in_reply_to_id, :in_reply_to_id,
:quoted_status_id,
:quote_approval_policy,
:sensitive, :sensitive,
:spoiler_text, :spoiler_text,
:visibility, :visibility,

View File

@@ -3,8 +3,14 @@
class Api::V1::Timelines::BaseController < Api::BaseController class Api::V1::Timelines::BaseController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { @statuses.empty? } after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
before_action :require_user!, if: :require_auth?
private private
def require_auth?
!Setting.timeline_preview
end
def pagination_collection def pagination_collection
@statuses @statuses
end end

View File

@@ -3,8 +3,8 @@
class Api::V1::Timelines::HomeController < Api::V1::Timelines::BaseController class Api::V1::Timelines::HomeController < Api::V1::Timelines::BaseController
include AsyncRefreshesConcern include AsyncRefreshesConcern
before_action -> { doorkeeper_authorize! :read, :'read:statuses' } before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:show]
before_action :require_user! before_action :require_user!, only: [:show]
PERMITTED_PARAMS = %i(local limit).freeze PERMITTED_PARAMS = %i(local limit).freeze

View File

@@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Timelines::LinkController < Api::V1::Timelines::TopicController class Api::V1::Timelines::LinkController < Api::V1::Timelines::BaseController
before_action -> { authorize_if_got_token! :read, :'read:statuses' } before_action -> { authorize_if_got_token! :read, :'read:statuses' }
before_action :set_preview_card before_action :set_preview_card
before_action :set_statuses before_action :set_statuses

View File

@@ -2,7 +2,6 @@
class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
before_action -> { authorize_if_got_token! :read, :'read:statuses' } before_action -> { authorize_if_got_token! :read, :'read:statuses' }
before_action :require_user!, if: :require_auth?
PERMITTED_PARAMS = %i(local remote limit only_media allow_local_only).freeze PERMITTED_PARAMS = %i(local remote limit only_media allow_local_only).freeze
@@ -14,16 +13,6 @@ class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
private private
def require_auth?
if truthy_param?(:local)
Setting.local_live_feed_access != 'public'
elsif truthy_param?(:remote)
Setting.remote_live_feed_access != 'public'
else
Setting.local_live_feed_access != 'public' || Setting.remote_live_feed_access != 'public'
end
end
def load_statuses def load_statuses
preloaded_public_statuses_page preloaded_public_statuses_page
end end

View File

@@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Timelines::TagController < Api::V1::Timelines::TopicController class Api::V1::Timelines::TagController < Api::V1::Timelines::BaseController
before_action -> { authorize_if_got_token! :read, :'read:statuses' } before_action -> { authorize_if_got_token! :read, :'read:statuses' }
before_action :load_tag before_action :load_tag
@@ -14,6 +14,10 @@ class Api::V1::Timelines::TagController < Api::V1::Timelines::TopicController
private private
def require_auth?
!Setting.timeline_preview
end
def load_tag def load_tag
@tag = Tag.find_normalized(params[:id]) @tag = Tag.find_normalized(params[:id])
end end

View File

@@ -1,17 +0,0 @@
# frozen_string_literal: true
class Api::V1::Timelines::TopicController < Api::V1::Timelines::BaseController
before_action :require_user!, if: :require_auth?
private
def require_auth?
if truthy_param?(:local)
Setting.local_topic_feed_access != 'public'
elsif truthy_param?(:remote)
Setting.remote_topic_feed_access != 'public'
else
Setting.local_topic_feed_access != 'public' || Setting.remote_topic_feed_access != 'public'
end
end
end

View File

@@ -20,7 +20,7 @@ class Api::V2::SearchController < Api::BaseController
@search = Search.new(search_results) @search = Search.new(search_results)
render json: @search, serializer: REST::SearchSerializer render json: @search, serializer: REST::SearchSerializer
rescue Mastodon::SyntaxError rescue Mastodon::SyntaxError
unprocessable_content unprocessable_entity
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
not_found not_found
end end

View File

@@ -49,7 +49,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
{ {
policy: 'all', policy: 'all',
alerts: Notification::TYPES.index_with { alerts_enabled }, alerts: Notification::TYPES.index_with { alerts_enabled },
}.deep_stringify_keys }
end end
def alerts_enabled def alerts_enabled

View File

@@ -31,7 +31,7 @@ class ApplicationController < ActionController::Base
rescue_from Mastodon::NotPermittedError, with: :forbidden rescue_from Mastodon::NotPermittedError, with: :forbidden
rescue_from ActionController::RoutingError, ActiveRecord::RecordNotFound, with: :not_found rescue_from ActionController::RoutingError, ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::UnknownFormat, with: :not_acceptable rescue_from ActionController::UnknownFormat, with: :not_acceptable
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_content rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests
rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, with: :internal_server_error) rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, with: :internal_server_error)
@@ -126,7 +126,7 @@ class ApplicationController < ActionController::Base
respond_with_error(410) respond_with_error(410)
end end
def unprocessable_content def unprocessable_entity
respond_with_error(422) respond_with_error(422)
end end

View File

@@ -38,7 +38,8 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
private private
def record_login_activity def record_login_activity
@user.login_activities.create( LoginActivity.create(
user: @user,
success: true, success: true,
authentication_method: :omniauth, authentication_method: :omniauth,
provider: @provider, provider: @provider,

View File

@@ -19,7 +19,8 @@ class Auth::PasswordsController < Devise::PasswordsController
private private
def redirect_invalid_reset_token def redirect_invalid_reset_token
redirect_to new_password_path(resource_name), flash: { error: t('auth.invalid_reset_password_token') } flash[:error] = I18n.t('auth.invalid_reset_password_token')
redirect_to new_password_path(resource_name)
end end
def reset_password_token_is_valid? def reset_password_token_is_valid?

View File

@@ -23,11 +23,11 @@ class Auth::RegistrationsController < Devise::RegistrationsController
super(&:build_invite_request) super(&:build_invite_request)
end end
def edit def edit # rubocop:disable Lint/UselessMethodDefinition
super super
end end
def create def create # rubocop:disable Lint/UselessMethodDefinition
super super
end end
@@ -89,7 +89,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end end
def check_enabled_registrations def check_enabled_registrations
redirect_to new_user_session_path, alert: I18n.t('devise.failure.closed_registrations', email: Setting.site_contact_email) unless allowed_registration?(request.remote_ip, @invite) redirect_to root_path unless allowed_registration?(request.remote_ip, @invite)
end end
def invite_code def invite_code

View File

@@ -12,8 +12,6 @@ class Auth::SessionsController < Devise::SessionsController
skip_before_action :require_functional! skip_before_action :require_functional!
skip_before_action :update_user_sign_in skip_before_action :update_user_sign_in
around_action :preserve_stored_location, only: :destroy, if: :continue_after?
prepend_before_action :check_suspicious!, only: [:create] prepend_before_action :check_suspicious!, only: [:create]
include Auth::TwoFactorAuthenticationConcern include Auth::TwoFactorAuthenticationConcern
@@ -33,9 +31,11 @@ class Auth::SessionsController < Devise::SessionsController
end end
def destroy def destroy
tmp_stored_location = stored_location_for(:user)
super super
session.delete(:challenge_passed_at) session.delete(:challenge_passed_at)
flash.delete(:notice) flash.delete(:notice)
store_location_for(:user, tmp_stored_location) if continue_after?
end end
def webauthn_options def webauthn_options
@@ -96,12 +96,6 @@ class Auth::SessionsController < Devise::SessionsController
private private
def preserve_stored_location
original_stored_location = stored_location_for(:user)
yield
store_location_for(:user, original_stored_location)
end
def check_suspicious! def check_suspicious!
user = find_user user = find_user
@login_is_suspicious = suspicious_sign_in?(user) unless user.nil? @login_is_suspicious = suspicious_sign_in?(user) unless user.nil?
@@ -157,11 +151,12 @@ class Auth::SessionsController < Devise::SessionsController
sign_in(user) sign_in(user)
flash.delete(:notice) flash.delete(:notice)
user.login_activities.create( LoginActivity.create(
request_details.merge( user: user,
authentication_method: security_measure, success: true,
success: true authentication_method: security_measure,
) ip: request.remote_ip,
user_agent: request.user_agent
) )
UserMailer.suspicious_sign_in(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later! if @login_is_suspicious UserMailer.suspicious_sign_in(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later! if @login_is_suspicious
@@ -172,12 +167,13 @@ class Auth::SessionsController < Devise::SessionsController
end end
def on_authentication_failure(user, security_measure, failure_reason) def on_authentication_failure(user, security_measure, failure_reason)
user.login_activities.create( LoginActivity.create(
request_details.merge( user: user,
authentication_method: security_measure, success: false,
failure_reason: failure_reason, authentication_method: security_measure,
success: false failure_reason: failure_reason,
) ip: request.remote_ip,
user_agent: request.user_agent
) )
# Only send a notification email every hour at most # Only send a notification email every hour at most
@@ -186,13 +182,6 @@ class Auth::SessionsController < Devise::SessionsController
UserMailer.failed_2fa(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later! UserMailer.failed_2fa(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later!
end end
def request_details
{
ip: request.remote_ip,
user_agent: request.user_agent,
}
end
def second_factor_attempts_key(user) def second_factor_attempts_key(user)
"2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}" "2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}"
end end

View File

@@ -18,11 +18,7 @@ module AccountOwnedConcern
end end
def set_account def set_account
@account = username_param.present? ? Account.find_local!(username_param) : Account.local.find(account_id_param) @account = Account.find_local!(username_param)
end
def account_id_param
params[:account_id]
end end
def username_param def username_param

View File

@@ -1,19 +0,0 @@
# frozen_string_literal: true
module Api::InteractionPoliciesConcern
extend ActiveSupport::Concern
def quote_approval_policy
case status_params[:quote_approval_policy].presence || current_user.setting_default_quote_policy
when 'public'
Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16
when 'followers'
Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16
when 'nobody'
0
else
# TODO: raise more useful message
raise ActiveRecord::RecordInvalid
end
end
end

View File

@@ -6,9 +6,6 @@ module AsyncRefreshesConcern
def add_async_refresh_header(async_refresh, retry_seconds: 3) def add_async_refresh_header(async_refresh, retry_seconds: 3)
return unless async_refresh.running? return unless async_refresh.running?
value = "id=\"#{async_refresh.id}\", retry=#{retry_seconds}" response.headers['Mastodon-Async-Refresh'] = "id=\"#{async_refresh.id}\", retry=#{retry_seconds}"
value += ", result_count=#{async_refresh.result_count}" unless async_refresh.result_count.nil?
response.headers['Mastodon-Async-Refresh'] = value
end end
end end

View File

@@ -5,18 +5,6 @@ module Auth::CaptchaConcern
include Hcaptcha::Adapters::ViewMethods include Hcaptcha::Adapters::ViewMethods
CAPTCHA_DIRECTIVES = %w(
connect_src
frame_src
script_src
style_src
).freeze
CAPTCHA_SOURCES = %w(
https://*.hcaptcha.com
https://hcaptcha.com
).freeze
included do included do
helper_method :render_captcha helper_method :render_captcha
end end
@@ -54,9 +42,20 @@ module Auth::CaptchaConcern
end end
def extend_csp_for_captcha! def extend_csp_for_captcha!
return unless captcha_required? && request.content_security_policy.present? policy = request.content_security_policy&.clone
request.content_security_policy = captcha_adjusted_policy return unless captcha_required? && policy.present?
%w(script_src frame_src style_src connect_src).each do |directive|
values = policy.send(directive)
values << 'https://hcaptcha.com' unless values.include?('https://hcaptcha.com') || values.include?('https:')
values << 'https://*.hcaptcha.com' unless values.include?('https://*.hcaptcha.com') || values.include?('https:')
policy.send(directive, *values)
end
request.content_security_policy = policy
end end
def render_captcha def render_captcha
@@ -64,24 +63,4 @@ module Auth::CaptchaConcern
hcaptcha_tags hcaptcha_tags
end end
private
def captcha_adjusted_policy
request.content_security_policy.clone.tap do |policy|
populate_captcha_policy(policy)
end
end
def populate_captcha_policy(policy)
CAPTCHA_DIRECTIVES.each do |directive|
values = policy.send(directive)
CAPTCHA_SOURCES.each do |source|
values << source unless values.include?(source) || values.include?('https:')
end
policy.send(directive, *values)
end
end
end end

View File

@@ -9,8 +9,6 @@ module SignatureVerification
EXPIRATION_WINDOW_LIMIT = 12.hours EXPIRATION_WINDOW_LIMIT = 12.hours
CLOCK_SKEW_MARGIN = 1.hour CLOCK_SKEW_MARGIN = 1.hour
STOPLIGHT_COOL_OFF_TIME = 5.minutes.seconds
STOPLIGHT_THRESHOLD = 1
def require_account_signature! def require_account_signature!
render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
@@ -109,12 +107,10 @@ module SignatureVerification
end end
def stoplight_wrapper def stoplight_wrapper
Stoplight( Stoplight("source:#{request.remote_ip}")
"source:#{request.remote_ip}", .with_threshold(1)
cool_off_time: STOPLIGHT_COOL_OFF_TIME, .with_cool_off_time(5.minutes.seconds)
threshold: STOPLIGHT_THRESHOLD, .with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) }
tracked_errors: [HTTP::Error, OpenSSL::SSL::SSLError]
)
end end
def actor_refresh_key!(actor) def actor_refresh_key!(actor)

View File

@@ -58,22 +58,20 @@ class FollowerAccountsController < ApplicationController
end end
def collection_presenter def collection_presenter
options = {} options = { type: :ordered }
options[:size] = @account.followers_count unless Setting.hide_followers_count || @account.user&.setting_hide_followers_count options[:size] = @account.followers_count unless Setting.hide_followers_count || @account.user&.setting_hide_followers_count
if page_requested? if page_requested?
ActivityPub::CollectionPresenter.new( ActivityPub::CollectionPresenter.new(
id: page_url(params.fetch(:page, 1)), id: account_followers_url(@account, page: params.fetch(:page, 1)),
type: :ordered,
items: follows.map { |follow| ActivityPub::TagManager.instance.uri_for(follow.account) }, items: follows.map { |follow| ActivityPub::TagManager.instance.uri_for(follow.account) },
part_of: ActivityPub::TagManager.instance.followers_uri_for(@account), part_of: account_followers_url(@account),
next: next_page_url, next: next_page_url,
prev: prev_page_url, prev: prev_page_url,
**options **options
) )
else else
ActivityPub::CollectionPresenter.new( ActivityPub::CollectionPresenter.new(
id: ActivityPub::TagManager.instance.followers_uri_for(@account), id: account_followers_url(@account),
type: :ordered,
first: page_url(1), first: page_url(1),
**options **options
) )

View File

@@ -49,7 +49,7 @@ class FollowingAccountsController < ApplicationController
end end
def page_url(page) def page_url(page)
ActivityPub::TagManager.instance.following_uri_for(@account, page: page) unless page.nil? account_following_index_url(@account, page: page) unless page.nil?
end end
def next_page_url def next_page_url
@@ -63,17 +63,17 @@ class FollowingAccountsController < ApplicationController
def collection_presenter def collection_presenter
if page_requested? if page_requested?
ActivityPub::CollectionPresenter.new( ActivityPub::CollectionPresenter.new(
id: page_url(params.fetch(:page, 1)), id: account_following_index_url(@account, page: params.fetch(:page, 1)),
type: :ordered, type: :ordered,
size: @account.following_count, size: @account.following_count,
items: follows.map { |follow| ActivityPub::TagManager.instance.uri_for(follow.target_account) }, items: follows.map { |follow| ActivityPub::TagManager.instance.uri_for(follow.target_account) },
part_of: ActivityPub::TagManager.instance.following_uri_for(@account), part_of: account_following_index_url(@account),
next: next_page_url, next: next_page_url,
prev: prev_page_url prev: prev_page_url
) )
else else
ActivityPub::CollectionPresenter.new( ActivityPub::CollectionPresenter.new(
id: ActivityPub::TagManager.instance.following_uri_for(@account), id: account_following_index_url(@account),
type: :ordered, type: :ordered,
size: @account.following_count, size: @account.following_count,
first: page_url(1) first: page_url(1)

View File

@@ -5,6 +5,6 @@ class Settings::LoginActivitiesController < Settings::BaseController
skip_before_action :require_functional! skip_before_action :require_functional!
def index def index
@login_activities = current_user.login_activities.order(id: :desc).page(params[:page]) @login_activities = LoginActivity.where(user: current_user).order(id: :desc).page(params[:page])
end end
end end

View File

@@ -22,7 +22,7 @@ class Settings::Migration::RedirectsController < Settings::BaseController
end end
def destroy def destroy
if current_account.moved? if current_account.moved_to_account_id.present?
current_account.update!(moved_to_account: nil) current_account.update!(moved_to_account: nil)
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id) ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
end end

View File

@@ -1,15 +0,0 @@
# frozen_string_literal: true
class Settings::Preferences::PostingDefaultsController < Settings::Preferences::BaseController
private
def after_update_redirect_path
settings_preferences_posting_defaults_path
end
def user_params
super.tap do |params|
params[:settings_attributes][:default_quote_policy] = 'nobody' if params[:settings_attributes][:default_privacy] == 'private'
end
end
end

View File

@@ -8,7 +8,8 @@ class Settings::SessionsController < Settings::BaseController
def destroy def destroy
@session.destroy! @session.destroy!
redirect_to edit_user_registration_path, notice: t('sessions.revoke_success') flash[:notice] = I18n.t('sessions.revoke_success')
redirect_to edit_user_registration_path
end end
private private

View File

@@ -52,7 +52,7 @@ module Settings
end end
else else
flash[:error] = I18n.t('webauthn_credentials.create.error') flash[:error] = I18n.t('webauthn_credentials.create.error')
status = :unprocessable_content status = :unprocessable_entity
end end
else else
flash[:error] = t('webauthn_credentials.create.error') flash[:error] = t('webauthn_credentials.create.error')
@@ -86,11 +86,13 @@ module Settings
private private
def redirect_invalid_otp def redirect_invalid_otp
redirect_to settings_two_factor_authentication_methods_path, flash: { error: t('webauthn_credentials.otp_required') } flash[:error] = t('webauthn_credentials.otp_required')
redirect_to settings_two_factor_authentication_methods_path
end end
def redirect_invalid_webauthn def redirect_invalid_webauthn
redirect_to settings_two_factor_authentication_methods_path, flash: { error: t('webauthn_credentials.not_enabled') } flash[:error] = t('webauthn_credentials.not_enabled')
redirect_to settings_two_factor_authentication_methods_path
end end
end end
end end

View File

@@ -11,7 +11,6 @@ class StatusesController < ApplicationController
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? } before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_status before_action :set_status
before_action :redirect_to_original, only: :show before_action :redirect_to_original, only: :show
before_action :verify_embed_allowed, only: :embed
after_action :set_link_headers after_action :set_link_headers
@@ -41,6 +40,8 @@ class StatusesController < ApplicationController
end end
def embed def embed
return not_found if @status.hidden? || @status.reblog?
expires_in 180, public: true expires_in 180, public: true
response.headers.delete('X-Frame-Options') response.headers.delete('X-Frame-Options')
@@ -49,10 +50,6 @@ class StatusesController < ApplicationController
private private
def verify_embed_allowed
not_found if @status.hidden? || @status.reblog?
end
def set_link_headers def set_link_headers
response.headers['Link'] = LinkHeader.new( response.headers['Link'] = LinkHeader.new(
[[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]] [[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]]

View File

@@ -13,8 +13,6 @@ module Admin::ActionLogsHelper
end end
when 'UserRole' when 'UserRole'
link_to log.human_identifier, admin_roles_path(log.target_id) link_to log.human_identifier, admin_roles_path(log.target_id)
when 'UsernameBlock'
link_to log.human_identifier, edit_admin_username_block_path(log.target_id)
when 'Report' when 'Report'
link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id) link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id)
when 'Instance', 'DomainBlock', 'DomainAllow', 'UnavailableDomain' when 'Instance', 'DomainBlock', 'DomainAllow', 'UnavailableDomain'

View File

@@ -102,16 +102,6 @@ module ApplicationHelper
policy(record).public_send(:"#{action}?") policy(record).public_send(:"#{action}?")
end end
def conditional_link_to(condition, name, options = {}, html_options = {}, &block)
if condition && !current_page?(block_given? ? name : options)
link_to(name, options, html_options, &block)
elsif block_given?
content_tag(:span, options, html_options, &block)
else
content_tag(:span, name, html_options)
end
end
def material_symbol(icon, attributes = {}) def material_symbol(icon, attributes = {})
safe_join( safe_join(
[ [
@@ -244,10 +234,6 @@ module ApplicationHelper
tag.input(type: :text, maxlength: 999, spellcheck: false, readonly: true, **options) tag.input(type: :text, maxlength: 999, spellcheck: false, readonly: true, **options)
end end
def recent_tag_users(tag)
tag.statuses.public_visibility.joins(:account).merge(Account.without_suspended.without_silenced).includes(:account).limit(3).map(&:account)
end
def recent_tag_usage(tag) def recent_tag_usage(tag)
people = tag.history.aggregate(2.days.ago.to_date..Time.zone.today).accounts people = tag.history.aggregate(2.days.ago.to_date..Time.zone.today).accounts
I18n.t 'user_mailer.welcome.hashtags_recent_count', people: number_with_delimiter(people), count: people I18n.t 'user_mailer.welcome.hashtags_recent_count', people: number_with_delimiter(people), count: people
@@ -261,10 +247,6 @@ module ApplicationHelper
'https://play.google.com/store/apps/details?id=org.joinmastodon.android' 'https://play.google.com/store/apps/details?id=org.joinmastodon.android'
end end
def within_authorization_flow?
session[:user_return_to].present? && Rails.application.routes.recognize_path(session[:user_return_to])[:controller] == 'oauth/authorizations'
end
# glitch-soc addition to handle the multiple flavors # glitch-soc addition to handle the multiple flavors
def flavoured_vite_typescript_tag(pack_name, **) def flavoured_vite_typescript_tag(pack_name, **)
vite_typescript_tag("#{Themes.instance.flavour(current_flavour)['pack_directory'].delete_prefix('app/javascript/')}/#{pack_name}", **) vite_typescript_tag("#{Themes.instance.flavour(current_flavour)['pack_directory'].delete_prefix('app/javascript/')}/#{pack_name}", **)

View File

@@ -40,12 +40,6 @@ module ContextHelper
'automaticApproval' => { '@id' => 'gts:automaticApproval', '@type' => '@id' }, 'automaticApproval' => { '@id' => 'gts:automaticApproval', '@type' => '@id' },
'manualApproval' => { '@id' => 'gts:manualApproval', '@type' => '@id' }, 'manualApproval' => { '@id' => 'gts:manualApproval', '@type' => '@id' },
}, },
quote_authorizations: {
'gts' => 'https://gotosocial.org/ns#',
'quoteAuthorization' => { '@id' => 'https://w3id.org/fep/044f#quoteAuthorization', '@type' => '@id' },
'interactingObject' => { '@id' => 'gts:interactingObject' },
'interactionTarget' => { '@id' => 'gts:interactionTarget' },
},
}.freeze }.freeze
def full_context def full_context

View File

@@ -0,0 +1,18 @@
# frozen_string_literal: true
module EmailHelper
def self.included(base)
base.extend(self)
end
def email_to_canonical_email(str)
username, domain = str.downcase.split('@', 2)
username, = username.delete('.').split('+', 2)
"#{username}@#{domain}"
end
def email_to_canonical_email_hash(str)
Digest::SHA2.new(256).hexdigest(email_to_canonical_email(str))
end
end

View File

@@ -27,9 +27,7 @@ module FormattingHelper
module_function :extract_status_plain_text module_function :extract_status_plain_text
def status_content_format(status) def status_content_format(status)
quoted_status = status.quote&.quoted_status if status.local? html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type)
html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), quoted_status: quoted_status, content_type: status.content_type)
end end
def rss_status_content_format(status) def rss_status_content_format(status)
@@ -67,12 +65,12 @@ module FormattingHelper
end end
def rss_content_preroll(status) def rss_content_preroll(status)
return unless status.spoiler_text? if status.spoiler_text?
safe_join [
safe_join [ tag.p { spoiler_with_warning(status) },
tag.p { spoiler_with_warning(status) }, tag.hr,
tag.hr, ]
] end
end end
def spoiler_with_warning(status) def spoiler_with_warning(status)
@@ -83,10 +81,10 @@ module FormattingHelper
end end
def rss_content_postroll(status) def rss_content_postroll(status)
return unless status.preloadable_poll if status.preloadable_poll
tag.p do
tag.p do poll_option_tags(status)
poll_option_tags(status) end
end end
end end

View File

@@ -21,13 +21,7 @@ module HomeHelper
end end
end end
else else
account_url = if account.suspended? link_to(path || ActivityPub::TagManager.instance.url_for(account), class: 'account__display-name') do
ActivityPub::TagManager.instance.url_for(account)
else
web_url("@#{account.pretty_acct}")
end
link_to(path || account_url, class: 'account__display-name') do
content_tag(:div, class: 'account__avatar-wrapper') do content_tag(:div, class: 'account__avatar-wrapper') do
image_tag(full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url), class: 'account__avatar', width: 46, height: 46) image_tag(full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url), class: 'account__avatar', width: 46, height: 46)
end + end +
@@ -45,8 +39,18 @@ module HomeHelper
end end
end end
def field_verified_class(verified) def obscured_counter(count)
if verified if count <= 0
'0'
elsif count == 1
'1'
else
'1+'
end
end
def custom_field_classes(field)
if field.verified?
'verified' 'verified'
else else
'emojify' 'emojify'

View File

@@ -134,7 +134,7 @@ module JsonLdHelper
patch_for_forwarding!(value, compacted_value) patch_for_forwarding!(value, compacted_value)
elsif value.is_a?(Array) elsif value.is_a?(Array)
compacted_value = [compacted_value] unless compacted_value.is_a?(Array) compacted_value = [compacted_value] unless compacted_value.is_a?(Array)
return nil if value.size != compacted_value.size return if value.size != compacted_value.size
compacted[key] = value.zip(compacted_value).map do |v, vc| compacted[key] = value.zip(compacted_value).map do |v, vc|
if v.is_a?(Hash) && vc.is_a?(Hash) if v.is_a?(Hash) && vc.is_a?(Hash)

View File

@@ -107,7 +107,6 @@ module LanguagesHelper
mk: ['Macedonian', 'македонски јазик'].freeze, mk: ['Macedonian', 'македонски јазик'].freeze,
ml: ['Malayalam', 'മലയാളം'].freeze, ml: ['Malayalam', 'മലയാളം'].freeze,
mn: ['Mongolian', 'Монгол хэл'].freeze, mn: ['Mongolian', 'Монгол хэл'].freeze,
'mn-Mong': ['Traditional Mongolian', 'ᠮᠣᠩᠭᠣᠯ ᠬᠡᠯᠡ'].freeze,
mr: ['Marathi', 'मराठी'].freeze, mr: ['Marathi', 'मराठी'].freeze,
ms: ['Malay', 'Bahasa Melayu'].freeze, ms: ['Malay', 'Bahasa Melayu'].freeze,
'ms-Arab': ['Jawi Malay', 'بهاس ملايو'].freeze, 'ms-Arab': ['Jawi Malay', 'بهاس ملايو'].freeze,

View File

@@ -57,20 +57,6 @@ module StatusesHelper
components.compact_blank.join("\n\n") components.compact_blank.join("\n\n")
end end
# This logic should be kept in sync with https://github.com/mastodon/mastodon/blob/425311e1d95c8a64ddac6c724fca247b8b893a82/app/javascript/mastodon/features/status/components/card.jsx#L160
def preview_card_aspect_ratio_classname(preview_card)
interactive = preview_card.type == 'video'
large_image = (preview_card.image.present? && preview_card.width > preview_card.height) || interactive
if large_image && interactive
'status-card__image--video'
elsif large_image
'status-card__image--large'
else
'status-card__image--normal'
end
end
def visibility_icon(status) def visibility_icon(status)
VISIBLITY_ICONS[status.visibility.to_sym] VISIBLITY_ICONS[status.visibility.to_sym]
end end
@@ -78,16 +64,4 @@ module StatusesHelper
def prefers_autoplay? def prefers_autoplay?
ActiveModel::Type::Boolean.new.cast(params[:autoplay]) || current_user&.setting_auto_play_gif ActiveModel::Type::Boolean.new.cast(params[:autoplay]) || current_user&.setting_auto_play_gif
end end
def render_seo_schema(status)
json = ActiveModelSerializers::SerializableResource.new(
status,
serializer: SEO::SocialMediaPostingSerializer,
adapter: SEO::Adapter
).to_json
# rubocop:disable Rails/OutputSafety
content_tag(:script, json_escape(json).html_safe, type: 'application/ld+json')
# rubocop:enable Rails/OutputSafety
end
end end

View File

@@ -28,24 +28,24 @@ module ThemeHelper
end end
def custom_stylesheet def custom_stylesheet
return if active_custom_stylesheet.blank? if active_custom_stylesheet.present?
stylesheet_link_tag(
stylesheet_link_tag( custom_css_path(active_custom_stylesheet),
custom_css_path(active_custom_stylesheet), host: root_url,
host: root_url, media: :all,
media: :all, skip_pipeline: true
skip_pipeline: true )
) end
end end
private private
def active_custom_stylesheet def active_custom_stylesheet
return if cached_custom_css_digest.blank? if cached_custom_css_digest.present?
[:custom, cached_custom_css_digest.to_s.first(8)]
[:custom, cached_custom_css_digest.to_s.first(8)] .compact_blank
.compact_blank .join('-')
.join('-') end
end end
def cached_custom_css_digest def cached_custom_css_digest

View File

@@ -1,78 +0,0 @@
{
"global": {
"class": "className",
"id": true,
"title": true,
"dir": true,
"lang": true
},
"tags": {
"p": {},
"br": {
"children": false
},
"span": {
"attributes": {
"translate": true
}
},
"a": {
"attributes": {
"href": true,
"rel": true,
"translate": true,
"target": true,
"title": true
}
},
"abbr": {
"attributes": {
"title": true
}
},
"del": {},
"s": {},
"pre": {},
"blockquote": {
"attributes": {
"cite": true
}
},
"code": {},
"b": {},
"strong": {},
"u": {},
"sub": {},
"sup": {},
"i": {},
"img": {
"children": false,
"attributes": {
"src": true,
"alt": true,
"title": true
}
},
"em": {},
"h1": {},
"h2": {},
"h3": {},
"h4": {},
"h5": {},
"ul": {},
"ol": {
"attributes": {
"start": true,
"reversed": true
}
},
"li": {
"attributes": {
"value": true
}
},
"ruby": {},
"rt": {},
"rp": {}
}
}

View File

@@ -1,7 +1,6 @@
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import Rails from '@rails/ujs'; import Rails from '@rails/ujs';
import { decode, ValidationError } from 'blurhash';
import ready from '../mastodon/ready'; import ready from '../mastodon/ready';
@@ -363,46 +362,6 @@ ready(() => {
document.querySelectorAll('[data-admin-component]').forEach((element) => { document.querySelectorAll('[data-admin-component]').forEach((element) => {
void mountReactComponent(element); void mountReactComponent(element);
}); });
document
.querySelectorAll<HTMLCanvasElement>('canvas[data-blurhash]')
.forEach((canvas) => {
const blurhash = canvas.dataset.blurhash;
if (blurhash) {
try {
// decode returns a Uint8ClampedArray<ArrayBufferLike> not Uint8ClampedArray<ArrayBuffer>
const pixels = decode(
blurhash,
32,
32,
) as Uint8ClampedArray<ArrayBuffer>;
const ctx = canvas.getContext('2d');
const imageData = new ImageData(pixels, 32, 32);
ctx?.putImageData(imageData, 0, 0);
} catch (err) {
if (err instanceof ValidationError) {
// ignore blurhash validation errors
return;
}
throw err;
}
}
});
document
.querySelectorAll<HTMLDivElement>('.preview-card')
.forEach((previewCard) => {
const spoilerButton = previewCard.querySelector('.spoiler-button');
if (!spoilerButton) {
return;
}
spoilerButton.addEventListener('click', () => {
previewCard.classList.toggle('preview-card--image-visible');
});
});
}).catch((reason: unknown) => { }).catch((reason: unknown) => {
throw reason; throw reason;
}); });

View File

@@ -70,7 +70,7 @@ function loaded() {
}; };
document.querySelectorAll('.emojify').forEach((content) => { document.querySelectorAll('.emojify').forEach((content) => {
content.innerHTML = emojify(content.innerHTML, {}, true); // Force emojify as public doesn't load the new emoji system. content.innerHTML = emojify(content.innerHTML);
}); });
document document
@@ -145,10 +145,6 @@ function loaded() {
); );
}); });
updateDefaultQuotePrivacyFromPrivacy(
document.querySelector('#user_settings_attributes_default_privacy'),
);
const reactComponents = document.querySelectorAll('[data-component]'); const reactComponents = document.querySelectorAll('[data-component]');
if (reactComponents.length > 0) { if (reactComponents.length > 0) {
@@ -351,31 +347,6 @@ const setInputDisabled = (
} }
}; };
const setInputHint = (
input: HTMLInputElement | HTMLSelectElement,
hintPrefix: string,
) => {
const fieldWrapper = input.closest<HTMLElement>('.fields-group > .input');
if (!fieldWrapper) return;
const hint = fieldWrapper.dataset[`${hintPrefix}Hint`];
const hintElement =
fieldWrapper.querySelector<HTMLSpanElement>(':scope > .hint');
if (hint) {
if (hintElement) {
hintElement.textContent = hint;
} else {
const newHintElement = document.createElement('span');
newHintElement.className = 'hint';
newHintElement.textContent = hint;
fieldWrapper.appendChild(newHintElement);
}
} else {
hintElement?.remove();
}
};
Rails.delegate( Rails.delegate(
document, document,
'#account_statuses_cleanup_policy_enabled', '#account_statuses_cleanup_policy_enabled',
@@ -393,36 +364,6 @@ Rails.delegate(
}, },
); );
const updateDefaultQuotePrivacyFromPrivacy = (
privacySelect: EventTarget | null,
) => {
if (!(privacySelect instanceof HTMLSelectElement) || !privacySelect.form)
return;
const select = privacySelect.form.querySelector<HTMLSelectElement>(
'select#user_settings_attributes_default_quote_policy',
);
if (!select) return;
setInputHint(select, privacySelect.value);
if (privacySelect.value === 'private') {
select.value = 'nobody';
setInputDisabled(select, true);
} else {
setInputDisabled(select, false);
}
};
Rails.delegate(
document,
'#user_settings_attributes_default_privacy',
'change',
({ target }) => {
updateDefaultQuotePrivacyFromPrivacy(target);
},
);
// Empty the honeypot fields in JS in case something like an extension // Empty the honeypot fields in JS in case something like an extension
// automatically filled them. // automatically filled them.
Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => { Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => {

View File

@@ -89,7 +89,6 @@ export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
const messages = defineMessages({ const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
uploadQuote: { id: 'upload_error.quote', defaultMessage: 'File upload not allowed with quotes.' },
open: { id: 'compose.published.open', defaultMessage: 'Open' }, open: { id: 'compose.published.open', defaultMessage: 'Open' },
published: { id: 'compose.published.body', defaultMessage: 'Post published.' }, published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' }, saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
@@ -102,18 +101,13 @@ export const ensureComposeIsVisible = (getState) => {
}; };
export function setComposeToStatus(status, text, spoiler_text, content_type) { export function setComposeToStatus(status, text, spoiler_text, content_type) {
return (dispatch, getState) => { return{
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']); type: COMPOSE_SET_STATUS,
status,
dispatch({ text,
type: COMPOSE_SET_STATUS, spoiler_text,
status, content_type,
text, };
spoiler_text,
content_type,
maxOptions,
});
}
} }
export function changeCompose(text) { export function changeCompose(text) {
@@ -160,7 +154,7 @@ export function resetCompose() {
}; };
} }
export const focusCompose = (defaultText = '') => (dispatch, getState) => { export const focusCompose = (defaultText) => (dispatch, getState) => {
dispatch({ dispatch({
type: COMPOSE_FOCUS, type: COMPOSE_FOCUS,
defaultText, defaultText,
@@ -199,18 +193,16 @@ export function directCompose(account) {
/** /**
* @param {null | string} overridePrivacy * @param {null | string} overridePrivacy
* @param {undefined | Function} successCallback
*/ */
export function submitCompose(overridePrivacy = null, successCallback = undefined) { export function submitCompose(overridePrivacy = null) {
return function (dispatch, getState) { return function (dispatch, getState) {
let status = getState().getIn(['compose', 'text'], ''); let status = getState().getIn(['compose', 'text'], '');
const media = getState().getIn(['compose', 'media_attachments']); const media = getState().getIn(['compose', 'media_attachments']);
const statusId = getState().getIn(['compose', 'id'], null); const statusId = getState().getIn(['compose', 'id'], null);
const hasQuote = !!getState().getIn(['compose', 'quoted_status_id']);
const spoilers = getState().getIn(['compose', 'spoiler']) || getState().getIn(['local_settings', 'always_show_spoilers_field']); const spoilers = getState().getIn(['compose', 'spoiler']) || getState().getIn(['local_settings', 'always_show_spoilers_field']);
const spoiler_text = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : ''; let spoilerText = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : '';
if (!(status?.length || media.size !== 0 || (hasQuote && spoiler_text?.length))) { if ((!status || !status.length) && media.size === 0) {
return; return;
} }
@@ -240,23 +232,20 @@ export function submitCompose(overridePrivacy = null, successCallback = undefine
}); });
} }
const visibility = overridePrivacy || getState().getIn(['compose', 'privacy']);
api().request({ api().request({
url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`, url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
method: statusId === null ? 'post' : 'put', method: statusId === null ? 'post' : 'put',
data: { data: {
status, status,
spoiler_text,
content_type: getState().getIn(['compose', 'content_type']), content_type: getState().getIn(['compose', 'content_type']),
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
media_ids: media.map(item => item.get('id')), media_ids: media.map(item => item.get('id')),
media_attributes, media_attributes,
sensitive: getState().getIn(['compose', 'sensitive']) || (spoiler_text.length > 0 && media.size !== 0), sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
visibility: visibility, spoiler_text: spoilerText,
visibility: overridePrivacy || getState().getIn(['compose', 'privacy']),
poll: getState().getIn(['compose', 'poll'], null), poll: getState().getIn(['compose', 'poll'], null),
language: getState().getIn(['compose', 'language']), language: getState().getIn(['compose', 'language']),
quoted_status_id: getState().getIn(['compose', 'quoted_status_id']),
quote_approval_policy: visibility === 'private' || visibility === 'direct' ? 'nobody' : getState().getIn(['compose', 'quote_policy']),
}, },
headers: { headers: {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
@@ -270,9 +259,6 @@ export function submitCompose(overridePrivacy = null, successCallback = undefine
dispatch(insertIntoTagHistory(response.data.tags, status)); dispatch(insertIntoTagHistory(response.data.tags, status));
dispatch(submitComposeSuccess({ ...response.data })); dispatch(submitComposeSuccess({ ...response.data }));
if (typeof successCallback === 'function') {
successCallback(response.data);
}
// To make the app more responsive, immediately push the status // To make the app more responsive, immediately push the status
// into the columns // into the columns
@@ -344,11 +330,6 @@ export function doodleSet(options) {
export function uploadCompose(files) { export function uploadCompose(files) {
return function (dispatch, getState) { return function (dispatch, getState) {
// Exit if there's a quote.
if (getState().compose.get('quoted_status_id')) {
dispatch(showAlert({ message: messages.uploadQuote }));
return;
}
const uploadLimit = getState().getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']); const uploadLimit = getState().getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']);
const media = getState().getIn(['compose', 'media_attachments']); const media = getState().getIn(['compose', 'media_attachments']);
const pending = getState().getIn(['compose', 'pending_media_attachments']); const pending = getState().getIn(['compose', 'pending_media_attachments']);

View File

@@ -1,47 +1,9 @@
import { defineMessages } from 'react-intl';
import { createAction } from '@reduxjs/toolkit';
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable'; import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { apiUpdateMedia } from 'flavours/glitch/api/compose'; import { apiUpdateMedia } from 'flavours/glitch/api/compose';
import { apiGetSearch } from 'flavours/glitch/api/search';
import type { ApiMediaAttachmentJSON } from 'flavours/glitch/api_types/media_attachments'; import type { ApiMediaAttachmentJSON } from 'flavours/glitch/api_types/media_attachments';
import type { MediaAttachment } from 'flavours/glitch/models/media_attachment'; import type { MediaAttachment } from 'flavours/glitch/models/media_attachment';
import { import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions';
createDataLoadingThunk,
createAppThunk,
} from 'flavours/glitch/store/typed_functions';
import type { ApiQuotePolicy } from '../api_types/quotes';
import type { Status } from '../models/status';
import { showAlert } from './alerts';
import { focusCompose } from './compose';
import { importFetchedStatuses } from './importer';
import { openModal } from './modal';
const messages = defineMessages({
quoteErrorEdit: {
id: 'quote_error.edit',
defaultMessage: 'Quotes cannot be added when editing a post.',
},
quoteErrorUpload: {
id: 'quote_error.upload',
defaultMessage: 'Quoting is not allowed with media attachments.',
},
quoteErrorPoll: {
id: 'quote_error.poll',
defaultMessage: 'Quoting is not allowed with polls.',
},
quoteErrorQuote: {
id: 'quote_error.quote',
defaultMessage: 'Only one quote at a time is allowed.',
},
quoteErrorUnauthorized: {
id: 'quote_error.unauthorized',
defaultMessage: 'You are not authorized to quote this post.',
},
});
type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & { type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & {
unattached?: boolean; unattached?: boolean;
@@ -106,111 +68,3 @@ export const changeUploadCompose = createDataLoadingThunk(
useLoadingBar: false, useLoadingBar: false,
}, },
); );
export const quoteCompose = createAppThunk(
'compose/quoteComposeStatus',
(status: Status, { dispatch }) => {
dispatch(focusCompose());
return status;
},
);
export const quoteComposeByStatus = createAppThunk(
(status: Status, { dispatch, getState }) => {
const state = getState();
const composeState = state.compose;
const mediaAttachments = composeState.get('media_attachments');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const wasQuietPostHintModalDismissed: boolean =
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
state.settings.getIn(
['dismissed_banners', 'quote/quiet_post_hint'],
false,
);
if (composeState.get('id')) {
dispatch(showAlert({ message: messages.quoteErrorEdit }));
} else if (composeState.get('poll')) {
dispatch(showAlert({ message: messages.quoteErrorPoll }));
} else if (
composeState.get('is_uploading') ||
(mediaAttachments &&
typeof mediaAttachments !== 'string' &&
typeof mediaAttachments !== 'number' &&
typeof mediaAttachments !== 'boolean' &&
mediaAttachments.size !== 0)
) {
dispatch(showAlert({ message: messages.quoteErrorUpload }));
} else if (composeState.get('quoted_status_id')) {
dispatch(showAlert({ message: messages.quoteErrorQuote }));
} else if (
status.getIn(['quote_approval', 'current_user']) !== 'automatic' &&
status.getIn(['quote_approval', 'current_user']) !== 'manual'
) {
dispatch(showAlert({ message: messages.quoteErrorUnauthorized }));
} else if (
status.get('visibility') === 'unlisted' &&
!wasQuietPostHintModalDismissed
) {
dispatch(
openModal({
modalType: 'CONFIRM_QUIET_QUOTE',
modalProps: { status },
}),
);
} else {
dispatch(quoteCompose(status));
}
},
);
export const quoteComposeById = createAppThunk(
(statusId: string, { dispatch, getState }) => {
const status = getState().statuses.get(statusId);
if (status) {
dispatch(quoteComposeByStatus(status));
}
},
);
export const pasteLinkCompose = createDataLoadingThunk(
'compose/pasteLink',
async ({ url }: { url: string }) => {
return await apiGetSearch({
q: url,
type: 'statuses',
resolve: true,
limit: 2,
});
},
(data, { dispatch, getState }) => {
const composeState = getState().compose;
if (
composeState.get('quoted_status_id') ||
composeState.get('is_submitting') ||
composeState.get('poll') ||
composeState.get('is_uploading') ||
composeState.get('id')
)
return;
dispatch(importFetchedStatuses(data.statuses));
if (
data.statuses.length === 1 &&
data.statuses[0] &&
['automatic', 'manual'].includes(
data.statuses[0].quote_approval?.current_user ?? 'denied',
)
) {
dispatch(quoteComposeById(data.statuses[0].id));
}
},
);
export const quoteComposeCancel = createAction('compose/quoteComposeCancel');
export const setComposeQuotePolicy = createAction<ApiQuotePolicy>(
'compose/setQuotePolicy',
);

View File

@@ -21,15 +21,6 @@ export function normalizeFilterResult(result) {
return normalResult; return normalResult;
} }
function stripQuoteFallback(text) {
const wrapper = document.createElement('div');
wrapper.innerHTML = text;
wrapper.querySelector('.quote-inline')?.remove();
return wrapper.innerHTML;
}
export function normalizeStatus(status, normalOldStatus, settings) { export function normalizeStatus(status, normalOldStatus, settings) {
const normalStatus = { ...status }; const normalStatus = { ...status };
@@ -87,11 +78,6 @@ export function normalizeStatus(status, normalOldStatus, settings) {
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap); normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText); normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText);
// Remove quote fallback link from the DOM so it doesn't mess with paragraph margins
if (normalStatus.quote) {
normalStatus.contentHtml = stripQuoteFallback(normalStatus.contentHtml);
}
if (normalStatus.url && !(normalStatus.url.startsWith('http://') || normalStatus.url.startsWith('https://'))) { if (normalStatus.url && !(normalStatus.url.startsWith('http://') || normalStatus.url.startsWith('https://'))) {
normalStatus.url = null; normalStatus.url = null;
} }
@@ -131,11 +117,6 @@ export function normalizeStatusTranslation(translation, status) {
spoiler_text: translation.spoiler_text, spoiler_text: translation.spoiler_text,
}; };
// Remove quote fallback link from the DOM so it doesn't mess with paragraph margins
if (status.get('quote')) {
normalTranslation.contentHtml = stripQuoteFallback(normalTranslation.contentHtml);
}
return normalTranslation; return normalTranslation;
} }

View File

@@ -1,13 +1,8 @@
import { import { apiReblog, apiUnreblog } from 'flavours/glitch/api/interactions';
apiReblog,
apiUnreblog,
apiRevokeQuote,
apiGetQuotes,
} from 'flavours/glitch/api/interactions';
import type { StatusVisibility } from 'flavours/glitch/models/status'; import type { StatusVisibility } from 'flavours/glitch/models/status';
import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions'; import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions';
import { importFetchedStatus, importFetchedStatuses } from './importer'; import { importFetchedStatus } from './importer';
export const reblog = createDataLoadingThunk( export const reblog = createDataLoadingThunk(
'status/reblog', 'status/reblog',
@@ -38,35 +33,3 @@ export const unreblog = createDataLoadingThunk(
return discardLoadData; return discardLoadData;
}, },
); );
export const revokeQuote = createDataLoadingThunk(
'status/revoke_quote',
({
statusId,
quotedStatusId,
}: {
statusId: string;
quotedStatusId: string;
}) => apiRevokeQuote(quotedStatusId, statusId),
(data, { dispatch, discardLoadData }) => {
dispatch(importFetchedStatus(data));
return discardLoadData;
},
);
export const fetchQuotes = createDataLoadingThunk(
'status/fetch_quotes',
async ({ statusId, next }: { statusId: string; next?: string }) => {
const { links, statuses } = await apiGetQuotes(statusId, next);
return {
links,
statuses,
replace: !next,
};
},
(payload, { dispatch }) => {
dispatch(importFetchedStatuses(payload.statuses));
},
);

View File

@@ -30,21 +30,8 @@ import { importFetchedAccounts, importFetchedStatuses } from './importer';
import { NOTIFICATIONS_FILTER_SET } from './notifications'; import { NOTIFICATIONS_FILTER_SET } from './notifications';
import { saveSettings } from './settings'; import { saveSettings } from './settings';
function notificationTypeForFilter(type: NotificationType) {
if (type === 'quoted_update') return 'update';
else return type;
}
function notificationTypeForQuickFilter(type: NotificationType) {
if (type === 'quoted_update') return 'update';
else if (type === 'quote') return 'mention';
else return type;
}
function excludeAllTypesExcept(filter: string) { function excludeAllTypesExcept(filter: string) {
return allNotificationTypes.filter( return allNotificationTypes.filter((item) => item !== filter);
(item) => notificationTypeForQuickFilter(item) !== filter,
);
} }
function getExcludedTypes(state: RootState) { function getExcludedTypes(state: RootState) {
@@ -168,17 +155,13 @@ export const processNewNotificationForGroups = createAppAsyncThunk(
const showInColumn = const showInColumn =
activeFilter === 'all' activeFilter === 'all'
? notificationShows[notificationTypeForFilter(notification.type)] !== ? notificationShows[notification.type] !== false
false : activeFilter === notification.type;
: activeFilter === notificationTypeForQuickFilter(notification.type);
if (!showInColumn) return; if (!showInColumn) return;
if ( if (
(notification.type === 'mention' || (notification.type === 'mention' || notification.type === 'update') &&
notification.type === 'quote' ||
notification.type === 'update' ||
notification.type === 'quoted_update') &&
notification.status?.filtered notification.status?.filtered
) { ) {
const filters = notification.status.filtered.filter((result) => const filters = notification.status.filtered.filter((result) =>

View File

@@ -31,7 +31,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
let filtered = false; let filtered = false;
if (['mention', 'quote', 'status'].includes(notification.type) && notification.status.filtered) { if (['mention', 'status'].includes(notification.type) && notification.status.filtered) {
const filters = notification.status.filtered.filter(result => result.filter.context.includes('notifications')); const filters = notification.status.filtered.filter(result => result.filter.context.includes('notifications'));
if (filters.some(result => result.filter.filter_action === 'hide')) { if (filters.some(result => result.filter.filter_action === 'hide')) {

View File

@@ -1,12 +1,9 @@
import { defineMessages } from 'react-intl';
import { browserHistory } from 'flavours/glitch/components/router'; import { browserHistory } from 'flavours/glitch/components/router';
import api from '../api'; import api from '../api';
import { showAlert } from './alerts';
import { ensureComposeIsVisible, setComposeToStatus } from './compose'; import { ensureComposeIsVisible, setComposeToStatus } from './compose';
import { importFetchedStatus, importFetchedAccount } from './importer'; import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
import { fetchContext } from './statuses_typed'; import { fetchContext } from './statuses_typed';
import { deleteFromTimelines } from './timelines'; import { deleteFromTimelines } from './timelines';
@@ -43,10 +40,6 @@ export const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS';
export const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL'; export const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL';
export const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO'; export const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO';
const messages = defineMessages({
deleteSuccess: { id: 'status.delete.success', defaultMessage: 'Post deleted' },
});
export function fetchStatusRequest(id, skipLoading) { export function fetchStatusRequest(id, skipLoading) {
return { return {
type: STATUS_FETCH_REQUEST, type: STATUS_FETCH_REQUEST,
@@ -55,18 +48,7 @@ export function fetchStatusRequest(id, skipLoading) {
}; };
} }
/** export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) {
* @param {string} id
* @param {Object} [options]
* @param {boolean} [options.forceFetch]
* @param {boolean} [options.alsoFetchContext]
* @param {string | null | undefined} [options.parentQuotePostId]
*/
export function fetchStatus(id, {
forceFetch = false,
alsoFetchContext = true,
parentQuotePostId,
} = {}) {
return (dispatch, getState) => { return (dispatch, getState) => {
const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null; const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
@@ -84,7 +66,7 @@ export function fetchStatus(id, {
dispatch(importFetchedStatus(response.data)); dispatch(importFetchedStatus(response.data));
dispatch(fetchStatusSuccess(skipLoading)); dispatch(fetchStatusSuccess(skipLoading));
}).catch(error => { }).catch(error => {
dispatch(fetchStatusFail(id, error, skipLoading, parentQuotePostId)); dispatch(fetchStatusFail(id, error, skipLoading));
}); });
}; };
} }
@@ -96,28 +78,22 @@ export function fetchStatusSuccess(skipLoading) {
}; };
} }
export function fetchStatusFail(id, error, skipLoading, parentQuotePostId) { export function fetchStatusFail(id, error, skipLoading) {
return { return {
type: STATUS_FETCH_FAIL, type: STATUS_FETCH_FAIL,
id, id,
error, error,
parentQuotePostId,
skipLoading, skipLoading,
skipAlert: true, skipAlert: true,
}; };
} }
export function redraft(status, raw_text, content_type) { export function redraft(status, raw_text, content_type) {
return (dispatch, getState) => { return {
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']); type: REDRAFT,
status,
dispatch({ raw_text,
type: REDRAFT, content_type,
status,
raw_text,
content_type,
maxOptions,
});
}; };
} }
@@ -162,7 +138,7 @@ export function deleteStatus(id, withRedraft = false) {
dispatch(deleteStatusRequest(id)); dispatch(deleteStatusRequest(id));
return api().delete(`/api/v1/statuses/${id}`, { params: { delete_media: !withRedraft } }).then(response => { api().delete(`/api/v1/statuses/${id}`, { params: { delete_media: !withRedraft } }).then(response => {
dispatch(deleteStatusSuccess(id)); dispatch(deleteStatusSuccess(id));
dispatch(deleteFromTimelines(id)); dispatch(deleteFromTimelines(id));
dispatch(importFetchedAccount(response.data.account)); dispatch(importFetchedAccount(response.data.account));
@@ -170,14 +146,9 @@ export function deleteStatus(id, withRedraft = false) {
if (withRedraft) { if (withRedraft) {
dispatch(redraft(status, response.data.text, response.data.content_type)); dispatch(redraft(status, response.data.text, response.data.content_type));
ensureComposeIsVisible(getState); ensureComposeIsVisible(getState);
} else {
dispatch(showAlert({ message: messages.deleteSuccess }));
} }
return response;
}).catch(error => { }).catch(error => {
dispatch(deleteStatusFail(id, error)); dispatch(deleteStatusFail(id, error));
throw error;
}); });
}; };
} }

View File

@@ -1,44 +1,18 @@
import { createAction } from '@reduxjs/toolkit'; import { apiGetContext } from 'flavours/glitch/api/statuses';
import { apiGetContext, apiSetQuotePolicy } from 'flavours/glitch/api/statuses';
import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions'; import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions';
import type { ApiQuotePolicy } from '../api_types/quotes';
import { importFetchedStatuses } from './importer'; import { importFetchedStatuses } from './importer';
export const fetchContext = createDataLoadingThunk( export const fetchContext = createDataLoadingThunk(
'status/context', 'status/context',
({ statusId }: { statusId: string; prefetchOnly?: boolean }) => ({ statusId }: { statusId: string }) => apiGetContext(statusId),
apiGetContext(statusId), (context, { dispatch }) => {
({ context, refresh }, { dispatch, actionArg: { prefetchOnly = false } }) => {
const statuses = context.ancestors.concat(context.descendants); const statuses = context.ancestors.concat(context.descendants);
dispatch(importFetchedStatuses(statuses)); dispatch(importFetchedStatuses(statuses));
return { return {
context, context,
refresh,
prefetchOnly,
}; };
}, },
); );
export const completeContextRefresh = createAction<{ statusId: string }>(
'status/context/complete',
);
export const showPendingReplies = createAction<{ statusId: string }>(
'status/context/showPendingReplies',
);
export const clearPendingReplies = createAction<{ statusId: string }>(
'status/context/clearPendingReplies',
);
export const setStatusQuotePolicy = createDataLoadingThunk(
'status/setQuotePolicy',
({ statusId, policy }: { statusId: string; policy: ApiQuotePolicy }) => {
return apiSetQuotePolicy(statusId, policy);
},
);

View File

@@ -15,50 +15,6 @@ export const getLinks = (response: AxiosResponse) => {
return LinkHeader.parse(value); return LinkHeader.parse(value);
}; };
export interface AsyncRefreshHeader {
id: string;
retry: number;
}
const isAsyncRefreshHeader = (obj: object): obj is AsyncRefreshHeader =>
'id' in obj && 'retry' in obj;
export const getAsyncRefreshHeader = (
response: AxiosResponse,
): AsyncRefreshHeader | null => {
const value = response.headers['mastodon-async-refresh'] as
| string
| undefined;
if (!value) {
return null;
}
const asyncRefreshHeader: Record<string, unknown> = {};
value.split(/,\s*/).forEach((pair) => {
const [key, val] = pair.split('=', 2);
let typedValue: string | number;
if (key && ['id', 'retry'].includes(key) && val) {
if (val.startsWith('"')) {
typedValue = val.slice(1, -1);
} else {
typedValue = parseInt(val);
}
asyncRefreshHeader[key] = typedValue;
}
});
if (isAsyncRefreshHeader(asyncRefreshHeader)) {
return asyncRefreshHeader;
}
return null;
};
const csrfHeader: RawAxiosRequestHeaders = {}; const csrfHeader: RawAxiosRequestHeaders = {};
const setCSRFHeader = () => { const setCSRFHeader = () => {
@@ -106,7 +62,7 @@ export default function api(withAuthorization = true) {
}); });
} }
type ApiUrl = `v${1 | '1_alpha' | 2}/${string}`; type ApiUrl = `v${1 | 2}/${string}`;
type RequestParamsOrData = Record<string, unknown>; type RequestParamsOrData = Record<string, unknown>;
export async function apiRequest<ApiResponse = unknown>( export async function apiRequest<ApiResponse = unknown>(

View File

@@ -1,5 +0,0 @@
import { apiRequestGet } from 'flavours/glitch/api';
import type { ApiAsyncRefreshJSON } from 'flavours/glitch/api_types/async_refreshes';
export const apiGetAsyncRefresh = (id: string) =>
apiRequestGet<ApiAsyncRefreshJSON>(`v1_alpha/async_refreshes/${id}`);

View File

@@ -1,28 +1,10 @@
import api, { apiRequestPost, getLinks } from 'flavours/glitch/api'; import { apiRequestPost } from 'flavours/glitch/api';
import type { ApiStatusJSON } from 'flavours/glitch/api_types/statuses'; import type { Status, StatusVisibility } from 'flavours/glitch/models/status';
import type { StatusVisibility } from 'flavours/glitch/models/status';
export const apiReblog = (statusId: string, visibility: StatusVisibility) => export const apiReblog = (statusId: string, visibility: StatusVisibility) =>
apiRequestPost<{ reblog: ApiStatusJSON }>(`v1/statuses/${statusId}/reblog`, { apiRequestPost<{ reblog: Status }>(`v1/statuses/${statusId}/reblog`, {
visibility, visibility,
}); });
export const apiUnreblog = (statusId: string) => export const apiUnreblog = (statusId: string) =>
apiRequestPost<ApiStatusJSON>(`v1/statuses/${statusId}/unreblog`); apiRequestPost<Status>(`v1/statuses/${statusId}/unreblog`);
export const apiRevokeQuote = (quotedStatusId: string, statusId: string) =>
apiRequestPost<ApiStatusJSON>(
`v1/statuses/${quotedStatusId}/quotes/${statusId}/revoke`,
);
export const apiGetQuotes = async (statusId: string, url?: string) => {
const response = await api().request<ApiStatusJSON[]>({
method: 'GET',
url: url ?? `/api/v1/statuses/${statusId}/quotes`,
});
return {
statuses: response.data,
links: getLinks(response),
};
};

View File

@@ -1,31 +1,5 @@
import api, { apiRequestPut, getAsyncRefreshHeader } from 'flavours/glitch/api'; import { apiRequestGet } from 'flavours/glitch/api';
import type { import type { ApiContextJSON } from 'flavours/glitch/api_types/statuses';
ApiContextJSON,
ApiStatusJSON,
} from 'flavours/glitch/api_types/statuses';
import type { ApiQuotePolicy } from '../api_types/quotes'; export const apiGetContext = (statusId: string) =>
apiRequestGet<ApiContextJSON>(`v1/statuses/${statusId}/context`);
export const apiGetContext = async (statusId: string) => {
const response = await api().request<ApiContextJSON>({
method: 'GET',
url: `/api/v1/statuses/${statusId}/context`,
});
return {
context: response.data,
refresh: getAsyncRefreshHeader(response),
};
};
export const apiSetQuotePolicy = async (
statusId: string,
policy: ApiQuotePolicy,
) => {
return apiRequestPut<ApiStatusJSON>(
`v1/statuses/${statusId}/interaction_policy`,
{
quote_approval_policy: policy,
},
);
};

View File

@@ -37,7 +37,7 @@ export interface BaseApiAccountJSON {
roles?: ApiAccountJSON[]; roles?: ApiAccountJSON[];
statuses_count: number; statuses_count: number;
uri: string; uri: string;
url?: string; url: string;
username: string; username: string;
moved?: ApiAccountJSON; moved?: ApiAccountJSON;
suspended?: boolean; suspended?: boolean;

View File

@@ -1,28 +0,0 @@
// See app/serializers/rest/announcement_serializer.rb
import type { ApiCustomEmojiJSON } from './custom_emoji';
import type { ApiMentionJSON, ApiStatusJSON, ApiTagJSON } from './statuses';
export interface ApiAnnouncementJSON {
id: string;
content: string;
starts_at: null | string;
ends_at: null | string;
all_day: boolean;
published_at: string;
updated_at: null | string;
read: boolean;
mentions: ApiMentionJSON[];
statuses: ApiStatusJSON[];
tags: ApiTagJSON[];
emojis: ApiCustomEmojiJSON[];
reactions: ApiAnnouncementReactionJSON[];
}
export interface ApiAnnouncementReactionJSON {
name: string;
count: number;
me: boolean;
url?: string;
static_url?: string;
}

View File

@@ -1,7 +0,0 @@
export interface ApiAsyncRefreshJSON {
async_refresh: {
id: string;
status: 'running' | 'finished';
result_count: number;
};
}

Some files were not shown because too many files have changed in this diff Show More