diff --git a/.github/workflows/crowdin-download-stable.yml b/.github/workflows/crowdin-download-stable.yml
index f7f0a11a1f..d28bb66a3f 100644
--- a/.github/workflows/crowdin-download-stable.yml
+++ b/.github/workflows/crowdin-download-stable.yml
@@ -50,7 +50,7 @@ jobs:
# Create or update the pull request
- name: Create Pull Request
- uses: peter-evans/create-pull-request@v7.0.1
+ uses: peter-evans/create-pull-request@v7.0.5
with:
commit-message: 'New Crowdin translations'
title: 'New Crowdin Translations for ${{ github.base_ref || github.ref_name }} (automated)'
diff --git a/.github/workflows/crowdin-download.yml b/.github/workflows/crowdin-download.yml
index b3d908b77e..02d8a949de 100644
--- a/.github/workflows/crowdin-download.yml
+++ b/.github/workflows/crowdin-download.yml
@@ -53,7 +53,7 @@ jobs:
# Create or update the pull request
- name: Create Pull Request
- uses: peter-evans/create-pull-request@v7.0.1
+ uses: peter-evans/create-pull-request@v7.0.5
with:
commit-message: 'New Crowdin translations'
title: 'New Crowdin Translations (automated)'
diff --git a/Gemfile.lock b/Gemfile.lock
index b85d97761d..4c819ce9b2 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -601,7 +601,7 @@ GEM
actionmailer (>= 3)
net-smtp
premailer (~> 1.7, >= 1.7.9)
- propshaft (1.0.1)
+ propshaft (1.1.0)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
@@ -698,7 +698,7 @@ GEM
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
- rexml (3.3.7)
+ rexml (3.3.8)
rotp (6.3.0)
rouge (4.3.0)
rpam2 (4.0.2)
@@ -748,15 +748,15 @@ GEM
parser (>= 3.3.1.0)
rubocop-capybara (2.21.0)
rubocop (~> 1.41)
- rubocop-performance (1.21.1)
+ rubocop-performance (1.22.1)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
- rubocop-rails (2.25.1)
+ rubocop-rails (2.26.2)
activesupport (>= 4.2.0)
rack (>= 1.1)
- rubocop (>= 1.33.0, < 2.0)
+ rubocop (>= 1.52.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
- rubocop-rspec (3.0.4)
+ rubocop-rspec (3.0.5)
rubocop (~> 1.61)
rubocop-rspec_rails (2.30.0)
rubocop (~> 1.61)
@@ -884,7 +884,7 @@ GEM
webfinger (1.2.0)
activesupport
httpclient (>= 2.4)
- webmock (3.23.1)
+ webmock (3.24.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb
index f930a4510e..3447bb0d5c 100644
--- a/app/controllers/concerns/web_app_controller_concern.rb
+++ b/app/controllers/concerns/web_app_controller_concern.rb
@@ -13,7 +13,7 @@ module WebAppControllerConcern
policy = ContentSecurityPolicy.new
if policy.sso_host.present?
- p.form_action policy.sso_host
+ p.form_action policy.sso_host, -> { "https://#{request.host}/auth/auth/" }
else
p.form_action :none
end
diff --git a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb
index 0bff01ec27..ca8d46afe4 100644
--- a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb
+++ b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb
@@ -15,7 +15,7 @@ module Settings
end
def create
- session[:new_otp_secret] = User.generate_otp_secret(32)
+ session[:new_otp_secret] = User.generate_otp_secret
redirect_to new_settings_two_factor_authentication_confirmation_path
end
diff --git a/app/controllers/well_known/host_meta_controller.rb b/app/controllers/well_known/host_meta_controller.rb
index 201da9fbc3..6dee587baf 100644
--- a/app/controllers/well_known/host_meta_controller.rb
+++ b/app/controllers/well_known/host_meta_controller.rb
@@ -7,7 +7,23 @@ module WellKnown
def show
@webfinger_template = "#{webfinger_url}?resource={uri}"
expires_in 3.days, public: true
- render content_type: 'application/xrd+xml', formats: [:xml]
+
+ respond_to do |format|
+ format.any do
+ render content_type: 'application/xrd+xml', formats: [:xml]
+ end
+
+ format.json do
+ render json: {
+ links: [
+ {
+ rel: 'lrdd',
+ template: @webfinger_template,
+ },
+ ],
+ }
+ end
+ end
end
end
end
diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb
index e8d5634126..51e28d8b4e 100644
--- a/app/helpers/admin/action_logs_helper.rb
+++ b/app/helpers/admin/action_logs_helper.rb
@@ -35,4 +35,11 @@ module Admin::ActionLogsHelper
end
end
end
+
+ def sorted_action_log_types
+ Admin::ActionLogFilter::ACTION_TYPE_MAP
+ .keys
+ .map { |key| [I18n.t("admin.action_logs.action_types.#{key}"), key] }
+ .sort_by(&:first)
+ end
end
diff --git a/app/helpers/admin/dashboard_helper.rb b/app/helpers/admin/dashboard_helper.rb
index 6096ff1381..f87fdad708 100644
--- a/app/helpers/admin/dashboard_helper.rb
+++ b/app/helpers/admin/dashboard_helper.rb
@@ -18,6 +18,11 @@ module Admin::DashboardHelper
end
end
+ def date_range(range)
+ [l(range.first), l(range.last)]
+ .join(' - ')
+ end
+
def relevant_account_timestamp(account)
timestamp, exact = if account.user_current_sign_in_at && account.user_current_sign_in_at < 24.hours.ago
[account.user_current_sign_in_at, true]
@@ -25,6 +30,8 @@ module Admin::DashboardHelper
[account.user_current_sign_in_at, false]
elsif account.user_pending?
[account.user_created_at, true]
+ elsif account.suspended_at.present? && account.local? && account.user.nil?
+ [account.suspended_at, true]
elsif account.last_status_at.present?
[account.last_status_at, true]
else
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index a7ae4d6a04..08c34a2c0b 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -1,12 +1,6 @@
# frozen_string_literal: true
module ApplicationHelper
- DANGEROUS_SCOPES = %w(
- read
- write
- follow
- ).freeze
-
RTL_LOCALES = %i(
ar
ckb
@@ -95,8 +89,11 @@ module ApplicationHelper
Rails.env.production? ? site_title : "#{site_title} (Dev)"
end
- def class_for_scope(scope)
- 'scope-danger' if DANGEROUS_SCOPES.include?(scope.to_s)
+ def label_for_scope(scope)
+ safe_join [
+ tag.samp(scope, class: { 'scope-danger' => SessionActivation::DEFAULT_SCOPES.include?(scope.to_s) }),
+ tag.span(t("doorkeeper.scopes.#{scope}"), class: :hint),
+ ]
end
def can?(action, record)
@@ -244,6 +241,10 @@ module ApplicationHelper
full_asset_url(instance_presenter.mascot&.file&.url || frontend_asset_path('images/elephant_ui_plane.svg'))
end
+ def copyable_input(options = {})
+ tag.input(type: :text, maxlength: 999, spellcheck: false, readonly: true, **options)
+ end
+
# glitch-soc addition to handle the multiple flavors
def preload_locale_pack
supported_locales = Themes.instance.flavour(current_flavour)['locales']
diff --git a/app/helpers/webfinger_helper.rb b/app/helpers/webfinger_helper.rb
deleted file mode 100644
index 482f4e19ea..0000000000
--- a/app/helpers/webfinger_helper.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-module WebfingerHelper
- def webfinger!(uri)
- Webfinger.new(uri).perform
- end
-end
diff --git a/app/javascript/mastodon/actions/markers.ts b/app/javascript/mastodon/actions/markers.ts
index 0b3280c212..251546cb9a 100644
--- a/app/javascript/mastodon/actions/markers.ts
+++ b/app/javascript/mastodon/actions/markers.ts
@@ -37,8 +37,7 @@ export const synchronouslySubmitMarkers = createAppAsyncThunk(
});
return;
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- } else if ('navigator' && 'sendBeacon' in navigator) {
+ } else if ('sendBeacon' in navigator) {
// Failing that, we can use sendBeacon, but we have to encode the data as
// FormData for DoorKeeper to recognize the token.
const formData = new FormData();
diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts
index b40b04f8cc..a359913e61 100644
--- a/app/javascript/mastodon/actions/notification_groups.ts
+++ b/app/javascript/mastodon/actions/notification_groups.ts
@@ -70,6 +70,10 @@ function dispatchAssociatedRecords(
const supportedGroupedNotificationTypes = ['favourite', 'reblog'];
+export function shouldGroupNotificationType(type: string) {
+ return supportedGroupedNotificationTypes.includes(type);
+}
+
export const fetchNotifications = createDataLoadingThunk(
'notificationGroups/fetch',
async (_params, { getState }) =>
diff --git a/app/javascript/mastodon/components/media_gallery.jsx b/app/javascript/mastodon/components/media_gallery.jsx
index 84cb4e04dc..1380d244ad 100644
--- a/app/javascript/mastodon/components/media_gallery.jsx
+++ b/app/javascript/mastodon/components/media_gallery.jsx
@@ -196,7 +196,7 @@ class Item extends PureComponent {
{visible && thumbnail}
- {badges && (
+ {visible && badges && (
{badges}
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 9f66c09631..4e16c5b351 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -402,7 +402,7 @@ export default function compose(state = initialState, action) {
.set('isUploadingThumbnail', false)
.update('media_attachments', list => list.map(item => {
if (item.get('id') === action.media.id) {
- return fromJS(action.media);
+ return fromJS(action.media).set('unattached', item.get('unattached'));
}
return item;
diff --git a/app/javascript/mastodon/reducers/notification_groups.ts b/app/javascript/mastodon/reducers/notification_groups.ts
index f3c83ccd8d..8b033f0fc7 100644
--- a/app/javascript/mastodon/reducers/notification_groups.ts
+++ b/app/javascript/mastodon/reducers/notification_groups.ts
@@ -21,6 +21,7 @@ import {
unmountNotifications,
refreshStaleNotificationGroups,
pollRecentNotifications,
+ shouldGroupNotificationType,
} from 'mastodon/actions/notification_groups';
import {
disconnectTimeline,
@@ -205,6 +206,13 @@ function processNewNotification(
groups: NotificationGroupsState['groups'],
notification: ApiNotificationJSON,
) {
+ if (!shouldGroupNotificationType(notification.type)) {
+ notification = {
+ ...notification,
+ group_key: `ungrouped-${notification.id}`,
+ };
+ }
+
const existingGroupIndex = groups.findIndex(
(group) =>
group.type !== 'gap' && group.group_key === notification.group_key,
@@ -242,7 +250,7 @@ function processNewNotification(
groups.unshift(existingGroup);
}
} else {
- // Create a new group
+ // We have not found an existing group, create a new one
groups.unshift(createNotificationGroupFromNotificationJSON(notification));
}
}
diff --git a/app/javascript/material-icons/400-24px/breaking_news-fill.svg b/app/javascript/material-icons/400-24px/breaking_news-fill.svg
new file mode 100644
index 0000000000..633ca48d57
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/breaking_news-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/breaking_news.svg b/app/javascript/material-icons/400-24px/breaking_news.svg
index d7dd0c12f4..c043f11a8b 100644
--- a/app/javascript/material-icons/400-24px/breaking_news.svg
+++ b/app/javascript/material-icons/400-24px/breaking_news.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/captive_portal-fill.svg b/app/javascript/material-icons/400-24px/captive_portal-fill.svg
new file mode 100644
index 0000000000..5c0b26fb64
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/captive_portal-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/captive_portal.svg b/app/javascript/material-icons/400-24px/captive_portal.svg
index 1f0f09c773..5c0b26fb64 100644
--- a/app/javascript/material-icons/400-24px/captive_portal.svg
+++ b/app/javascript/material-icons/400-24px/captive_portal.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/chat_bubble-fill.svg b/app/javascript/material-icons/400-24px/chat_bubble-fill.svg
new file mode 100644
index 0000000000..b47338a6c9
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/chat_bubble-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/chat_bubble.svg b/app/javascript/material-icons/400-24px/chat_bubble.svg
index 7d210b4608..05d976d242 100644
--- a/app/javascript/material-icons/400-24px/chat_bubble.svg
+++ b/app/javascript/material-icons/400-24px/chat_bubble.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/cloud-fill.svg b/app/javascript/material-icons/400-24px/cloud-fill.svg
new file mode 100644
index 0000000000..d049a74c01
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/cloud-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/cloud.svg b/app/javascript/material-icons/400-24px/cloud.svg
index 75b4e957fc..a36bddda91 100644
--- a/app/javascript/material-icons/400-24px/cloud.svg
+++ b/app/javascript/material-icons/400-24px/cloud.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/cloud_download-fill.svg b/app/javascript/material-icons/400-24px/cloud_download-fill.svg
new file mode 100644
index 0000000000..c55d49f7e5
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/cloud_download-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/cloud_download.svg b/app/javascript/material-icons/400-24px/cloud_download.svg
index 2fc3717ff9..8e9314800c 100644
--- a/app/javascript/material-icons/400-24px/cloud_download.svg
+++ b/app/javascript/material-icons/400-24px/cloud_download.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/cloud_sync-fill.svg b/app/javascript/material-icons/400-24px/cloud_sync-fill.svg
new file mode 100644
index 0000000000..0c648e19e4
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/cloud_sync-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/cloud_sync.svg b/app/javascript/material-icons/400-24px/cloud_sync.svg
index dbf6adc000..461796e323 100644
--- a/app/javascript/material-icons/400-24px/cloud_sync.svg
+++ b/app/javascript/material-icons/400-24px/cloud_sync.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/cloud_upload-fill.svg b/app/javascript/material-icons/400-24px/cloud_upload-fill.svg
new file mode 100644
index 0000000000..66a7bb22d3
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/cloud_upload-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/cloud_upload.svg b/app/javascript/material-icons/400-24px/cloud_upload.svg
index 5e1a4b9aef..94968cb947 100644
--- a/app/javascript/material-icons/400-24px/cloud_upload.svg
+++ b/app/javascript/material-icons/400-24px/cloud_upload.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/code.svg b/app/javascript/material-icons/400-24px/code.svg
index 5bdc338f7f..8ef5c55cd4 100644
--- a/app/javascript/material-icons/400-24px/code.svg
+++ b/app/javascript/material-icons/400-24px/code.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/computer-fill.svg b/app/javascript/material-icons/400-24px/computer-fill.svg
new file mode 100644
index 0000000000..91295d6846
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/computer-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/computer.svg b/app/javascript/material-icons/400-24px/computer.svg
index 8c5bd9110e..b8af5d4644 100644
--- a/app/javascript/material-icons/400-24px/computer.svg
+++ b/app/javascript/material-icons/400-24px/computer.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/contact_mail-fill.svg b/app/javascript/material-icons/400-24px/contact_mail-fill.svg
new file mode 100644
index 0000000000..c42c799955
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/contact_mail-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/contact_mail.svg b/app/javascript/material-icons/400-24px/contact_mail.svg
index 1ae26cc4d1..4547c48ec5 100644
--- a/app/javascript/material-icons/400-24px/contact_mail.svg
+++ b/app/javascript/material-icons/400-24px/contact_mail.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/database-fill.svg b/app/javascript/material-icons/400-24px/database-fill.svg
new file mode 100644
index 0000000000..3520f69614
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/database-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/database.svg b/app/javascript/material-icons/400-24px/database.svg
index 54ca2f4e56..a3bc2bfbc2 100644
--- a/app/javascript/material-icons/400-24px/database.svg
+++ b/app/javascript/material-icons/400-24px/database.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/diamond-fill.svg b/app/javascript/material-icons/400-24px/diamond-fill.svg
new file mode 100644
index 0000000000..474968ad6f
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/diamond-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/diamond.svg b/app/javascript/material-icons/400-24px/diamond.svg
index 26f4814b44..b604492fa8 100644
--- a/app/javascript/material-icons/400-24px/diamond.svg
+++ b/app/javascript/material-icons/400-24px/diamond.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/filter_alt-fill.svg b/app/javascript/material-icons/400-24px/filter_alt-fill.svg
new file mode 100644
index 0000000000..ec1d90bba6
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/filter_alt-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/filter_alt.svg b/app/javascript/material-icons/400-24px/filter_alt.svg
index 0294cf1da5..e4af9efd5d 100644
--- a/app/javascript/material-icons/400-24px/filter_alt.svg
+++ b/app/javascript/material-icons/400-24px/filter_alt.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/groups-fill.svg b/app/javascript/material-icons/400-24px/groups-fill.svg
new file mode 100644
index 0000000000..754eb0946c
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/groups-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/groups.svg b/app/javascript/material-icons/400-24px/groups.svg
index 0e795eb301..998ff03729 100644
--- a/app/javascript/material-icons/400-24px/groups.svg
+++ b/app/javascript/material-icons/400-24px/groups.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/hide_source-fill.svg b/app/javascript/material-icons/400-24px/hide_source-fill.svg
new file mode 100644
index 0000000000..959631bc1a
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/hide_source-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/hide_source.svg b/app/javascript/material-icons/400-24px/hide_source.svg
index d103ed770a..09633cef8c 100644
--- a/app/javascript/material-icons/400-24px/hide_source.svg
+++ b/app/javascript/material-icons/400-24px/hide_source.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/inbox-fill.svg b/app/javascript/material-icons/400-24px/inbox-fill.svg
new file mode 100644
index 0000000000..15ae2d8f3c
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/inbox-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/inbox.svg b/app/javascript/material-icons/400-24px/inbox.svg
index 427817958c..32c727e810 100644
--- a/app/javascript/material-icons/400-24px/inbox.svg
+++ b/app/javascript/material-icons/400-24px/inbox.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/list-fill.svg b/app/javascript/material-icons/400-24px/list-fill.svg
new file mode 100644
index 0000000000..c9cbe35eb5
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/list-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/list.svg b/app/javascript/material-icons/400-24px/list.svg
index 457a820ab1..c9cbe35eb5 100644
--- a/app/javascript/material-icons/400-24px/list.svg
+++ b/app/javascript/material-icons/400-24px/list.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/mail.svg b/app/javascript/material-icons/400-24px/mail.svg
index a92ea7b198..15e1d12d4e 100644
--- a/app/javascript/material-icons/400-24px/mail.svg
+++ b/app/javascript/material-icons/400-24px/mail.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/mood-fill.svg b/app/javascript/material-icons/400-24px/mood-fill.svg
new file mode 100644
index 0000000000..9480d0fb92
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/mood-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/mood.svg b/app/javascript/material-icons/400-24px/mood.svg
index 27b3534244..46cafa7680 100644
--- a/app/javascript/material-icons/400-24px/mood.svg
+++ b/app/javascript/material-icons/400-24px/mood.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/report-fill.svg b/app/javascript/material-icons/400-24px/report-fill.svg
new file mode 100644
index 0000000000..50c638869d
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/report-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/report.svg b/app/javascript/material-icons/400-24px/report.svg
index f281f0e1fa..b08b5a1c98 100644
--- a/app/javascript/material-icons/400-24px/report.svg
+++ b/app/javascript/material-icons/400-24px/report.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/safety_check-fill.svg b/app/javascript/material-icons/400-24px/safety_check-fill.svg
new file mode 100644
index 0000000000..b38091a8ec
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/safety_check-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/safety_check.svg b/app/javascript/material-icons/400-24px/safety_check.svg
index f4eab46fb7..87bdba21fe 100644
--- a/app/javascript/material-icons/400-24px/safety_check.svg
+++ b/app/javascript/material-icons/400-24px/safety_check.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/speed-fill.svg b/app/javascript/material-icons/400-24px/speed-fill.svg
new file mode 100644
index 0000000000..dca22ac521
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/speed-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/speed.svg b/app/javascript/material-icons/400-24px/speed.svg
index ceb855c684..0837877f42 100644
--- a/app/javascript/material-icons/400-24px/speed.svg
+++ b/app/javascript/material-icons/400-24px/speed.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/trending_up-fill.svg b/app/javascript/material-icons/400-24px/trending_up-fill.svg
new file mode 100644
index 0000000000..cd0e368964
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/trending_up-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/trending_up.svg b/app/javascript/material-icons/400-24px/trending_up.svg
index 06f9ba2063..cd0e368964 100644
--- a/app/javascript/material-icons/400-24px/trending_up.svg
+++ b/app/javascript/material-icons/400-24px/trending_up.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 4d217cf5b7..062c7f1122 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -226,6 +226,10 @@ $content-width: 840px;
gap: 5px;
white-space: nowrap;
+ @media screen and (max-width: $mobile-breakpoint) {
+ flex: 1 0 50%;
+ }
+
&:hover,
&:focus,
&:active {
@@ -1070,6 +1074,10 @@ a.name-tag,
}
}
+ .icon {
+ vertical-align: middle;
+ }
+
a.announcements-list__item__title {
&:hover,
&:focus,
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 498cfe9639..801906d5c2 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -3611,6 +3611,7 @@ $ui-header-logo-wordmark-width: 99px;
overflow-y: auto;
width: 100%;
height: 100%;
+ z-index: 0;
}
.drawer__inner__mastodon {
diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss
index af8ccf5b38..0cbf5c1d55 100644
--- a/app/javascript/styles/mastodon/tables.scss
+++ b/app/javascript/styles/mastodon/tables.scss
@@ -137,6 +137,7 @@ a.table-action-link {
padding: 0 10px;
color: $darker-text-color;
font-weight: 500;
+ white-space: nowrap;
&:hover {
color: $highlight-text-color;
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index 7f9429c5e4..ce5cfb688c 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -42,6 +42,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def process_status
@tags = []
@mentions = []
+ @unresolved_mentions = []
@silenced_account_ids = []
@params = {}
@@ -55,6 +56,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end
resolve_thread(@status)
+ resolve_unresolved_mentions(@status)
fetch_replies(@status)
distribute
forward_for_reply
@@ -197,6 +199,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
return if account.nil?
@mentions << Mention.new(account: account, silent: false)
+ rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError
+ @unresolved_mentions << tag['href']
end
def process_emoji(tag)
@@ -301,6 +305,12 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
ThreadResolveWorker.perform_async(status.id, in_reply_to_uri, { 'request_id' => @options[:request_id] })
end
+ def resolve_unresolved_mentions(status)
+ @unresolved_mentions.uniq.each do |uri|
+ MentionResolveWorker.perform_in(rand(30...600).seconds, status.id, uri, { 'request_id' => @options[:request_id] })
+ end
+ end
+
def fetch_replies(status)
collection = @object['replies']
return if collection.blank?
diff --git a/app/lib/vacuum/imports_vacuum.rb b/app/lib/vacuum/imports_vacuum.rb
index 700bd81847..b67865194f 100644
--- a/app/lib/vacuum/imports_vacuum.rb
+++ b/app/lib/vacuum/imports_vacuum.rb
@@ -9,10 +9,10 @@ class Vacuum::ImportsVacuum
private
def clean_unconfirmed_imports!
- BulkImport.state_unconfirmed.where(created_at: ..10.minutes.ago).reorder(nil).in_batches.delete_all
+ BulkImport.state_unconfirmed.where(created_at: ..10.minutes.ago).in_batches.delete_all
end
def clean_old_imports!
- BulkImport.where(created_at: ..1.week.ago).reorder(nil).in_batches.delete_all
+ BulkImport.where(created_at: ..1.week.ago).in_batches.delete_all
end
end
diff --git a/app/lib/web_push_request.rb b/app/lib/web_push_request.rb
new file mode 100644
index 0000000000..a43e22480e
--- /dev/null
+++ b/app/lib/web_push_request.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+class WebPushRequest
+ SIGNATURE_ALGORITHM = 'p256ecdsa'
+ AUTH_HEADER = 'WebPush'
+ PAYLOAD_EXPIRATION = 24.hours
+ JWT_ALGORITHM = 'ES256'
+ JWT_TYPE = 'JWT'
+
+ attr_reader :web_push_subscription
+
+ delegate(
+ :endpoint,
+ :key_auth,
+ :key_p256dh,
+ to: :web_push_subscription
+ )
+
+ def initialize(web_push_subscription)
+ @web_push_subscription = web_push_subscription
+ end
+
+ def audience
+ @audience ||= Addressable::URI.parse(endpoint).normalized_site
+ end
+
+ def authorization_header
+ [AUTH_HEADER, encoded_json_web_token].join(' ')
+ end
+
+ def crypto_key_header
+ [SIGNATURE_ALGORITHM, vapid_key.public_key_for_push_header].join('=')
+ end
+
+ def encrypt(payload)
+ Webpush::Encryption.encrypt(payload, key_p256dh, key_auth)
+ end
+
+ private
+
+ def encoded_json_web_token
+ JWT.encode(
+ web_token_payload,
+ vapid_key.curve,
+ JWT_ALGORITHM,
+ typ: JWT_TYPE
+ )
+ end
+
+ def web_token_payload
+ {
+ aud: audience,
+ exp: PAYLOAD_EXPIRATION.from_now.to_i,
+ sub: payload_subject,
+ }
+ end
+
+ def payload_subject
+ [:mailto, contact_email].join(':')
+ end
+
+ def vapid_key
+ @vapid_key ||= Webpush::VapidKey.from_keys(
+ Rails.configuration.x.vapid_public_key,
+ Rails.configuration.x.vapid_private_key
+ )
+ end
+
+ def contact_email
+ @contact_email ||= ::Setting.site_contact_email
+ end
+end
diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb
index 42b1c49538..e2f359a8c3 100644
--- a/app/models/account_filter.rb
+++ b/app/models/account_filter.rb
@@ -21,7 +21,7 @@ class AccountFilter
end
def results
- scope = Account.includes(:account_stat, user: [:ips, :invite_request]).without_instance_actor.reorder(nil)
+ scope = Account.includes(:account_stat, user: [:ips, :invite_request]).without_instance_actor
relevant_params.each do |key, value|
next if key.to_s == 'page'
diff --git a/app/models/admin/tag_filter.rb b/app/models/admin/tag_filter.rb
index 6149c52175..5e75757b23 100644
--- a/app/models/admin/tag_filter.rb
+++ b/app/models/admin/tag_filter.rb
@@ -14,7 +14,7 @@ class Admin::TagFilter
end
def results
- scope = Tag.reorder(nil)
+ scope = Tag.all
params.each do |key, value|
next if key == :page
diff --git a/app/models/concerns/account/avatar.rb b/app/models/concerns/account/avatar.rb
index 39f599db18..5ca8fa862f 100644
--- a/app/models/concerns/account/avatar.rb
+++ b/app/models/concerns/account/avatar.rb
@@ -6,10 +6,13 @@ module Account::Avatar
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
LIMIT = 2.megabytes
+ AVATAR_DIMENSIONS = [400, 400].freeze
+ AVATAR_GEOMETRY = [AVATAR_DIMENSIONS.first, AVATAR_DIMENSIONS.last].join('x')
+
class_methods do
def avatar_styles(file)
- styles = { original: { geometry: '400x400#', file_geometry_parser: FastGeometryParser } }
- styles[:static] = { geometry: '400x400#', format: 'png', convert_options: '-coalesce', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif'
+ styles = { original: { geometry: "#{AVATAR_GEOMETRY}#", file_geometry_parser: FastGeometryParser } }
+ styles[:static] = { geometry: "#{AVATAR_GEOMETRY}#", format: 'png', convert_options: '-coalesce', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif'
styles
end
diff --git a/app/models/concerns/account/header.rb b/app/models/concerns/account/header.rb
index 44ae774e94..2a47097fcf 100644
--- a/app/models/concerns/account/header.rb
+++ b/app/models/concerns/account/header.rb
@@ -5,7 +5,10 @@ module Account::Header
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
LIMIT = 2.megabytes
- MAX_PIXELS = 750_000 # 1500x500px
+
+ HEADER_DIMENSIONS = [1500, 500].freeze
+ HEADER_GEOMETRY = [HEADER_DIMENSIONS.first, HEADER_DIMENSIONS.last].join('x')
+ MAX_PIXELS = HEADER_DIMENSIONS.first * HEADER_DIMENSIONS.last
class_methods do
def header_styles(file)
diff --git a/app/models/remote_follow.rb b/app/models/remote_follow.rb
index 10715ac97d..fa0586f57e 100644
--- a/app/models/remote_follow.rb
+++ b/app/models/remote_follow.rb
@@ -3,7 +3,6 @@
class RemoteFollow
include ActiveModel::Validations
include RoutingHelper
- include WebfingerHelper
attr_accessor :acct, :addressable_template
@@ -66,7 +65,7 @@ class RemoteFollow
end
def acct_resource
- @acct_resource ||= webfinger!("acct:#{acct}")
+ @acct_resource ||= Webfinger.new("acct:#{acct}").perform
rescue Webfinger::Error, HTTP::ConnectionError
nil
end
diff --git a/app/models/report_filter.rb b/app/models/report_filter.rb
index fd0e23cb81..9d2b0fb374 100644
--- a/app/models/report_filter.rb
+++ b/app/models/report_filter.rb
@@ -18,13 +18,25 @@ class ReportFilter
def results
scope = Report.unresolved
- params.each do |key, value|
+ relevant_params.each do |key, value|
scope = scope.merge scope_for(key, value)
end
scope
end
+ private
+
+ def relevant_params
+ params.tap do |args|
+ args.delete(:target_origin) if origin_is_remote_and_domain_present?
+ end
+ end
+
+ def origin_is_remote_and_domain_present?
+ params[:target_origin] == 'remote' && params[:by_target_domain].present?
+ end
+
def scope_for(key, value)
case key.to_sym
when :by_target_domain
diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb
index d0a77daf0a..8b8e533d30 100644
--- a/app/models/session_activation.rb
+++ b/app/models/session_activation.rb
@@ -28,6 +28,8 @@ class SessionActivation < ApplicationRecord
before_create :assign_access_token
+ DEFAULT_SCOPES = %w(read write follow).freeze
+
class << self
def active?(id)
id && exists?(session_id: id)
@@ -64,7 +66,7 @@ class SessionActivation < ApplicationRecord
{
application_id: Doorkeeper::Application.find_by(superapp: true)&.id,
resource_owner_id: user_id,
- scopes: 'read write follow',
+ scopes: DEFAULT_SCOPES.join(' '),
expires_in: Doorkeeper.configuration.access_token_expires_in,
use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?,
}
diff --git a/app/models/user.rb b/app/models/user.rb
index df8444e2d3..e685ce65d3 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -71,7 +71,8 @@ class User < ApplicationRecord
ACTIVE_DURATION = ENV.fetch('USER_ACTIVE_DAYS', 7).to_i.days.freeze
devise :two_factor_authenticatable,
- otp_secret_encryption_key: Rails.configuration.x.otp_secret
+ otp_secret_encryption_key: Rails.configuration.x.otp_secret,
+ otp_secret_length: 32
include LegacyOtpSecret # Must be after the above `devise` line in order to override the legacy method
diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb
index ddfd08146e..9d30881bf3 100644
--- a/app/models/web/push_subscription.rb
+++ b/app/models/web/push_subscription.rb
@@ -29,26 +29,6 @@ class Web::PushSubscription < ApplicationRecord
delegate :locale, to: :associated_user
- def encrypt(payload)
- Webpush::Encryption.encrypt(payload, key_p256dh, key_auth)
- end
-
- def audience
- @audience ||= Addressable::URI.parse(endpoint).normalized_site
- end
-
- def crypto_key_header
- p256ecdsa = vapid_key.public_key_for_push_header
-
- "p256ecdsa=#{p256ecdsa}"
- end
-
- def authorization_header
- jwt = JWT.encode({ aud: audience, exp: 24.hours.from_now.to_i, sub: "mailto:#{contact_email}" }, vapid_key.curve, 'ES256', typ: 'JWT')
-
- "WebPush #{jwt}"
- end
-
def pushable?(notification)
policy_allows_notification?(notification) && alert_enabled_for_notification_type?(notification)
end
@@ -92,14 +72,6 @@ class Web::PushSubscription < ApplicationRecord
)
end
- def vapid_key
- @vapid_key ||= Webpush::VapidKey.from_keys(Rails.configuration.x.vapid_public_key, Rails.configuration.x.vapid_private_key)
- end
-
- def contact_email
- @contact_email ||= ::Setting.site_contact_email
- end
-
def alert_enabled_for_notification_type?(notification)
truthy?(data&.dig('alerts', notification.type.to_s))
end
diff --git a/app/services/activitypub/fetch_remote_actor_service.rb b/app/services/activitypub/fetch_remote_actor_service.rb
index 2c372c2ec3..560cf424e1 100644
--- a/app/services/activitypub/fetch_remote_actor_service.rb
+++ b/app/services/activitypub/fetch_remote_actor_service.rb
@@ -3,7 +3,6 @@
class ActivityPub::FetchRemoteActorService < BaseService
include JsonLdHelper
include DomainControlHelper
- include WebfingerHelper
class Error < StandardError; end
@@ -45,7 +44,7 @@ class ActivityPub::FetchRemoteActorService < BaseService
private
def check_webfinger!
- webfinger = webfinger!("acct:#{@username}@#{@domain}")
+ webfinger = Webfinger.new("acct:#{@username}@#{@domain}").perform
confirmed_username, confirmed_domain = split_acct(webfinger.subject)
if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
@@ -54,7 +53,7 @@ class ActivityPub::FetchRemoteActorService < BaseService
return
end
- webfinger = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}")
+ webfinger = Webfinger.new("acct:#{confirmed_username}@#{confirmed_domain}").perform
@username, @domain = split_acct(webfinger.subject)
raise Webfinger::RedirectError, "Too many webfinger redirects for URI #{@uri} (stopped at #{@username}@#{@domain})" unless confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
diff --git a/app/services/activitypub/process_collection_service.rb b/app/services/activitypub/process_collection_service.rb
index 4f049a5ae9..cadc7d2d10 100644
--- a/app/services/activitypub/process_collection_service.rb
+++ b/app/services/activitypub/process_collection_service.rb
@@ -2,6 +2,7 @@
class ActivityPub::ProcessCollectionService < BaseService
include JsonLdHelper
+ include DomainControlHelper
def call(body, actor, **options)
@account = actor
@@ -69,6 +70,9 @@ class ActivityPub::ProcessCollectionService < BaseService
end
def verify_account!
+ return unless @json['signature'].is_a?(Hash)
+ return if domain_not_allowed?(@json['signature']['creator'])
+
@options[:relayed_through_actor] = @account
@account = ActivityPub::LinkedDataSignature.new(@json).verify_actor!
@account = nil unless @account.is_a?(Account)
diff --git a/app/services/purge_domain_service.rb b/app/services/purge_domain_service.rb
index ca0f0d441f..feab8aa1dd 100644
--- a/app/services/purge_domain_service.rb
+++ b/app/services/purge_domain_service.rb
@@ -16,12 +16,12 @@ class PurgeDomainService < BaseService
end
def purge_accounts!
- Account.remote.where(domain: @domain).reorder(nil).find_each do |account|
+ Account.remote.where(domain: @domain).find_each do |account|
DeleteAccountService.new.call(account, reserve_username: false, skip_side_effects: true)
end
end
def purge_emojis!
- CustomEmoji.remote.where(domain: @domain).reorder(nil).find_each(&:destroy)
+ CustomEmoji.remote.where(domain: @domain).find_each(&:destroy)
end
end
diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb
index 8a5863baba..cd96b55c74 100644
--- a/app/services/resolve_account_service.rb
+++ b/app/services/resolve_account_service.rb
@@ -2,7 +2,6 @@
class ResolveAccountService < BaseService
include DomainControlHelper
- include WebfingerHelper
include Redisable
include Lockable
@@ -81,7 +80,7 @@ class ResolveAccountService < BaseService
end
def process_webfinger!(uri)
- @webfinger = webfinger!("acct:#{uri}")
+ @webfinger = Webfinger.new("acct:#{uri}").perform
confirmed_username, confirmed_domain = split_acct(@webfinger.subject)
if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
@@ -91,7 +90,7 @@ class ResolveAccountService < BaseService
end
# Account doesn't match, so it may have been redirected
- @webfinger = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}")
+ @webfinger = Webfinger.new("acct:#{confirmed_username}@#{confirmed_domain}").perform
@username, @domain = split_acct(@webfinger.subject)
raise Webfinger::RedirectError, "Too many webfinger redirects for URI #{uri} (stopped at #{@username}@#{@domain})" unless confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
diff --git a/app/views/admin/action_logs/index.html.haml b/app/views/admin/action_logs/index.html.haml
index c02c8f0ad4..a5d4188294 100644
--- a/app/views/admin/action_logs/index.html.haml
+++ b/app/views/admin/action_logs/index.html.haml
@@ -16,7 +16,7 @@
%strong= t('admin.action_logs.filter_by_action')
.input.select.optional
= form.select :action_type,
- options_for_select(Admin::ActionLogFilter::ACTION_TYPE_MAP.keys.map { |key| [I18n.t("admin.action_logs.action_types.#{key}"), key] }, params[:action_type]),
+ options_for_select(sorted_action_log_types, params[:action_type]),
prompt: I18n.t('admin.accounts.moderation.all')
- if @action_logs.empty?
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 27d8f4790b..2b4d02fa67 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -2,9 +2,7 @@
= t('admin.dashboard.title')
- content_for :heading_actions do
- = l(@time_period.first)
- = ' - '
- = l(@time_period.last)
+ = date_range(@time_period)
- unless @system_checks.empty?
.flash-message-stack
diff --git a/app/views/admin/email_domain_blocks/new.html.haml b/app/views/admin/email_domain_blocks/new.html.haml
index dd4b83ee3f..2dfdca9376 100644
--- a/app/views/admin/email_domain_blocks/new.html.haml
+++ b/app/views/admin/email_domain_blocks/new.html.haml
@@ -16,7 +16,7 @@
label: I18n.t('admin.email_domain_blocks.allow_registrations_with_approval'),
wrapper: :with_label
- - if defined?(@resolved_records)
+ - if defined?(@resolved_records) && @resolved_records.any?
%p.hint= t('admin.email_domain_blocks.resolved_dns_records_hint_html')
.batch-table
diff --git a/app/views/admin/instances/_dashboard.html.haml b/app/views/admin/instances/_dashboard.html.haml
new file mode 100644
index 0000000000..ef8500103b
--- /dev/null
+++ b/app/views/admin/instances/_dashboard.html.haml
@@ -0,0 +1,66 @@
+-# locals: (instance_domain:, period_end_at:, period_start_at:)
+%p
+ = material_symbol 'info'
+ = t('admin.instances.totals_time_period_hint_html')
+
+.dashboard
+ .dashboard__item
+ = react_admin_component :counter,
+ end_at: period_end_at,
+ href: admin_accounts_path(origin: 'remote', by_domain: instance_domain),
+ label: t('admin.instances.dashboard.instance_accounts_measure'),
+ measure: 'instance_accounts',
+ params: { domain: instance_domain },
+ start_at: period_start_at
+ .dashboard__item
+ = react_admin_component :counter,
+ end_at: period_end_at,
+ label: t('admin.instances.dashboard.instance_statuses_measure'),
+ measure: 'instance_statuses',
+ params: { domain: instance_domain },
+ start_at: period_start_at
+ .dashboard__item
+ = react_admin_component :counter,
+ end_at: period_end_at,
+ label: t('admin.instances.dashboard.instance_media_attachments_measure'),
+ measure: 'instance_media_attachments',
+ params: { domain: instance_domain },
+ start_at: period_start_at
+ .dashboard__item
+ = react_admin_component :counter,
+ end_at: period_end_at,
+ label: t('admin.instances.dashboard.instance_follows_measure'),
+ measure: 'instance_follows',
+ params: { domain: instance_domain },
+ start_at: period_start_at
+ .dashboard__item
+ = react_admin_component :counter,
+ end_at: period_end_at,
+ label: t('admin.instances.dashboard.instance_followers_measure'),
+ measure: 'instance_followers',
+ params: { domain: instance_domain },
+ start_at: period_start_at
+ .dashboard__item
+ = react_admin_component :counter,
+ end_at: period_end_at,
+ href: admin_reports_path(by_target_domain: instance_domain),
+ label: t('admin.instances.dashboard.instance_reports_measure'),
+ measure: 'instance_reports',
+ params: { domain: instance_domain },
+ start_at: period_start_at
+ .dashboard__item
+ = react_admin_component :dimension,
+ dimension: 'instance_accounts',
+ end_at: period_end_at,
+ label: t('admin.instances.dashboard.instance_accounts_dimension'),
+ limit: 8,
+ params: { domain: instance_domain },
+ start_at: period_start_at
+ .dashboard__item
+ = react_admin_component :dimension,
+ dimension: 'instance_languages',
+ end_at: period_end_at,
+ label: t('admin.instances.dashboard.instance_languages_dimension'),
+ limit: 8,
+ params: { domain: instance_domain },
+ start_at: period_start_at
diff --git a/app/views/admin/instances/show.html.haml b/app/views/admin/instances/show.html.haml
index c55eb89dc9..812a9c8870 100644
--- a/app/views/admin/instances/show.html.haml
+++ b/app/views/admin/instances/show.html.haml
@@ -3,77 +3,10 @@
- if current_user.can?(:view_dashboard)
- content_for :heading_actions do
- = l(@time_period.first)
- = ' - '
- = l(@time_period.last)
+ = date_range(@time_period)
- if @instance.persisted?
- %p
- = material_symbol 'info'
- = t('admin.instances.totals_time_period_hint_html')
-
- .dashboard
- .dashboard__item
- = react_admin_component :counter,
- end_at: @time_period.last,
- href: admin_accounts_path(origin: 'remote', by_domain: @instance.domain),
- label: t('admin.instances.dashboard.instance_accounts_measure'),
- measure: 'instance_accounts',
- params: { domain: @instance.domain },
- start_at: @time_period.first
- .dashboard__item
- = react_admin_component :counter,
- end_at: @time_period.last,
- label: t('admin.instances.dashboard.instance_statuses_measure'),
- measure: 'instance_statuses',
- params: { domain: @instance.domain },
- start_at: @time_period.first
- .dashboard__item
- = react_admin_component :counter,
- end_at: @time_period.last,
- label: t('admin.instances.dashboard.instance_media_attachments_measure'),
- measure: 'instance_media_attachments',
- params: { domain: @instance.domain },
- start_at: @time_period.first
- .dashboard__item
- = react_admin_component :counter,
- end_at: @time_period.last,
- label: t('admin.instances.dashboard.instance_follows_measure'),
- measure: 'instance_follows',
- params: { domain: @instance.domain },
- start_at: @time_period.first
- .dashboard__item
- = react_admin_component :counter,
- end_at: @time_period.last,
- label: t('admin.instances.dashboard.instance_followers_measure'),
- measure: 'instance_followers',
- params: { domain: @instance.domain },
- start_at: @time_period.first
- .dashboard__item
- = react_admin_component :counter,
- end_at: @time_period.last,
- href: admin_reports_path(by_target_domain: @instance.domain),
- label: t('admin.instances.dashboard.instance_reports_measure'),
- measure: 'instance_reports',
- params: { domain: @instance.domain },
- start_at: @time_period.first
- .dashboard__item
- = react_admin_component :dimension,
- dimension: 'instance_accounts',
- end_at: @time_period.last,
- label: t('admin.instances.dashboard.instance_accounts_dimension'),
- limit: 8,
- params: { domain: @instance.domain },
- start_at: @time_period.first
- .dashboard__item
- = react_admin_component :dimension,
- dimension: 'instance_languages',
- end_at: @time_period.last,
- label: t('admin.instances.dashboard.instance_languages_dimension'),
- limit: 8,
- params: { domain: @instance.domain },
- start_at: @time_period.first
-
+ = render 'dashboard', instance_domain: @instance.domain, period_end_at: @time_period.last, period_start_at: @time_period.first
- else
%p
= t('admin.instances.unknown_instance')
diff --git a/app/views/admin/invites/_invite.html.haml b/app/views/admin/invites/_invite.html.haml
index 8bd5f10fee..53eac1d0cd 100644
--- a/app/views/admin/invites/_invite.html.haml
+++ b/app/views/admin/invites/_invite.html.haml
@@ -2,7 +2,7 @@
%td
.input-copy
.input-copy__wrapper
- %input{ type: :text, maxlength: '999', spellcheck: 'false', readonly: 'true', value: public_invite_url(invite_code: invite.code) }
+ = copyable_input value: public_invite_url(invite_code: invite.code)
%button{ type: :button }= t('generic.copy')
%td
diff --git a/app/views/admin/tags/show.html.haml b/app/views/admin/tags/show.html.haml
index 93387843b2..462ca312a0 100644
--- a/app/views/admin/tags/show.html.haml
+++ b/app/views/admin/tags/show.html.haml
@@ -4,9 +4,7 @@
- content_for :heading_actions do
- if current_user.can?(:view_dashboard)
.time-period
- = l(@time_period.first)
- = ' - '
- = l(@time_period.last)
+ = date_range(@time_period)
= link_to t('admin.tags.open'), tag_url(@tag), class: 'button', target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/invites/_invite.html.haml b/app/views/invites/_invite.html.haml
index 892fdc5a0e..7c94062de4 100644
--- a/app/views/invites/_invite.html.haml
+++ b/app/views/invites/_invite.html.haml
@@ -2,7 +2,7 @@
%td
.input-copy
.input-copy__wrapper
- %input{ type: :text, maxlength: '999', spellcheck: 'false', readonly: 'true', value: public_invite_url(invite_code: invite.code) }
+ = copyable_input value: public_invite_url(invite_code: invite.code)
%button{ type: :button }= t('generic.copy')
- if invite.valid_for_use?
diff --git a/app/views/oauth/authorizations/show.html.haml b/app/views/oauth/authorizations/show.html.haml
index a5122a87fc..bdff336368 100644
--- a/app/views/oauth/authorizations/show.html.haml
+++ b/app/views/oauth/authorizations/show.html.haml
@@ -3,5 +3,5 @@
%p= t('doorkeeper.authorizations.show.title')
.input-copy
.input-copy__wrapper
- %input.oauth-code{ type: 'text', spellcheck: 'false', readonly: true, value: params[:code] }
+ = copyable_input value: params[:code], class: 'oauth-code'
%button{ type: :button }= t('generic.copy')
diff --git a/app/views/settings/applications/_form.html.haml b/app/views/settings/applications/_form.html.haml
index 66ea8bc12b..85fdefa0fc 100644
--- a/app/views/settings/applications/_form.html.haml
+++ b/app/views/settings/applications/_form.html.haml
@@ -18,7 +18,7 @@
.field-group
.input.with_block_label
- %label= t('activerecord.attributes.doorkeeper/application.scopes')
+ = form.label t('activerecord.attributes.doorkeeper/application.scopes'), required: true
%span.hint= t('simple_form.hints.defaults.scopes')
- Doorkeeper.configuration.scopes.group_by { |s| s.split(':').first }.each_value do |value|
@@ -29,7 +29,7 @@
hint: false,
include_blank: false,
item_wrapper_tag: 'li',
- label_method: ->(scope) { safe_join([content_tag(:samp, scope, class: class_for_scope(scope)), content_tag(:span, t("doorkeeper.scopes.#{scope}"), class: 'hint')]) },
+ label_method: ->(scope) { label_for_scope(scope) },
label: false,
required: false,
selected: form.object.scopes.all,
diff --git a/app/views/settings/applications/index.html.haml b/app/views/settings/applications/index.html.haml
index 80eaed5c39..e3011947a6 100644
--- a/app/views/settings/applications/index.html.haml
+++ b/app/views/settings/applications/index.html.haml
@@ -18,7 +18,9 @@
- @applications.each do |application|
%tr
%td= link_to application.name, settings_application_path(application)
- %th= application.scopes
+ %th
+ - application.scopes.to_s.split.each do |scope|
+ = tag.samp(scope, class: 'information-badge', title: t("doorkeeper.scopes.#{scope}"))
%td
= table_link_to 'close', t('doorkeeper.applications.index.delete'), settings_application_path(application), method: :delete, data: { confirm: t('doorkeeper.applications.confirmations.destroy') }
diff --git a/app/views/settings/applications/show.html.haml b/app/views/settings/applications/show.html.haml
index 19630cf49b..bfde27daa9 100644
--- a/app/views/settings/applications/show.html.haml
+++ b/app/views/settings/applications/show.html.haml
@@ -15,15 +15,16 @@
%td
%code= @application.secret
%tr
- %th{ rowspan: 2 }= t('applications.your_token')
+ %th= t('applications.your_token')
%td
%code= current_user.token_for_app(@application).token
%tr
+ %th
%td= table_link_to 'refresh', t('applications.regenerate_token'), regenerate_settings_application_path(@application), method: :post
%hr/
-= simple_form_for @application, url: settings_application_path(@application), method: :put do |form|
+= simple_form_for @application, url: settings_application_path(@application) do |form|
= render form
.actions
diff --git a/app/views/settings/exports/show.html.haml b/app/views/settings/exports/show.html.haml
index 320bb0c7ce..273c5a4ba6 100644
--- a/app/views/settings/exports/show.html.haml
+++ b/app/views/settings/exports/show.html.haml
@@ -61,7 +61,8 @@
%tbody
- @backups.each do |backup|
%tr
- %td= l backup.created_at
+ %td
+ %time.formatted{ datetime: backup.created_at.iso8601, title: l(backup.created_at) }= l backup.created_at
- if backup.processed?
%td= number_to_human_size backup.dump_file_size
%td= table_link_to 'download', t('exports.archive_takeout.download'), download_backup_url(backup)
diff --git a/app/views/settings/imports/index.html.haml b/app/views/settings/imports/index.html.haml
index 634631b5aa..55421991e1 100644
--- a/app/views/settings/imports/index.html.haml
+++ b/app/views/settings/imports/index.html.haml
@@ -55,7 +55,10 @@
= t("imports.states.#{import.state}")
%td
#{import.imported_items} / #{import.total_items}
- %td= l(import.created_at)
+ %td
+ %time.formatted{ datetime: import.created_at.iso8601, title: l(import.created_at) }
+ = l(import.created_at)
+
%td
- num_failed = import.processed_items - import.imported_items
- if num_failed.positive?
diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml
index b0cd1cba09..0d127bc81d 100644
--- a/app/views/settings/preferences/appearance/show.html.haml
+++ b/app/views/settings/preferences/appearance/show.html.haml
@@ -4,7 +4,7 @@
- content_for :heading_actions do
= button_tag t('generic.save_changes'), class: 'button', form: 'edit_user'
-= simple_form_for current_user, url: settings_preferences_appearance_path, html: { method: :put, id: 'edit_user' } do |f|
+= simple_form_for current_user, url: settings_preferences_appearance_path, html: { id: :edit_user } do |f|
.fields-row
.fields-group.fields-row__column.fields-row__column-6
= f.input :locale,
diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml
index 17d5ac20fa..9fc293f0a2 100644
--- a/app/views/settings/preferences/notifications/show.html.haml
+++ b/app/views/settings/preferences/notifications/show.html.haml
@@ -4,7 +4,7 @@
- content_for :heading_actions do
= button_tag t('generic.save_changes'), class: 'button', form: 'edit_notification'
-= simple_form_for current_user, url: settings_preferences_notifications_path, html: { method: :put, id: 'edit_notification' } do |f|
+= simple_form_for current_user, url: settings_preferences_notifications_path, html: { id: :edit_notification } do |f|
= render 'shared/error_messages', object: current_user
%h4= t 'notifications.email_events'
diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml
index 49d03ca00a..b260826fe5 100644
--- a/app/views/settings/preferences/other/show.html.haml
+++ b/app/views/settings/preferences/other/show.html.haml
@@ -4,7 +4,7 @@
- content_for :heading_actions do
= button_tag t('generic.save_changes'), class: 'button', form: 'edit_preferences'
-= simple_form_for current_user, url: settings_preferences_other_path, html: { method: :put, id: 'edit_preferences' } do |f|
+= simple_form_for current_user, url: settings_preferences_other_path, html: { id: :edit_preferences } do |f|
= render 'shared/error_messages', object: current_user
= f.simple_fields_for :settings, current_user.settings do |ff|
diff --git a/app/views/settings/privacy/show.html.haml b/app/views/settings/privacy/show.html.haml
index 3fb14023b0..e8e5bdb906 100644
--- a/app/views/settings/privacy/show.html.haml
+++ b/app/views/settings/privacy/show.html.haml
@@ -5,7 +5,7 @@
%h2= t('settings.profile')
= render partial: 'settings/shared/profile_navigation'
-= simple_form_for @account, url: settings_privacy_path, html: { method: :put } do |f|
+= simple_form_for @account, url: settings_privacy_path do |f|
= render 'shared/error_messages', object: @account
%p.lead= t('privacy.hint_html')
diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml
index 8fb2132519..427a4fa95a 100644
--- a/app/views/settings/profiles/show.html.haml
+++ b/app/views/settings/profiles/show.html.haml
@@ -5,7 +5,7 @@
%h2= t('settings.profile')
= render partial: 'settings/shared/profile_navigation'
-= simple_form_for @account, url: settings_profile_path, html: { method: :put, id: 'edit_profile' } do |f|
+= simple_form_for @account, url: settings_profile_path, html: { id: :edit_profile } do |f|
= render 'shared/error_messages', object: @account
%p.lead= t('edit_profile.hint_html')
@@ -34,7 +34,7 @@
.fields-row__column.fields-row__column-6
.fields-group
= f.input :avatar,
- hint: t('simple_form.hints.defaults.avatar', dimensions: '400x400', size: number_to_human_size(Account::Avatar::LIMIT)),
+ hint: t('simple_form.hints.defaults.avatar', dimensions: Account::Avatar::AVATAR_GEOMETRY, size: number_to_human_size(Account::Avatar::LIMIT)),
input_html: { accept: Account::Avatar::IMAGE_MIME_TYPES.join(',') },
wrapper: :with_block_label
@@ -50,7 +50,7 @@
.fields-row__column.fields-row__column-6
.fields-group
= f.input :header,
- hint: t('simple_form.hints.defaults.header', dimensions: '1500x500', size: number_to_human_size(Account::Header::LIMIT)),
+ hint: t('simple_form.hints.defaults.header', dimensions: Account::Header::HEADER_GEOMETRY, size: number_to_human_size(Account::Header::LIMIT)),
input_html: { accept: Account::Header::IMAGE_MIME_TYPES.join(',') },
wrapper: :with_block_label
diff --git a/app/views/settings/shared/_profile_navigation.html.haml b/app/views/settings/shared/_profile_navigation.html.haml
index d490bd7566..2f81cb5cfd 100644
--- a/app/views/settings/shared/_profile_navigation.html.haml
+++ b/app/views/settings/shared/_profile_navigation.html.haml
@@ -1,7 +1,7 @@
.content__heading__tabs
= render_navigation renderer: :links do |primary|
:ruby
- primary.item :profile, safe_join([material_symbol('person'), t('settings.edit_profile')]), settings_profile_path
- primary.item :privacy, safe_join([material_symbol('lock'), t('privacy.title')]), settings_privacy_path
+ primary.item :edit_profile, safe_join([material_symbol('person'), t('settings.edit_profile')]), settings_profile_path
+ primary.item :privacy_reach, safe_join([material_symbol('lock'), t('privacy.title')]), settings_privacy_path
primary.item :verification, safe_join([material_symbol('check'), t('verification.verification')]), settings_verification_path
primary.item :featured_tags, safe_join([material_symbol('tag'), t('settings.featured_tags')]), settings_featured_tags_path
diff --git a/app/views/settings/two_factor_authentication/confirmations/new.html.haml b/app/views/settings/two_factor_authentication/confirmations/new.html.haml
index 0b8278a104..a35479b84e 100644
--- a/app/views/settings/two_factor_authentication/confirmations/new.html.haml
+++ b/app/views/settings/two_factor_authentication/confirmations/new.html.haml
@@ -5,7 +5,7 @@
%p.hint= t('otp_authentication.instructions_html')
.qr-wrapper
- .qr-code!= @qrcode.as_svg(padding: 0, module_size: 4)
+ .qr-code!= @qrcode.as_svg(padding: 0, module_size: 4, use_path: true)
.qr-alternative
%p.hint= t('otp_authentication.manual_instructions')
diff --git a/app/views/settings/verifications/show.html.haml b/app/views/settings/verifications/show.html.haml
index 5318b0767d..560807f27c 100644
--- a/app/views/settings/verifications/show.html.haml
+++ b/app/views/settings/verifications/show.html.haml
@@ -16,7 +16,7 @@
.input-copy.lead
.input-copy__wrapper
- %input{ type: :text, maxlength: '999', spellcheck: 'false', readonly: 'true', value: link_to('Mastodon', ActivityPub::TagManager.instance.url_for(@account), rel: 'me').to_str }
+ = copyable_input value: link_to('Mastodon', ActivityPub::TagManager.instance.url_for(@account), rel: :me)
%button{ type: :button }= t('generic.copy')
%p.lead= t('verification.extra_instructions_html')
@@ -31,7 +31,7 @@
= material_symbol 'check', class: 'verified-badge__mark'
%span= field.value
-= simple_form_for @account, url: settings_verification_path, html: { method: :put, class: 'form-section' } do |f|
+= simple_form_for @account, url: settings_verification_path, html: { class: 'form-section' } do |f|
= render 'shared/error_messages', object: @account
%h3= t('author_attribution.title')
@@ -50,13 +50,13 @@
= image_tag frontend_asset_url('images/preview.png'), alt: '', class: 'status-card__image-image'
.status-card__content
%span.status-card__host
- %span= t('author_attribution.s_blog', name: @account.username)
+ %span= t('author_attribution.s_blog', name: display_name(@account))
·
%time.time-ago{ datetime: 1.year.ago.to_date.iso8601 }
%strong.status-card__title= t('author_attribution.example_title')
.more-from-author
= logo_as_symbol(:icon)
- = t('author_attribution.more_from_html', name: link_to(root_url, class: 'story__details__shared__author-link') { image_tag(@account.avatar.url, class: 'account__avatar', width: 16, height: 16, alt: '') + content_tag(:bdi, display_name(@account)) })
+ = t('author_attribution.more_from_html', name: link_to(root_url, class: 'story__details__shared__author-link') { image_tag(@account.avatar.url, class: 'account__avatar', width: 16, height: 16, alt: '') + tag.bdi(display_name(@account)) })
.actions
= f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/severed_relationships/index.html.haml b/app/views/severed_relationships/index.html.haml
index 7c599e9c0e..cc9439b468 100644
--- a/app/views/severed_relationships/index.html.haml
+++ b/app/views/severed_relationships/index.html.haml
@@ -15,7 +15,9 @@
%tbody
- @events.each do |event|
%tr
- %td= l event.created_at
+ %td
+ %time.formatted{ datetime: event.created_at.iso8601, title: l(event.created_at) }
+ = l(event.created_at)
%td= t("severed_relationships.event_type.#{event.type}", target_name: event.target_name)
- if event.purged?
%td{ rowspan: 2 }= t('severed_relationships.purged')
diff --git a/app/workers/filtered_notification_cleanup_worker.rb b/app/workers/filtered_notification_cleanup_worker.rb
index 2b955da3c0..87ff6a9eb5 100644
--- a/app/workers/filtered_notification_cleanup_worker.rb
+++ b/app/workers/filtered_notification_cleanup_worker.rb
@@ -4,6 +4,6 @@ class FilteredNotificationCleanupWorker
include Sidekiq::Worker
def perform(account_id, from_account_id)
- Notification.where(account_id: account_id, from_account_id: from_account_id, filtered: true).reorder(nil).in_batches(order: :desc).delete_all
+ Notification.where(account_id: account_id, from_account_id: from_account_id, filtered: true).in_batches(order: :desc).delete_all
end
end
diff --git a/app/workers/mention_resolve_worker.rb b/app/workers/mention_resolve_worker.rb
new file mode 100644
index 0000000000..72dcd9633f
--- /dev/null
+++ b/app/workers/mention_resolve_worker.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+class MentionResolveWorker
+ include Sidekiq::Worker
+ include ExponentialBackoff
+ include JsonLdHelper
+
+ sidekiq_options queue: 'pull', retry: 7
+
+ def perform(status_id, uri, options = {})
+ status = Status.find_by(id: status_id)
+ return if status.nil?
+
+ account = account_from_uri(uri)
+ account = ActivityPub::FetchRemoteAccountService.new.call(uri, request_id: options[:request_id]) if account.nil?
+
+ return if account.nil?
+
+ status.mentions.create!(account: account, silent: false)
+ rescue ActiveRecord::RecordNotFound
+ # Do nothing
+ rescue Mastodon::UnexpectedResponseError => e
+ response = e.response
+
+ if response_error_unsalvageable?(response)
+ # Give up
+ else
+ raise e
+ end
+ end
+
+ private
+
+ def account_from_uri(uri)
+ ActivityPub::TagManager.instance.uri_to_resource(uri, Account)
+ end
+end
diff --git a/app/workers/scheduler/user_cleanup_scheduler.rb b/app/workers/scheduler/user_cleanup_scheduler.rb
index 9f58d9225b..f755128332 100644
--- a/app/workers/scheduler/user_cleanup_scheduler.rb
+++ b/app/workers/scheduler/user_cleanup_scheduler.rb
@@ -16,7 +16,7 @@ class Scheduler::UserCleanupScheduler
private
def clean_unconfirmed_accounts!
- User.unconfirmed.where(confirmation_sent_at: ..UNCONFIRMED_ACCOUNTS_MAX_AGE_DAYS.days.ago).reorder(nil).find_in_batches do |batch|
+ User.unconfirmed.where(confirmation_sent_at: ..UNCONFIRMED_ACCOUNTS_MAX_AGE_DAYS.days.ago).find_in_batches do |batch|
# We have to do it separately because of missing database constraints
AccountModerationNote.where(target_account_id: batch.map(&:account_id)).delete_all
Account.where(id: batch.map(&:account_id)).delete_all
diff --git a/app/workers/web/push_notification_worker.rb b/app/workers/web/push_notification_worker.rb
index 7e9691aaba..104503f130 100644
--- a/app/workers/web/push_notification_worker.rb
+++ b/app/workers/web/push_notification_worker.rb
@@ -16,10 +16,10 @@ class Web::PushNotificationWorker
# in the meantime, so we have to double-check before proceeding
return unless @notification.activity.present? && @subscription.pushable?(@notification)
- payload = @subscription.encrypt(push_notification_json)
+ payload = web_push_request.encrypt(push_notification_json)
- request_pool.with(@subscription.audience) do |http_client|
- request = Request.new(:post, @subscription.endpoint, body: payload.fetch(:ciphertext), http_client: http_client)
+ request_pool.with(web_push_request.audience) do |http_client|
+ request = Request.new(:post, web_push_request.endpoint, body: payload.fetch(:ciphertext), http_client: http_client)
request.add_headers(
'Content-Type' => 'application/octet-stream',
@@ -27,8 +27,8 @@ class Web::PushNotificationWorker
'Urgency' => URGENCY,
'Content-Encoding' => 'aesgcm',
'Encryption' => "salt=#{Webpush.encode64(payload.fetch(:salt)).delete('=')}",
- 'Crypto-Key' => "dh=#{Webpush.encode64(payload.fetch(:server_public_key)).delete('=')};#{@subscription.crypto_key_header}",
- 'Authorization' => @subscription.authorization_header
+ 'Crypto-Key' => "dh=#{Webpush.encode64(payload.fetch(:server_public_key)).delete('=')};#{web_push_request.crypto_key_header}",
+ 'Authorization' => web_push_request.authorization_header
)
request.perform do |response|
@@ -50,17 +50,27 @@ class Web::PushNotificationWorker
private
- def push_notification_json
- json = I18n.with_locale(@subscription.locale.presence || I18n.default_locale) do
- ActiveModelSerializers::SerializableResource.new(
- @notification,
- serializer: Web::NotificationSerializer,
- scope: @subscription,
- scope_name: :current_push_subscription
- ).as_json
- end
+ def web_push_request
+ @web_push_request || WebPushRequest.new(@subscription)
+ end
- Oj.dump(json)
+ def push_notification_json
+ Oj.dump(serialized_notification_in_subscription_locale.as_json)
+ end
+
+ def serialized_notification_in_subscription_locale
+ I18n.with_locale(@subscription.locale.presence || I18n.default_locale) do
+ serialized_notification
+ end
+ end
+
+ def serialized_notification
+ ActiveModelSerializers::SerializableResource.new(
+ @notification,
+ serializer: Web::NotificationSerializer,
+ scope: @subscription,
+ scope_name: :current_push_subscription
+ )
end
def request_pool
diff --git a/config/navigation.rb b/config/navigation.rb
index f5269d18ce..2480e1741b 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -30,18 +30,18 @@ SimpleNavigation::Configuration.run do |navigation|
n.item :filters, safe_join([material_symbol('filter_alt'), t('filters.index.title')]), filters_path, highlights_on: %r{/filters}, if: -> { current_user.functional? && !self_destruct }
n.item :statuses_cleanup, safe_join([material_symbol('history'), t('settings.statuses_cleanup')]), statuses_cleanup_path, if: -> { current_user.functional_or_moved? && !self_destruct }
- n.item :security, safe_join([material_symbol('lock'), t('settings.account')]), edit_user_registration_path do |s|
- s.item :password, safe_join([material_symbol('lock'), t('settings.account_settings')]), edit_user_registration_path, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases|/settings/login_activities|^/disputes}
+ n.item :security, safe_join([material_symbol('account_circle'), t('settings.account')]), edit_user_registration_path do |s|
+ s.item :password, safe_join([material_symbol('lock'), t('settings.account_settings')]), edit_user_registration_path, highlights_on: %r{^/auth|/settings/delete|/settings/migration|/settings/aliases|/settings/login_activities|^/disputes}
s.item :two_factor_authentication, safe_join([material_symbol('safety_check'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_methods_path, highlights_on: %r{/settings/two_factor_authentication|/settings/otp_authentication|/settings/security_keys}
s.item :authorized_apps, safe_join([material_symbol('list_alt'), t('settings.authorized_apps')]), oauth_authorized_applications_path, if: -> { !self_destruct }
end
n.item :data, safe_join([material_symbol('cloud_sync'), t('settings.import_and_export')]), settings_export_path do |s|
- s.item :import, safe_join([material_symbol('cloud_upload'), t('settings.import')]), settings_imports_path, if: -> { current_user.functional? && !self_destruct }
+ s.item :import, safe_join([material_symbol('cloud_upload'), t('settings.import')]), settings_imports_path, highlights_on: %r{/settings/imports}, if: -> { current_user.functional? && !self_destruct }
s.item :export, safe_join([material_symbol('cloud_download'), t('settings.export')]), settings_export_path
end
- n.item :invites, safe_join([material_symbol('person_add'), t('invites.title')]), invites_path, if: -> { current_user.can?(:invite_users) && current_user.functional? && !self_destruct }
+ n.item :user_invites, safe_join([material_symbol('person_add'), t('invites.title')]), invites_path, if: -> { current_user.can?(:invite_users) && current_user.functional? && !self_destruct }
n.item :development, safe_join([material_symbol('code'), t('settings.development')]), settings_applications_path, highlights_on: %r{/settings/applications}, if: -> { current_user.functional? && !self_destruct }
n.item :trends, safe_join([material_symbol('trending_up'), t('admin.trends.title')]), admin_trends_statuses_path, if: -> { current_user.can?(:manage_taxonomies) && !self_destruct } do |s|
@@ -57,7 +57,9 @@ SimpleNavigation::Configuration.run do |navigation|
s.item :accounts, safe_join([material_symbol('groups'), t('admin.accounts.title')]), admin_accounts_path(origin: 'local'), highlights_on: %r{/admin/accounts|admin/account_moderation_notes|/admin/pending_accounts|/admin/users}, if: -> { current_user.can?(:manage_users) }
s.item :tags, safe_join([material_symbol('tag'), t('admin.tags.title')]), admin_tags_path, highlights_on: %r{/admin/tags}, if: -> { current_user.can?(:manage_taxonomies) }
s.item :invites, safe_join([material_symbol('person_add'), t('admin.invites.title')]), admin_invites_path, if: -> { current_user.can?(:manage_invites) }
- s.item :instances, safe_join([material_symbol('cloud'), t('admin.instances.title')]), admin_instances_path(limited: limited_federation_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.can?(:manage_federation) }
+ s.item :instances, safe_join([material_symbol('cloud'), t('admin.instances.title')]), admin_instances_path(limited: limited_federation_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows|/admin/export_domain_blocks}, if: lambda {
+ current_user.can?(:manage_federation)
+ }
s.item :email_domain_blocks, safe_join([material_symbol('mail'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_path, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.can?(:manage_blocks) }
s.item :ip_blocks, safe_join([material_symbol('hide_source'), t('admin.ip_blocks.title')]), admin_ip_blocks_path, highlights_on: %r{/admin/ip_blocks}, if: -> { current_user.can?(:manage_blocks) }
s.item :action_logs, safe_join([material_symbol('list'), t('admin.action_logs.title')]), admin_action_logs_path, if: -> { current_user.can?(:view_audit_log) }
diff --git a/config/routes.rb b/config/routes.rb
index 79b374e413..b04ffe096c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -68,7 +68,7 @@ Rails.application.routes.draw do
scope path: '.well-known' do
scope module: :well_known do
get 'oauth-authorization-server', to: 'oauth_metadata#show', as: :oauth_metadata, defaults: { format: 'json' }
- get 'host-meta', to: 'host_meta#show', as: :host_meta, defaults: { format: 'xml' }
+ get 'host-meta', to: 'host_meta#show', as: :host_meta
get 'nodeinfo', to: 'node_info#index', as: :nodeinfo, defaults: { format: 'json' }
get 'webfinger', to: 'webfinger#show', as: :webfinger
end
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 78194f5789..334e806227 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -145,8 +145,10 @@ namespace :admin do
end
resources :users, only: [] do
- resource :two_factor_authentication, only: [:destroy], controller: 'users/two_factor_authentications'
- resource :role, only: [:show, :update], controller: 'users/roles'
+ scope module: :users do
+ resource :two_factor_authentication, only: [:destroy]
+ resource :role, only: [:show, :update]
+ end
end
resources :custom_emojis, only: [:index, :new, :create] do
diff --git a/db/migrate/.rubocop.yml b/db/migrate/.rubocop.yml
index 4e23800dd1..6f8b6cc60d 100644
--- a/db/migrate/.rubocop.yml
+++ b/db/migrate/.rubocop.yml
@@ -2,3 +2,8 @@ inherit_from: ../../.rubocop.yml
Naming/VariableNumber:
CheckSymbols: false
+
+# Enabled here as workaround for https://docs.rubocop.org/rubocop/configuration.html#path-relativity
+Rails/CreateTableWithTimestamps:
+ Include:
+ - '*.rb'
diff --git a/db/migrate/20170508230434_create_conversation_mutes.rb b/db/migrate/20170508230434_create_conversation_mutes.rb
index 01122c4516..4beba9dfa3 100644
--- a/db/migrate/20170508230434_create_conversation_mutes.rb
+++ b/db/migrate/20170508230434_create_conversation_mutes.rb
@@ -2,7 +2,7 @@
class CreateConversationMutes < ActiveRecord::Migration[5.0]
def change
- create_table :conversation_mutes do |t|
+ create_table :conversation_mutes do |t| # rubocop:disable Rails/CreateTableWithTimestamps
t.integer :account_id, null: false
t.bigint :conversation_id, null: false
end
diff --git a/db/migrate/20170823162448_create_status_pins.rb b/db/migrate/20170823162448_create_status_pins.rb
index c8d3fab3a5..2cf3a85ca9 100644
--- a/db/migrate/20170823162448_create_status_pins.rb
+++ b/db/migrate/20170823162448_create_status_pins.rb
@@ -2,7 +2,7 @@
class CreateStatusPins < ActiveRecord::Migration[5.1]
def change
- create_table :status_pins do |t|
+ create_table :status_pins do |t| # rubocop:disable Rails/CreateTableWithTimestamps
t.belongs_to :account, foreign_key: { on_delete: :cascade }, null: false
t.belongs_to :status, foreign_key: { on_delete: :cascade }, null: false
end
diff --git a/db/migrate/20171116161857_create_list_accounts.rb b/db/migrate/20171116161857_create_list_accounts.rb
index ff9ab3faad..b0371e4c88 100644
--- a/db/migrate/20171116161857_create_list_accounts.rb
+++ b/db/migrate/20171116161857_create_list_accounts.rb
@@ -2,7 +2,7 @@
class CreateListAccounts < ActiveRecord::Migration[5.2]
def change
- create_table :list_accounts do |t|
+ create_table :list_accounts do |t| # rubocop:disable Rails/CreateTableWithTimestamps
t.belongs_to :list, foreign_key: { on_delete: :cascade }, null: false
t.belongs_to :account, foreign_key: { on_delete: :cascade }, null: false
t.belongs_to :follow, foreign_key: { on_delete: :cascade }, null: false
diff --git a/db/migrate/20180929222014_create_account_conversations.rb b/db/migrate/20180929222014_create_account_conversations.rb
index 9386b86e7c..4e85e68d47 100644
--- a/db/migrate/20180929222014_create_account_conversations.rb
+++ b/db/migrate/20180929222014_create_account_conversations.rb
@@ -2,7 +2,7 @@
class CreateAccountConversations < ActiveRecord::Migration[5.2]
def change
- create_table :account_conversations do |t|
+ create_table :account_conversations do |t| # rubocop:disable Rails/CreateTableWithTimestamps
t.belongs_to :account, foreign_key: { on_delete: :cascade }
t.belongs_to :conversation, foreign_key: { on_delete: :cascade }
t.bigint :participant_account_ids, array: true, null: false, default: []
diff --git a/db/migrate/20181007025445_create_pghero_space_stats.rb b/db/migrate/20181007025445_create_pghero_space_stats.rb
index ddaf4aef31..696b53d8d7 100644
--- a/db/migrate/20181007025445_create_pghero_space_stats.rb
+++ b/db/migrate/20181007025445_create_pghero_space_stats.rb
@@ -2,7 +2,7 @@
class CreatePgheroSpaceStats < ActiveRecord::Migration[5.2]
def change
- create_table :pghero_space_stats do |t|
+ create_table :pghero_space_stats do |t| # rubocop:disable Rails/CreateTableWithTimestamps
t.text :database
t.text :schema
t.text :relation
diff --git a/db/migrate/20190103124649_create_scheduled_statuses.rb b/db/migrate/20190103124649_create_scheduled_statuses.rb
index a66546187e..02b4916be8 100644
--- a/db/migrate/20190103124649_create_scheduled_statuses.rb
+++ b/db/migrate/20190103124649_create_scheduled_statuses.rb
@@ -2,7 +2,7 @@
class CreateScheduledStatuses < ActiveRecord::Migration[5.2]
def change
- create_table :scheduled_statuses do |t|
+ create_table :scheduled_statuses do |t| # rubocop:disable Rails/CreateTableWithTimestamps
t.belongs_to :account, foreign_key: { on_delete: :cascade }
t.datetime :scheduled_at, index: true
t.jsonb :params
diff --git a/db/migrate/20220824233535_create_status_trends.rb b/db/migrate/20220824233535_create_status_trends.rb
index 52dcbf8f38..e68e5b7c11 100644
--- a/db/migrate/20220824233535_create_status_trends.rb
+++ b/db/migrate/20220824233535_create_status_trends.rb
@@ -2,7 +2,7 @@
class CreateStatusTrends < ActiveRecord::Migration[6.1]
def change
- create_table :status_trends do |t|
+ create_table :status_trends do |t| # rubocop:disable Rails/CreateTableWithTimestamps
t.references :status, null: false, foreign_key: { on_delete: :cascade }, index: { unique: true }
t.references :account, null: false, foreign_key: { on_delete: :cascade }
t.float :score, null: false, default: 0
diff --git a/db/migrate/20221006061337_create_preview_card_trends.rb b/db/migrate/20221006061337_create_preview_card_trends.rb
index 934a06e24d..266f644023 100644
--- a/db/migrate/20221006061337_create_preview_card_trends.rb
+++ b/db/migrate/20221006061337_create_preview_card_trends.rb
@@ -2,7 +2,7 @@
class CreatePreviewCardTrends < ActiveRecord::Migration[6.1]
def change
- create_table :preview_card_trends do |t|
+ create_table :preview_card_trends do |t| # rubocop:disable Rails/CreateTableWithTimestamps
t.references :preview_card, null: false, foreign_key: { on_delete: :cascade }, index: { unique: true }
t.float :score, null: false, default: 0
t.integer :rank, null: false, default: 0
diff --git a/lib/tasks/icons.rake b/lib/tasks/icons.rake
index 96e0a14315..9a05cf3499 100644
--- a/lib/tasks/icons.rake
+++ b/lib/tasks/icons.rake
@@ -38,6 +38,20 @@ def find_used_icons
end
end
+ Rails.root.join('config', 'navigation.rb').open('r') do |file|
+ pattern = /material_symbol\('(?[^']*)'\)/
+ file.each_line do |line|
+ match = pattern.match(line)
+ next if match.blank?
+
+ # navigation.rb only uses 400x24 icons, per material_symbol() in
+ # app/helpers/application_helper.rb
+ icons_by_weight_and_size[400] ||= {}
+ icons_by_weight_and_size[400][24] ||= Set.new
+ icons_by_weight_and_size[400][24] << match['icon']
+ end
+ end
+
icons_by_weight_and_size
end
diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb
index 713ea3ff16..4a6956cb09 100644
--- a/spec/controllers/auth/sessions_controller_spec.rb
+++ b/spec/controllers/auth/sessions_controller_spec.rb
@@ -208,7 +208,7 @@ RSpec.describe Auth::SessionsController do
context 'when using two-factor authentication' do
context 'with OTP enabled as second factor' do
let!(:user) do
- Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
+ Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret)
end
let!(:recovery_codes) do
@@ -230,7 +230,7 @@ RSpec.describe Auth::SessionsController do
context 'when using email and password after an unfinished log-in attempt to a 2FA-protected account' do
let!(:other_user) do
- Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
+ Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret)
end
before do
@@ -342,7 +342,7 @@ RSpec.describe Auth::SessionsController do
context 'with WebAuthn and OTP enabled as second factor' do
let!(:user) do
- Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
+ Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret)
end
let!(:webauthn_credential) do
diff --git a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
index 34eaacdf49..224310b7ef 100644
--- a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
+++ b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe Settings::TwoFactorAuthentication::ConfirmationsController do
def qr_code_markup
RQRCode::QRCode.new(
'otpauth://totp/cb6e6126.ngrok.io:local-part%40domain?secret=thisisasecretforthespecofnewview&issuer=cb6e6126.ngrok.io'
- ).as_svg(padding: 0, module_size: 4)
+ ).as_svg(padding: 0, module_size: 4, use_path: true)
end
end
diff --git a/spec/fabricators/account_domain_block_fabricator.rb b/spec/fabricators/account_domain_block_fabricator.rb
index 83df509da2..a211b7c666 100644
--- a/spec/fabricators/account_domain_block_fabricator.rb
+++ b/spec/fabricators/account_domain_block_fabricator.rb
@@ -2,5 +2,5 @@
Fabricator(:account_domain_block) do
account { Fabricate.build(:account) }
- domain 'example.com'
+ domain { sequence { |n| "host-#{n}.example" } }
end
diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb
index 83ea6566e4..ab69a4693d 100644
--- a/spec/lib/activitypub/activity/create_spec.rb
+++ b/spec/lib/activitypub/activity/create_spec.rb
@@ -63,6 +63,24 @@ RSpec.describe ActivityPub::Activity::Create do
}
end
+ let(:invalid_mention_json) do
+ {
+ id: [ActivityPub::TagManager.instance.uri_for(sender), 'post2'].join('/'),
+ type: 'Note',
+ to: [
+ 'https://www.w3.org/ns/activitystreams#Public',
+ ActivityPub::TagManager.instance.uri_for(follower),
+ ],
+ content: '@bob lorem ipsum',
+ published: 1.hour.ago.utc.iso8601,
+ updated: 1.hour.ago.utc.iso8601,
+ tag: {
+ type: 'Mention',
+ href: 'http://notexisting.dontexistingtld/actor',
+ },
+ }
+ end
+
def activity_for_object(json)
{
'@context': 'https://www.w3.org/ns/activitystreams',
@@ -117,6 +135,25 @@ RSpec.describe ActivityPub::Activity::Create do
# Creates two notifications
expect(Notification.count).to eq 2
end
+
+ it 'ignores unprocessable mention', :aggregate_failures do
+ stub_request(:get, invalid_mention_json[:tag][:href]).to_raise(HTTP::ConnectionError)
+ # When receiving the post that contains an invalid mention…
+ described_class.new(activity_for_object(invalid_mention_json), sender, delivery: true).perform
+
+ # NOTE: Refering explicitly to the workers is a bit awkward
+ DistributionWorker.drain
+ FeedInsertWorker.drain
+
+ # …it creates a status
+ status = Status.find_by(uri: invalid_mention_json[:id])
+
+ # Check the process did not crash
+ expect(status.nil?).to be false
+
+ # It has queued a mention resolve job
+ expect(MentionResolveWorker).to have_enqueued_sidekiq_job(status.id, invalid_mention_json[:tag][:href], anything)
+ end
end
describe '#perform' do
diff --git a/spec/models/export_spec.rb b/spec/models/export_spec.rb
index 06bf07ed78..48e78830dd 100644
--- a/spec/models/export_spec.rb
+++ b/spec/models/export_spec.rb
@@ -3,66 +3,175 @@
require 'rails_helper'
RSpec.describe Export do
+ subject { described_class.new(account) }
+
let(:account) { Fabricate(:account) }
let(:target_accounts) do
- [{}, { username: 'one', domain: 'local.host' }].map(&method(:Fabricate).curry(2).call(:account))
+ [
+ Fabricate(:account),
+ Fabricate(:account, username: 'one', domain: 'local.host'),
+ ]
end
- describe 'to_csv' do
- it 'returns a csv of the blocked accounts' do
- target_accounts.each { |target_account| account.block!(target_account) }
+ describe '#to_bookmarks_csv' do
+ before { Fabricate.times(2, :bookmark, account: account) }
- export = described_class.new(account).to_blocked_accounts_csv
- results = export.strip.split
+ let(:export) { CSV.parse(subject.to_bookmarks_csv) }
- expect(results.size).to eq 2
- expect(results.first).to eq 'one@local.host'
+ it 'returns a csv of bookmarks' do
+ expect(export)
+ .to contain_exactly(
+ include(/statuses/),
+ include(/statuses/)
+ )
end
+ end
+
+ describe '#to_blocked_accounts_csv' do
+ before { target_accounts.each { |target_account| account.block!(target_account) } }
+
+ let(:export) { CSV.parse(subject.to_blocked_accounts_csv) }
+
+ it 'returns a csv of the blocked accounts' do
+ expect(export)
+ .to contain_exactly(
+ include('one@local.host'),
+ include(be_present)
+ )
+ end
+ end
+
+ describe '#to_muted_accounts_csv' do
+ before { target_accounts.each { |target_account| account.mute!(target_account) } }
+
+ let(:export) { CSV.parse(subject.to_muted_accounts_csv) }
it 'returns a csv of the muted accounts' do
- target_accounts.each { |target_account| account.mute!(target_account) }
-
- export = described_class.new(account).to_muted_accounts_csv
- results = export.strip.split("\n")
-
- expect(results.size).to eq 3
- expect(results.first).to eq 'Account address,Hide notifications'
- expect(results.second).to eq 'one@local.host,true'
+ expect(export)
+ .to contain_exactly(
+ contain_exactly('Account address', 'Hide notifications'),
+ include('one@local.host', 'true'),
+ include(be_present)
+ )
end
+ end
+
+ describe '#to_following_accounts_csv' do
+ before { target_accounts.each { |target_account| account.follow!(target_account) } }
+
+ let(:export) { CSV.parse(subject.to_following_accounts_csv) }
it 'returns a csv of the following accounts' do
- target_accounts.each { |target_account| account.follow!(target_account) }
-
- export = described_class.new(account).to_following_accounts_csv
- results = export.strip.split("\n")
-
- expect(results.size).to eq 3
- expect(results.first).to eq 'Account address,Show boosts,Notify on new posts,Languages'
- expect(results.second).to eq 'one@local.host,true,false,'
+ expect(export)
+ .to contain_exactly(
+ contain_exactly('Account address', 'Show boosts', 'Notify on new posts', 'Languages'),
+ include('one@local.host', 'true', 'false', be_blank),
+ include(be_present)
+ )
end
end
- describe 'total_storage' do
+ describe '#to_lists_csv' do
+ before do
+ target_accounts.each do |target_account|
+ account.follow!(target_account)
+ Fabricate(:list, account: account).accounts << target_account
+ end
+ end
+
+ let(:export) { CSV.parse(subject.to_lists_csv) }
+
+ it 'returns a csv of the lists' do
+ expect(export)
+ .to contain_exactly(
+ include('one@local.host'),
+ include(be_present)
+ )
+ end
+ end
+
+ describe '#to_blocked_domains_csv' do
+ before { Fabricate.times(2, :account_domain_block, account: account) }
+
+ let(:export) { CSV.parse(subject.to_blocked_domains_csv) }
+
+ it 'returns a csv of the blocked domains' do
+ expect(export)
+ .to contain_exactly(
+ include(/example/),
+ include(/example/)
+ )
+ end
+ end
+
+ describe '#total_storage' do
it 'returns the total size of the media attachments' do
media_attachment = Fabricate(:media_attachment, account: account)
- expect(described_class.new(account).total_storage).to eq media_attachment.file_file_size || 0
+ expect(subject.total_storage).to eq media_attachment.file_file_size || 0
end
end
- describe 'total_follows' do
- it 'returns the total number of the followed accounts' do
- target_accounts.each { |target_account| account.follow!(target_account) }
- expect(described_class.new(account.reload).total_follows).to eq 2
+ describe '#total_statuses' do
+ before { Fabricate.times(2, :status, account: account) }
+
+ it 'returns the total number of statuses' do
+ expect(subject.total_statuses).to eq(2)
end
+ end
+
+ describe '#total_bookmarks' do
+ before { Fabricate.times(2, :bookmark, account: account) }
+
+ it 'returns the total number of bookmarks' do
+ expect(subject.total_bookmarks).to eq(2)
+ end
+ end
+
+ describe '#total_follows' do
+ before { target_accounts.each { |target_account| account.follow!(target_account) } }
+
+ it 'returns the total number of the followed accounts' do
+ expect(subject.total_follows).to eq(2)
+ end
+ end
+
+ describe '#total_lists' do
+ before { Fabricate.times(2, :list, account: account) }
+
+ it 'returns the total number of lists' do
+ expect(subject.total_lists).to eq(2)
+ end
+ end
+
+ describe '#total_followers' do
+ before { target_accounts.each { |target_account| target_account.follow!(account) } }
+
+ it 'returns the total number of the follower accounts' do
+ expect(subject.total_followers).to eq(2)
+ end
+ end
+
+ describe '#total_blocks' do
+ before { target_accounts.each { |target_account| account.block!(target_account) } }
it 'returns the total number of the blocked accounts' do
- target_accounts.each { |target_account| account.block!(target_account) }
- expect(described_class.new(account.reload).total_blocks).to eq 2
+ expect(subject.total_blocks).to eq(2)
end
+ end
+
+ describe '#total_mutes' do
+ before { target_accounts.each { |target_account| account.mute!(target_account) } }
it 'returns the total number of the muted accounts' do
- target_accounts.each { |target_account| account.mute!(target_account) }
- expect(described_class.new(account.reload).total_mutes).to eq 2
+ expect(subject.total_mutes).to eq(2)
+ end
+ end
+
+ describe '#total_domain_blocks' do
+ before { Fabricate.times(2, :account_domain_block, account: account) }
+
+ it 'returns the total number of account domain blocks' do
+ expect(subject.total_domain_blocks).to eq(2)
end
end
end
diff --git a/spec/models/report_filter_spec.rb b/spec/models/report_filter_spec.rb
index 8668eb3d10..51933e475a 100644
--- a/spec/models/report_filter_spec.rb
+++ b/spec/models/report_filter_spec.rb
@@ -30,4 +30,17 @@ RSpec.describe ReportFilter do
expect(Report).to have_received(:resolved)
end
end
+
+ context 'when given remote target_origin and also by_target_domain' do
+ let!(:matching_report) { Fabricate :report, target_account: Fabricate(:account, domain: 'match.example') }
+ let!(:non_matching_report) { Fabricate :report, target_account: Fabricate(:account, domain: 'other.example') }
+
+ it 'preserves the domain value' do
+ filter = described_class.new(by_target_domain: 'match.example', target_origin: 'remote')
+
+ expect(filter.results)
+ .to include(matching_report)
+ .and not_include(non_matching_report)
+ end
+ end
end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index ee03b49bc6..84cee0974f 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -161,6 +161,11 @@ RSpec.configure do |config|
host! Rails.configuration.x.local_domain
end
+ config.before :each, type: :system do
+ # Align with capybara config so that rails helpers called from rspec use matching host
+ host! 'localhost:3000'
+ end
+
config.after do
Rails.cache.clear
redis.del(redis.keys)
diff --git a/spec/requests/auth/sessions/security_key_options_spec.rb b/spec/requests/auth/sessions/security_key_options_spec.rb
index 6246e4beb3..e53b9802b4 100644
--- a/spec/requests/auth/sessions/security_key_options_spec.rb
+++ b/spec/requests/auth/sessions/security_key_options_spec.rb
@@ -6,7 +6,7 @@ require 'webauthn/fake_client'
RSpec.describe 'Security Key Options' do
describe 'GET /auth/sessions/security_key_options' do
let!(:user) do
- Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
+ Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret)
end
context 'with WebAuthn and OTP enabled as second factor' do
diff --git a/spec/requests/well_known/host_meta_spec.rb b/spec/requests/well_known/host_meta_spec.rb
index 09f17baa89..726911dda1 100644
--- a/spec/requests/well_known/host_meta_spec.rb
+++ b/spec/requests/well_known/host_meta_spec.rb
@@ -9,19 +9,39 @@ RSpec.describe 'The /.well-known/host-meta request' do
expect(response)
.to have_http_status(200)
.and have_attributes(
- media_type: 'application/xrd+xml',
- body: host_meta_xml_template
+ media_type: 'application/xrd+xml'
+ )
+
+ doc = Nokogiri::XML(response.parsed_body)
+ expect(doc.at_xpath('/xrd:XRD/xrd:Link[@rel="lrdd"]/@template', 'xrd' => 'http://docs.oasis-open.org/ns/xri/xrd-1.0').value)
+ .to eq 'https://cb6e6126.ngrok.io/.well-known/webfinger?resource={uri}'
+ end
+
+ it 'returns http success with valid JSON response with .json extension' do
+ get '/.well-known/host-meta.json'
+
+ expect(response)
+ .to have_http_status(200)
+ .and have_attributes(
+ media_type: 'application/json'
+ )
+
+ expect(response.parsed_body)
+ .to include(
+ links: [
+ 'rel' => 'lrdd',
+ 'template' => 'https://cb6e6126.ngrok.io/.well-known/webfinger?resource={uri}',
+ ]
)
end
- private
+ it 'returns http success with valid JSON response with Accept header' do
+ get '/.well-known/host-meta', headers: { 'Accept' => 'application/json' }
- def host_meta_xml_template
- <<~XML
-
-
-
-
- XML
+ expect(response)
+ .to have_http_status(200)
+ .and have_attributes(
+ media_type: 'application/json'
+ )
end
end
diff --git a/spec/routing/well_known_routes_spec.rb b/spec/routing/well_known_routes_spec.rb
index 6578e939ae..84081059bb 100644
--- a/spec/routing/well_known_routes_spec.rb
+++ b/spec/routing/well_known_routes_spec.rb
@@ -4,9 +4,14 @@ require 'rails_helper'
RSpec.describe 'Well Known routes' do
describe 'the host-meta route' do
- it 'routes to correct place with xml format' do
+ it 'routes to correct place' do
expect(get('/.well-known/host-meta'))
- .to route_to('well_known/host_meta#show', format: 'xml')
+ .to route_to('well_known/host_meta#show')
+ end
+
+ it 'routes to correct place with json format' do
+ expect(get('/.well-known/host-meta.json'))
+ .to route_to('well_known/host_meta#show', format: 'json')
end
end
diff --git a/spec/system/invites_spec.rb b/spec/system/invites_spec.rb
index c57de871cc..fc60ce5913 100644
--- a/spec/system/invites_spec.rb
+++ b/spec/system/invites_spec.rb
@@ -7,10 +7,7 @@ RSpec.describe 'Invites' do
let(:user) { Fabricate :user }
- before do
- host! 'localhost:3000' # TODO: Move into before for all system specs?
- sign_in user
- end
+ before { sign_in user }
describe 'Viewing invites' do
it 'Lists existing user invites' do
diff --git a/spec/system/oauth_spec.rb b/spec/system/oauth_spec.rb
index 0f96a59675..64ac75879e 100644
--- a/spec/system/oauth_spec.rb
+++ b/spec/system/oauth_spec.rb
@@ -179,7 +179,7 @@ RSpec.describe 'Using OAuth from an external app' do
end
context 'when the user has set up TOTP' do
- let(:user) { Fabricate(:user, email: email, password: password, otp_required_for_login: true, otp_secret: User.generate_otp_secret(32)) }
+ let(:user) { Fabricate(:user, email: email, password: password, otp_required_for_login: true, otp_secret: User.generate_otp_secret) }
it 'when accepting the authorization request' do
params = { client_id: client_app.uid, response_type: 'code', redirect_uri: client_app.redirect_uri, scope: 'read' }
diff --git a/spec/workers/mention_resolve_worker_spec.rb b/spec/workers/mention_resolve_worker_spec.rb
new file mode 100644
index 0000000000..5e23876b4a
--- /dev/null
+++ b/spec/workers/mention_resolve_worker_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe MentionResolveWorker do
+ let(:status_id) { -42 }
+ let(:uri) { 'https://example.com/users/unknown' }
+
+ describe '#perform' do
+ subject { described_class.new.perform(status_id, uri, {}) }
+
+ context 'with a non-existent status' do
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'with a valid user' do
+ let(:status) { Fabricate(:status) }
+ let(:status_id) { status.id }
+
+ let(:service_double) { instance_double(ActivityPub::FetchRemoteAccountService) }
+
+ before do
+ allow(ActivityPub::FetchRemoteAccountService).to receive(:new).and_return(service_double)
+
+ allow(service_double).to receive(:call).with(uri, anything) { Fabricate(:account, domain: 'example.com', uri: uri) }
+ end
+
+ it 'resolves the account and adds a new mention', :aggregate_failures do
+ expect { subject }
+ .to change { status.reload.mentions }.from([]).to(a_collection_including(having_attributes(account: having_attributes(uri: uri), silent: false)))
+
+ expect(service_double).to have_received(:call).once
+ end
+ end
+ end
+end
diff --git a/spec/workers/web/push_notification_worker_spec.rb b/spec/workers/web/push_notification_worker_spec.rb
index ced21d5bf7..7f836d99e4 100644
--- a/spec/workers/web/push_notification_worker_spec.rb
+++ b/spec/workers/web/push_notification_worker_spec.rb
@@ -22,27 +22,48 @@ RSpec.describe Web::PushNotificationWorker do
let(:payload) { { ciphertext: ciphertext, salt: salt, server_public_key: server_public_key, shared_secret: shared_secret } }
describe 'perform' do
+ around do |example|
+ original_private = Rails.configuration.x.vapid_private_key
+ original_public = Rails.configuration.x.vapid_public_key
+ Rails.configuration.x.vapid_private_key = vapid_private_key
+ Rails.configuration.x.vapid_public_key = vapid_public_key
+ example.run
+ Rails.configuration.x.vapid_private_key = original_private
+ Rails.configuration.x.vapid_public_key = original_public
+ end
+
before do
- allow(subscription).to receive_messages(contact_email: contact_email, vapid_key: vapid_key)
- allow(Web::PushSubscription).to receive(:find).with(subscription.id).and_return(subscription)
+ Setting.site_contact_email = contact_email
+
allow(Webpush::Encryption).to receive(:encrypt).and_return(payload)
allow(JWT).to receive(:encode).and_return('jwt.encoded.payload')
stub_request(:post, endpoint).to_return(status: 201, body: '')
-
- subject.perform(subscription.id, notification.id)
end
it 'calls the relevant service with the correct headers' do
- expect(a_request(:post, endpoint).with(headers: {
- 'Content-Encoding' => 'aesgcm',
- 'Content-Type' => 'application/octet-stream',
- 'Crypto-Key' => "dh=BAgtUks5d90kFmxGevk9tH7GEmvz9DB0qcEMUsOBgKwMf-TMjsKIIG6LQvGcFAf6jcmAod15VVwmYwGIIxE4VWE;p256ecdsa=#{vapid_public_key.delete('=')}",
- 'Encryption' => 'salt=WJeVM-RY-F9351SVxTFx_g',
- 'Ttl' => '172800',
- 'Urgency' => 'normal',
- 'Authorization' => 'WebPush jwt.encoded.payload',
- }, body: "+\xB8\xDBT}\u0013\xB6\xDD.\xF9\xB0\xA7\xC8Ҁ\xFD\x99#\xF7\xAC\x83\xA4\xDB,\u001F\xB5\xB9w\x85>\xF7\xADr")).to have_been_made
+ subject.perform(subscription.id, notification.id)
+
+ expect(web_push_endpoint_request)
+ .to have_been_made
+ end
+
+ def web_push_endpoint_request
+ a_request(
+ :post,
+ endpoint
+ ).with(
+ headers: {
+ 'Content-Encoding' => 'aesgcm',
+ 'Content-Type' => 'application/octet-stream',
+ 'Crypto-Key' => "dh=BAgtUks5d90kFmxGevk9tH7GEmvz9DB0qcEMUsOBgKwMf-TMjsKIIG6LQvGcFAf6jcmAod15VVwmYwGIIxE4VWE;p256ecdsa=#{vapid_public_key.delete('=')}",
+ 'Encryption' => 'salt=WJeVM-RY-F9351SVxTFx_g',
+ 'Ttl' => '172800',
+ 'Urgency' => 'normal',
+ 'Authorization' => 'WebPush jwt.encoded.payload',
+ },
+ body: "+\xB8\xDBT}\u0013\xB6\xDD.\xF9\xB0\xA7\xC8Ҁ\xFD\x99#\xF7\xAC\x83\xA4\xDB,\u001F\xB5\xB9w\x85>\xF7\xADr"
+ )
end
end
end
diff --git a/yarn.lock b/yarn.lock
index 628b3c43ad..94a3bbbfef 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1763,9 +1763,9 @@ __metadata:
languageName: node
linkType: hard
-"@csstools/postcss-light-dark-function@npm:^2.0.2":
- version: 2.0.2
- resolution: "@csstools/postcss-light-dark-function@npm:2.0.2"
+"@csstools/postcss-light-dark-function@npm:^2.0.4":
+ version: 2.0.4
+ resolution: "@csstools/postcss-light-dark-function@npm:2.0.4"
dependencies:
"@csstools/css-parser-algorithms": "npm:^3.0.1"
"@csstools/css-tokenizer": "npm:^3.0.1"
@@ -1773,7 +1773,7 @@ __metadata:
"@csstools/utilities": "npm:^2.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10c0/f8973c435868998e5d6af1fc0c35b27bbf65fa9d0c35f5055c689b8ee2807a16802044e296f7def39a7253ae544fb49559e8273ee22eb4e21845aa980a1bc82b
+ checksum: 10c0/0176422ad9747953964b1ceff002df1ecb1952ebc481db6192070d68777135b582ea6fd32ae819b9c64c96cb9170bd6907c647c85b48daa4984b7ed3d7f9bccb
languageName: node
linkType: hard
@@ -13945,8 +13945,8 @@ __metadata:
linkType: hard
"postcss-preset-env@npm:^10.0.0":
- version: 10.0.3
- resolution: "postcss-preset-env@npm:10.0.3"
+ version: 10.0.5
+ resolution: "postcss-preset-env@npm:10.0.5"
dependencies:
"@csstools/postcss-cascade-layers": "npm:^5.0.0"
"@csstools/postcss-color-function": "npm:^4.0.2"
@@ -13960,7 +13960,7 @@ __metadata:
"@csstools/postcss-ic-unit": "npm:^4.0.0"
"@csstools/postcss-initial": "npm:^2.0.0"
"@csstools/postcss-is-pseudo-class": "npm:^5.0.0"
- "@csstools/postcss-light-dark-function": "npm:^2.0.2"
+ "@csstools/postcss-light-dark-function": "npm:^2.0.4"
"@csstools/postcss-logical-float-and-clear": "npm:^3.0.0"
"@csstools/postcss-logical-overflow": "npm:^2.0.0"
"@csstools/postcss-logical-overscroll-behavior": "npm:^2.0.0"
@@ -14011,7 +14011,7 @@ __metadata:
postcss-selector-not: "npm:^8.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10c0/da42caa2aab4d825fddfde00ebe2416d338c7b9a6f79a68840297888a8384f85991991c3fa10cf2d359fb230c885375f5cebd7bd63972725cd2a596d218f8b6a
+ checksum: 10c0/db5eb1175cb26bed3f1a4c47acc67935ffc784520321470520e59de366ac6f91be1e609fe36056af707ed20f7910721287cff0fae416c437dd3e944de13ffd05
languageName: node
linkType: hard