Merge remote-tracking branch 'upstream/main' into develop

# Conflicts:
#	Gemfile.lock
#	app/javascript/flavours/glitch/initial_state.js
#	app/javascript/mastodon/initial_state.js
#	app/lib/feed_manager.rb
#	app/services/batched_remove_status_service.rb
#	lib/mastodon/feeds_cli.rb
develop
Jeremy Kescher 1 year ago
commit ddc4022845
No known key found for this signature in database
GPG Key ID: 48DFE4BB15BA5940

@ -1,16 +1,14 @@
# [Choice] Ruby version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.1, 3.0, 2, 2.7, 2.6, 3-bullseye, 3.1-bullseye, 3.0-bullseye, 2-bullseye, 2.7-bullseye, 2.6-bullseye, 3-buster, 3.1-buster, 3.0-buster, 2-buster, 2.7-buster, 2.6-buster
ARG VARIANT=3.1-bullseye
FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT}
# For details, see https://github.com/devcontainers/images/tree/main/src/ruby
FROM mcr.microsoft.com/devcontainers/ruby:0-3.2-bullseye
# Install Rails
# RUN gem install rails webdrivers
# Default value to allow debug server to serve content over GitHub Codespace's port forwarding service
# The value is a comma-separated list of allowed domains
ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev"
ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev,.preview.app.github.dev,.app.github.dev"
# [Choice] Node.js version: lts/*, 18, 16, 14
ARG NODE_VERSION="lts/*"
ARG NODE_VERSION="16"
RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"
# [Optional] Uncomment this section to install additional OS packages.
@ -22,3 +20,5 @@ RUN gem install foreman
# [Optional] Uncomment this line to install global node packages.
RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g yarn" 2>&1
COPY welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt

@ -1,30 +1,13 @@
// For more details, see https://aka.ms/devcontainer.json.
{
"name": "Mastodon",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/mastodon",
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"EditorConfig.EditorConfig",
"dbaeumer.vscode-eslint",
"rebornix.Ruby",
"webben.browserslist"
]
}
},
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"ghcr.io/devcontainers/features/sshd:1": {
"version": "latest"
}
"ghcr.io/devcontainers/features/sshd:1": {}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
@ -33,7 +16,16 @@
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": ".devcontainer/post-create.sh",
"waitFor": "postCreateCommand",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode"
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {},
// Add the IDs of extensions you want installed when the container is created.
"extensions": ["EditorConfig.EditorConfig", "webben.browserslist"]
}
}
}

@ -5,15 +5,8 @@ services:
build:
context: .
dockerfile: Dockerfile
args:
# Update 'VARIANT' to pick a version of Ruby: 3, 3.1, 3.0, 2, 2.7, 2.6
# Append -bullseye or -buster to pin to an OS version.
# Use -bullseye variants on local arm64/Apple Silicon.
VARIANT: '3.0-bullseye'
# Optional Node.js version to install
NODE_VERSION: '16'
volumes:
- ..:/mastodon:cached
- ../..:/workspaces:cached
environment:
RAILS_ENV: development
NODE_ENV: development
@ -33,7 +26,6 @@ services:
networks:
- external_network
- internal_network
user: vscode
db:
image: postgres:14-alpine

@ -0,0 +1,8 @@
👋 Welcome to "Mastodon" in GitHub Codespaces!
🛠️ Your environment is fully setup with all the required software.
🔍 To explore VS Code to its fullest, search using the Command Palette (Cmd/Ctrl + Shift + P or F1).
📝 Edit away, run your app as usual, and we'll automatically make it available for you to access.

@ -19,7 +19,6 @@ AllCops:
NewCops: enable
Exclude:
- db/schema.rb
- 'app/views/**/*'
- 'config/**/*'
- 'bin/*'
- 'Rakefile'
@ -97,6 +96,10 @@ Rails/Exit:
- 'lib/mastodon/cli_helper.rb'
- 'lib/cli.rb'
RSpec/FilePath:
CustomTransform:
DeepL: deepl
RSpec/NotToNot:
EnforcedStyle: to_not
@ -123,3 +126,6 @@ Style/TrailingCommaInArrayLiteral:
Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: 'comma'
Style/SymbolArray:
Enabled: false

