Merge branch 'main' into glitch-soc/merge-upstream
Conflicts: - `README.md`: Upstream README has been changed, but we have a completely different one. Kept our `README.md`. - `lib/sanitize_ext/sanitize_config.rb`: Upstream added support for more incoming HTML tags (a large subset of what glitch-soc accepts). Change the code style to match upstream's but otherwise do not change our code. - `spec/lib/sanitize_config_spec.rb`: Upstream added support for more incoming HTML tags (a large subset of what glitch-soc accepts). Kept our version, since the tests are mostly glitch-soc's, except for cases which are purposefuly different.
This commit is contained in:
commit
7623e18124
|
@ -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'
|
||||
|
|
3
Gemfile
3
Gemfile
|
@ -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'
|
||||
|
|
16
Gemfile.lock
16
Gemfile.lock
|
@ -144,7 +144,7 @@ GEM
|
|||
bootsnap (1.16.0)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (5.4.0)
|
||||
browser (4.2.0)
|
||||
browser (5.3.1)
|
||||
brpoplpush-redis_script (0.1.3)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||
redis (>= 1.0, < 6)
|
||||
|
@ -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.1)
|
||||
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.5.1)
|
||||
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
|
||||
|
||||
|
|
|
@ -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('/');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -80,7 +80,6 @@
|
|||
* @property {boolean} use_blurhash
|
||||
* @property {boolean=} use_pending_items
|
||||
* @property {string} version
|
||||
* @property {boolean} translation_enabled
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -132,7 +131,6 @@ 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 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;
|
||||
}
|
||||
}
|
|
@ -322,27 +322,27 @@ class FeedManager
|
|||
def clean_feeds!(type, ids)
|
||||
reblogged_id_sets = {}
|
||||
|
||||
redis.pipelined do
|
||||
redis.pipelined do |pipeline|
|
||||
ids.each do |feed_id|
|
||||
redis.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] = redis.zrange(reblog_key, 0, -1)
|
||||
redis.del(reblog_key)
|
||||
reblogged_id_sets[feed_id] = pipeline.zrange(reblog_key, 0, -1)
|
||||
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
|
||||
reblogged_id_sets.each do |feed_id, future|
|
||||
future.value.each do |reblogged_id|
|
||||
reblog_set_key = key(type, feed_id, "reblogs:#{reblogged_id}")
|
||||
redis.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
|
||||
|
|
|
@ -20,9 +20,9 @@ class FollowRecommendationSuppression < ApplicationRecord
|
|||
private
|
||||
|
||||
def remove_follow_recommendations
|
||||
redis.pipelined do
|
||||
redis.pipelined do |pipeline|
|
||||
I18n.available_locales.each do |locale|
|
||||
redis.zrem("follow_recommendations:#{locale}", account_id)
|
||||
pipeline.zrem("follow_recommendations:#{locale}", account_id)
|
||||
end
|
||||
end
|
||||
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
|
||||
|
|
|
@ -237,6 +237,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?
|
||||
|
|
|
@ -44,7 +44,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
|
||||
|
||||
|
@ -52,6 +52,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
|
||||
|
|
|
@ -48,9 +48,9 @@ class BatchedRemoveStatusService < BaseService
|
|||
|
||||
# Cannot be batched
|
||||
@status_id_cutoff = Mastodon::Snowflake.id_at(2.weeks.ago)
|
||||
redis.pipelined do
|
||||
redis.pipelined do |pipeline|
|
||||
statuses.each do |status|
|
||||
unpush_from_public_timelines(status)
|
||||
unpush_from_public_timelines(status, pipeline)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -73,22 +73,22 @@ class BatchedRemoveStatusService < BaseService
|
|||
end
|
||||
end
|
||||
|
||||
def unpush_from_public_timelines(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)
|
||||
|
||||
redis.publish('timeline:public', payload)
|
||||
redis.publish(status.local? ? 'timeline:public:local' : 'timeline:public:remote', payload)
|
||||
pipeline.publish('timeline:public', payload)
|
||||
pipeline.publish(status.local? ? 'timeline:public:local' : 'timeline:public:remote', payload)
|
||||
|
||||
if status.media_attachments.any?
|
||||
redis.publish('timeline:public:media', payload)
|
||||
redis.publish(status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', payload)
|
||||
pipeline.publish('timeline:public:media', payload)
|
||||
pipeline.publish(status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', payload)
|
||||
end
|
||||
|
||||
status.tags.map { |tag| tag.name.mb_chars.downcase }.each do |hashtag|
|
||||
redis.publish("timeline:hashtag:#{hashtag}", payload)
|
||||
redis.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local?
|
||||
pipeline.publish("timeline:hashtag:#{hashtag}", payload)
|
||||
pipeline.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local?
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -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
|
|
@ -1408,6 +1408,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
|
||||
|
@ -1447,6 +1448,7 @@ en:
|
|||
electron: Electron
|
||||
firefox: Firefox
|
||||
generic: Unknown browser
|
||||
huawei_browser: Huawei Browser
|
||||
ie: Internet Explorer
|
||||
micro_messenger: MicroMessenger
|
||||
nokia: Nokia S40 Ovi Browser
|
||||
|
@ -1456,6 +1458,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}"
|
||||
|
@ -1468,9 +1471,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
|
||||
keys.each { |key| redis.del(key) }
|
||||
end
|
||||
|
||||
redis.del(keys)
|
||||
say('OK', :green)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -75,7 +75,6 @@ class Sanitize
|
|||
attributes: {
|
||||
'a' => %w(href rel class title),
|
||||
'span' => %w(class),
|
||||
'abbr' => %w(title),
|
||||
'blockquote' => %w(cite),
|
||||
'ol' => %w(start reversed),
|
||||
'li' => %w(value),
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe Api::V1::Admin::Trends::TagsController 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::DirectoriesController 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 #show' do
|
||||
it 'returns http success' do
|
||||
get :show
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe Api::V1::FeaturedTags::SuggestionsController 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::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,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe Api::V1::Instances::DomainBlocksController do
|
||||
render_views
|
||||
|
||||
describe 'GET #index' do
|
||||
it 'returns http success' do
|
||||
Setting.show_domain_blocks = 'all'
|
||||
get :index
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe Api::V1::Instances::ExtendedDescriptionsController do
|
||||
render_views
|
||||
|
||||
describe 'GET #show' do
|
||||
it 'returns http success' do
|
||||
get :show
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe Api::V1::Instances::PrivacyPoliciesController do
|
||||
render_views
|
||||
|
||||
describe 'GET #show' do
|
||||
it 'returns http success' do
|
||||
get :show
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe Api::V1::Instances::RulesController do
|
||||
render_views
|
||||
|
||||
describe 'GET #index' do
|
||||
it 'returns http success' do
|
||||
get :index
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe Api::V1::PreferencesController 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
|
||||
|
||||
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…
Reference in New Issue