@ -2235,134 +2235,3 @@ Style/SlicingWithRange:
- 'lib/active_record/batches.rb'
- 'lib/mastodon/premailer_webpack_strategy.rb'
- 'lib/tasks/repo.rake'
# Offense count: 272
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, MinSize.
# SupportedStyles: percent, brackets
Style/SymbolArray:
Exclude:
- 'app/controllers/accounts_controller.rb'
- 'app/controllers/activitypub/replies_controller.rb'
- 'app/controllers/admin/accounts_controller.rb'
- 'app/controllers/admin/announcements_controller.rb'
- 'app/controllers/admin/domain_blocks_controller.rb'
- 'app/controllers/admin/email_domain_blocks_controller.rb'
- 'app/controllers/admin/relationships_controller.rb'
- 'app/controllers/admin/relays_controller.rb'
- 'app/controllers/admin/roles_controller.rb'
- 'app/controllers/admin/rules_controller.rb'
- 'app/controllers/admin/statuses_controller.rb'
- 'app/controllers/admin/trends/statuses_controller.rb'
- 'app/controllers/admin/warning_presets_controller.rb'
- 'app/controllers/admin/webhooks_controller.rb'
- 'app/controllers/api/v1/accounts/credentials_controller.rb'
- 'app/controllers/api/v1/accounts_controller.rb'
- 'app/controllers/api/v1/admin/accounts_controller.rb'
- 'app/controllers/api/v1/admin/canonical_email_blocks_controller.rb'
- 'app/controllers/api/v1/admin/domain_allows_controller.rb'
- 'app/controllers/api/v1/admin/domain_blocks_controller.rb'
- 'app/controllers/api/v1/admin/email_domain_blocks_controller.rb'
- 'app/controllers/api/v1/admin/ip_blocks_controller.rb'
- 'app/controllers/api/v1/admin/reports_controller.rb'
- 'app/controllers/api/v1/crypto/deliveries_controller.rb'
- 'app/controllers/api/v1/crypto/keys/claims_controller.rb'
- 'app/controllers/api/v1/crypto/keys/uploads_controller.rb'
- 'app/controllers/api/v1/featured_tags_controller.rb'
- 'app/controllers/api/v1/filters_controller.rb'
- 'app/controllers/api/v1/lists_controller.rb'
- 'app/controllers/api/v1/notifications_controller.rb'
- 'app/controllers/api/v1/push/subscriptions_controller.rb'
- 'app/controllers/api/v1/scheduled_statuses_controller.rb'
- 'app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb'
- 'app/controllers/api/v1/statuses_controller.rb'
- 'app/controllers/api/v2/filters/keywords_controller.rb'
- 'app/controllers/api/v2/filters/statuses_controller.rb'
- 'app/controllers/api/v2/filters_controller.rb'
- 'app/controllers/api/web/push_subscriptions_controller.rb'
- 'app/controllers/application_controller.rb'
- 'app/controllers/auth/registrations_controller.rb'
- 'app/controllers/filters_controller.rb'
- 'app/controllers/settings/applications_controller.rb'
- 'app/controllers/settings/featured_tags_controller.rb'
- 'app/controllers/settings/profiles_controller.rb'
- 'app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb'
- 'app/controllers/statuses_controller.rb'
- 'app/lib/feed_manager.rb'
- 'app/models/account.rb'
- 'app/models/account_filter.rb'
- 'app/models/admin/status_filter.rb'
- 'app/models/announcement.rb'
- 'app/models/concerns/ldap_authenticable.rb'
- 'app/models/concerns/status_threading_concern.rb'
- 'app/models/custom_filter.rb'
- 'app/models/domain_block.rb'
- 'app/models/import.rb'
- 'app/models/list.rb'
- 'app/models/media_attachment.rb'
- 'app/models/preview_card.rb'
- 'app/models/relay.rb'
- 'app/models/report.rb'
- 'app/models/site_upload.rb'
- 'app/models/status.rb'
- 'app/serializers/initial_state_serializer.rb'
- 'app/serializers/rest/notification_serializer.rb'
- 'db/migrate/20160220174730_create_accounts.rb'
- 'db/migrate/20160221003621_create_follows.rb'
- 'db/migrate/20160223171800_create_favourites.rb'
- 'db/migrate/20160224223247_create_mentions.rb'
- 'db/migrate/20160314164231_add_owner_to_application.rb'
- 'db/migrate/20160316103650_add_missing_indices.rb'
- 'db/migrate/20160926213048_remove_owner_from_application.rb'
- 'db/migrate/20161003145426_create_blocks.rb'
- 'db/migrate/20161006213403_rails_settings_migration.rb'
- 'db/migrate/20161105130633_create_statuses_tags_join_table.rb'
- 'db/migrate/20161119211120_create_notifications.rb'
- 'db/migrate/20161128103007_create_subscriptions.rb'
- 'db/migrate/20161222204147_create_follow_requests.rb'
- 'db/migrate/20170112154826_migrate_settings.rb'
- 'db/migrate/20170301222600_create_mutes.rb'
- 'db/migrate/20170406215816_add_notifications_and_favourites_indices.rb'
- 'db/migrate/20170424003227_create_account_domain_blocks.rb'
- 'db/migrate/20170427011934_re_add_owner_to_application.rb'
- 'db/migrate/20170507141759_optimize_index_subscriptions.rb'
- 'db/migrate/20170508230434_create_conversation_mutes.rb'
- 'db/migrate/20170720000000_add_index_favourites_on_account_id_and_id.rb'
- 'db/migrate/20170823162448_create_status_pins.rb'
- 'db/migrate/20170901142658_create_join_table_preview_cards_statuses.rb'
- 'db/migrate/20170905044538_add_index_id_account_id_activity_type_on_notifications.rb'
- 'db/migrate/20170917153509_create_custom_emojis.rb'
- 'db/migrate/20170918125918_ids_to_bigints.rb'
- 'db/migrate/20171116161857_create_list_accounts.rb'
- 'db/migrate/20171122120436_add_index_account_and_reblog_of_id_to_statuses.rb'
- 'db/migrate/20171125185353_add_index_reblog_of_id_and_account_to_statuses.rb'
- 'db/migrate/20171125190735_remove_old_reblog_index_on_statuses.rb'
- 'db/migrate/20171129172043_add_index_on_stream_entries.rb'
- 'db/migrate/20171226094803_more_faster_index_on_notifications.rb'
- 'db/migrate/20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb'
- 'db/migrate/20180514140000_revert_index_change_on_statuses_for_api_v1_accounts_account_id_statuses.rb'
- 'db/migrate/20180808175627_create_account_pins.rb'
- 'db/migrate/20180831171112_create_bookmarks.rb'
- 'db/migrate/20180929222014_create_account_conversations.rb'
- 'db/migrate/20181007025445_create_pghero_space_stats.rb'
- 'db/migrate/20181203003808_create_accounts_tags_join_table.rb'
- 'db/migrate/20190316190352_create_account_identity_proofs.rb'
- 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb'
- 'db/migrate/20190820003045_update_statuses_index.rb'
- 'db/migrate/20190823221802_add_local_index_to_statuses.rb'
- 'db/migrate/20190904222339_create_markers.rb'
- 'db/migrate/20200113125135_create_announcement_mutes.rb'
- 'db/migrate/20200114113335_create_announcement_reactions.rb'
- 'db/migrate/20200119112504_add_public_index_to_statuses.rb'
- 'db/migrate/20200628133322_create_account_notes.rb'
- 'db/migrate/20200917222316_add_index_notifications_on_type.rb'
- 'db/migrate/20210425135952_add_index_on_media_attachments_account_id_status_id.rb'
- 'db/migrate/20220714171049_create_tag_follows.rb'
- 'db/migrate/20221021055441_add_index_featured_tags_on_account_id_and_tag_id.rb'
- 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb'
- 'db/post_migrate/20200917222734_remove_index_notifications_on_account_activity.rb'
- 'spec/controllers/api/v1/streaming_controller_spec.rb'
- 'spec/controllers/api/v2/admin/accounts_controller_spec.rb'
- 'spec/controllers/concerns/signature_verification_spec.rb'
- 'spec/fabricators/notification_fabricator.rb'
- 'spec/models/public_feed_spec.rb'

@ -104,8 +104,6 @@ group :development, :test do
gem 'fabrication', '~> 2.30'
gem 'fuubar', '~> 2.5'
gem 'i18n-tasks', '~> 1.0', require: false
gem 'pry-byebug', '~> 3.10'
gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 6.0'
gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false
@ -119,7 +117,6 @@ end
group :test do
gem 'capybara', '~> 3.38'
gem 'climate_control', '~> 0.2'
gem 'faker', '~> 3.1'
gem 'json-schema', '~> 3.0'
gem 'rack-test', '~> 2.0'

@ -155,7 +155,6 @@ GEM
bundler-audit (0.9.1)
bundler (>= 1.2.0, < 3)
thor (~> 1.0)
byebug (11.1.3)
capistrano (3.17.2)
airbrussh (>= 1.0.0)
i18n
@ -499,14 +498,6 @@ GEM
net-smtp
premailer (~> 1.7, >= 1.7.9)
private_address_check (0.5.0)
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
pry-byebug (3.10.1)
byebug (~> 11.0)
pry (>= 0.13, < 0.15)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (5.0.1)
puma (6.1.0)
nio4r (~> 2.0)
@ -569,7 +560,7 @@ GEM
rdf-normalize (0.5.1)
rdf (~> 3.2)
redcarpet (3.6.0)
redis (4.8.0)
redis (4.8.1)
redis-namespace (1.10.0)
redis (>= 4)
redlock (1.3.2)
@ -794,7 +785,6 @@ DEPENDENCIES
capybara (~> 3.38)
charlock_holmes (~> 0.7.7)
chewy (~> 7.2)
climate_control (~> 0.2)
cocoon (~> 1.2)
color_diff (~> 0.1)
concurrent-ruby
@ -853,8 +843,6 @@ DEPENDENCIES
posix-spawn
premailer-rails
private_address_check (~> 0.5)
pry-byebug (~> 3.10)
pry-rails (~> 0.3)
public_suffix (~> 5.0)
puma (~> 6.1)
pundit (~> 2.3)

@ -20,6 +20,8 @@ class RelationshipsController < ApplicationController
@form.save
rescue ActionController::ParameterMissing
# Do nothing
rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound
flash[:alert] = I18n.t('relationships.follow_failure') if action_from_button == 'follow'
ensure
redirect_to relationships_path(filter_params)
end
@ -61,8 +63,8 @@ class RelationshipsController < ApplicationController
'unfollow'
elsif params[:remove_from_followers]
'remove_from_followers'
elsif params[:block_domains]
'block_domains'
elsif params[:block_domains] || params[:remove_domains_from_followers]
'remove_domains_from_followers'
end
end

@ -5,7 +5,7 @@ import { FormattedMessage, injectIntl } from 'react-intl';
import Permalink from './permalink';
import classnames from 'classnames';
import Icon from 'flavours/glitch/components/icon';
import { autoPlayGif, languages as preloadedLanguages, translationEnabled } from 'flavours/glitch/initial_state';
import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state';
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';
const textMatchesTarget = (text, origin, host) => {
@ -315,7 +315,7 @@ class StatusContent extends React.PureComponent {
} = this.props;
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const renderTranslate = translationEnabled && this.context.identity.signedIn && this.props.onTranslate && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && status.get('language') !== null && intl.locale !== status.get('language');
const renderTranslate = this.props.onTranslate && status.get('translatable');
const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') };
const spoilerContent = { __html: status.get('spoilerHtml') };

@ -23,8 +23,8 @@ const mapDispatchToProps = (dispatch) => ({
},
});
export default @connect(null, mapDispatchToProps)
@withRouter
export default @withRouter
@connect(null, mapDispatchToProps)
class Header extends React.PureComponent {
static contextTypes = {

@ -78,8 +78,8 @@ class NavigationPanel extends React.Component {
{signedIn && (
<React.Fragment>
<ColumnLink transparent to='/conversations' icon='at' text={intl.formatMessage(messages.direct)} />
<ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} />
<ColumnLink transparent to='/bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} />
<ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} />
<ColumnLink transparent to='/lists' icon='list-ul' text={intl.formatMessage(messages.lists)} />
<ListPanel />

@ -143,8 +143,7 @@ export const unfollowModal = getMeta('unfollow_modal');
export const useBlurhash = getMeta('use_blurhash');
export const usePendingItems = getMeta('use_pending_items');
export const version = getMeta('version');
export const visibleReactions = getMeta('visible_reactions');
export const translationEnabled = getMeta('translation_enabled');
export const visibleReactions = getMeta('visible_reactions')
export const languages = initialState?.languages;
export const statusPageUrl = getMeta('status_page_url');

@ -164,8 +164,7 @@
white-space: pre-wrap;
p,
pre,
blockquote {
pre {
margin-bottom: 20px;
white-space: pre-wrap;
@ -174,79 +173,6 @@
}
}
h1,
h2,
h3,
h4,
h5 {
margin-top: 20px;
margin-bottom: 20px;
}
h1,
h2 {
font-weight: 700;
font-size: 18px;
}
h2 {
font-size: 16px;
}
h3,
h4,
h5 {
font-weight: 500;
}
blockquote {
padding-left: 10px;
border-left: 3px solid $inverted-text-color;
color: $inverted-text-color;
white-space: normal;
p:last-child {
margin-bottom: 0;
}
}
b,
strong {
font-weight: 700;
}
em,
i {
font-style: italic;
}
sub {
font-size: smaller;
vertical-align: sub;
}
sup {
font-size: smaller;
vertical-align: super;
}
ul,
ol {
margin-left: 1em;
p {
margin: 0;
}
}
ul {
list-style-type: disc;
}
ol {
list-style-type: decimal;
}
a {
color: $lighter-text-color;
text-decoration: none;

@ -68,8 +68,7 @@
}
p,
pre,
blockquote {
pre {
margin-bottom: 20px;
white-space: pre-wrap;
unicode-bidi: plaintext;
@ -79,89 +78,6 @@
}
}
.status__content__text,
.e-content {
overflow: hidden;
& > ul,
& > ol {
margin-bottom: 20px;
}
h1,
h2,
h3,
h4,
h5 {
margin-top: 20px;
margin-bottom: 20px;
}
h1,
h2 {
font-weight: 700;
font-size: 1.2em;
}
h2 {
font-size: 1.1em;
}
h3,
h4,
h5 {
font-weight: 500;
}
blockquote {
padding-left: 10px;
border-left: 3px solid $darker-text-color;
color: $darker-text-color;
white-space: normal;
p:last-child {
margin-bottom: 0;
}
}
b,
strong {
font-weight: 700;
}
em,
i {
font-style: italic;
}
sub {
font-size: smaller;
vertical-align: sub;
}
sup {
font-size: smaller;
vertical-align: super;
}
ul,
ol {
margin-left: 2em;
p {
margin: 0;
}
}
ul {
list-style-type: disc;
}
ol {
list-style-type: decimal;
}
}
a {
color: $secondary-text-color;
text-decoration: none;

@ -21,3 +21,4 @@
@import 'accessibility';
@import 'rtl';
@import 'dashboard';
@import 'rich_text';

@ -0,0 +1,99 @@
.status__content__text,
.e-content,
.reply-indicator__content {
pre,
blockquote {
margin-bottom: 20px;
white-space: pre-wrap;
unicode-bidi: plaintext;
&:last-child {
margin-bottom: 0;
}
}
blockquote {
padding-left: 10px;
border-left: 3px solid $darker-text-color;
color: $darker-text-color;
white-space: normal;
p:last-child {
margin-bottom: 0;
}
}
& > ul,
& > ol {
margin-bottom: 20px;
}
h1,
h2,
h3,
h4,
h5 {
margin-top: 20px;
margin-bottom: 20px;
}
h1,
h2 {
font-weight: 700;
font-size: 1.2em;
}
h2 {
font-size: 1.1em;
}
h3,
h4,
h5 {
font-weight: 500;
}
b,
strong {
font-weight: 700;
}
em,
i {
font-style: italic;
}
sub {
font-size: smaller;
vertical-align: sub;
}
sup {
font-size: smaller;
vertical-align: super;
}
ul,
ol {
margin-left: 2em;
p {
margin: 0;
}
}
ul {
list-style-type: disc;
}
ol {
list-style-type: decimal;
}
}
.reply-indicator__content {
blockquote {
border-left-color: $inverted-text-color;
color: $inverted-text-color;
}
}

@ -15,10 +15,10 @@ export default class ColumnBackButton extends React.PureComponent {
};
handleClick = () => {
if (window.history && window.history.length === 1) {
this.context.router.history.push('/');
} else {
if (window.history && window.history.state) {
this.context.router.history.goBack();
} else {
this.context.router.history.push('/');
}
};

@ -43,14 +43,6 @@ class ColumnHeader extends React.PureComponent {
animating: false,
};
historyBack = () => {
if (window.history && window.history.length === 1) {
this.context.router.history.push('/');
} else {
this.context.router.history.goBack();
}
};
handleToggleClick = (e) => {
e.stopPropagation();
this.setState({ collapsed: !this.state.collapsed, animating: true });
@ -69,7 +61,11 @@ class ColumnHeader extends React.PureComponent {
};
handleBackClick = () => {
this.historyBack();
if (window.history && window.history.state) {
this.context.router.history.goBack();
} else {
this.context.router.history.push('/');
}
};
handleTransitionEnd = () => {

@ -6,7 +6,7 @@ import { Link } from 'react-router-dom';
import classnames from 'classnames';
import PollContainer from 'mastodon/containers/poll_container';
import Icon from 'mastodon/components/icon';
import { autoPlayGif, languages as preloadedLanguages, translationEnabled } from 'mastodon/initial_state';
import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state';
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
@ -220,7 +220,7 @@ class StatusContent extends React.PureComponent {
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const renderReadMore = this.props.onClick && status.get('collapsed');
const renderTranslate = translationEnabled && this.context.identity.signedIn && this.props.onTranslate && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && status.get('language') !== null && intl.locale !== status.get('language');
const renderTranslate = this.props.onTranslate && status.get('translatable');
const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') };
const spoilerContent = { __html: status.get('spoilerHtml') };

@ -22,8 +22,8 @@ const mapDispatchToProps = (dispatch) => ({
},
});
export default @connect(null, mapDispatchToProps)
@withRouter
export default @withRouter
@connect(null, mapDispatchToProps)
class Header extends React.PureComponent {
static contextTypes = {

@ -82,8 +82,8 @@ class NavigationPanel extends React.Component {
{signedIn && (
<React.Fragment>
<ColumnLink transparent to='/conversations' icon='at' text={intl.formatMessage(messages.direct)} />
<ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} />
<ColumnLink transparent to='/bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} />
<ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} />
<ColumnLink transparent to='/lists' icon='list-ul' text={intl.formatMessage(messages.lists)} />
<ListPanel />

@ -474,10 +474,10 @@ class UI extends React.PureComponent {
};
handleHotkeyBack = () => {
if (window.history && window.history.length === 1) {
this.context.router.history.push('/');
} else {
if (window.history && window.history.state) {
this.context.router.history.goBack();
} else {
this.context.router.history.push('/');
}
};

@ -82,7 +82,6 @@
* @property {boolean=} use_pending_items
* @property {string} version
* @property {number} visible_reactions
* @property {boolean} translation_enabled
*/
/**
@ -136,7 +135,6 @@ export const useBlurhash = getMeta('use_blurhash');
export const usePendingItems = getMeta('use_pending_items');
export const version = getMeta('version');
export const visibleReactions = getMeta('visible_reactions');
export const translationEnabled = getMeta('translation_enabled');
export const languages = initialState?.languages;
export const statusPageUrl = getMeta('status_page_url');

@ -23,3 +23,4 @@
@import 'mastodon/dashboard';
@import 'mastodon/rtl';
@import 'mastodon/accessibility';
@import 'mastodon/rich_text';

@ -0,0 +1,64 @@
.status__content__text,
.e-content,
.reply-indicator__content {
pre,
blockquote {
margin-bottom: 20px;
white-space: pre-wrap;
unicode-bidi: plaintext;
&:last-child {
margin-bottom: 0;
}
}
blockquote {
padding-left: 10px;
border-left: 3px solid $darker-text-color;
color: $darker-text-color;
white-space: normal;
p:last-child {
margin-bottom: 0;
}
}
& > ul,
& > ol {
margin-bottom: 20px;
}
b,
strong {
font-weight: 700;
}
em,
i {
font-style: italic;
}
ul,
ol {
margin-left: 2em;
p {
margin: 0;
}
}
ul {
list-style-type: disc;
}
ol {
list-style-type: decimal;
}
}
.reply-indicator__content {
blockquote {
border-left-color: $inverted-text-color;
color: $inverted-text-color;
}
}

@ -324,25 +324,25 @@ class FeedManager
redis.pipelined do |pipeline|
ids.each do |feed_id|
pipeline.del(key(type, feed_id))
reblog_key = key(type, feed_id, 'reblogs')
# We collect a future for this: we don't block while getting
# it, but we can iterate over it later.
reblogged_id_sets[feed_id] = pipeline.zrange(reblog_key, 0, -1)
pipeline.del(reblog_key)
pipeline.del(key(type, feed_id), reblog_key)
end
end
# Remove all of the reblog tracking keys we just removed the
# references to.
redis.pipelined do |pipeline|
reblogged_id_sets.each do |feed_id, future|
future.value.each do |reblogged_id|
reblog_set_key = key(type, feed_id, "reblogs:#{reblogged_id}")
pipeline.del(reblog_set_key)
end
keys_to_delete = reblogged_id_sets.flat_map do |feed_id, future|
future.value.map do |reblogged_id|
key(type, feed_id, "reblogs:#{reblogged_id}")
end
end
redis.del(keys_to_delete) unless keys_to_delete.empty?
nil
end
private

@ -21,6 +21,10 @@ class TranslationService
ENV['DEEPL_API_KEY'].present? || ENV['LIBRE_TRANSLATE_ENDPOINT'].present?
end
def supported?(_source_language, _target_language)
false
end
def translate(_text, _source_language, _target_language)
raise NotImplementedError
end

@ -11,33 +11,53 @@ class TranslationService::DeepL < TranslationService
end
def translate(text, source_language, target_language)
request(text, source_language, target_language).perform do |res|
form = { text: text, source_lang: source_language&.upcase, target_lang: target_language, tag_handling: 'html' }
request(:post, '/v2/translate', form: form) do |res|
transform_response(res.body_with_limit)
end
end
def supported?(source_language, target_language)
source_language.in?(languages('source')) && target_language.in?(languages('target'))
end
private
def languages(type)
Rails.cache.fetch("translation_service/deepl/languages/#{type}", expires_in: 7.days, race_condition_ttl: 1.minute) do
request(:get, "/v2/languages?type=#{type}") do |res|
# In DeepL, EN and PT are deprecated in favor of EN-GB/EN-US and PT-BR/PT-PT, so
# they are supported but not returned by the API.
extra = type == 'source' ? [nil] : %w(en pt)
languages = Oj.load(res.body_with_limit).map { |language| language['language'].downcase }
languages + extra
end
end
end
def request(verb, path, **options)
req = Request.new(verb, "#{base_url}#{path}", **options)
req.add_headers(Authorization: "DeepL-Auth-Key #{@api_key}")
req.perform do |res|
case res.code
when 429
raise TooManyRequestsError
when 456
raise QuotaExceededError
when 200...300
transform_response(res.body_with_limit)
yield res
else
raise UnexpectedResponseError
end
end
end
private
def request(text, source_language, target_language)
req = Request.new(:post, endpoint_url, form: { text: text, source_lang: source_language&.upcase, target_lang: target_language, tag_handling: 'html' })
req.add_headers(Authorization: "DeepL-Auth-Key #{@api_key}")
req
end
def endpoint_url
def base_url
if @plan == 'free'
'https://api-free.deepl.com/v2/translate'
'https://api-free.deepl.com'
else
'https://api.deepl.com/v2/translate'
'https://api.deepl.com'
end
end

@ -9,29 +9,45 @@ class TranslationService::LibreTranslate < TranslationService
end
def translate(text, source_language, target_language)
request(text, source_language, target_language).perform do |res|
body = Oj.dump(q: text, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key)
request(:post, '/translate', body: body) do |res|
transform_response(res.body_with_limit, source_language)
end
end
def supported?(source_language, target_language)
languages.key?(source_language) && languages[source_language].include?(target_language)
end
private
def languages
Rails.cache.fetch('translation_service/libre_translate/languages', expires_in: 7.days, race_condition_ttl: 1.minute) do
request(:get, '/languages') do |res|
languages = Oj.load(res.body_with_limit).to_h { |language| [language['code'], language['targets']] }
languages[nil] = languages.values.flatten.uniq
languages
end
end
end
def request(verb, path, **options)
req = Request.new(verb, "#{@base_url}#{path}", allow_local: true, **options)
req.add_headers('Content-Type': 'application/json')
req.perform do |res|
case res.code
when 429
raise TooManyRequestsError
when 403
raise QuotaExceededError
when 200...300
transform_response(res.body_with_limit, source_language)
yield res
else
raise UnexpectedResponseError
end
end
end
private
def request(text, source_language, target_language)
body = Oj.dump(q: text, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key)
req = Request.new(:post, "#{@base_url}/translate", body: body, allow_local: true)
req.add_headers('Content-Type': 'application/json')
req
end
def transform_response(str, source_language)
json = Oj.load(str, mode: :strict)

@ -7,9 +7,17 @@ class ApplicationMailer < ActionMailer::Base
helper :instance
helper :formatting
after_action :set_autoreply_headers!
protected
def locale_for_account(account, &block)
I18n.with_locale(account.user_locale || I18n.default_locale, &block)
end
def set_autoreply_headers!
headers['Precedence'] = 'list'
headers['X-Auto-Response-Suppress'] = 'All'
headers['Auto-Submitted'] = 'auto-generated'
end
end

@ -61,7 +61,7 @@ module Omniauthable
user.account.avatar_remote_url = nil
end
user.skip_confirmation! if email_is_verified
user.confirm! if email_is_verified
user.save!
user
end

@ -17,8 +17,8 @@ class Form::AccountBatch
unfollow!
when 'remove_from_followers'
remove_from_followers!
when 'block_domains'
block_domains!
when 'remove_domains_from_followers'
remove_domains_from_followers!
when 'approve'
approve!
when 'reject'
@ -35,9 +35,15 @@ class Form::AccountBatch
private
def follow!
error = nil
accounts.each do |target_account|
FollowService.new.call(current_account, target_account)
rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound => e
error ||= e
end
raise error if error.present?
end
def unfollow!
@ -50,10 +56,8 @@ class Form::AccountBatch
RemoveFromFollowersService.new.call(current_account, account_ids)
end
def block_domains!
AfterAccountDomainBlockWorker.push_bulk(account_domains) do |domain|
[current_account.id, domain]
end
def remove_domains_from_followers!
RemoveDomainsFromFollowersService.new.call(current_account, account_domains)
end
def account_domains

@ -238,6 +238,16 @@ class Status < ApplicationRecord
public_visibility? || unlisted_visibility?
end
def translatable?
translate_target_locale = I18n.locale.to_s.split(/[_-]/).first
distributable? &&
content.present? &&
language != translate_target_locale &&
TranslationService.configured? &&
TranslationService.configured.supported?(language, translate_target_locale)
end
alias sign? distributable?
def with_media?

@ -49,7 +49,6 @@ class InitialStateSerializer < ActiveModel::Serializer
timeline_preview: Setting.timeline_preview,
activity_api_enabled: Setting.activity_api_enabled,
single_user_mode: Rails.configuration.x.single_user_mode,
translation_enabled: TranslationService.configured?,
trends_as_landing_page: Setting.trends_as_landing_page,
status_page_url: Setting.status_page_url,
}

@ -4,7 +4,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
include FormattingHelper
attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id,
:sensitive, :spoiler_text, :visibility, :language,
:sensitive, :spoiler_text, :visibility, :language, :translatable,
:uri, :url, :replies_count, :reblogs_count,
:favourites_count, :edited_at
@ -53,6 +53,10 @@ class REST::StatusSerializer < ActiveModel::Serializer
object.account.user_shows_application? || (current_user? && current_user.account_id == object.account_id)
end
def translatable
current_user? && object.translatable?
end
def visibility
# This visibility is masked behind "private"
# to avoid API changes because there are no

@ -50,7 +50,7 @@ class BatchedRemoveStatusService < BaseService
@status_id_cutoff = Mastodon::Snowflake.id_at(2.weeks.ago)
redis.pipelined do |pipeline|
statuses.each do |status|
unpush_from_public_timelines(pipeline, status)
unpush_from_public_timelines(status, pipeline)
end
end
end
@ -73,7 +73,7 @@ class BatchedRemoveStatusService < BaseService
end
end
def unpush_from_public_timelines(pipeline, status)
def unpush_from_public_timelines(status, pipeline)
return unless status.public_visibility? && status.id > @status_id_cutoff
payload = Oj.dump(event: :delete, payload: status.id.to_s)

@ -0,0 +1,40 @@
# frozen_string_literal: true
class FollowMigrationService < FollowService
# Follow an account with the same settings as another account, and unfollow the old account once the request is sent
# @param [Account] source_account From which to follow
# @param [Account] target_account Account to follow
# @param [Account] old_target_account Account to unfollow once the follow request has been sent to the new one
# @option [Boolean] bypass_locked Whether to immediately follow the new account even if it is locked
def call(source_account, target_account, old_target_account, bypass_locked: false)
@old_target_account = old_target_account
follow = source_account.active_relationships.find_by(target_account: old_target_account)
reblogs = follow&.show_reblogs?
notify = follow&.notify?
languages = follow&.languages
super(source_account, target_account, reblogs: reblogs, notify: notify, languages: languages, bypass_locked: bypass_locked, bypass_limit: true)
end
private
def request_follow!
follow_request = @source_account.request_follow!(@target_account, **follow_options.merge(rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit]))
if @target_account.local?
LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request')
UnfollowService.new.call(@source_account, @old_target_account, skip_unmerge: true)
elsif @target_account.activitypub?
ActivityPub::MigratedFollowDeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url, @old_target_account.id)
end
follow_request
end
def direct_follow!
follow = super
UnfollowService.new.call(@source_account, @old_target_account, skip_unmerge: true)
follow
end
end

@ -0,0 +1,23 @@
# frozen_string_literal: true
class RemoveDomainsFromFollowersService < BaseService
include Payloadable
def call(source_account, target_domains)
source_account.passive_relationships.where(account_id: Account.where(domain: target_domains)).find_each do |follow|
follow.destroy
create_notification(follow) if source_account.local? && !follow.account.local? && follow.account.activitypub?
end
end
private
def create_notification(follow)
ActivityPub::DeliveryWorker.perform_async(build_json(follow), follow.target_account_id, follow.account.inbox_url)
end
def build_json(follow)
Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
end
end

@ -6,7 +6,7 @@ class TranslateStatusService < BaseService
include FormattingHelper
def call(status, target_language)
raise Mastodon::NotPermittedError unless status.public_visibility? || status.unlisted_visibility?
raise Mastodon::NotPermittedError unless status.translatable?
@status = status
@content = status_content_format(@status)

@ -6,7 +6,7 @@ class Ed25519KeyValidator < ActiveModel::EachValidator
key = Base64.decode64(value)
record.errors[attribute] << I18n.t('crypto.errors.invalid_key') unless verified?(key)
record.errors.add(attribute, I18n.t('crypto.errors.invalid_key')) unless verified?(key)
end
private

@ -8,7 +8,7 @@ class Ed25519SignatureValidator < ActiveModel::EachValidator
signature = Base64.decode64(value)
message = option_to_value(record, :message)
record.errors[attribute] << I18n.t('crypto.errors.invalid_signature') unless verified?(verify_key, signature, message)
record.errors.add(attribute, I18n.t('crypto.errors.invalid_signature')) unless verified?(verify_key, signature, message)
end
private

@ -5,7 +5,7 @@ RSS::Builder.build do |doc|
doc.image(full_asset_url(@account.avatar.url(:original)), display_name(@account), params[:tag].present? ? short_account_tag_url(@account, params[:tag]) : short_account_url(@account))
doc.last_build_date(@statuses.first.created_at) if @statuses.any?
doc.icon(full_asset_url(@account.avatar.url(:original)))
doc.generator("Mastodon v#{Mastodon::Version.to_s}")
doc.generator("Mastodon v#{Mastodon::Version}")
@statuses.each do |status|
doc.item do |item|
@ -18,12 +18,12 @@ RSS::Builder.build do |doc|
item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size)
end
status.ordered_media_attachments.each do |media|
item.media_content(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size) do |media_content|
media_content.medium(media.gifv? ? 'image' : media.type.to_s)
status.ordered_media_attachments.each do |media_attachment|
item.media_content(full_asset_url(media_attachment.file.url(:original, false)), media_attachment.file.content_type, media_attachment.file.size) do |media_content|
media_content.medium(media_attachment.gifv? ? 'image' : media_attachment.type.to_s)
media_content.rating(status.sensitive? ? 'adult' : 'nonadult')
media_content.description(media.description) if media.description.present?
media_content.thumbnail(media.thumbnail.url(:original, false)) if media.thumbnail?
media_content.description(media_attachment.description) if media_attachment.description.present?
media_content.thumbnail(media_attachment.thumbnail.url(:original, false)) if media_attachment.thumbnail?
end
end

@ -31,7 +31,7 @@
%td
- if @status.trend.allowed?
%abbr{ title: t('admin.trends.tags.current_score', score: @status.trend.score) }= t('admin.trends.tags.trending_rank', rank: @status.trend.rank)
- elsif @status.trend.requires_review?
- elsif @status.requires_review?
= t('admin.trends.pending_review')
- else
= t('admin.trends.not_allowed_to_trend')

@ -45,7 +45,7 @@
= f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_followers')]), name: :remove_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('relationships.confirm_remove_selected_followers') } unless following_relationship?
= f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :block_domains, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship?
= f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :remove_domains_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship?
.batch-table__body
- if @accounts.empty?
= nothing_here 'nothing-here--under-tabs'

@ -3,7 +3,7 @@ RSS::Builder.build do |doc|
doc.description(I18n.t('rss.descriptions.tag', hashtag: @tag.display_name))
doc.link(tag_url(@tag))
doc.last_build_date(@statuses.first.created_at) if @statuses.any?
doc.generator("Mastodon v#{Mastodon::Version.to_s}")
doc.generator("Mastodon v#{Mastodon::Version}")
@statuses.each do |status|
doc.item do |item|
@ -16,12 +16,12 @@ RSS::Builder.build do |doc|
item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size)
end
status.ordered_media_attachments.each do |media|
item.media_content(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size) do |media_content|
media_content.medium(media.gifv? ? 'image' : media.type.to_s)
status.ordered_media_attachments.each do |media_attachment|
item.media_content(full_asset_url(media_attachment.file.url(:original, false)), media_attachment.file.content_type, media_attachment.file.size) do |media_content|
media_content.medium(media_attachment.gifv? ? 'image' : media_attachment.type.to_s)
media_content.rating(status.sensitive? ? 'adult' : 'nonadult')
media_content.description(media.description) if media.description.present?
media_content.thumbnail(media.thumbnail.url(:original, false)) if media.thumbnail?
media_content.description(media_attachment.description) if media_attachment.description.present?
media_content.thumbnail(media_attachment.thumbnail.url(:original, false)) if media_attachment.thumbnail?
end
end

@ -17,7 +17,7 @@
%tbody
%tr
%td
= image_tag full_pack_url('media/images/mailer/icon_warning.png'), alt: ''
= image_tag full_pack_url('media/images/mailer/icon_flag.png'), alt: ''
%h1= t 'user_mailer.appeal_rejected.title'

@ -9,4 +9,4 @@ doc << Ox::Element.new('XRD').tap do |xrd|
end
end
('<?xml version="1.0" encoding="UTF-8"?>' + Ox.dump(doc, effort: :tolerant)).force_encoding('UTF-8')
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>#{Ox.dump(doc, effort: :tolerant)}".force_encoding('UTF-8')

@ -10,6 +10,16 @@ class ActivityPub::DeliveryWorker
sidekiq_options queue: 'push', retry: 16, dead: false
# Unfortunately, we cannot control Sidekiq's jitter, so add our own
sidekiq_retry_in do |count|
# This is Sidekiq's default delay
delay = (count**4) + 15
# Our custom jitter, that will be added to Sidekiq's built-in one.
# Sidekiq's built-in jitter is `rand(10) * (count + 1)`
jitter = rand(0.5 * (count**4))
delay + jitter
end
HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze
def perform(json, source_account_id, inbox_url, options = {})

@ -0,0 +1,17 @@
# frozen_string_literal: true
class ActivityPub::MigratedFollowDeliveryWorker < ActivityPub::DeliveryWorker
def perform(json, source_account_id, inbox_url, old_target_account_id, options = {})
super(json, source_account_id, inbox_url, options)
unfollow_old_account!(old_target_account_id)
end
private
def unfollow_old_account!(old_target_account_id)
old_target_account = Account.find(old_target_account_id)
UnfollowService.new.call(@source_account, old_target_account, skip_unmerge: true)
rescue
true
end
end

@ -20,7 +20,7 @@ class Scheduler::FollowRecommendationsScheduler
Trends.available_locales.each do |locale|
recommendations = if AccountSummary.safe.filtered.localized(locale).exists? # We can skip the work if no accounts with that language exist
FollowRecommendation.localized(locale).order(rank: :desc).limit(SET_SIZE).map { |recommendation| [recommendation.account_id, recommendation.rank] }
FollowRecommendation.localized(locale).order(rank: :desc).limit(SET_SIZE).map { |recommendation| [recommendation.rank, recommendation.account_id] }
else
[]
end
@ -33,14 +33,14 @@ class Scheduler::FollowRecommendationsScheduler
# Language-specific results should be above language-agnostic ones,
# otherwise language-agnostic ones will always overshadow them
recommendations.map! { |(account_id, rank)| [account_id, rank + max_fallback_rank] }
recommendations.map! { |(rank, account_id)| [rank + max_fallback_rank, account_id] }
added = 0
fallback_recommendations.each do |recommendation|
next if recommendations.any? { |(account_id, _)| account_id == recommendation.account_id }
next if recommendations.any? { |(_, account_id)| account_id == recommendation.account_id }
recommendations << [recommendation.account_id, recommendation.rank]
recommendations << [recommendation.rank, recommendation.account_id]
added += 1
break if added >= missing
@ -49,10 +49,7 @@ class Scheduler::FollowRecommendationsScheduler
redis.multi do |multi|
multi.del(key(locale))
recommendations.each do |(account_id, rank)|
multi.zadd(key(locale), rank, account_id)
end
multi.zadd(key(locale), recommendations)
end
end
end

@ -10,13 +10,7 @@ class UnfollowFollowWorker
old_target_account = Account.find(old_target_account_id)
new_target_account = Account.find(new_target_account_id)
follow = follower_account.active_relationships.find_by(target_account: old_target_account)
reblogs = follow&.show_reblogs?
notify = follow&.notify?
languages = follow&.languages
FollowService.new.call(follower_account, new_target_account, reblogs: reblogs, notify: notify, languages: languages, bypass_locked: bypass_locked, bypass_limit: true)
UnfollowService.new.call(follower_account, old_target_account, skip_unmerge: true)
FollowMigrationService.new.call(follower_account, new_target_account, old_target_account, bypass_locked: bypass_locked)
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
true
end

@ -5,6 +5,7 @@ default: &default
connect_timeout: 15
encoding: unicode
sslmode: <%= ENV['DB_SSLMODE'] || "prefer" %>
application_name: ''
development:
<<: *default

@ -87,6 +87,8 @@ Rails.application.configure do
config.x.otp_secret = ENV.fetch('OTP_SECRET', '1fc2b87989afa6351912abeebe31ffc5c476ead9bf8b3d74cbc4a302c7b69a45b40b1bbef3506ddad73e942e15ed5ca4b402bf9a66423626051104f4b5f05109')
end
Redis.raise_deprecations = true
ActiveRecordQueryTrace.enabled = ENV['QUERY_TRACE_ENABLED'] == 'true'
module PrivateAddressCheck

@ -128,6 +128,7 @@ Rails.application.configure do
enable_starttls_auto: enable_starttls_auto,
tls: ENV['SMTP_TLS'].presence && ENV['SMTP_TLS'] == 'true',
ssl: ENV['SMTP_SSL'].presence && ENV['SMTP_SSL'] == 'true',
read_timeout: 20,
}
config.action_mailer.delivery_method = ENV.fetch('SMTP_DELIVERY_METHOD', 'smtp').to_sym

@ -73,3 +73,5 @@ end
# Catch serialization warnings early
Sidekiq.strict_args!
Redis.raise_deprecations = true

@ -90,6 +90,12 @@ if ENV['S3_ENABLED'] == 'true'
)
end
if ENV.has_key?('S3_STORAGE_CLASS')
Paperclip::Attachment.default_options[:s3_headers].merge!(
'X-Amz-Storage-Class' => ENV['S3_STORAGE_CLASS']
)
end
# Some S3-compatible providers might not actually be compatible with some APIs
# used by kt-paperclip, see https://github.com/mastodon/mastodon/issues/16822
if ENV['S3_FORCE_SINGLE_REQUEST'] == 'true'

@ -0,0 +1 @@
Redis.sadd_returns_boolean = false

@ -2,8 +2,8 @@
es:
admin:
custom_emojis:
batch_copy_error: 'Se produjo un error cuando se copian algunos emojis seleccionados %{message}'
batch_error: 'Ocurrió un error %{message}'
batch_copy_error: Se produjo un error cuando se copian algunos emojis seleccionados %{message}
batch_error: Ocurrió un error %{message}
settings:
captcha_enabled:
title: Requerir que usuarixs nuevxs resuelvan un CAPTCHA para confirmar su cuenta

@ -1412,6 +1412,7 @@ en:
confirm_remove_selected_followers: Are you sure you want to remove selected followers?
confirm_remove_selected_follows: Are you sure you want to remove selected follows?
dormant: Dormant
follow_failure: Could not follow some of the selected accounts.
follow_selected_followers: Follow selected followers
followers: Followers
following: Following
@ -1451,6 +1452,7 @@ en:
electron: Electron
firefox: Firefox
generic: Unknown browser
huawei_browser: Huawei Browser
ie: Internet Explorer
micro_messenger: MicroMessenger
nokia: Nokia S40 Ovi Browser
@ -1460,6 +1462,7 @@ en:
qq: QQ Browser
safari: Safari
uc_browser: UC Browser
unknown_browser: Unknown Browser
weibo: Weibo
current_session: Current session
description: "%{browser} on %{platform}"
@ -1472,9 +1475,10 @@ en:
chrome_os: ChromeOS
firefox_os: Firefox OS
ios: iOS
kai_os: KaiOS
linux: Linux
mac: macOS
other: unknown platform
unknown_platform: Unknown Platform
windows: Windows
windows_mobile: Windows Mobile
windows_phone: Windows Phone

@ -1,6 +1,6 @@
class FixReblogsInFeeds < ActiveRecord::Migration[5.1]
def up
redis = Redis.current
redis = RedisConfiguration.pool.checkout
fm = FeedManager.instance
# Old scheme:

@ -2,7 +2,8 @@ class MigrateUnavailableInboxes < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
urls = Redis.current.smembers('unavailable_inboxes')
redis = RedisConfiguration.pool.checkout
urls = redis.smembers('unavailable_inboxes')
hosts = urls.map do |url|
Addressable::URI.parse(url).normalized_host
@ -14,7 +15,7 @@ class MigrateUnavailableInboxes < ActiveRecord::Migration[5.2]
UnavailableDomain.create(domain: host)
end
Redis.current.del(*(['unavailable_inboxes'] + Redis.current.keys('exhausted_deliveries:*')))
redis.del(*(['unavailable_inboxes'] + redis.keys('exhausted_deliveries:*')))
end
def down; end

@ -627,7 +627,7 @@ module Mastodon
exit(1)
end
unless options[:force] || migration.target_acount_id == account.moved_to_account_id
unless options[:force] || migration.target_account_id == account.moved_to_account_id
say('The specified account is not redirecting to its last migration target. Use --force if you want to replay the migration anyway', :red)
exit(1)
end

@ -53,11 +53,7 @@ module Mastodon
desc 'clear', 'Remove all home and list feeds from Redis'
def clear
keys = redis.keys('feed:*')
redis.pipelined do |pipeline|
keys.each { |key| pipeline.del(key) }
end
redis.del(keys)
say('OK', :green)
end
end

@ -25,7 +25,7 @@ module Mastodon
end
def suffix_version
'+2.0.2'
'+2.0.3'
end
def to_a

@ -73,12 +73,11 @@ class Sanitize
elements: %w(p br span a abbr del pre blockquote code b strong u sub sup i em h1 h2 h3 h4 h5 ul ol li),
attributes: {
'a' => %w(href rel class title),
'span' => %w(class),
'abbr' => %w(title),
'a' => %w(href rel class title),
'span' => %w(class),
'blockquote' => %w(cite),
'ol' => %w(start reversed),
'li' => %w(value),
'ol' => %w(start reversed),
'li' => %w(value),
},
add_attributes: {

@ -85,6 +85,7 @@
"object.values": "^1.1.6",
"path-complete-extname": "^1.0.0",
"pg": "^8.5.0",
"pg-connection-string": "^2.5.0",
"postcss": "^8.4.21",
"postcss-loader": "^3.0.0",
"promise.prototype.finally": "^3.1.4",

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::AccountActionsController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #new' do
let(:account) { Fabricate(:account) }
it 'returns http success' do
get :new, params: { account_id: account.id }
expect(response).to have_http_status(:success)
end
end
end

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::AnnouncementsController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #index' do
it 'returns http success' do
get :index
expect(response).to have_http_status(:success)
end
end
end

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::FollowRecommendationsController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #show' do
it 'returns http success' do
get :show
expect(response).to have_http_status(:success)
end
end
end

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::IpBlocksController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #index' do
it 'returns http success' do
get :index
expect(response).to have_http_status(:success)
end
end
end

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::RelationshipsController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #index' do
let(:account) { Fabricate(:account) }
it 'returns http success' do
get :index, params: { account_id: account.id }
expect(response).to have_http_status(:success)
end
end
end

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::RelaysController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #index' do
it 'returns http success' do
get :index
expect(response).to have_http_status(:success)
end
end
end

@ -57,6 +57,9 @@ describe Admin::Reports::ActionsController do
let!(:media) { Fabricate(:media_attachment, account: target_account, status: statuses[0]) }
let(:report) { Fabricate(:report, target_account: target_account, status_ids: statuses.map(&:id)) }
let(:text) { 'hello' }
let(:common_params) do
{ report_id: report.id, text: text }
end
shared_examples 'common behavior' do
it 'closes the report' do
@ -72,6 +75,26 @@ describe Admin::Reports::ActionsController do
subject
expect(response).to redirect_to(admin_reports_path)
end
context 'when text is unset' do
let(:common_params) do
{ report_id: report.id }
end
it 'closes the report' do
expect { subject }.to change { report.reload.action_taken? }.from(false).to(true)
end
it 'creates a strike with the expected text' do
expect { subject }.to change { report.target_account.strikes.count }.by(1)
expect(report.target_account.strikes.last.text).to eq ''
end
it 'redirects' do
subject
expect(response).to redirect_to(admin_reports_path)
end
end
end
shared_examples 'all action types' do
@ -124,13 +147,13 @@ describe Admin::Reports::ActionsController do
end
context 'action as submit button' do
subject { post :create, params: { report_id: report.id, text: text, action => '' } }
subject { post :create, params: common_params.merge({ action => '' }) }
it_behaves_like 'all action types'
end
context 'action as submit button' do
subject { post :create, params: { report_id: report.id, text: text, moderation_action: action } }
subject { post :create, params: common_params.merge({ moderation_action: action }) }
it_behaves_like 'all action types'
end

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::RulesController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #index' do
it 'returns http success' do
get :index
expect(response).to have_http_status(:success)
end
end
end

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::Settings::AboutController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #show' do
it 'returns http success' do
get :show
expect(response).to have_http_status(:success)
end
end
end

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::Settings::AppearanceController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #show' do
it 'returns http success' do
get :show
expect(response).to have_http_status(:success)
end
end
end

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::Settings::ContentRetentionController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #show' do
it 'returns http success' do
get :show
expect(response).to have_http_status(:success)
end
end
end

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::Settings::DiscoveryController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #show' do
it 'returns http success' do
get :show
expect(response).to have_http_status(:success)
end
end
end

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::Settings::RegistrationsController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #show' do
it 'returns http success' do
get :show
expect(response).to have_http_status(:success)
end
end
end

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::SiteUploadsController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'DELETE #destroy' do
let(:site_upload) { Fabricate(:site_upload, var: 'thumbnail') }
it 'returns http success' do
delete :destroy, params: { id: site_upload.id }
expect(response).to redirect_to(admin_settings_path)
end
end
end

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::Trends::Links::PreviewCardProvidersController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #index' do
it 'returns http success' do
get :index
expect(response).to have_http_status(:success)
end
end
end

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::Trends::LinksController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #index' do
it 'returns http success' do
get :index
expect(response).to have_http_status(:success)
end
end
end

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::Trends::StatusesController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #index' do
it 'returns http success' do
get :index
expect(response).to have_http_status(:success)
end
end
end

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::Trends::TagsController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #index' do
it 'returns http success' do
get :index
expect(response).to have_http_status(:success)
end
end
end

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::WarningPresetsController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #index' do
it 'returns http success' do
get :index
expect(response).to have_http_status(:success)
end
end
end

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::Webhooks::SecretsController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'POST #rotate' do
let(:webhook) { Fabricate(:webhook) }
it 'returns http success' do
post :rotate, params: { webhook_id: webhook.id }
expect(response).to redirect_to(admin_webhook_path(webhook))
end
end
end

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::WebhooksController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #index' do
it 'returns http success' do
get :index
expect(response).to have_http_status(:success)
end
end
end

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'rails_helper'
describe Api::V1::Accounts::FamiliarFollowersController do
render_views
let(:user) { Fabricate(:user) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:follows') }
let(:account) { Fabricate(:account) }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'GET #index' do
it 'returns http success' do
get :index, params: { account_id: account.id, limit: 2 }
expect(response).to have_http_status(200)
end
end
end

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'rails_helper'
describe Api::V1::Accounts::FeaturedTagsController do
render_views
let(:user) { Fabricate(:user) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
let(:account) { Fabricate(:account) }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'GET #index' do
it 'returns http success' do
get :index, params: { account_id: account.id, limit: 2 }
expect(response).to have_http_status(200)
end
end
end

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'rails_helper'
describe Api::V1::Accounts::IdentityProofsController do
render_views
let(:user) { Fabricate(:user) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
let(:account) { Fabricate(:account) }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'GET #index' do
it 'returns http success' do
get :index, params: { account_id: account.id, limit: 2 }
expect(response).to have_http_status(200)
end
end
end

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'rails_helper'
describe Api::V1::Accounts::LookupController do
render_views
let(:user) { Fabricate(:user) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
let(:account) { Fabricate(:account) }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'GET #show' do
it 'returns http success' do
get :show, params: { account_id: account.id, acct: account.acct }
expect(response).to have_http_status(200)
end
end
end

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'rails_helper'
describe Api::V1::Admin::CanonicalEmailBlocksController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') }
let(:account) { Fabricate(:account) }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'GET #index' do
it 'returns http success' do
get :index, params: { account_id: account.id, limit: 2 }
expect(response).to have_http_status(200)
end
end
end

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'rails_helper'
describe Api::V1::Admin::DimensionsController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') }
let(:account) { Fabricate(:account) }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'POST #create' do
it 'returns http success' do
post :create, params: { account_id: account.id, limit: 2 }
expect(response).to have_http_status(200)
end
end
end

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'rails_helper'
describe Api::V1::Admin::EmailDomainBlocksController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') }
let(:account) { Fabricate(:account) }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'GET #index' do
it 'returns http success' do
get :index, params: { account_id: account.id, limit: 2 }
expect(response).to have_http_status(200)
end
end
end

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'rails_helper'
describe Api::V1::Admin::IpBlocksController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') }
let(:account) { Fabricate(:account) }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'GET #index' do
it 'returns http success' do
get :index, params: { account_id: account.id, limit: 2 }
expect(response).to have_http_status(200)
end
end
end

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'rails_helper'
describe Api::V1::Admin::MeasuresController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') }
let(:account) { Fabricate(:account) }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'POST #create' do
it 'returns http success' do
post :create, params: { account_id: account.id, limit: 2 }
expect(response).to have_http_status(200)
end
end
end

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'rails_helper'
describe Api::V1::Admin::RetentionController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') }
let(:account) { Fabricate(:account) }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'POST #create' do
it 'returns http success' do
post :create, params: { account_id: account.id, limit: 2 }
expect(response).to have_http_status(200)
end
end
end

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'rails_helper'
describe Api::V1::Admin::Trends::LinksController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') }
let(:account) { Fabricate(:account) }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'GET #index' do
it 'returns http success' do
get :index, params: { account_id: account.id, limit: 2 }
expect(response).to have_http_status(200)
end
end
end

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'rails_helper'
describe Api::V1::Admin::Trends::StatusesController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') }
let(:account) { Fabricate(:account) }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'GET #index' do
it 'returns http success' do
get :index, params: { account_id: account.id, limit: 2 }
expect(response).to have_http_status(200)
end
end
end

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

Loading…
Cancel
Save