Merge pull request #2889 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 77cd16f4ee
pull/2890/head
commit
eb68d81013
|
@ -18,7 +18,7 @@ permissions:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-i18n:
|
check-i18n:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
|
@ -191,7 +191,7 @@ FROM build AS libvips
|
||||||
|
|
||||||
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
|
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
|
||||||
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
|
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
|
||||||
ARG VIPS_VERSION=8.15.3
|
ARG VIPS_VERSION=8.15.5
|
||||||
# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"]
|
# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"]
|
||||||
ARG VIPS_URL=https://github.com/libvips/libvips/releases/download
|
ARG VIPS_URL=https://github.com/libvips/libvips/releases/download
|
||||||
|
|
||||||
|
|
6
Gemfile
6
Gemfile
|
@ -61,7 +61,7 @@ gem 'irb', '~> 1.8'
|
||||||
gem 'kaminari', '~> 1.2'
|
gem 'kaminari', '~> 1.2'
|
||||||
gem 'link_header', '~> 0.0'
|
gem 'link_header', '~> 0.0'
|
||||||
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
||||||
gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar'
|
gem 'mime-types', '~> 3.6.0', require: 'mime/types/columnar'
|
||||||
gem 'nokogiri', '~> 1.15'
|
gem 'nokogiri', '~> 1.15'
|
||||||
gem 'oj', '~> 3.14'
|
gem 'oj', '~> 3.14'
|
||||||
gem 'ox', '~> 2.14'
|
gem 'ox', '~> 2.14'
|
||||||
|
@ -111,8 +111,8 @@ group :opentelemetry do
|
||||||
gem 'opentelemetry-instrumentation-http_client', '~> 0.22.3', require: false
|
gem 'opentelemetry-instrumentation-http_client', '~> 0.22.3', require: false
|
||||||
gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false
|
gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false
|
||||||
gem 'opentelemetry-instrumentation-pg', '~> 0.29.0', require: false
|
gem 'opentelemetry-instrumentation-pg', '~> 0.29.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-rack', '~> 0.24.1', require: false
|
gem 'opentelemetry-instrumentation-rack', '~> 0.25.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-rails', '~> 0.31.0', require: false
|
gem 'opentelemetry-instrumentation-rails', '~> 0.32.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false
|
gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false
|
||||||
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false
|
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false
|
||||||
gem 'opentelemetry-sdk', '~> 1.4', require: false
|
gem 'opentelemetry-sdk', '~> 1.4', require: false
|
||||||
|
|
51
Gemfile.lock
51
Gemfile.lock
|
@ -100,17 +100,17 @@ GEM
|
||||||
attr_required (1.0.2)
|
attr_required (1.0.2)
|
||||||
awrence (1.2.1)
|
awrence (1.2.1)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.983.0)
|
aws-partitions (1.992.0)
|
||||||
aws-sdk-core (3.209.1)
|
aws-sdk-core (3.210.0)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.9)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.94.0)
|
aws-sdk-kms (1.95.0)
|
||||||
aws-sdk-core (~> 3, >= 3.207.0)
|
aws-sdk-core (~> 3, >= 3.210.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.167.0)
|
aws-sdk-s3 (1.169.0)
|
||||||
aws-sdk-core (~> 3, >= 3.207.0)
|
aws-sdk-core (~> 3, >= 3.210.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.10.0)
|
aws-sigv4 (1.10.0)
|
||||||
|
@ -137,7 +137,7 @@ GEM
|
||||||
blurhash (0.1.8)
|
blurhash (0.1.8)
|
||||||
bootsnap (1.18.4)
|
bootsnap (1.18.4)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
brakeman (6.2.1)
|
brakeman (6.2.2)
|
||||||
racc
|
racc
|
||||||
browser (5.3.1)
|
browser (5.3.1)
|
||||||
brpoplpush-redis_script (0.1.3)
|
brpoplpush-redis_script (0.1.3)
|
||||||
|
@ -233,7 +233,7 @@ GEM
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (0.111.0)
|
excon (0.111.0)
|
||||||
fabrication (2.31.0)
|
fabrication (2.31.0)
|
||||||
faker (3.4.2)
|
faker (3.5.1)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (1.10.3)
|
faraday (1.10.3)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
|
@ -429,9 +429,10 @@ GEM
|
||||||
azure-storage-blob (~> 2.0.1)
|
azure-storage-blob (~> 2.0.1)
|
||||||
hashie (~> 5.0)
|
hashie (~> 5.0)
|
||||||
memory_profiler (1.1.0)
|
memory_profiler (1.1.0)
|
||||||
mime-types (3.5.2)
|
mime-types (3.6.0)
|
||||||
|
logger
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2024.0820)
|
mime-types-data (3.2024.1001)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.7)
|
mini_portile2 (2.8.7)
|
||||||
minitest (5.25.1)
|
minitest (5.25.1)
|
||||||
|
@ -503,7 +504,7 @@ GEM
|
||||||
opentelemetry-semantic_conventions
|
opentelemetry-semantic_conventions
|
||||||
opentelemetry-helpers-sql-obfuscation (0.2.0)
|
opentelemetry-helpers-sql-obfuscation (0.2.0)
|
||||||
opentelemetry-common (~> 0.21)
|
opentelemetry-common (~> 0.21)
|
||||||
opentelemetry-instrumentation-action_mailer (0.1.0)
|
opentelemetry-instrumentation-action_mailer (0.2.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-active_support (~> 0.1)
|
opentelemetry-instrumentation-active_support (~> 0.1)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
|
@ -515,13 +516,13 @@ GEM
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-active_support (~> 0.1)
|
opentelemetry-instrumentation-active_support (~> 0.1)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-active_job (0.7.7)
|
opentelemetry-instrumentation-active_job (0.7.8)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-active_model_serializers (0.20.2)
|
opentelemetry-instrumentation-active_model_serializers (0.20.2)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-active_record (0.7.3)
|
opentelemetry-instrumentation-active_record (0.8.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-active_support (0.6.0)
|
opentelemetry-instrumentation-active_support (0.6.0)
|
||||||
|
@ -553,16 +554,16 @@ GEM
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-helpers-sql-obfuscation
|
opentelemetry-helpers-sql-obfuscation
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-rack (0.24.6)
|
opentelemetry-instrumentation-rack (0.25.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-rails (0.31.2)
|
opentelemetry-instrumentation-rails (0.32.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-action_mailer (~> 0.1.0)
|
opentelemetry-instrumentation-action_mailer (~> 0.2.0)
|
||||||
opentelemetry-instrumentation-action_pack (~> 0.9.0)
|
opentelemetry-instrumentation-action_pack (~> 0.9.0)
|
||||||
opentelemetry-instrumentation-action_view (~> 0.7.0)
|
opentelemetry-instrumentation-action_view (~> 0.7.0)
|
||||||
opentelemetry-instrumentation-active_job (~> 0.7.0)
|
opentelemetry-instrumentation-active_job (~> 0.7.0)
|
||||||
opentelemetry-instrumentation-active_record (~> 0.7.0)
|
opentelemetry-instrumentation-active_record (~> 0.8.0)
|
||||||
opentelemetry-instrumentation-active_support (~> 0.6.0)
|
opentelemetry-instrumentation-active_support (~> 0.6.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-redis (0.25.7)
|
opentelemetry-instrumentation-redis (0.25.7)
|
||||||
|
@ -590,7 +591,7 @@ GEM
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.5.8)
|
pg (1.5.9)
|
||||||
pghero (3.6.1)
|
pghero (3.6.1)
|
||||||
activerecord (>= 6.1)
|
activerecord (>= 6.1)
|
||||||
premailer (1.27.0)
|
premailer (1.27.0)
|
||||||
|
@ -761,7 +762,7 @@ GEM
|
||||||
rubocop-rspec_rails (2.30.0)
|
rubocop-rspec_rails (2.30.0)
|
||||||
rubocop (~> 1.61)
|
rubocop (~> 1.61)
|
||||||
rubocop-rspec (~> 3, >= 3.0.1)
|
rubocop-rspec (~> 3, >= 3.0.1)
|
||||||
ruby-prof (1.7.0)
|
ruby-prof (1.7.1)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby-saml (1.17.0)
|
ruby-saml (1.17.0)
|
||||||
nokogiri (>= 1.13.10)
|
nokogiri (>= 1.13.10)
|
||||||
|
@ -970,7 +971,7 @@ DEPENDENCIES
|
||||||
mario-redis-lock (~> 1.2)
|
mario-redis-lock (~> 1.2)
|
||||||
md-paperclip-azure (~> 2.2)
|
md-paperclip-azure (~> 2.2)
|
||||||
memory_profiler
|
memory_profiler
|
||||||
mime-types (~> 3.5.0)
|
mime-types (~> 3.6.0)
|
||||||
net-http (~> 0.4.0)
|
net-http (~> 0.4.0)
|
||||||
net-ldap (~> 0.18)
|
net-ldap (~> 0.18)
|
||||||
nokogiri (~> 1.15)
|
nokogiri (~> 1.15)
|
||||||
|
@ -991,8 +992,8 @@ DEPENDENCIES
|
||||||
opentelemetry-instrumentation-http_client (~> 0.22.3)
|
opentelemetry-instrumentation-http_client (~> 0.22.3)
|
||||||
opentelemetry-instrumentation-net_http (~> 0.22.4)
|
opentelemetry-instrumentation-net_http (~> 0.22.4)
|
||||||
opentelemetry-instrumentation-pg (~> 0.29.0)
|
opentelemetry-instrumentation-pg (~> 0.29.0)
|
||||||
opentelemetry-instrumentation-rack (~> 0.24.1)
|
opentelemetry-instrumentation-rack (~> 0.25.0)
|
||||||
opentelemetry-instrumentation-rails (~> 0.31.0)
|
opentelemetry-instrumentation-rails (~> 0.32.0)
|
||||||
opentelemetry-instrumentation-redis (~> 0.25.3)
|
opentelemetry-instrumentation-redis (~> 0.25.3)
|
||||||
opentelemetry-instrumentation-sidekiq (~> 0.25.2)
|
opentelemetry-instrumentation-sidekiq (~> 0.25.2)
|
||||||
opentelemetry-sdk (~> 1.4)
|
opentelemetry-sdk (~> 1.4)
|
||||||
|
@ -1057,7 +1058,7 @@ DEPENDENCIES
|
||||||
xorcist (~> 1.1)
|
xorcist (~> 1.1)
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 3.3.4p94
|
ruby 3.3.5p100
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.18
|
2.5.22
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
||||||
before_action :require_user!
|
before_action :require_user!, except: :destroy
|
||||||
before_action :set_push_subscription, only: :update
|
before_action :set_push_subscription, only: :update
|
||||||
before_action :destroy_previous_subscriptions, only: :create, if: :prior_subscriptions?
|
before_action :destroy_previous_subscriptions, only: :create, if: :prior_subscriptions?
|
||||||
after_action :update_session_with_subscription, only: :create
|
after_action :update_session_with_subscription, only: :create
|
||||||
|
@ -17,6 +17,13 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
||||||
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
push_subscription = ::Web::PushSubscription.find_by_token_for(:unsubscribe, params[:id])
|
||||||
|
push_subscription&.destroy
|
||||||
|
|
||||||
|
head 200
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def active_session
|
def active_session
|
||||||
|
|
|
@ -10,7 +10,7 @@ module Auth::CaptchaConcern
|
||||||
end
|
end
|
||||||
|
|
||||||
def captcha_available?
|
def captcha_available?
|
||||||
ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present?
|
Rails.configuration.x.captcha.secret_key.present? && Rails.configuration.x.captcha.site_key.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def captcha_enabled?
|
def captcha_enabled?
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
module Admin::SettingsHelper
|
module Admin::SettingsHelper
|
||||||
def captcha_available?
|
def captcha_available?
|
||||||
ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present?
|
Rails.configuration.x.captcha.secret_key.present? && Rails.configuration.x.captcha.site_key.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def login_activity_title(activity)
|
def login_activity_title(activity)
|
||||||
|
|
|
@ -120,18 +120,6 @@ module ApplicationHelper
|
||||||
inline_svg_tag 'check.svg'
|
inline_svg_tag 'check.svg'
|
||||||
end
|
end
|
||||||
|
|
||||||
def visibility_icon(status)
|
|
||||||
if status.public_visibility?
|
|
||||||
material_symbol('globe', title: I18n.t('statuses.visibilities.public'))
|
|
||||||
elsif status.unlisted_visibility?
|
|
||||||
material_symbol('lock_open', title: I18n.t('statuses.visibilities.unlisted'))
|
|
||||||
elsif status.private_visibility? || status.limited_visibility?
|
|
||||||
material_symbol('lock', title: I18n.t('statuses.visibilities.private'))
|
|
||||||
elsif status.direct_visibility?
|
|
||||||
material_symbol('alternate_email', title: I18n.t('statuses.visibilities.direct'))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def interrelationships_icon(relationships, account_id)
|
def interrelationships_icon(relationships, account_id)
|
||||||
if relationships.following[account_id] && relationships.followed_by[account_id]
|
if relationships.following[account_id] && relationships.followed_by[account_id]
|
||||||
material_symbol('sync_alt', title: I18n.t('relationships.mutual'), class: 'active passive')
|
material_symbol('sync_alt', title: I18n.t('relationships.mutual'), class: 'active passive')
|
||||||
|
|
|
@ -162,7 +162,7 @@ module LanguagesHelper
|
||||||
th: ['Thai', 'ไทย'].freeze,
|
th: ['Thai', 'ไทย'].freeze,
|
||||||
ti: ['Tigrinya', 'ትግርኛ'].freeze,
|
ti: ['Tigrinya', 'ትግርኛ'].freeze,
|
||||||
tk: ['Turkmen', 'Türkmen'].freeze,
|
tk: ['Turkmen', 'Türkmen'].freeze,
|
||||||
tl: ['Tagalog', 'Wikang Tagalog'].freeze,
|
tl: ['Tagalog', 'Tagalog'].freeze,
|
||||||
tn: ['Tswana', 'Setswana'].freeze,
|
tn: ['Tswana', 'Setswana'].freeze,
|
||||||
to: ['Tonga', 'faka Tonga'].freeze,
|
to: ['Tonga', 'faka Tonga'].freeze,
|
||||||
tr: ['Turkish', 'Türkçe'].freeze,
|
tr: ['Turkish', 'Türkçe'].freeze,
|
||||||
|
|
|
@ -327,31 +327,24 @@ Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
|
||||||
|
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
|
|
||||||
const oldReadOnly = input.readOnly;
|
navigator.clipboard
|
||||||
|
.writeText(input.value)
|
||||||
input.readOnly = false;
|
.then(() => {
|
||||||
input.focus();
|
|
||||||
input.select();
|
|
||||||
input.setSelectionRange(0, input.value.length);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (document.execCommand('copy')) {
|
|
||||||
input.blur();
|
|
||||||
|
|
||||||
const parent = target.parentElement;
|
const parent = target.parentElement;
|
||||||
|
|
||||||
if (!parent) return;
|
if (parent) {
|
||||||
parent.classList.add('copied');
|
parent.classList.add('copied');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
parent.classList.remove('copied');
|
parent.classList.remove('copied');
|
||||||
}, 700);
|
}, 700);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
input.readOnly = oldReadOnly;
|
return true;
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren, JSX } from 'react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
|
@ -17,9 +17,15 @@ export const ContentWarning: React.FC<{
|
||||||
aria-expanded={expanded}
|
aria-expanded={expanded}
|
||||||
>
|
>
|
||||||
{expanded ? (
|
{expanded ? (
|
||||||
<FormattedMessage id='status.show_less' defaultMessage='Show less' />
|
<FormattedMessage
|
||||||
|
id='content_warning.hide'
|
||||||
|
defaultMessage='Hide post'
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FormattedMessage id='status.show_more' defaultMessage='Show more' />
|
<FormattedMessage
|
||||||
|
id='content_warning.show_more'
|
||||||
|
defaultMessage='Show more'
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{icons}
|
{icons}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -10,13 +10,16 @@ export const FilterWarning: React.FC<{
|
||||||
<StatusBanner
|
<StatusBanner
|
||||||
expanded={expanded}
|
expanded={expanded}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
variant={BannerVariant.Blue}
|
variant={BannerVariant.Filter}
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='filter_warning.matches_filter'
|
id='filter_warning.matches_filter'
|
||||||
defaultMessage='Matches filter “{title}”'
|
defaultMessage='Matches filter “<span>{title}</span>”'
|
||||||
values={{ title }}
|
values={{
|
||||||
|
title,
|
||||||
|
span: (chunks) => <span className='filter-name'>{chunks}</span>,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</StatusBanner>
|
</StatusBanner>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
import type { JSX } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||||
|
|
||||||
|
|
|
@ -648,7 +648,7 @@ class Status extends ImmutablePureComponent {
|
||||||
media={status.get('media_attachments')}
|
media={status.get('media_attachments')}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
} else if (['image', 'gifv'].includes(status.getIn(['media_attachments', 0, 'type'])) || status.get('media_attachments').size > 1) {
|
} else if (['image', 'gifv', 'unknown'].includes(status.getIn(['media_attachments', 0, 'type'])) || status.get('media_attachments').size > 1) {
|
||||||
media.push(
|
media.push(
|
||||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
||||||
{Component => (
|
{Component => (
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
export enum BannerVariant {
|
export enum BannerVariant {
|
||||||
Yellow = 'yellow',
|
Warning = 'warning',
|
||||||
Blue = 'blue',
|
Filter = 'filter',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StatusBanner: React.FC<{
|
export const StatusBanner: React.FC<{
|
||||||
|
@ -11,9 +11,9 @@ export const StatusBanner: React.FC<{
|
||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}> = ({ children, variant, expanded, onClick }) => (
|
}> = ({ children, variant, expanded, onClick }) => (
|
||||||
<div
|
<label
|
||||||
className={
|
className={
|
||||||
variant === BannerVariant.Yellow
|
variant === BannerVariant.Warning
|
||||||
? 'content-warning'
|
? 'content-warning'
|
||||||
: 'content-warning content-warning--filter'
|
: 'content-warning content-warning--filter'
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,11 @@ export const StatusBanner: React.FC<{
|
||||||
id='content_warning.hide'
|
id='content_warning.hide'
|
||||||
defaultMessage='Hide post'
|
defaultMessage='Hide post'
|
||||||
/>
|
/>
|
||||||
|
) : variant === BannerVariant.Warning ? (
|
||||||
|
<FormattedMessage
|
||||||
|
id='content_warning.show_more'
|
||||||
|
defaultMessage='Show more'
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='content_warning.show'
|
id='content_warning.show'
|
||||||
|
@ -33,5 +38,5 @@ export const StatusBanner: React.FC<{
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</label>
|
||||||
);
|
);
|
||||||
|
|
|
@ -327,31 +327,24 @@ Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
|
||||||
|
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
|
|
||||||
const oldReadOnly = input.readOnly;
|
navigator.clipboard
|
||||||
|
.writeText(input.value)
|
||||||
input.readOnly = false;
|
.then(() => {
|
||||||
input.focus();
|
|
||||||
input.select();
|
|
||||||
input.setSelectionRange(0, input.value.length);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (document.execCommand('copy')) {
|
|
||||||
input.blur();
|
|
||||||
|
|
||||||
const parent = target.parentElement;
|
const parent = target.parentElement;
|
||||||
|
|
||||||
if (!parent) return;
|
if (parent) {
|
||||||
parent.classList.add('copied');
|
parent.classList.add('copied');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
parent.classList.remove('copied');
|
parent.classList.remove('copied');
|
||||||
}, 700);
|
}, 700);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
input.readOnly = oldReadOnly;
|
return true;
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { JSX } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import type { JSX } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
|
@ -154,7 +154,5 @@
|
||||||
"status.is_poll": "This toot is a poll",
|
"status.is_poll": "This toot is a poll",
|
||||||
"status.local_only": "Only visible from your instance",
|
"status.local_only": "Only visible from your instance",
|
||||||
"status.show_filter_reason": "Show anyway",
|
"status.show_filter_reason": "Show anyway",
|
||||||
"status.show_less": "Show less",
|
|
||||||
"status.show_more": "Show more",
|
|
||||||
"status.uncollapse": "Uncollapse"
|
"status.uncollapse": "Uncollapse"
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,10 @@ export const accountsReducer: Reducer<typeof initialState> = (
|
||||||
return state.setIn([action.payload.id, 'hidden'], false);
|
return state.setIn([action.payload.id, 'hidden'], false);
|
||||||
else if (importAccounts.match(action))
|
else if (importAccounts.match(action))
|
||||||
return normalizeAccounts(state, action.payload.accounts);
|
return normalizeAccounts(state, action.payload.accounts);
|
||||||
else if (followAccountSuccess.match(action)) {
|
else if (
|
||||||
|
followAccountSuccess.match(action) &&
|
||||||
|
!action.payload.alreadyFollowing
|
||||||
|
) {
|
||||||
return state
|
return state
|
||||||
.update(action.payload.relationship.id, (account) =>
|
.update(action.payload.relationship.id, (account) =>
|
||||||
account?.update('followers_count', (n) => n + 1),
|
account?.update('followers_count', (n) => n + 1),
|
||||||
|
|
|
@ -11648,19 +11648,21 @@ noscript {
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-warning {
|
.content-warning {
|
||||||
|
display: block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: rgba($ui-highlight-color, 0.05);
|
background: rgba($ui-highlight-color, 0.05);
|
||||||
color: $secondary-text-color;
|
color: $secondary-text-color;
|
||||||
border-top: 1px solid;
|
border: 1px solid rgba($ui-highlight-color, 0.15);
|
||||||
border-bottom: 1px solid;
|
border-radius: 8px;
|
||||||
border-color: rgba($ui-highlight-color, 0.15);
|
|
||||||
padding: 8px (5px + 8px);
|
padding: 8px (5px + 8px);
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-button {
|
.link-button {
|
||||||
|
@ -11669,31 +11671,16 @@ noscript {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before,
|
&--filter {
|
||||||
&::after {
|
color: $darker-text-color;
|
||||||
content: '';
|
|
||||||
display: block;
|
p {
|
||||||
position: absolute;
|
font-weight: normal;
|
||||||
height: 100%;
|
|
||||||
background: url('~images/warning-stripes.svg') repeat-y;
|
|
||||||
width: 5px;
|
|
||||||
top: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before {
|
.filter-name {
|
||||||
border-start-start-radius: 4px;
|
font-weight: 500;
|
||||||
border-end-start-radius: 4px;
|
color: $secondary-text-color;
|
||||||
inset-inline-start: 0;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
border-start-end-radius: 4px;
|
|
||||||
border-end-end-radius: 4px;
|
|
||||||
inset-inline-end: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--filter::before,
|
|
||||||
&--filter::after {
|
|
||||||
background-image: url('~images/filter-stripes.svg');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,4 +76,7 @@ body {
|
||||||
--background-color-tint: rgba(255, 255, 255, 80%);
|
--background-color-tint: rgba(255, 255, 255, 80%);
|
||||||
--background-filter: blur(10px);
|
--background-filter: blur(10px);
|
||||||
--on-surface-color: #{transparentize($ui-base-color, 0.65)};
|
--on-surface-color: #{transparentize($ui-base-color, 0.65)};
|
||||||
|
--rich-text-container-color: rgba(255, 216, 231, 100%);
|
||||||
|
--rich-text-text-color: rgba(114, 47, 83, 100%);
|
||||||
|
--rich-text-decorations-color: rgba(255, 175, 212, 100%);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,29 @@
|
||||||
.e-content,
|
.e-content,
|
||||||
.edit-indicator__content,
|
.edit-indicator__content,
|
||||||
.reply-indicator__content {
|
.reply-indicator__content {
|
||||||
|
code {
|
||||||
|
background: var(--rich-text-container-color);
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--rich-text-text-color);
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: var(--rich-text-container-color);
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--rich-text-text-color);
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pre,
|
pre,
|
||||||
blockquote {
|
blockquote {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 22px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
unicode-bidi: plaintext;
|
unicode-bidi: plaintext;
|
||||||
|
|
||||||
|
@ -14,19 +34,45 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
padding-inline-start: 10px;
|
padding-inline-start: 32px;
|
||||||
border-inline-start: 3px solid $darker-text-color;
|
color: var(--rich-text-text-color);
|
||||||
color: $darker-text-color;
|
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
p:last-child {
|
&::before {
|
||||||
|
display: block;
|
||||||
|
content: '';
|
||||||
|
width: 24px;
|
||||||
|
height: 20px;
|
||||||
|
mask-image: url('~images/quote.svg');
|
||||||
|
background-color: var(--rich-text-decorations-color);
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-start: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin-top: 4px;
|
||||||
|
border-inline-start: 3px solid var(--rich-text-decorations-color);
|
||||||
|
padding-inline-start: 16px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p:last-of-type {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > ul,
|
& > ul,
|
||||||
& > ol {
|
& > ol {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 22px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
|
@ -76,7 +122,15 @@
|
||||||
|
|
||||||
ul,
|
ul,
|
||||||
ol {
|
ol {
|
||||||
margin-inline-start: 2em;
|
padding-inline-start: 24px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding-inline-start: 8px;
|
||||||
|
|
||||||
|
&::marker {
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -84,7 +138,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style-type: disc;
|
list-style-type: '•';
|
||||||
|
|
||||||
|
li::marker {
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ol {
|
ol {
|
||||||
|
|
|
@ -122,4 +122,7 @@ $dismiss-overlay-width: 4rem;
|
||||||
--error-background-color: #{darken($error-red, 16%)};
|
--error-background-color: #{darken($error-red, 16%)};
|
||||||
--error-active-background-color: #{darken($error-red, 12%)};
|
--error-active-background-color: #{darken($error-red, 12%)};
|
||||||
--on-error-color: #fff;
|
--on-error-color: #fff;
|
||||||
|
--rich-text-container-color: rgba(87, 24, 60, 100%);
|
||||||
|
--rich-text-text-color: rgba(255, 175, 212, 100%);
|
||||||
|
--rich-text-decorations-color: rgba(128, 58, 95, 100%);
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 5.6 KiB |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg"><symbol id="mastodon-svg-logo" viewBox="0 0 216.4144 232.00976"><path d="M107.86523 0C78.203984.2425 49.672422 3.4535937 33.044922 11.089844c0 0-32.97656262 14.752031-32.97656262 65.082031 0 11.525-.224375 25.306175.140625 39.919925 1.19750002 49.22 9.02375002 97.72843 54.53124962 109.77343 20.9825 5.55375 38.99711 6.71547 53.505856 5.91797 26.31125-1.45875 41.08203-9.38867 41.08203-9.38867l-.86914-19.08984s-18.80171 5.92758-39.91796 5.20508c-20.921254-.7175-43.006879-2.25516-46.390629-27.94141-.3125-2.25625-.46875-4.66938-.46875-7.20313 0 0 20.536953 5.0204 46.564449 6.21289 15.915.73001 30.8393-.93343 45.99805-2.74218 29.07-3.47125 54.38125-21.3818 57.5625-37.74805 5.0125-25.78125 4.59961-62.916015 4.59961-62.916015 0-50.33-32.97461-65.082031-32.97461-65.082031C166.80539 3.4535938 138.255.2425 108.59375 0h-.72852zM74.296875 39.326172c12.355 0 21.710234 4.749297 27.896485 14.248047l6.01367 10.080078 6.01563-10.080078c6.185-9.49875 15.54023-14.248047 27.89648-14.248047 10.6775 0 19.28156 3.753672 25.85156 11.076172 6.36875 7.3225 9.53907 17.218828 9.53907 29.673828v60.941408h-24.14454V81.869141c0-12.46875-5.24453-18.798829-15.73828-18.798829-11.6025 0-17.41797 7.508516-17.41797 22.353516v32.375002H96.207031V85.423828c0-14.845-5.815468-22.353515-17.417969-22.353516-10.49375 0-15.740234 6.330079-15.740234 18.798829v59.148439H38.904297V80.076172c0-12.455 3.171016-22.351328 9.541015-29.673828 6.568751-7.3225 15.172813-11.076172 25.851563-11.076172z" /></symbol></svg>
|
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="20" viewBox="0 0 24 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M23.933 2.82414C22.324 4.07931 21.0726 5.3569 20.1788 6.6569C19.3296 7.91207 18.905 9.1 18.905 10.2207C19.0838 10.131 19.3073 10.0862 19.5754 10.0862C19.8883 10.0414 20.1564 10.019 20.3799 10.019C21.4078 10.019 22.257 10.4448 22.9274 11.2966C23.6425 12.1034 24 13.1121 24 14.3224C24 15.8017 23.5084 17.0345 22.5251 18.0207C21.5419 19.0069 20.3129 19.5 18.838 19.5C17.2737 19.5 16.0447 18.9397 15.1508 17.819C14.257 16.6535 13.8101 15.1069 13.8101 13.1793C13.8101 10.8931 14.5028 8.62931 15.8883 6.38793C17.2737 4.14655 19.3073 2.01724 21.9888 0L23.933 2.82414ZM10.1229 2.82414C8.51397 4.07931 7.26257 5.3569 6.36872 6.6569C5.51955 7.91207 5.09497 9.1 5.09497 10.2207C5.27374 10.131 5.49721 10.0862 5.76536 10.0862C6.07821 10.0414 6.34637 10.019 6.56983 10.019C7.59777 10.019 8.44693 10.4448 9.11732 11.2966C9.8324 12.1034 10.1899 13.1121 10.1899 14.3224C10.1899 15.8017 9.69832 17.0345 8.71508 18.0207C7.73184 19.0069 6.50279 19.5 5.02793 19.5C3.46369 19.5 2.23464 18.9397 1.34078 17.819C0.446927 16.6535 0 15.1069 0 13.1793C0 10.8931 0.692738 8.62931 2.07821 6.38793C3.46369 4.14655 5.49721 2.01724 8.17877 0L10.1229 2.82414Z" fill="currentColor" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -1,4 +1,4 @@
|
||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren, JSX } from 'react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
|
@ -8,7 +8,7 @@ export const ContentWarning: React.FC<{
|
||||||
<StatusBanner
|
<StatusBanner
|
||||||
expanded={expanded}
|
expanded={expanded}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
variant={BannerVariant.Yellow}
|
variant={BannerVariant.Warning}
|
||||||
>
|
>
|
||||||
<p dangerouslySetInnerHTML={{ __html: text }} />
|
<p dangerouslySetInnerHTML={{ __html: text }} />
|
||||||
</StatusBanner>
|
</StatusBanner>
|
||||||
|
|
|
@ -10,13 +10,16 @@ export const FilterWarning: React.FC<{
|
||||||
<StatusBanner
|
<StatusBanner
|
||||||
expanded={expanded}
|
expanded={expanded}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
variant={BannerVariant.Blue}
|
variant={BannerVariant.Filter}
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='filter_warning.matches_filter'
|
id='filter_warning.matches_filter'
|
||||||
defaultMessage='Matches filter “{title}”'
|
defaultMessage='Matches filter “<span>{title}</span>”'
|
||||||
values={{ title }}
|
values={{
|
||||||
|
title,
|
||||||
|
span: (chunks) => <span className='filter-name'>{chunks}</span>,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</StatusBanner>
|
</StatusBanner>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
import type { JSX } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||||
|
|
||||||
|
|
|
@ -449,7 +449,7 @@ class Status extends ImmutablePureComponent {
|
||||||
} else if (status.get('media_attachments').size > 0) {
|
} else if (status.get('media_attachments').size > 0) {
|
||||||
const language = status.getIn(['translation', 'language']) || status.get('language');
|
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||||
|
|
||||||
if (['image', 'gifv'].includes(status.getIn(['media_attachments', 0, 'type'])) || status.get('media_attachments').size > 1) {
|
if (['image', 'gifv', 'unknown'].includes(status.getIn(['media_attachments', 0, 'type'])) || status.get('media_attachments').size > 1) {
|
||||||
media = (
|
media = (
|
||||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
||||||
{Component => (
|
{Component => (
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
export enum BannerVariant {
|
export enum BannerVariant {
|
||||||
Yellow = 'yellow',
|
Warning = 'warning',
|
||||||
Blue = 'blue',
|
Filter = 'filter',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StatusBanner: React.FC<{
|
export const StatusBanner: React.FC<{
|
||||||
|
@ -11,9 +11,9 @@ export const StatusBanner: React.FC<{
|
||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}> = ({ children, variant, expanded, onClick }) => (
|
}> = ({ children, variant, expanded, onClick }) => (
|
||||||
<div
|
<label
|
||||||
className={
|
className={
|
||||||
variant === BannerVariant.Yellow
|
variant === BannerVariant.Warning
|
||||||
? 'content-warning'
|
? 'content-warning'
|
||||||
: 'content-warning content-warning--filter'
|
: 'content-warning content-warning--filter'
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,11 @@ export const StatusBanner: React.FC<{
|
||||||
id='content_warning.hide'
|
id='content_warning.hide'
|
||||||
defaultMessage='Hide post'
|
defaultMessage='Hide post'
|
||||||
/>
|
/>
|
||||||
|
) : variant === BannerVariant.Warning ? (
|
||||||
|
<FormattedMessage
|
||||||
|
id='content_warning.show_more'
|
||||||
|
defaultMessage='Show more'
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='content_warning.show'
|
id='content_warning.show'
|
||||||
|
@ -33,5 +38,5 @@ export const StatusBanner: React.FC<{
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</label>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { JSX } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import type { JSX } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
|
@ -197,6 +197,7 @@
|
||||||
"confirmations.unfollow.title": "Unfollow user?",
|
"confirmations.unfollow.title": "Unfollow user?",
|
||||||
"content_warning.hide": "Hide post",
|
"content_warning.hide": "Hide post",
|
||||||
"content_warning.show": "Show anyway",
|
"content_warning.show": "Show anyway",
|
||||||
|
"content_warning.show_more": "Show more",
|
||||||
"conversation.delete": "Delete conversation",
|
"conversation.delete": "Delete conversation",
|
||||||
"conversation.mark_as_read": "Mark as read",
|
"conversation.mark_as_read": "Mark as read",
|
||||||
"conversation.open": "View conversation",
|
"conversation.open": "View conversation",
|
||||||
|
@ -305,7 +306,7 @@
|
||||||
"filter_modal.select_filter.subtitle": "Use an existing category or create a new one",
|
"filter_modal.select_filter.subtitle": "Use an existing category or create a new one",
|
||||||
"filter_modal.select_filter.title": "Filter this post",
|
"filter_modal.select_filter.title": "Filter this post",
|
||||||
"filter_modal.title.status": "Filter a post",
|
"filter_modal.title.status": "Filter a post",
|
||||||
"filter_warning.matches_filter": "Matches filter “{title}”",
|
"filter_warning.matches_filter": "Matches filter “<span>{title}</span>”",
|
||||||
"filtered_notifications_banner.pending_requests": "From {count, plural, =0 {no one} one {one person} other {# people}} you may know",
|
"filtered_notifications_banner.pending_requests": "From {count, plural, =0 {no one} one {one person} other {# people}} you may know",
|
||||||
"filtered_notifications_banner.title": "Filtered notifications",
|
"filtered_notifications_banner.title": "Filtered notifications",
|
||||||
"firehose.all": "All",
|
"firehose.all": "All",
|
||||||
|
|
|
@ -57,7 +57,10 @@ export const accountsReducer: Reducer<typeof initialState> = (
|
||||||
return state.setIn([action.payload.id, 'hidden'], false);
|
return state.setIn([action.payload.id, 'hidden'], false);
|
||||||
else if (importAccounts.match(action))
|
else if (importAccounts.match(action))
|
||||||
return normalizeAccounts(state, action.payload.accounts);
|
return normalizeAccounts(state, action.payload.accounts);
|
||||||
else if (followAccountSuccess.match(action)) {
|
else if (
|
||||||
|
followAccountSuccess.match(action) &&
|
||||||
|
!action.payload.alreadyFollowing
|
||||||
|
) {
|
||||||
return state
|
return state
|
||||||
.update(action.payload.relationship.id, (account) =>
|
.update(action.payload.relationship.id, (account) =>
|
||||||
account?.update('followers_count', (n) => n + 1),
|
account?.update('followers_count', (n) => n + 1),
|
||||||
|
|
|
@ -76,4 +76,7 @@ body {
|
||||||
--background-color-tint: rgba(255, 255, 255, 80%);
|
--background-color-tint: rgba(255, 255, 255, 80%);
|
||||||
--background-filter: blur(10px);
|
--background-filter: blur(10px);
|
||||||
--on-surface-color: #{transparentize($ui-base-color, 0.65)};
|
--on-surface-color: #{transparentize($ui-base-color, 0.65)};
|
||||||
|
--rich-text-container-color: rgba(255, 216, 231, 100%);
|
||||||
|
--rich-text-text-color: rgba(114, 47, 83, 100%);
|
||||||
|
--rich-text-decorations-color: rgba(255, 175, 212, 100%);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11109,19 +11109,21 @@ noscript {
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-warning {
|
.content-warning {
|
||||||
|
display: block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: rgba($ui-highlight-color, 0.05);
|
background: rgba($ui-highlight-color, 0.05);
|
||||||
color: $secondary-text-color;
|
color: $secondary-text-color;
|
||||||
border-top: 1px solid;
|
border: 1px solid rgba($ui-highlight-color, 0.15);
|
||||||
border-bottom: 1px solid;
|
border-radius: 8px;
|
||||||
border-color: rgba($ui-highlight-color, 0.15);
|
|
||||||
padding: 8px (5px + 8px);
|
padding: 8px (5px + 8px);
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-button {
|
.link-button {
|
||||||
|
@ -11130,31 +11132,16 @@ noscript {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before,
|
&--filter {
|
||||||
&::after {
|
color: $darker-text-color;
|
||||||
content: '';
|
|
||||||
display: block;
|
p {
|
||||||
position: absolute;
|
font-weight: normal;
|
||||||
height: 100%;
|
|
||||||
background: url('~images/warning-stripes.svg') repeat-y;
|
|
||||||
width: 5px;
|
|
||||||
top: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before {
|
.filter-name {
|
||||||
border-start-start-radius: 4px;
|
font-weight: 500;
|
||||||
border-end-start-radius: 4px;
|
color: $secondary-text-color;
|
||||||
inset-inline-start: 0;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
border-start-end-radius: 4px;
|
|
||||||
border-end-end-radius: 4px;
|
|
||||||
inset-inline-end: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--filter::before,
|
|
||||||
&--filter::after {
|
|
||||||
background-image: url('~images/filter-stripes.svg');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,29 @@
|
||||||
.e-content,
|
.e-content,
|
||||||
.edit-indicator__content,
|
.edit-indicator__content,
|
||||||
.reply-indicator__content {
|
.reply-indicator__content {
|
||||||
|
code {
|
||||||
|
background: var(--rich-text-container-color);
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--rich-text-text-color);
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: var(--rich-text-container-color);
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--rich-text-text-color);
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pre,
|
pre,
|
||||||
blockquote {
|
blockquote {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 22px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
unicode-bidi: plaintext;
|
unicode-bidi: plaintext;
|
||||||
|
|
||||||
|
@ -14,19 +34,45 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
padding-inline-start: 10px;
|
padding-inline-start: 32px;
|
||||||
border-inline-start: 3px solid $darker-text-color;
|
color: var(--rich-text-text-color);
|
||||||
color: $darker-text-color;
|
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
p:last-child {
|
&::before {
|
||||||
|
display: block;
|
||||||
|
content: '';
|
||||||
|
width: 24px;
|
||||||
|
height: 20px;
|
||||||
|
mask-image: url('~images/quote.svg');
|
||||||
|
background-color: var(--rich-text-decorations-color);
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-start: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin-top: 4px;
|
||||||
|
border-inline-start: 3px solid var(--rich-text-decorations-color);
|
||||||
|
padding-inline-start: 16px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p:last-of-type {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > ul,
|
& > ul,
|
||||||
& > ol {
|
& > ol {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 22px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
b,
|
b,
|
||||||
|
@ -41,7 +87,15 @@
|
||||||
|
|
||||||
ul,
|
ul,
|
||||||
ol {
|
ol {
|
||||||
margin-inline-start: 2em;
|
padding-inline-start: 24px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding-inline-start: 8px;
|
||||||
|
|
||||||
|
&::marker {
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -49,7 +103,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style-type: disc;
|
list-style-type: '•';
|
||||||
|
|
||||||
|
li::marker {
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ol {
|
ol {
|
||||||
|
|
|
@ -116,4 +116,7 @@ $font-monospace: 'mastodon-font-monospace' !default;
|
||||||
--error-background-color: #{darken($error-red, 16%)};
|
--error-background-color: #{darken($error-red, 16%)};
|
||||||
--error-active-background-color: #{darken($error-red, 12%)};
|
--error-active-background-color: #{darken($error-red, 12%)};
|
||||||
--on-error-color: #fff;
|
--on-error-color: #fff;
|
||||||
|
--rich-text-container-color: rgba(87, 24, 60, 100%);
|
||||||
|
--rich-text-text-color: rgba(255, 175, 212, 100%);
|
||||||
|
--rich-text-decorations-color: rgba(128, 58, 95, 100%);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,17 +8,27 @@ class TranslationService
|
||||||
class UnexpectedResponseError < Error; end
|
class UnexpectedResponseError < Error; end
|
||||||
|
|
||||||
def self.configured
|
def self.configured
|
||||||
if ENV['DEEPL_API_KEY'].present?
|
if configuration.deepl[:api_key].present?
|
||||||
TranslationService::DeepL.new(ENV.fetch('DEEPL_PLAN', 'free'), ENV['DEEPL_API_KEY'])
|
TranslationService::DeepL.new(
|
||||||
elsif ENV['LIBRE_TRANSLATE_ENDPOINT'].present?
|
configuration.deepl[:plan],
|
||||||
TranslationService::LibreTranslate.new(ENV['LIBRE_TRANSLATE_ENDPOINT'], ENV['LIBRE_TRANSLATE_API_KEY'])
|
configuration.deepl[:api_key]
|
||||||
|
)
|
||||||
|
elsif configuration.libre_translate[:endpoint].present?
|
||||||
|
TranslationService::LibreTranslate.new(
|
||||||
|
configuration.libre_translate[:endpoint],
|
||||||
|
configuration.libre_translate[:api_key]
|
||||||
|
)
|
||||||
else
|
else
|
||||||
raise NotConfiguredError
|
raise NotConfiguredError
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.configured?
|
def self.configured?
|
||||||
ENV['DEEPL_API_KEY'].present? || ENV['LIBRE_TRANSLATE_ENDPOINT'].present?
|
configuration.deepl[:api_key].present? || configuration.libre_translate[:endpoint].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.configuration
|
||||||
|
Rails.configuration.x.translation
|
||||||
end
|
end
|
||||||
|
|
||||||
def languages
|
def languages
|
||||||
|
|
|
@ -65,6 +65,8 @@ class Account < ApplicationRecord
|
||||||
)
|
)
|
||||||
|
|
||||||
BACKGROUND_REFRESH_INTERVAL = 1.week.freeze
|
BACKGROUND_REFRESH_INTERVAL = 1.week.freeze
|
||||||
|
REFRESH_DEADLINE = 6.hours
|
||||||
|
STALE_THRESHOLD = 1.day
|
||||||
DEFAULT_FIELDS_SIZE = (ENV['MAX_PROFILE_FIELDS'] || 4).to_i
|
DEFAULT_FIELDS_SIZE = (ENV['MAX_PROFILE_FIELDS'] || 4).to_i
|
||||||
INSTANCE_ACTOR_ID = -99
|
INSTANCE_ACTOR_ID = -99
|
||||||
|
|
||||||
|
@ -229,13 +231,13 @@ class Account < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def possibly_stale?
|
def possibly_stale?
|
||||||
last_webfingered_at.nil? || last_webfingered_at <= 1.day.ago
|
last_webfingered_at.nil? || last_webfingered_at <= STALE_THRESHOLD.ago
|
||||||
end
|
end
|
||||||
|
|
||||||
def schedule_refresh_if_stale!
|
def schedule_refresh_if_stale!
|
||||||
return unless last_webfingered_at.present? && last_webfingered_at <= BACKGROUND_REFRESH_INTERVAL.ago
|
return unless last_webfingered_at.present? && last_webfingered_at <= BACKGROUND_REFRESH_INTERVAL.ago
|
||||||
|
|
||||||
AccountRefreshWorker.perform_in(rand(6.hours.to_i), id)
|
AccountRefreshWorker.perform_in(rand(REFRESH_DEADLINE), id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def refresh!
|
def refresh!
|
||||||
|
|
|
@ -36,9 +36,14 @@ class IpBlock < ApplicationRecord
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def blocked?(remote_ip)
|
def blocked?(remote_ip)
|
||||||
blocked_ips_map = Rails.cache.fetch(CACHE_KEY) { FastIpMap.new(IpBlock.where(severity: :no_access).pluck(:ip)) }
|
|
||||||
blocked_ips_map.include?(remote_ip)
|
blocked_ips_map.include?(remote_ip)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def blocked_ips_map
|
||||||
|
Rails.cache.fetch(CACHE_KEY) { FastIpMap.new(severity_no_access.pluck(:ip)) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -19,6 +19,8 @@ class LinkFeed < PublicFeed
|
||||||
|
|
||||||
scope.merge!(discoverable)
|
scope.merge!(discoverable)
|
||||||
scope.merge!(attached_to_preview_card)
|
scope.merge!(attached_to_preview_card)
|
||||||
|
scope.merge!(account_filters_scope) if account?
|
||||||
|
scope.merge!(language_scope) if account&.chosen_languages.present?
|
||||||
|
|
||||||
scope.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id)
|
scope.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id)
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,6 +29,8 @@ class Web::PushSubscription < ApplicationRecord
|
||||||
|
|
||||||
delegate :locale, to: :associated_user
|
delegate :locale, to: :associated_user
|
||||||
|
|
||||||
|
generates_token_for :unsubscribe, expires_in: Web::PushNotificationWorker::TTL
|
||||||
|
|
||||||
def pushable?(notification)
|
def pushable?(notification)
|
||||||
policy_allows_notification?(notification) && alert_enabled_for_notification_type?(notification)
|
policy_allows_notification?(notification) && alert_enabled_for_notification_type?(notification)
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
class Web::PushNotificationWorker
|
class Web::PushNotificationWorker
|
||||||
include Sidekiq::Worker
|
include Sidekiq::Worker
|
||||||
|
include RoutingHelper
|
||||||
|
|
||||||
sidekiq_options queue: 'push', retry: 5
|
sidekiq_options queue: 'push', retry: 5
|
||||||
|
|
||||||
TTL = 48.hours.to_s
|
TTL = 48.hours
|
||||||
URGENCY = 'normal'
|
URGENCY = 'normal'
|
||||||
|
|
||||||
def perform(subscription_id, notification_id)
|
def perform(subscription_id, notification_id)
|
||||||
|
@ -23,12 +24,13 @@ class Web::PushNotificationWorker
|
||||||
|
|
||||||
request.add_headers(
|
request.add_headers(
|
||||||
'Content-Type' => 'application/octet-stream',
|
'Content-Type' => 'application/octet-stream',
|
||||||
'Ttl' => TTL,
|
'Ttl' => TTL.to_s,
|
||||||
'Urgency' => URGENCY,
|
'Urgency' => URGENCY,
|
||||||
'Content-Encoding' => 'aesgcm',
|
'Content-Encoding' => 'aesgcm',
|
||||||
'Encryption' => "salt=#{Webpush.encode64(payload.fetch(:salt)).delete('=')}",
|
'Encryption' => "salt=#{Webpush.encode64(payload.fetch(:salt)).delete('=')}",
|
||||||
'Crypto-Key' => "dh=#{Webpush.encode64(payload.fetch(:server_public_key)).delete('=')};#{web_push_request.crypto_key_header}",
|
'Crypto-Key' => "dh=#{Webpush.encode64(payload.fetch(:server_public_key)).delete('=')};#{web_push_request.crypto_key_header}",
|
||||||
'Authorization' => web_push_request.authorization_header
|
'Authorization' => web_push_request.authorization_header,
|
||||||
|
'Unsubscribe-URL' => subscription_url
|
||||||
)
|
)
|
||||||
|
|
||||||
request.perform do |response|
|
request.perform do |response|
|
||||||
|
@ -72,4 +74,8 @@ class Web::PushNotificationWorker
|
||||||
def request_pool
|
def request_pool
|
||||||
RequestPool.current
|
RequestPool.current
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def subscription_url
|
||||||
|
api_web_push_subscription_url(id: @subscription.generate_token_for(:unsubscribe))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -109,6 +109,9 @@ module Mastodon
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
config.x.captcha = config_for(:captcha)
|
||||||
|
config.x.translation = config_for(:translation)
|
||||||
|
|
||||||
config.to_prepare do
|
config.to_prepare do
|
||||||
Doorkeeper::AuthorizationsController.layout 'modal'
|
Doorkeeper::AuthorizationsController.layout 'modal'
|
||||||
Doorkeeper::AuthorizedApplicationsController.layout 'admin'
|
Doorkeeper::AuthorizedApplicationsController.layout 'admin'
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
shared:
|
||||||
|
secret_key: <%= ENV.fetch('HCAPTCHA_SECRET_KEY', nil) %>
|
||||||
|
site_key: <%= ENV.fetch('HCAPTCHA_SITE_KEY', nil) %>
|
|
@ -16,7 +16,7 @@ Rails.application.configure do
|
||||||
# Show full error reports.
|
# Show full error reports.
|
||||||
config.consider_all_requests_local = true
|
config.consider_all_requests_local = true
|
||||||
|
|
||||||
# Enable server timing
|
# Enable server timing.
|
||||||
config.server_timing = true
|
config.server_timing = true
|
||||||
|
|
||||||
# Enable/disable caching. By default caching is disabled.
|
# Enable/disable caching. By default caching is disabled.
|
||||||
|
@ -77,9 +77,6 @@ Rails.application.configure do
|
||||||
# Annotate rendered view with file names.
|
# Annotate rendered view with file names.
|
||||||
# config.action_view.annotate_rendered_view_with_filenames = true
|
# config.action_view.annotate_rendered_view_with_filenames = true
|
||||||
|
|
||||||
# Uncomment if you wish to allow Action Cable access from any origin.
|
|
||||||
# config.action_cable.disable_request_forgery_protection = true
|
|
||||||
|
|
||||||
config.action_mailer.default_options = { from: 'notifications@localhost' }
|
config.action_mailer.default_options = { from: 'notifications@localhost' }
|
||||||
|
|
||||||
# If using a Heroku, Vagrant or generic remote development environment,
|
# If using a Heroku, Vagrant or generic remote development environment,
|
||||||
|
@ -90,7 +87,7 @@ Rails.application.configure do
|
||||||
# TODO: Remove once devise-two-factor data migration complete
|
# TODO: Remove once devise-two-factor data migration complete
|
||||||
config.x.otp_secret = ENV.fetch('OTP_SECRET', '1fc2b87989afa6351912abeebe31ffc5c476ead9bf8b3d74cbc4a302c7b69a45b40b1bbef3506ddad73e942e15ed5ca4b402bf9a66423626051104f4b5f05109')
|
config.x.otp_secret = ENV.fetch('OTP_SECRET', '1fc2b87989afa6351912abeebe31ffc5c476ead9bf8b3d74cbc4a302c7b69a45b40b1bbef3506ddad73e942e15ed5ca4b402bf9a66423626051104f4b5f05109')
|
||||||
|
|
||||||
# Raise error when a before_action's only/except options reference missing actions
|
# Raise error when a before_action's only/except options reference missing actions.
|
||||||
config.action_controller.raise_on_missing_callback_actions = true
|
config.action_controller.raise_on_missing_callback_actions = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -23,9 +23,6 @@ Rails.application.configure do
|
||||||
# key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files).
|
# key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files).
|
||||||
# config.require_master_key = true
|
# config.require_master_key = true
|
||||||
|
|
||||||
# Compress CSS using a preprocessor.
|
|
||||||
# config.assets.css_compressor = :sass
|
|
||||||
|
|
||||||
# Do not fallback to assets pipeline if a precompiled asset is missed.
|
# Do not fallback to assets pipeline if a precompiled asset is missed.
|
||||||
config.assets.compile = false
|
config.assets.compile = false
|
||||||
|
|
||||||
|
@ -42,6 +39,7 @@ Rails.application.configure do
|
||||||
|
|
||||||
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
|
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
|
||||||
config.force_ssl = true
|
config.force_ssl = true
|
||||||
|
# Skip http-to-https redirect for the default health check endpoint.
|
||||||
config.ssl_options = {
|
config.ssl_options = {
|
||||||
redirect: {
|
redirect: {
|
||||||
exclude: ->(request) { request.path.start_with?('/health') || request.headers['Host'].end_with?('.onion') || request.headers['Host'].end_with?('.i2p') },
|
exclude: ->(request) { request.path.start_with?('/health') || request.headers['Host'].end_with?('.onion') || request.headers['Host'].end_with?('.i2p') },
|
||||||
|
@ -70,7 +68,7 @@ Rails.application.configure do
|
||||||
# config.action_mailer.raise_delivery_errors = false
|
# config.action_mailer.raise_delivery_errors = false
|
||||||
|
|
||||||
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
|
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
|
||||||
# English when a translation cannot be found).
|
# the I18n.default_locale when a translation cannot be found).
|
||||||
# This setting would typically be `true` to use the `I18n.default_locale`.
|
# This setting would typically be `true` to use the `I18n.default_locale`.
|
||||||
# Some locales are missing translation entries and would have errors:
|
# Some locales are missing translation entries and would have errors:
|
||||||
# https://github.com/mastodon/mastodon/pull/24727
|
# https://github.com/mastodon/mastodon/pull/24727
|
||||||
|
|
|
@ -26,7 +26,7 @@ Rails.application.configure do
|
||||||
config.action_controller.perform_caching = false
|
config.action_controller.perform_caching = false
|
||||||
config.cache_store = :memory_store
|
config.cache_store = :memory_store
|
||||||
|
|
||||||
# Raise exceptions instead of rendering exception templates.
|
# Render exception templates for rescuable exceptions and raise for other exceptions.
|
||||||
config.action_dispatch.show_exceptions = :rescuable
|
config.action_dispatch.show_exceptions = :rescuable
|
||||||
|
|
||||||
# Disable request forgery protection in test environment.
|
# Disable request forgery protection in test environment.
|
||||||
|
@ -70,7 +70,7 @@ Rails.application.configure do
|
||||||
# Annotate rendered view with file names.
|
# Annotate rendered view with file names.
|
||||||
# config.action_view.annotate_rendered_view_with_filenames = true
|
# config.action_view.annotate_rendered_view_with_filenames = true
|
||||||
|
|
||||||
# Raise error when a before_action's only/except options reference missing actions
|
# Raise error when a before_action's only/except options reference missing actions.
|
||||||
config.action_controller.raise_on_missing_callback_actions = true
|
config.action_controller.raise_on_missing_callback_actions = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -348,7 +348,7 @@ namespace :api, format: false do
|
||||||
namespace :web do
|
namespace :web do
|
||||||
resource :settings, only: [:update]
|
resource :settings, only: [:update]
|
||||||
resources :embeds, only: [:show]
|
resources :embeds, only: [:show]
|
||||||
resources :push_subscriptions, only: [:create] do
|
resources :push_subscriptions, only: [:create, :destroy] do
|
||||||
member do
|
member do
|
||||||
put :update
|
put :update
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
shared:
|
||||||
|
deepl:
|
||||||
|
api_key: <%= ENV.fetch('DEEPL_API_KEY', nil) %>
|
||||||
|
plan: <%= ENV.fetch('DEEPL_PLAN', 'free') %>
|
||||||
|
libre_translate:
|
||||||
|
api_key: <%= ENV.fetch('LIBRE_TRANSLATE_API_KEY', nil) %>
|
||||||
|
endpoint: <%= ENV.fetch('LIBRE_TRANSLATE_ENDPOINT', nil) %>
|
|
@ -0,0 +1,10 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RemoveDuplicateIndexes < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
remove_index :account_aliases, :account_id
|
||||||
|
remove_index :account_relationship_severance_events, :account_id
|
||||||
|
remove_index :custom_filter_statuses, :status_id
|
||||||
|
remove_index :webauthn_credentials, :user_id
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 2024_10_07_071624) do
|
ActiveRecord::Schema[7.1].define(version: 2024_10_14_010506) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
||||||
|
@ -21,7 +21,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_10_07_071624) do
|
||||||
t.datetime "created_at", precision: nil, null: false
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
t.index ["account_id", "uri"], name: "index_account_aliases_on_account_id_and_uri", unique: true
|
t.index ["account_id", "uri"], name: "index_account_aliases_on_account_id_and_uri", unique: true
|
||||||
t.index ["account_id"], name: "index_account_aliases_on_account_id"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "account_conversations", force: :cascade do |t|
|
create_table "account_conversations", force: :cascade do |t|
|
||||||
|
@ -99,7 +98,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_10_07_071624) do
|
||||||
t.integer "followers_count", default: 0, null: false
|
t.integer "followers_count", default: 0, null: false
|
||||||
t.integer "following_count", default: 0, null: false
|
t.integer "following_count", default: 0, null: false
|
||||||
t.index ["account_id", "relationship_severance_event_id"], name: "idx_on_account_id_relationship_severance_event_id_7bd82bf20e", unique: true
|
t.index ["account_id", "relationship_severance_event_id"], name: "idx_on_account_id_relationship_severance_event_id_7bd82bf20e", unique: true
|
||||||
t.index ["account_id"], name: "index_account_relationship_severance_events_on_account_id"
|
|
||||||
t.index ["relationship_severance_event_id"], name: "idx_on_relationship_severance_event_id_403f53e707"
|
t.index ["relationship_severance_event_id"], name: "idx_on_relationship_severance_event_id_403f53e707"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -397,7 +395,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_10_07_071624) do
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.index ["custom_filter_id"], name: "index_custom_filter_statuses_on_custom_filter_id"
|
t.index ["custom_filter_id"], name: "index_custom_filter_statuses_on_custom_filter_id"
|
||||||
t.index ["status_id", "custom_filter_id"], name: "index_custom_filter_statuses_on_status_id_and_custom_filter_id", unique: true
|
t.index ["status_id", "custom_filter_id"], name: "index_custom_filter_statuses_on_status_id_and_custom_filter_id", unique: true
|
||||||
t.index ["status_id"], name: "index_custom_filter_statuses_on_status_id"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "custom_filters", force: :cascade do |t|
|
create_table "custom_filters", force: :cascade do |t|
|
||||||
|
@ -1205,7 +1202,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_10_07_071624) do
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
t.index ["external_id"], name: "index_webauthn_credentials_on_external_id", unique: true
|
t.index ["external_id"], name: "index_webauthn_credentials_on_external_id", unique: true
|
||||||
t.index ["user_id", "nickname"], name: "index_webauthn_credentials_on_user_id_and_nickname", unique: true
|
t.index ["user_id", "nickname"], name: "index_webauthn_credentials_on_user_id_and_nickname", unique: true
|
||||||
t.index ["user_id"], name: "index_webauthn_credentials_on_user_id"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "webhooks", force: :cascade do |t|
|
create_table "webhooks", force: :cascade do |t|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@mastodon/mastodon",
|
"name": "@mastodon/mastodon",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"packageManager": "yarn@4.5.0",
|
"packageManager": "yarn@4.5.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
|
@ -180,13 +180,13 @@
|
||||||
"eslint": "^8.41.0",
|
"eslint": "^8.41.0",
|
||||||
"eslint-define-config": "^2.0.0",
|
"eslint-define-config": "^2.0.0",
|
||||||
"eslint-import-resolver-typescript": "^3.5.5",
|
"eslint-import-resolver-typescript": "^3.5.5",
|
||||||
"eslint-plugin-formatjs": "^4.10.1",
|
"eslint-plugin-formatjs": "^5.0.0",
|
||||||
"eslint-plugin-import": "~2.30.0",
|
"eslint-plugin-import": "~2.30.0",
|
||||||
"eslint-plugin-jsdoc": "^50.0.0",
|
"eslint-plugin-jsdoc": "^50.0.0",
|
||||||
"eslint-plugin-jsx-a11y": "~6.10.0",
|
"eslint-plugin-jsx-a11y": "~6.10.0",
|
||||||
"eslint-plugin-promise": "~7.1.0",
|
"eslint-plugin-promise": "~7.1.0",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
"jest-environment-jsdom": "^29.5.0",
|
"jest-environment-jsdom": "^29.5.0",
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Fabricator(:account_conversation) do
|
||||||
|
account
|
||||||
|
conversation
|
||||||
|
status_ids { [Fabricate(:status).id] }
|
||||||
|
end
|
|
@ -230,28 +230,6 @@ RSpec.describe ApplicationHelper do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'visibility_icon' do
|
|
||||||
it 'returns a globe icon for a public visible status' do
|
|
||||||
result = helper.visibility_icon Status.new(visibility: 'public')
|
|
||||||
expect(result).to match(/globe/)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns an unlock icon for a unlisted visible status' do
|
|
||||||
result = helper.visibility_icon Status.new(visibility: 'unlisted')
|
|
||||||
expect(result).to match(/lock_open/)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns a lock icon for a private visible status' do
|
|
||||||
result = helper.visibility_icon Status.new(visibility: 'private')
|
|
||||||
expect(result).to match(/lock/)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns an at icon for a direct visible status' do
|
|
||||||
result = helper.visibility_icon Status.new(visibility: 'direct')
|
|
||||||
expect(result).to match(/alternate_email/)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'title' do
|
describe 'title' do
|
||||||
it 'returns site title on production environment' do
|
it 'returns site title on production environment' do
|
||||||
Setting.site_title = 'site title'
|
Setting.site_title = 'site title'
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe DatabaseHelper do
|
||||||
|
context 'when a replica is enabled' do
|
||||||
|
around do |example|
|
||||||
|
ClimateControl.modify REPLICA_DB_NAME: 'prod-relay-quantum-tunnel-mirror' do
|
||||||
|
example.run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
before { allow(ApplicationRecord).to receive(:connected_to) }
|
||||||
|
|
||||||
|
describe '#with_read_replica' do
|
||||||
|
it 'uses the replica for connections' do
|
||||||
|
helper.with_read_replica { _x = 1 }
|
||||||
|
|
||||||
|
expect(ApplicationRecord)
|
||||||
|
.to have_received(:connected_to).with(role: :reading, prevent_writes: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#with_primary' do
|
||||||
|
it 'uses the primary for connections' do
|
||||||
|
helper.with_primary { _x = 1 }
|
||||||
|
|
||||||
|
expect(ApplicationRecord)
|
||||||
|
.to have_received(:connected_to).with(role: :writing)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a replica is not enabled' do
|
||||||
|
around do |example|
|
||||||
|
ClimateControl.modify REPLICA_DB_NAME: nil do
|
||||||
|
example.run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
before { allow(ApplicationRecord).to receive(:connected_to) }
|
||||||
|
|
||||||
|
describe '#with_read_replica' do
|
||||||
|
it 'does not use the replica for connections' do
|
||||||
|
helper.with_read_replica { _x = 1 }
|
||||||
|
|
||||||
|
expect(ApplicationRecord)
|
||||||
|
.to_not have_received(:connected_to).with(role: :reading, prevent_writes: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#with_primary' do
|
||||||
|
it 'does not use the primary for connections' do
|
||||||
|
helper.with_primary { _x = 1 }
|
||||||
|
|
||||||
|
expect(ApplicationRecord)
|
||||||
|
.to_not have_received(:connected_to).with(role: :writing)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,6 +3,8 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe FeedManager do
|
RSpec.describe FeedManager do
|
||||||
|
subject { described_class.instance }
|
||||||
|
|
||||||
before do |example|
|
before do |example|
|
||||||
unless example.metadata[:skip_stub]
|
unless example.metadata[:skip_stub]
|
||||||
stub_const 'FeedManager::MAX_ITEMS', 10
|
stub_const 'FeedManager::MAX_ITEMS', 10
|
||||||
|
@ -32,26 +34,26 @@ RSpec.describe FeedManager do
|
||||||
it 'returns false for followee\'s status' do
|
it 'returns false for followee\'s status' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
expect(described_class.instance.filter?(:home, status, bob)).to be false
|
expect(subject.filter?(:home, status, bob)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns false for reblog by followee' do
|
it 'returns false for reblog by followee' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||||
reblog = Fabricate(:status, reblog: status, account: alice)
|
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
expect(described_class.instance.filter?(:home, reblog, bob)).to be false
|
expect(subject.filter?(:home, reblog, bob)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for post from account who blocked me' do
|
it 'returns true for post from account who blocked me' do
|
||||||
status = Fabricate(:status, text: 'Hello, World', account: alice)
|
status = Fabricate(:status, text: 'Hello, World', account: alice)
|
||||||
alice.block!(bob)
|
alice.block!(bob)
|
||||||
expect(described_class.instance.filter?(:home, status, bob)).to be true
|
expect(subject.filter?(:home, status, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for post from blocked account' do
|
it 'returns true for post from blocked account' do
|
||||||
status = Fabricate(:status, text: 'Hello, World', account: alice)
|
status = Fabricate(:status, text: 'Hello, World', account: alice)
|
||||||
bob.block!(alice)
|
bob.block!(alice)
|
||||||
expect(described_class.instance.filter?(:home, status, bob)).to be true
|
expect(subject.filter?(:home, status, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for reblog by followee of blocked account' do
|
it 'returns true for reblog by followee of blocked account' do
|
||||||
|
@ -59,7 +61,7 @@ RSpec.describe FeedManager do
|
||||||
reblog = Fabricate(:status, reblog: status, account: alice)
|
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
bob.block!(jeff)
|
bob.block!(jeff)
|
||||||
expect(described_class.instance.filter?(:home, reblog, bob)).to be true
|
expect(subject.filter?(:home, reblog, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for reblog by followee of muted account' do
|
it 'returns true for reblog by followee of muted account' do
|
||||||
|
@ -67,7 +69,7 @@ RSpec.describe FeedManager do
|
||||||
reblog = Fabricate(:status, reblog: status, account: alice)
|
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
bob.mute!(jeff)
|
bob.mute!(jeff)
|
||||||
expect(described_class.instance.filter?(:home, reblog, bob)).to be true
|
expect(subject.filter?(:home, reblog, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for reblog by followee of someone who is blocking recipient' do
|
it 'returns true for reblog by followee of someone who is blocking recipient' do
|
||||||
|
@ -75,14 +77,14 @@ RSpec.describe FeedManager do
|
||||||
reblog = Fabricate(:status, reblog: status, account: alice)
|
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
jeff.block!(bob)
|
jeff.block!(bob)
|
||||||
expect(described_class.instance.filter?(:home, reblog, bob)).to be true
|
expect(subject.filter?(:home, reblog, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for reblog from account with reblogs disabled' do
|
it 'returns true for reblog from account with reblogs disabled' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||||
reblog = Fabricate(:status, reblog: status, account: alice)
|
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||||
bob.follow!(alice, reblogs: false)
|
bob.follow!(alice, reblogs: false)
|
||||||
expect(described_class.instance.filter?(:home, reblog, bob)).to be true
|
expect(subject.filter?(:home, reblog, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns false for reply by followee to another followee' do
|
it 'returns false for reply by followee to another followee' do
|
||||||
|
@ -90,49 +92,49 @@ RSpec.describe FeedManager do
|
||||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
bob.follow!(jeff)
|
bob.follow!(jeff)
|
||||||
expect(described_class.instance.filter?(:home, reply, bob)).to be false
|
expect(subject.filter?(:home, reply, bob)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns false for reply by followee to recipient' do
|
it 'returns false for reply by followee to recipient' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: bob)
|
status = Fabricate(:status, text: 'Hello world', account: bob)
|
||||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
expect(described_class.instance.filter?(:home, reply, bob)).to be false
|
expect(subject.filter?(:home, reply, bob)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns false for reply by followee to self' do
|
it 'returns false for reply by followee to self' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
expect(described_class.instance.filter?(:home, reply, bob)).to be false
|
expect(subject.filter?(:home, reply, bob)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for reply by followee to non-followed account' do
|
it 'returns true for reply by followee to non-followed account' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
expect(described_class.instance.filter?(:home, reply, bob)).to be true
|
expect(subject.filter?(:home, reply, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for the second reply by followee to a non-federated status' do
|
it 'returns true for the second reply by followee to a non-federated status' do
|
||||||
reply = Fabricate(:status, text: 'Reply 1', reply: true, account: alice)
|
reply = Fabricate(:status, text: 'Reply 1', reply: true, account: alice)
|
||||||
second_reply = Fabricate(:status, text: 'Reply 2', thread: reply, account: alice)
|
second_reply = Fabricate(:status, text: 'Reply 2', thread: reply, account: alice)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
expect(described_class.instance.filter?(:home, second_reply, bob)).to be true
|
expect(subject.filter?(:home, second_reply, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns false for status by followee mentioning another account' do
|
it 'returns false for status by followee mentioning another account' do
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
jeff.follow!(alice)
|
jeff.follow!(alice)
|
||||||
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
||||||
expect(described_class.instance.filter?(:home, status, bob)).to be false
|
expect(subject.filter?(:home, status, bob)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for status by followee mentioning blocked account' do
|
it 'returns true for status by followee mentioning blocked account' do
|
||||||
bob.block!(jeff)
|
bob.block!(jeff)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
||||||
expect(described_class.instance.filter?(:home, status, bob)).to be true
|
expect(subject.filter?(:home, status, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for status by followee mentioning muted account' do
|
it 'returns true for status by followee mentioning muted account' do
|
||||||
|
@ -147,19 +149,19 @@ RSpec.describe FeedManager do
|
||||||
alice.follow!(jeff)
|
alice.follow!(jeff)
|
||||||
status = Fabricate(:status, text: 'Hello world', account: bob)
|
status = Fabricate(:status, text: 'Hello world', account: bob)
|
||||||
reblog = Fabricate(:status, reblog: status, account: jeff)
|
reblog = Fabricate(:status, reblog: status, account: jeff)
|
||||||
expect(described_class.instance.filter?(:home, reblog, alice)).to be true
|
expect(subject.filter?(:home, reblog, alice)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for German post when follow is set to English only' do
|
it 'returns true for German post when follow is set to English only' do
|
||||||
alice.follow!(bob, languages: %w(en))
|
alice.follow!(bob, languages: %w(en))
|
||||||
status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de')
|
status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de')
|
||||||
expect(described_class.instance.filter?(:home, status, alice)).to be true
|
expect(subject.filter?(:home, status, alice)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns false for German post when follow is set to German' do
|
it 'returns false for German post when follow is set to German' do
|
||||||
alice.follow!(bob, languages: %w(de))
|
alice.follow!(bob, languages: %w(de))
|
||||||
status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de')
|
status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de')
|
||||||
expect(described_class.instance.filter?(:home, status, alice)).to be false
|
expect(subject.filter?(:home, status, alice)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for post from followee on exclusive list' do
|
it 'returns true for post from followee on exclusive list' do
|
||||||
|
@ -168,7 +170,7 @@ RSpec.describe FeedManager do
|
||||||
list.accounts << bob
|
list.accounts << bob
|
||||||
allow(List).to receive(:where).and_return(list)
|
allow(List).to receive(:where).and_return(list)
|
||||||
status = Fabricate(:status, text: 'I post a lot', account: bob)
|
status = Fabricate(:status, text: 'I post a lot', account: bob)
|
||||||
expect(described_class.instance.filter?(:home, status, alice)).to be true
|
expect(subject.filter?(:home, status, alice)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for reblog from followee on exclusive list' do
|
it 'returns true for reblog from followee on exclusive list' do
|
||||||
|
@ -178,7 +180,7 @@ RSpec.describe FeedManager do
|
||||||
allow(List).to receive(:where).and_return(list)
|
allow(List).to receive(:where).and_return(list)
|
||||||
status = Fabricate(:status, text: 'I post a lot', account: bob)
|
status = Fabricate(:status, text: 'I post a lot', account: bob)
|
||||||
reblog = Fabricate(:status, reblog: status, account: jeff)
|
reblog = Fabricate(:status, reblog: status, account: jeff)
|
||||||
expect(described_class.instance.filter?(:home, reblog, alice)).to be true
|
expect(subject.filter?(:home, reblog, alice)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns false for post from followee on non-exclusive list' do
|
it 'returns false for post from followee on non-exclusive list' do
|
||||||
|
@ -186,7 +188,7 @@ RSpec.describe FeedManager do
|
||||||
alice.follow!(bob)
|
alice.follow!(bob)
|
||||||
list.accounts << bob
|
list.accounts << bob
|
||||||
status = Fabricate(:status, text: 'I post a lot', account: bob)
|
status = Fabricate(:status, text: 'I post a lot', account: bob)
|
||||||
expect(described_class.instance.filter?(:home, status, alice)).to be false
|
expect(subject.filter?(:home, status, alice)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns false for reblog from followee on non-exclusive list' do
|
it 'returns false for reblog from followee on non-exclusive list' do
|
||||||
|
@ -195,7 +197,7 @@ RSpec.describe FeedManager do
|
||||||
list.accounts << jeff
|
list.accounts << jeff
|
||||||
status = Fabricate(:status, text: 'I post a lot', account: bob)
|
status = Fabricate(:status, text: 'I post a lot', account: bob)
|
||||||
reblog = Fabricate(:status, reblog: status, account: jeff)
|
reblog = Fabricate(:status, reblog: status, account: jeff)
|
||||||
expect(described_class.instance.filter?(:home, reblog, alice)).to be false
|
expect(subject.filter?(:home, reblog, alice)).to be false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -203,27 +205,27 @@ RSpec.describe FeedManager do
|
||||||
it 'returns true for status that mentions blocked account' do
|
it 'returns true for status that mentions blocked account' do
|
||||||
bob.block!(jeff)
|
bob.block!(jeff)
|
||||||
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
||||||
expect(described_class.instance.filter?(:mentions, status, bob)).to be true
|
expect(subject.filter?(:mentions, status, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for status that replies to a blocked account' do
|
it 'returns true for status that replies to a blocked account' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||||
bob.block!(jeff)
|
bob.block!(jeff)
|
||||||
expect(described_class.instance.filter?(:mentions, reply, bob)).to be true
|
expect(subject.filter?(:mentions, reply, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns false for status by limited account who recipient is not following' do
|
it 'returns false for status by limited account who recipient is not following' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||||
alice.silence!
|
alice.silence!
|
||||||
expect(described_class.instance.filter?(:mentions, status, bob)).to be false
|
expect(subject.filter?(:mentions, status, bob)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns false for status by followed limited account' do
|
it 'returns false for status by followed limited account' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||||
alice.silence!
|
alice.silence!
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
expect(described_class.instance.filter?(:mentions, status, bob)).to be false
|
expect(subject.filter?(:mentions, status, bob)).to be false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -235,7 +237,7 @@ RSpec.describe FeedManager do
|
||||||
members = Array.new(described_class::MAX_ITEMS) { |count| [count, count] }
|
members = Array.new(described_class::MAX_ITEMS) { |count| [count, count] }
|
||||||
redis.zadd("feed:home:#{account.id}", members)
|
redis.zadd("feed:home:#{account.id}", members)
|
||||||
|
|
||||||
described_class.instance.push_to_home(account, status)
|
subject.push_to_home(account, status)
|
||||||
|
|
||||||
expect(redis.zcard("feed:home:#{account.id}")).to eq described_class::MAX_ITEMS
|
expect(redis.zcard("feed:home:#{account.id}")).to eq described_class::MAX_ITEMS
|
||||||
end
|
end
|
||||||
|
@ -246,7 +248,7 @@ RSpec.describe FeedManager do
|
||||||
reblogged = Fabricate(:status)
|
reblogged = Fabricate(:status)
|
||||||
reblog = Fabricate(:status, reblog: reblogged)
|
reblog = Fabricate(:status, reblog: reblogged)
|
||||||
|
|
||||||
expect(described_class.instance.push_to_home(account, reblog)).to be true
|
expect(subject.push_to_home(account, reblog)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not save a new reblog of a recent status' do
|
it 'does not save a new reblog of a recent status' do
|
||||||
|
@ -254,9 +256,9 @@ RSpec.describe FeedManager do
|
||||||
reblogged = Fabricate(:status)
|
reblogged = Fabricate(:status)
|
||||||
reblog = Fabricate(:status, reblog: reblogged)
|
reblog = Fabricate(:status, reblog: reblogged)
|
||||||
|
|
||||||
described_class.instance.push_to_home(account, reblogged)
|
subject.push_to_home(account, reblogged)
|
||||||
|
|
||||||
expect(described_class.instance.push_to_home(account, reblog)).to be false
|
expect(subject.push_to_home(account, reblog)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'saves a new reblog of an old status' do
|
it 'saves a new reblog of an old status' do
|
||||||
|
@ -264,14 +266,14 @@ RSpec.describe FeedManager do
|
||||||
reblogged = Fabricate(:status)
|
reblogged = Fabricate(:status)
|
||||||
reblog = Fabricate(:status, reblog: reblogged)
|
reblog = Fabricate(:status, reblog: reblogged)
|
||||||
|
|
||||||
described_class.instance.push_to_home(account, reblogged)
|
subject.push_to_home(account, reblogged)
|
||||||
|
|
||||||
# Fill the feed with intervening statuses
|
# Fill the feed with intervening statuses
|
||||||
described_class::REBLOG_FALLOFF.times do
|
described_class::REBLOG_FALLOFF.times do
|
||||||
described_class.instance.push_to_home(account, Fabricate(:status))
|
subject.push_to_home(account, Fabricate(:status))
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(described_class.instance.push_to_home(account, reblog)).to be true
|
expect(subject.push_to_home(account, reblog)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not save a new reblog of a recently-reblogged status' do
|
it 'does not save a new reblog of a recently-reblogged status' do
|
||||||
|
@ -280,10 +282,10 @@ RSpec.describe FeedManager do
|
||||||
reblogs = Array.new(2) { Fabricate(:status, reblog: reblogged) }
|
reblogs = Array.new(2) { Fabricate(:status, reblog: reblogged) }
|
||||||
|
|
||||||
# The first reblog will be accepted
|
# The first reblog will be accepted
|
||||||
described_class.instance.push_to_home(account, reblogs.first)
|
subject.push_to_home(account, reblogs.first)
|
||||||
|
|
||||||
# The second reblog should be ignored
|
# The second reblog should be ignored
|
||||||
expect(described_class.instance.push_to_home(account, reblogs.last)).to be false
|
expect(subject.push_to_home(account, reblogs.last)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'saves a new reblog of a recently-reblogged status when previous reblog has been deleted' do
|
it 'saves a new reblog of a recently-reblogged status when previous reblog has been deleted' do
|
||||||
|
@ -292,15 +294,15 @@ RSpec.describe FeedManager do
|
||||||
old_reblog = Fabricate(:status, reblog: reblogged)
|
old_reblog = Fabricate(:status, reblog: reblogged)
|
||||||
|
|
||||||
# The first reblog should be accepted
|
# The first reblog should be accepted
|
||||||
expect(described_class.instance.push_to_home(account, old_reblog)).to be true
|
expect(subject.push_to_home(account, old_reblog)).to be true
|
||||||
|
|
||||||
# The first reblog should be successfully removed
|
# The first reblog should be successfully removed
|
||||||
expect(described_class.instance.unpush_from_home(account, old_reblog)).to be true
|
expect(subject.unpush_from_home(account, old_reblog)).to be true
|
||||||
|
|
||||||
reblog = Fabricate(:status, reblog: reblogged)
|
reblog = Fabricate(:status, reblog: reblogged)
|
||||||
|
|
||||||
# The second reblog should be accepted
|
# The second reblog should be accepted
|
||||||
expect(described_class.instance.push_to_home(account, reblog)).to be true
|
expect(subject.push_to_home(account, reblog)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not save a new reblog of a multiply-reblogged-then-unreblogged status' do
|
it 'does not save a new reblog of a multiply-reblogged-then-unreblogged status' do
|
||||||
|
@ -309,14 +311,14 @@ RSpec.describe FeedManager do
|
||||||
reblogs = Array.new(3) { Fabricate(:status, reblog: reblogged) }
|
reblogs = Array.new(3) { Fabricate(:status, reblog: reblogged) }
|
||||||
|
|
||||||
# Accept the reblogs
|
# Accept the reblogs
|
||||||
described_class.instance.push_to_home(account, reblogs[0])
|
subject.push_to_home(account, reblogs[0])
|
||||||
described_class.instance.push_to_home(account, reblogs[1])
|
subject.push_to_home(account, reblogs[1])
|
||||||
|
|
||||||
# Unreblog the first one
|
# Unreblog the first one
|
||||||
described_class.instance.unpush_from_home(account, reblogs[0])
|
subject.unpush_from_home(account, reblogs[0])
|
||||||
|
|
||||||
# The last reblog should still be ignored
|
# The last reblog should still be ignored
|
||||||
expect(described_class.instance.push_to_home(account, reblogs.last)).to be false
|
expect(subject.push_to_home(account, reblogs.last)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'saves a new reblog of a long-ago-reblogged status' do
|
it 'saves a new reblog of a long-ago-reblogged status' do
|
||||||
|
@ -325,15 +327,15 @@ RSpec.describe FeedManager do
|
||||||
reblogs = Array.new(2) { Fabricate(:status, reblog: reblogged) }
|
reblogs = Array.new(2) { Fabricate(:status, reblog: reblogged) }
|
||||||
|
|
||||||
# The first reblog will be accepted
|
# The first reblog will be accepted
|
||||||
described_class.instance.push_to_home(account, reblogs.first)
|
subject.push_to_home(account, reblogs.first)
|
||||||
|
|
||||||
# Fill the feed with intervening statuses
|
# Fill the feed with intervening statuses
|
||||||
described_class::REBLOG_FALLOFF.times do
|
described_class::REBLOG_FALLOFF.times do
|
||||||
described_class.instance.push_to_home(account, Fabricate(:status))
|
subject.push_to_home(account, Fabricate(:status))
|
||||||
end
|
end
|
||||||
|
|
||||||
# The second reblog should also be accepted
|
# The second reblog should also be accepted
|
||||||
expect(described_class.instance.push_to_home(account, reblogs.last)).to be true
|
expect(subject.push_to_home(account, reblogs.last)).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -341,9 +343,9 @@ RSpec.describe FeedManager do
|
||||||
account = Fabricate(:account)
|
account = Fabricate(:account)
|
||||||
reblog = Fabricate(:status)
|
reblog = Fabricate(:status)
|
||||||
status = Fabricate(:status, reblog: reblog)
|
status = Fabricate(:status, reblog: reblog)
|
||||||
described_class.instance.push_to_home(account, status)
|
subject.push_to_home(account, status)
|
||||||
|
|
||||||
expect(described_class.instance.push_to_home(account, reblog)).to be false
|
expect(subject.push_to_home(account, reblog)).to be false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -366,9 +368,9 @@ RSpec.describe FeedManager do
|
||||||
it "does not push when the given status's reblog is already inserted" do
|
it "does not push when the given status's reblog is already inserted" do
|
||||||
reblog = Fabricate(:status)
|
reblog = Fabricate(:status)
|
||||||
status = Fabricate(:status, reblog: reblog)
|
status = Fabricate(:status, reblog: reblog)
|
||||||
described_class.instance.push_to_list(list, status)
|
subject.push_to_list(list, status)
|
||||||
|
|
||||||
expect(described_class.instance.push_to_list(list, reblog)).to be false
|
expect(subject.push_to_list(list, reblog)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when replies policy is set to no replies' do
|
context 'when replies policy is set to no replies' do
|
||||||
|
@ -378,19 +380,19 @@ RSpec.describe FeedManager do
|
||||||
|
|
||||||
it 'pushes statuses that are not replies' do
|
it 'pushes statuses that are not replies' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: bob)
|
status = Fabricate(:status, text: 'Hello world', account: bob)
|
||||||
expect(described_class.instance.push_to_list(list, status)).to be true
|
expect(subject.push_to_list(list, status)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'pushes statuses that are replies to list owner' do
|
it 'pushes statuses that are replies to list owner' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: owner)
|
status = Fabricate(:status, text: 'Hello world', account: owner)
|
||||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
||||||
expect(described_class.instance.push_to_list(list, reply)).to be true
|
expect(subject.push_to_list(list, reply)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not push replies to another member of the list' do
|
it 'does not push replies to another member of the list' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
||||||
expect(described_class.instance.push_to_list(list, reply)).to be false
|
expect(subject.push_to_list(list, reply)).to be false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -401,25 +403,25 @@ RSpec.describe FeedManager do
|
||||||
|
|
||||||
it 'pushes statuses that are not replies' do
|
it 'pushes statuses that are not replies' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: bob)
|
status = Fabricate(:status, text: 'Hello world', account: bob)
|
||||||
expect(described_class.instance.push_to_list(list, status)).to be true
|
expect(subject.push_to_list(list, status)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'pushes statuses that are replies to list owner' do
|
it 'pushes statuses that are replies to list owner' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: owner)
|
status = Fabricate(:status, text: 'Hello world', account: owner)
|
||||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
||||||
expect(described_class.instance.push_to_list(list, reply)).to be true
|
expect(subject.push_to_list(list, reply)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'pushes replies to another member of the list' do
|
it 'pushes replies to another member of the list' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
||||||
expect(described_class.instance.push_to_list(list, reply)).to be true
|
expect(subject.push_to_list(list, reply)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not push replies to someone not a member of the list' do
|
it 'does not push replies to someone not a member of the list' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: eve)
|
status = Fabricate(:status, text: 'Hello world', account: eve)
|
||||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
||||||
expect(described_class.instance.push_to_list(list, reply)).to be false
|
expect(subject.push_to_list(list, reply)).to be false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -430,25 +432,25 @@ RSpec.describe FeedManager do
|
||||||
|
|
||||||
it 'pushes statuses that are not replies' do
|
it 'pushes statuses that are not replies' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: bob)
|
status = Fabricate(:status, text: 'Hello world', account: bob)
|
||||||
expect(described_class.instance.push_to_list(list, status)).to be true
|
expect(subject.push_to_list(list, status)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'pushes statuses that are replies to list owner' do
|
it 'pushes statuses that are replies to list owner' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: owner)
|
status = Fabricate(:status, text: 'Hello world', account: owner)
|
||||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
||||||
expect(described_class.instance.push_to_list(list, reply)).to be true
|
expect(subject.push_to_list(list, reply)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'pushes replies to another member of the list' do
|
it 'pushes replies to another member of the list' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
||||||
expect(described_class.instance.push_to_list(list, reply)).to be true
|
expect(subject.push_to_list(list, reply)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'pushes replies to someone not a member of the list' do
|
it 'pushes replies to someone not a member of the list' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: eve)
|
status = Fabricate(:status, text: 'Hello world', account: eve)
|
||||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
||||||
expect(described_class.instance.push_to_list(list, reply)).to be true
|
expect(subject.push_to_list(list, reply)).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -458,9 +460,9 @@ RSpec.describe FeedManager do
|
||||||
account = Fabricate(:account, id: 0)
|
account = Fabricate(:account, id: 0)
|
||||||
reblog = Fabricate(:status)
|
reblog = Fabricate(:status)
|
||||||
status = Fabricate(:status, reblog: reblog)
|
status = Fabricate(:status, reblog: reblog)
|
||||||
described_class.instance.push_to_home(account, status)
|
subject.push_to_home(account, status)
|
||||||
|
|
||||||
described_class.instance.merge_into_home(account, reblog.account)
|
subject.merge_into_home(account, reblog.account)
|
||||||
|
|
||||||
expect(redis.zscore('feed:home:0', reblog.id)).to be_nil
|
expect(redis.zscore('feed:home:0', reblog.id)).to be_nil
|
||||||
end
|
end
|
||||||
|
@ -473,14 +475,14 @@ RSpec.describe FeedManager do
|
||||||
reblogged = Fabricate(:status)
|
reblogged = Fabricate(:status)
|
||||||
status = Fabricate(:status, reblog: reblogged)
|
status = Fabricate(:status, reblog: reblogged)
|
||||||
|
|
||||||
described_class.instance.push_to_home(receiver, reblogged)
|
subject.push_to_home(receiver, reblogged)
|
||||||
described_class::REBLOG_FALLOFF.times { described_class.instance.push_to_home(receiver, Fabricate(:status)) }
|
described_class::REBLOG_FALLOFF.times { subject.push_to_home(receiver, Fabricate(:status)) }
|
||||||
described_class.instance.push_to_home(receiver, status)
|
subject.push_to_home(receiver, status)
|
||||||
|
|
||||||
# The reblogging status should show up under normal conditions.
|
# The reblogging status should show up under normal conditions.
|
||||||
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s)
|
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s)
|
||||||
|
|
||||||
described_class.instance.unpush_from_home(receiver, status)
|
subject.unpush_from_home(receiver, status)
|
||||||
|
|
||||||
# Restore original status
|
# Restore original status
|
||||||
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s)
|
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s)
|
||||||
|
@ -491,12 +493,12 @@ RSpec.describe FeedManager do
|
||||||
reblogged = Fabricate(:status)
|
reblogged = Fabricate(:status)
|
||||||
status = Fabricate(:status, reblog: reblogged)
|
status = Fabricate(:status, reblog: reblogged)
|
||||||
|
|
||||||
described_class.instance.push_to_home(receiver, status)
|
subject.push_to_home(receiver, status)
|
||||||
|
|
||||||
# The reblogging status should show up under normal conditions.
|
# The reblogging status should show up under normal conditions.
|
||||||
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [status.id.to_s]
|
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [status.id.to_s]
|
||||||
|
|
||||||
described_class.instance.unpush_from_home(receiver, status)
|
subject.unpush_from_home(receiver, status)
|
||||||
|
|
||||||
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to be_empty
|
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to be_empty
|
||||||
end
|
end
|
||||||
|
@ -506,14 +508,14 @@ RSpec.describe FeedManager do
|
||||||
reblogs = Array.new(3) { Fabricate(:status, reblog: reblogged) }
|
reblogs = Array.new(3) { Fabricate(:status, reblog: reblogged) }
|
||||||
|
|
||||||
reblogs.each do |reblog|
|
reblogs.each do |reblog|
|
||||||
described_class.instance.push_to_home(receiver, reblog)
|
subject.push_to_home(receiver, reblog)
|
||||||
end
|
end
|
||||||
|
|
||||||
# The reblogging status should show up under normal conditions.
|
# The reblogging status should show up under normal conditions.
|
||||||
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.first.id.to_s]
|
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.first.id.to_s]
|
||||||
|
|
||||||
reblogs[0...-1].each do |reblog|
|
reblogs[0...-1].each do |reblog|
|
||||||
described_class.instance.unpush_from_home(receiver, reblog)
|
subject.unpush_from_home(receiver, reblog)
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.last.id.to_s]
|
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.last.id.to_s]
|
||||||
|
@ -522,10 +524,10 @@ RSpec.describe FeedManager do
|
||||||
it 'sends push updates' do
|
it 'sends push updates' do
|
||||||
status = Fabricate(:status)
|
status = Fabricate(:status)
|
||||||
|
|
||||||
described_class.instance.push_to_home(receiver, status)
|
subject.push_to_home(receiver, status)
|
||||||
|
|
||||||
allow(redis).to receive_messages(publish: nil)
|
allow(redis).to receive_messages(publish: nil)
|
||||||
described_class.instance.unpush_from_home(receiver, status)
|
subject.unpush_from_home(receiver, status)
|
||||||
|
|
||||||
deletion = Oj.dump(event: :delete, payload: status.id.to_s)
|
deletion = Oj.dump(event: :delete, payload: status.id.to_s)
|
||||||
expect(redis).to have_received(:publish).with("timeline:#{receiver.id}", deletion)
|
expect(redis).to have_received(:publish).with("timeline:#{receiver.id}", deletion)
|
||||||
|
@ -539,9 +541,9 @@ RSpec.describe FeedManager do
|
||||||
it 'leaves a tagged status' do
|
it 'leaves a tagged status' do
|
||||||
status = Fabricate(:status)
|
status = Fabricate(:status)
|
||||||
status.tags << tag
|
status.tags << tag
|
||||||
described_class.instance.push_to_home(receiver, status)
|
subject.push_to_home(receiver, status)
|
||||||
|
|
||||||
described_class.instance.unmerge_tag_from_home(tag, receiver)
|
subject.unmerge_tag_from_home(tag, receiver)
|
||||||
|
|
||||||
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s)
|
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s)
|
||||||
end
|
end
|
||||||
|
@ -552,9 +554,9 @@ RSpec.describe FeedManager do
|
||||||
|
|
||||||
status = Fabricate(:status, account: followee)
|
status = Fabricate(:status, account: followee)
|
||||||
status.tags << tag
|
status.tags << tag
|
||||||
described_class.instance.push_to_home(receiver, status)
|
subject.push_to_home(receiver, status)
|
||||||
|
|
||||||
described_class.instance.unmerge_tag_from_home(tag, receiver)
|
subject.unmerge_tag_from_home(tag, receiver)
|
||||||
|
|
||||||
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s)
|
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s)
|
||||||
end
|
end
|
||||||
|
@ -562,9 +564,9 @@ RSpec.describe FeedManager do
|
||||||
it 'remains a tagged status written by receiver' do
|
it 'remains a tagged status written by receiver' do
|
||||||
status = Fabricate(:status, account: receiver)
|
status = Fabricate(:status, account: receiver)
|
||||||
status.tags << tag
|
status.tags << tag
|
||||||
described_class.instance.push_to_home(receiver, status)
|
subject.push_to_home(receiver, status)
|
||||||
|
|
||||||
described_class.instance.unmerge_tag_from_home(tag, receiver)
|
subject.unmerge_tag_from_home(tag, receiver)
|
||||||
|
|
||||||
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s)
|
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s)
|
||||||
end
|
end
|
||||||
|
@ -595,7 +597,7 @@ RSpec.describe FeedManager do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'correctly cleans the home timeline' do
|
it 'correctly cleans the home timeline' do
|
||||||
described_class.instance.clear_from_home(account, target_account)
|
subject.clear_from_home(account, target_account)
|
||||||
|
|
||||||
expect(redis.zrange("feed:home:#{account.id}", 0, -1)).to eq [status_from_followed_account_first.id.to_s, status_from_followed_account_next.id.to_s]
|
expect(redis.zrange("feed:home:#{account.id}", 0, -1)).to eq [status_from_followed_account_first.id.to_s, status_from_followed_account_next.id.to_s]
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,4 +8,26 @@ RSpec.describe AccountAlias do
|
||||||
it { is_expected.to normalize(:acct).from(' @username@domain ').to('username@domain') }
|
it { is_expected.to normalize(:acct).from(' @username@domain ').to('username@domain') }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'Validations' do
|
||||||
|
subject { described_class.new(account:) }
|
||||||
|
|
||||||
|
let(:account) { Fabricate :account }
|
||||||
|
|
||||||
|
it { is_expected.to_not allow_values(nil, '').for(:uri).against(:acct).with_message(not_found_message) }
|
||||||
|
|
||||||
|
it { is_expected.to_not allow_values(account_uri).for(:uri).against(:acct).with_message(self_move_message) }
|
||||||
|
|
||||||
|
def account_uri
|
||||||
|
ActivityPub::TagManager.instance.uri_for(subject.account)
|
||||||
|
end
|
||||||
|
|
||||||
|
def not_found_message
|
||||||
|
I18n.t('migrations.errors.not_found')
|
||||||
|
end
|
||||||
|
|
||||||
|
def self_move_message
|
||||||
|
I18n.t('migrations.errors.move_to_self')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,8 +9,8 @@ RSpec.describe AccountMigration do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'validations' do
|
describe 'Validations' do
|
||||||
subject { described_class.new(account: source_account, acct: target_acct) }
|
subject { Fabricate.build :account_migration, account: source_account }
|
||||||
|
|
||||||
let(:source_account) { Fabricate(:account) }
|
let(:source_account) { Fabricate(:account) }
|
||||||
let(:target_acct) { target_account.acct }
|
let(:target_acct) { target_account.acct }
|
||||||
|
@ -26,9 +26,7 @@ RSpec.describe AccountMigration do
|
||||||
allow(service_double).to receive(:call).with(target_acct, anything).and_return(target_account)
|
allow(service_double).to receive(:call).with(target_acct, anything).and_return(target_account)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'passes validations' do
|
it { is_expected.to allow_value(target_account.acct).for(:acct) }
|
||||||
expect(subject).to be_valid
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with unresolvable account' do
|
context 'with unresolvable account' do
|
||||||
|
@ -40,17 +38,13 @@ RSpec.describe AccountMigration do
|
||||||
allow(service_double).to receive(:call).with(target_acct, anything).and_return(nil)
|
allow(service_double).to receive(:call).with(target_acct, anything).and_return(nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has errors on acct field' do
|
it { is_expected.to_not allow_value(target_acct).for(:acct) }
|
||||||
expect(subject).to model_have_error_on_field(:acct)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with a space in the domain part' do
|
context 'with a space in the domain part' do
|
||||||
let(:target_acct) { 'target@remote. org' }
|
let(:target_acct) { 'target@remote. org' }
|
||||||
|
|
||||||
it 'has errors on acct field' do
|
it { is_expected.to_not allow_value(target_acct).for(:acct) }
|
||||||
expect(subject).to model_have_error_on_field(:acct)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe AccountModerationNote do
|
RSpec.describe AccountModerationNote do
|
||||||
describe 'chronological scope' do
|
describe 'Scopes' do
|
||||||
|
describe '.chronological' do
|
||||||
it 'returns account moderation notes oldest to newest' do
|
it 'returns account moderation notes oldest to newest' do
|
||||||
account = Fabricate(:account)
|
account = Fabricate(:account)
|
||||||
note1 = Fabricate(:account_moderation_note, target_account: account)
|
note1 = Fabricate(:account_moderation_note, target_account: account)
|
||||||
|
@ -12,20 +13,14 @@ RSpec.describe AccountModerationNote do
|
||||||
expect(account.targeted_moderation_notes.chronological).to eq [note1, note2]
|
expect(account.targeted_moderation_notes.chronological).to eq [note1, note2]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'validations' do
|
|
||||||
it 'is invalid if the content is empty' do
|
|
||||||
report = Fabricate.build(:account_moderation_note, content: '')
|
|
||||||
expect(report.valid?).to be false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is invalid if content is longer than character limit' do
|
describe 'Validations' do
|
||||||
report = Fabricate.build(:account_moderation_note, content: comment_over_limit)
|
subject { Fabricate.build :account_moderation_note }
|
||||||
expect(report.valid?).to be false
|
|
||||||
end
|
|
||||||
|
|
||||||
def comment_over_limit
|
describe 'content' do
|
||||||
Faker::Lorem.paragraph_by_chars(number: described_class::CONTENT_SIZE_LIMIT * 2)
|
it { is_expected.to_not allow_value('').for(:content) }
|
||||||
|
it { is_expected.to validate_length_of(:content).is_at_most(described_class::CONTENT_SIZE_LIMIT) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,64 +10,6 @@ RSpec.describe Account do
|
||||||
|
|
||||||
let(:bob) { Fabricate(:account, username: 'bob') }
|
let(:bob) { Fabricate(:account, username: 'bob') }
|
||||||
|
|
||||||
describe '#suspended_locally?' do
|
|
||||||
context 'when the account is not suspended' do
|
|
||||||
it 'returns false' do
|
|
||||||
expect(subject.suspended_locally?).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the account is suspended locally' do
|
|
||||||
before do
|
|
||||||
subject.update!(suspended_at: 1.day.ago, suspension_origin: :local)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true' do
|
|
||||||
expect(subject.suspended_locally?).to be true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the account is suspended remotely' do
|
|
||||||
before do
|
|
||||||
subject.update!(suspended_at: 1.day.ago, suspension_origin: :remote)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false' do
|
|
||||||
expect(subject.suspended_locally?).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#suspend!' do
|
|
||||||
it 'marks the account as suspended and creates a deletion request' do
|
|
||||||
expect { subject.suspend! }
|
|
||||||
.to change(subject, :suspended?).from(false).to(true)
|
|
||||||
.and change(subject, :suspended_locally?).from(false).to(true)
|
|
||||||
.and(change { AccountDeletionRequest.exists?(account: subject) }.from(false).to(true))
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the account is of a local user' do
|
|
||||||
subject { local_user_account }
|
|
||||||
|
|
||||||
let!(:local_user_account) { Fabricate(:user, email: 'foo+bar@domain.org').account }
|
|
||||||
|
|
||||||
it 'creates a canonical domain block' do
|
|
||||||
subject.suspend!
|
|
||||||
expect(CanonicalEmailBlock.block?(subject.user_email)).to be true
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when a canonical domain block already exists for that email' do
|
|
||||||
before do
|
|
||||||
Fabricate(:canonical_email_block, email: subject.user_email)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not raise an error' do
|
|
||||||
expect { subject.suspend! }.to_not raise_error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#follow!' do
|
describe '#follow!' do
|
||||||
it 'creates a follow' do
|
it 'creates a follow' do
|
||||||
follow = subject.follow!(bob)
|
follow = subject.follow!(bob)
|
||||||
|
@ -208,16 +150,16 @@ RSpec.describe Account do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when last_webfingered_at is more than 24 hours before' do
|
context 'when last_webfingered_at is before the threshold' do
|
||||||
let(:last_webfingered_at) { 25.hours.ago }
|
let(:last_webfingered_at) { (described_class::STALE_THRESHOLD + 1.hour).ago }
|
||||||
|
|
||||||
it 'returns true' do
|
it 'returns true' do
|
||||||
expect(account.possibly_stale?).to be true
|
expect(account.possibly_stale?).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when last_webfingered_at is less than 24 hours before' do
|
context 'when last_webfingered_at is after the threshold' do
|
||||||
let(:last_webfingered_at) { 23.hours.ago }
|
let(:last_webfingered_at) { (described_class::STALE_THRESHOLD - 1.hour).ago }
|
||||||
|
|
||||||
it 'returns false' do
|
it 'returns false' do
|
||||||
expect(account.possibly_stale?).to be false
|
expect(account.possibly_stale?).to be false
|
||||||
|
@ -752,26 +694,42 @@ RSpec.describe Account do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#prepare_contents' do
|
describe 'Callbacks' do
|
||||||
subject { Fabricate.build :account, domain: domain, note: ' padded note ', display_name: ' padded name ' }
|
describe 'Stripping content when required' do
|
||||||
|
context 'with a remote account' do
|
||||||
|
subject { Fabricate.build :account, domain: 'host.example', note: ' note ', display_name: ' display name ' }
|
||||||
|
|
||||||
context 'with local account' do
|
it 'preserves content' do
|
||||||
let(:domain) { nil }
|
|
||||||
|
|
||||||
it 'strips values' do
|
|
||||||
expect { subject.valid? }
|
|
||||||
.to change(subject, :note).to('padded note')
|
|
||||||
.and(change(subject, :display_name).to('padded name'))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with remote account' do
|
|
||||||
let(:domain) { 'host.example' }
|
|
||||||
|
|
||||||
it 'preserves values' do
|
|
||||||
expect { subject.valid? }
|
expect { subject.valid? }
|
||||||
.to not_change(subject, :note)
|
.to not_change(subject, :note)
|
||||||
.and(not_change(subject, :display_name))
|
.and not_change(subject, :display_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a local account' do
|
||||||
|
subject { Fabricate.build :account, domain: nil, note:, display_name: }
|
||||||
|
|
||||||
|
context 'with populated fields' do
|
||||||
|
let(:note) { ' note ' }
|
||||||
|
let(:display_name) { ' display name ' }
|
||||||
|
|
||||||
|
it 'strips content' do
|
||||||
|
expect { subject.valid? }
|
||||||
|
.to change(subject, :note).to('note')
|
||||||
|
.and change(subject, :display_name).to('display name')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with empty fields' do
|
||||||
|
let(:note) { nil }
|
||||||
|
let(:display_name) { nil }
|
||||||
|
|
||||||
|
it 'preserves content' do
|
||||||
|
expect { subject.valid? }
|
||||||
|
.to not_change(subject, :note)
|
||||||
|
.and not_change(subject, :display_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -826,22 +784,19 @@ RSpec.describe Account do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'validations' do
|
describe 'Validations' do
|
||||||
it { is_expected.to validate_presence_of(:username) }
|
it { is_expected.to validate_presence_of(:username) }
|
||||||
|
|
||||||
context 'when is local' do
|
context 'when account is local' do
|
||||||
it 'is invalid if the username is not unique in case-insensitive comparison among local accounts' do
|
subject { Fabricate.build :account, domain: nil }
|
||||||
_account = Fabricate(:account, username: 'the_doctor')
|
|
||||||
non_unique_account = Fabricate.build(:account, username: 'the_Doctor')
|
context 'with an existing differently-cased username account' do
|
||||||
non_unique_account.valid?
|
before { Fabricate :account, username: 'the_doctor' }
|
||||||
expect(non_unique_account).to model_have_error_on_field(:username)
|
|
||||||
|
it { is_expected.to_not allow_value('the_Doctor').for(:username) }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is invalid if the username is reserved' do
|
it { is_expected.to_not allow_value('support').for(:username) }
|
||||||
account = Fabricate.build(:account, username: 'support')
|
|
||||||
account.valid?
|
|
||||||
expect(account).to model_have_error_on_field(:username)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is valid when username is reserved but record has already been created' do
|
it 'is valid when username is reserved but record has already been created' do
|
||||||
account = Fabricate.build(:account, username: 'support')
|
account = Fabricate.build(:account, username: 'support')
|
||||||
|
@ -849,9 +804,10 @@ RSpec.describe Account do
|
||||||
expect(account.valid?).to be true
|
expect(account.valid?).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is valid if we are creating an instance actor account with a period' do
|
context 'with the instance actor' do
|
||||||
account = Fabricate.build(:account, id: described_class::INSTANCE_ACTOR_ID, actor_type: 'Application', locked: true, username: 'example.com')
|
subject { Fabricate.build :account, id: described_class::INSTANCE_ACTOR_ID, actor_type: 'Application', locked: true }
|
||||||
expect(account.valid?).to be true
|
|
||||||
|
it { is_expected.to allow_value('example.com').for(:username) }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is valid if we are creating a possibly-conflicting instance actor account' do
|
it 'is valid if we are creating a possibly-conflicting instance actor account' do
|
||||||
|
@ -860,81 +816,31 @@ RSpec.describe Account do
|
||||||
expect(instance_account.valid?).to be true
|
expect(instance_account.valid?).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is invalid if the username doesn\'t only contains letters, numbers and underscores' do
|
it { is_expected.to_not allow_values('the-doctor', 'the.doctor').for(:username) }
|
||||||
account = Fabricate.build(:account, username: 'the-doctor')
|
|
||||||
account.valid?
|
it { is_expected.to validate_length_of(:username).is_at_most(described_class::USERNAME_LENGTH_LIMIT) }
|
||||||
expect(account).to model_have_error_on_field(:username)
|
it { is_expected.to validate_length_of(:display_name).is_at_most(described_class::DISPLAY_NAME_LENGTH_LIMIT) }
|
||||||
|
|
||||||
|
it { is_expected.to_not allow_values(account_note_over_limit).for(:note) }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is invalid if the username contains a period' do
|
context 'when account is remote' do
|
||||||
account = Fabricate.build(:account, username: 'the.doctor')
|
subject { Fabricate.build :account, domain: 'host.example' }
|
||||||
account.valid?
|
|
||||||
expect(account).to model_have_error_on_field(:username)
|
context 'when a normalized domain account exists' do
|
||||||
|
subject { Fabricate.build :account, domain: 'xn--r9j5b5b' }
|
||||||
|
|
||||||
|
before { Fabricate(:account, domain: 'にゃん', username: 'username') }
|
||||||
|
|
||||||
|
it { is_expected.to_not allow_values('username', 'Username').for(:username) }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is invalid if the username is longer than the character limit' do
|
it { is_expected.to allow_values('the-doctor', username_over_limit).for(:username) }
|
||||||
account = Fabricate.build(:account, username: username_over_limit)
|
it { is_expected.to_not allow_values('the doctor').for(:username) }
|
||||||
account.valid?
|
|
||||||
expect(account).to model_have_error_on_field(:username)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is invalid if the display name is longer than the character limit' do
|
it { is_expected.to allow_values(display_name_over_limit).for(:display_name) }
|
||||||
account = Fabricate.build(:account, display_name: display_name_over_limit)
|
|
||||||
account.valid?
|
|
||||||
expect(account).to model_have_error_on_field(:display_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is invalid if the note is longer than the character limit' do
|
it { is_expected.to allow_values(account_note_over_limit).for(:note) }
|
||||||
account = Fabricate.build(:account, note: account_note_over_limit)
|
|
||||||
account.valid?
|
|
||||||
expect(account).to model_have_error_on_field(:note)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when is remote' do
|
|
||||||
it 'is invalid if the username is same among accounts in the same normalized domain' do
|
|
||||||
Fabricate(:account, domain: 'にゃん', username: 'username')
|
|
||||||
account = Fabricate.build(:account, domain: 'xn--r9j5b5b', username: 'username')
|
|
||||||
account.valid?
|
|
||||||
expect(account).to model_have_error_on_field(:username)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is invalid if the username is not unique in case-insensitive comparison among accounts in the same normalized domain' do
|
|
||||||
Fabricate(:account, domain: 'にゃん', username: 'username')
|
|
||||||
account = Fabricate.build(:account, domain: 'xn--r9j5b5b', username: 'Username')
|
|
||||||
account.valid?
|
|
||||||
expect(account).to model_have_error_on_field(:username)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is valid even if the username contains hyphens' do
|
|
||||||
account = Fabricate.build(:account, domain: 'domain', username: 'the-doctor')
|
|
||||||
account.valid?
|
|
||||||
expect(account).to_not model_have_error_on_field(:username)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is invalid if the username doesn\'t only contains letters, numbers, underscores and hyphens' do
|
|
||||||
account = Fabricate.build(:account, domain: 'domain', username: 'the doctor')
|
|
||||||
account.valid?
|
|
||||||
expect(account).to model_have_error_on_field(:username)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is valid even if the username is longer than the character limit' do
|
|
||||||
account = Fabricate.build(:account, domain: 'domain', username: username_over_limit)
|
|
||||||
account.valid?
|
|
||||||
expect(account).to_not model_have_error_on_field(:username)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is valid even if the display name is longer than the character limit' do
|
|
||||||
account = Fabricate.build(:account, domain: 'domain', display_name: display_name_over_limit)
|
|
||||||
account.valid?
|
|
||||||
expect(account).to_not model_have_error_on_field(:display_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is valid even if the note is longer than the character limit' do
|
|
||||||
account = Fabricate.build(:account, domain: 'domain', note: account_note_over_limit)
|
|
||||||
account.valid?
|
|
||||||
expect(account).to_not model_have_error_on_field(:note)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def username_over_limit
|
def username_over_limit
|
||||||
|
@ -1085,14 +991,6 @@ RSpec.describe Account do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'suspended' do
|
|
||||||
it 'returns an array of accounts who are suspended' do
|
|
||||||
suspended_account = Fabricate(:account, suspended: true)
|
|
||||||
_account = Fabricate(:account, suspended: false)
|
|
||||||
expect(described_class.suspended).to contain_exactly(suspended_account)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'searchable' do
|
describe 'searchable' do
|
||||||
let!(:suspended_local) { Fabricate(:account, suspended: true, username: 'suspended_local') }
|
let!(:suspended_local) { Fabricate(:account, suspended: true, username: 'suspended_local') }
|
||||||
let!(:suspended_remote) { Fabricate(:account, suspended: true, domain: 'example.org', username: 'suspended_remote') }
|
let!(:suspended_remote) { Fabricate(:account, suspended: true, domain: 'example.org', username: 'suspended_remote') }
|
||||||
|
|
|
@ -5,13 +5,12 @@ require 'rails_helper'
|
||||||
RSpec.describe AccountStatusesCleanupPolicy do
|
RSpec.describe AccountStatusesCleanupPolicy do
|
||||||
let(:account) { Fabricate(:account, username: 'alice', domain: nil) }
|
let(:account) { Fabricate(:account, username: 'alice', domain: nil) }
|
||||||
|
|
||||||
describe 'validation' do
|
describe 'Validations' do
|
||||||
it 'disallow remote accounts' do
|
subject { Fabricate.build :account_statuses_cleanup_policy }
|
||||||
account.update(domain: 'example.com')
|
|
||||||
account_statuses_cleanup_policy = Fabricate.build(:account_statuses_cleanup_policy, account: account)
|
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
|
||||||
account_statuses_cleanup_policy.valid?
|
|
||||||
expect(account_statuses_cleanup_policy).to model_have_error_on_field(:account)
|
it { is_expected.to_not allow_value(remote_account).for(:account) }
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'save hooks' do
|
describe 'save hooks' do
|
||||||
|
@ -339,14 +338,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when policy is set to keep DMs and reject everything else' do
|
context 'when policy is set to keep DMs and reject everything else' do
|
||||||
before do
|
before { establish_policy(keep_direct: true) }
|
||||||
account_statuses_cleanup_policy.keep_direct = true
|
|
||||||
account_statuses_cleanup_policy.keep_pinned = false
|
|
||||||
account_statuses_cleanup_policy.keep_polls = false
|
|
||||||
account_statuses_cleanup_policy.keep_media = false
|
|
||||||
account_statuses_cleanup_policy.keep_self_fav = false
|
|
||||||
account_statuses_cleanup_policy.keep_self_bookmark = false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns every old status except does not return the old direct message for deletion' do
|
it 'returns every old status except does not return the old direct message for deletion' do
|
||||||
expect(subject.pluck(:id))
|
expect(subject.pluck(:id))
|
||||||
|
@ -356,14 +348,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when policy is set to keep self-bookmarked toots and reject everything else' do
|
context 'when policy is set to keep self-bookmarked toots and reject everything else' do
|
||||||
before do
|
before { establish_policy(keep_self_bookmark: true) }
|
||||||
account_statuses_cleanup_policy.keep_direct = false
|
|
||||||
account_statuses_cleanup_policy.keep_pinned = false
|
|
||||||
account_statuses_cleanup_policy.keep_polls = false
|
|
||||||
account_statuses_cleanup_policy.keep_media = false
|
|
||||||
account_statuses_cleanup_policy.keep_self_fav = false
|
|
||||||
account_statuses_cleanup_policy.keep_self_bookmark = true
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns every old status but does not return the old self-bookmarked message for deletion' do
|
it 'returns every old status but does not return the old self-bookmarked message for deletion' do
|
||||||
expect(subject.pluck(:id))
|
expect(subject.pluck(:id))
|
||||||
|
@ -373,14 +358,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when policy is set to keep self-faved toots and reject everything else' do
|
context 'when policy is set to keep self-faved toots and reject everything else' do
|
||||||
before do
|
before { establish_policy(keep_self_fav: true) }
|
||||||
account_statuses_cleanup_policy.keep_direct = false
|
|
||||||
account_statuses_cleanup_policy.keep_pinned = false
|
|
||||||
account_statuses_cleanup_policy.keep_polls = false
|
|
||||||
account_statuses_cleanup_policy.keep_media = false
|
|
||||||
account_statuses_cleanup_policy.keep_self_fav = true
|
|
||||||
account_statuses_cleanup_policy.keep_self_bookmark = false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns every old status but does not return the old self-faved message for deletion' do
|
it 'returns every old status but does not return the old self-faved message for deletion' do
|
||||||
expect(subject.pluck(:id))
|
expect(subject.pluck(:id))
|
||||||
|
@ -390,14 +368,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when policy is set to keep toots with media and reject everything else' do
|
context 'when policy is set to keep toots with media and reject everything else' do
|
||||||
before do
|
before { establish_policy(keep_media: true) }
|
||||||
account_statuses_cleanup_policy.keep_direct = false
|
|
||||||
account_statuses_cleanup_policy.keep_pinned = false
|
|
||||||
account_statuses_cleanup_policy.keep_polls = false
|
|
||||||
account_statuses_cleanup_policy.keep_media = true
|
|
||||||
account_statuses_cleanup_policy.keep_self_fav = false
|
|
||||||
account_statuses_cleanup_policy.keep_self_bookmark = false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns every old status but does not return the old message with media for deletion' do
|
it 'returns every old status but does not return the old message with media for deletion' do
|
||||||
expect(subject.pluck(:id))
|
expect(subject.pluck(:id))
|
||||||
|
@ -407,14 +378,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when policy is set to keep toots with polls and reject everything else' do
|
context 'when policy is set to keep toots with polls and reject everything else' do
|
||||||
before do
|
before { establish_policy(keep_polls: true) }
|
||||||
account_statuses_cleanup_policy.keep_direct = false
|
|
||||||
account_statuses_cleanup_policy.keep_pinned = false
|
|
||||||
account_statuses_cleanup_policy.keep_polls = true
|
|
||||||
account_statuses_cleanup_policy.keep_media = false
|
|
||||||
account_statuses_cleanup_policy.keep_self_fav = false
|
|
||||||
account_statuses_cleanup_policy.keep_self_bookmark = false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns every old status but does not return the old poll message for deletion' do
|
it 'returns every old status but does not return the old poll message for deletion' do
|
||||||
expect(subject.pluck(:id))
|
expect(subject.pluck(:id))
|
||||||
|
@ -424,14 +388,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when policy is set to keep pinned toots and reject everything else' do
|
context 'when policy is set to keep pinned toots and reject everything else' do
|
||||||
before do
|
before { establish_policy(keep_pinned: true) }
|
||||||
account_statuses_cleanup_policy.keep_direct = false
|
|
||||||
account_statuses_cleanup_policy.keep_pinned = true
|
|
||||||
account_statuses_cleanup_policy.keep_polls = false
|
|
||||||
account_statuses_cleanup_policy.keep_media = false
|
|
||||||
account_statuses_cleanup_policy.keep_self_fav = false
|
|
||||||
account_statuses_cleanup_policy.keep_self_bookmark = false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns every old status but does not return the old pinned message for deletion' do
|
it 'returns every old status but does not return the old pinned message for deletion' do
|
||||||
expect(subject.pluck(:id))
|
expect(subject.pluck(:id))
|
||||||
|
@ -441,14 +398,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when policy is to not keep any special messages' do
|
context 'when policy is to not keep any special messages' do
|
||||||
before do
|
before { establish_policy }
|
||||||
account_statuses_cleanup_policy.keep_direct = false
|
|
||||||
account_statuses_cleanup_policy.keep_pinned = false
|
|
||||||
account_statuses_cleanup_policy.keep_polls = false
|
|
||||||
account_statuses_cleanup_policy.keep_media = false
|
|
||||||
account_statuses_cleanup_policy.keep_self_fav = false
|
|
||||||
account_statuses_cleanup_policy.keep_self_bookmark = false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns every old status but does not return the recent or unrelated statuses' do
|
it 'returns every old status but does not return the recent or unrelated statuses' do
|
||||||
expect(subject.pluck(:id))
|
expect(subject.pluck(:id))
|
||||||
|
@ -459,14 +409,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when policy is set to keep every category of toots' do
|
context 'when policy is set to keep every category of toots' do
|
||||||
before do
|
before { establish_policy(keep_direct: true, keep_pinned: true, keep_polls: true, keep_media: true, keep_self_fav: true, keep_self_bookmark: true) }
|
||||||
account_statuses_cleanup_policy.keep_direct = true
|
|
||||||
account_statuses_cleanup_policy.keep_pinned = true
|
|
||||||
account_statuses_cleanup_policy.keep_polls = true
|
|
||||||
account_statuses_cleanup_policy.keep_media = true
|
|
||||||
account_statuses_cleanup_policy.keep_self_fav = true
|
|
||||||
account_statuses_cleanup_policy.keep_self_bookmark = true
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns normal statuses and does not return unrelated old status' do
|
it 'returns normal statuses and does not return unrelated old status' do
|
||||||
expect(subject.pluck(:id))
|
expect(subject.pluck(:id))
|
||||||
|
@ -502,5 +445,24 @@ RSpec.describe AccountStatusesCleanupPolicy do
|
||||||
.and include(very_old_status.id, faved_primary.id, reblogged_primary.id, reblogged_secondary.id)
|
.and include(very_old_status.id, faved_primary.id, reblogged_primary.id, reblogged_secondary.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def establish_policy(options = {})
|
||||||
|
default_policy_options.merge(options).each do |attribute, value|
|
||||||
|
account_statuses_cleanup_policy.send :"#{attribute}=", value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_policy_options
|
||||||
|
{
|
||||||
|
keep_direct: false,
|
||||||
|
keep_media: false,
|
||||||
|
keep_pinned: false,
|
||||||
|
keep_polls: false,
|
||||||
|
keep_self_bookmark: false,
|
||||||
|
keep_self_fav: false,
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -67,18 +67,30 @@ RSpec.describe Announcement do
|
||||||
it { is_expected.to validate_presence_of(:text) }
|
it { is_expected.to validate_presence_of(:text) }
|
||||||
|
|
||||||
describe 'ends_at' do
|
describe 'ends_at' do
|
||||||
it 'validates presence when starts_at is present' do
|
context 'when starts_at is present' do
|
||||||
record = Fabricate.build(:announcement, starts_at: 1.day.ago)
|
subject { Fabricate.build :announcement, starts_at: 1.day.ago }
|
||||||
|
|
||||||
expect(record).to_not be_valid
|
it { is_expected.to validate_presence_of(:ends_at) }
|
||||||
expect(record.errors[:ends_at]).to be_present
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not validate presence when starts_at is missing' do
|
context 'when starts_at is missing' do
|
||||||
record = Fabricate.build(:announcement, starts_at: nil)
|
subject { Fabricate.build :announcement, starts_at: nil }
|
||||||
|
|
||||||
expect(record).to be_valid
|
it { is_expected.to_not validate_presence_of(:ends_at) }
|
||||||
expect(record.errors[:ends_at]).to_not be_present
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'starts_at' do
|
||||||
|
context 'when ends_at is present' do
|
||||||
|
subject { Fabricate.build :announcement, ends_at: 1.day.ago }
|
||||||
|
|
||||||
|
it { is_expected.to validate_presence_of(:starts_at) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when ends_at is missing' do
|
||||||
|
subject { Fabricate.build :announcement, ends_at: nil }
|
||||||
|
|
||||||
|
it { is_expected.to_not validate_presence_of(:starts_at) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,20 +4,85 @@ require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe Appeal do
|
RSpec.describe Appeal do
|
||||||
describe 'Validations' do
|
describe 'Validations' do
|
||||||
it 'validates text length is under limit' do
|
subject { Fabricate.build :appeal, strike: Fabricate(:account_warning) }
|
||||||
appeal = Fabricate.build(
|
|
||||||
:appeal,
|
|
||||||
strike: Fabricate(:account_warning),
|
|
||||||
text: 'a' * described_class::TEXT_LENGTH_LIMIT * 2
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(appeal).to_not be_valid
|
it { is_expected.to validate_length_of(:text).is_at_most(described_class::TEXT_LENGTH_LIMIT) }
|
||||||
expect(appeal).to model_have_error_on_field(:text)
|
|
||||||
|
context 'with a strike created too long ago' do
|
||||||
|
let(:strike) { Fabricate.build :account_warning, created_at: 100.days.ago }
|
||||||
|
|
||||||
|
it { is_expected.to_not allow_values(strike).for(:strike).against(:base).on(:create) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'scopes' do
|
describe 'Query methods' do
|
||||||
describe 'approved' do
|
describe '#pending?' do
|
||||||
|
subject { Fabricate.build :appeal, approved_at:, rejected_at: }
|
||||||
|
|
||||||
|
context 'with not approved and not rejected' do
|
||||||
|
let(:approved_at) { nil }
|
||||||
|
let(:rejected_at) { nil }
|
||||||
|
|
||||||
|
it { expect(subject).to be_pending }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with approved and rejected' do
|
||||||
|
let(:approved_at) { 1.day.ago }
|
||||||
|
let(:rejected_at) { 1.day.ago }
|
||||||
|
|
||||||
|
it { expect(subject).to_not be_pending }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with approved and not rejected' do
|
||||||
|
let(:approved_at) { 1.day.ago }
|
||||||
|
let(:rejected_at) { nil }
|
||||||
|
|
||||||
|
it { expect(subject).to_not be_pending }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with not approved and rejected' do
|
||||||
|
let(:approved_at) { nil }
|
||||||
|
let(:rejected_at) { 1.day.ago }
|
||||||
|
|
||||||
|
it { expect(subject).to_not be_pending }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#approved?' do
|
||||||
|
subject { Fabricate.build :appeal, approved_at: }
|
||||||
|
|
||||||
|
context 'with not approved' do
|
||||||
|
let(:approved_at) { nil }
|
||||||
|
|
||||||
|
it { expect(subject).to_not be_approved }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with approved' do
|
||||||
|
let(:approved_at) { 1.day.ago }
|
||||||
|
|
||||||
|
it { expect(subject).to be_approved }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#rejected?' do
|
||||||
|
subject { Fabricate.build :appeal, rejected_at: }
|
||||||
|
|
||||||
|
context 'with not rejected' do
|
||||||
|
let(:rejected_at) { nil }
|
||||||
|
|
||||||
|
it { expect(subject).to_not be_rejected }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with rejected' do
|
||||||
|
let(:rejected_at) { 1.day.ago }
|
||||||
|
|
||||||
|
it { expect(subject).to be_rejected }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Scopes' do
|
||||||
|
describe '.approved' do
|
||||||
let(:approved_appeal) { Fabricate(:appeal, approved_at: 10.days.ago) }
|
let(:approved_appeal) { Fabricate(:appeal, approved_at: 10.days.ago) }
|
||||||
let(:not_approved_appeal) { Fabricate(:appeal, approved_at: nil) }
|
let(:not_approved_appeal) { Fabricate(:appeal, approved_at: nil) }
|
||||||
|
|
||||||
|
@ -27,7 +92,7 @@ RSpec.describe Appeal do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'rejected' do
|
describe '.rejected' do
|
||||||
let(:rejected_appeal) { Fabricate(:appeal, rejected_at: 10.days.ago) }
|
let(:rejected_appeal) { Fabricate(:appeal, rejected_at: 10.days.ago) }
|
||||||
let(:not_rejected_appeal) { Fabricate(:appeal, rejected_at: nil) }
|
let(:not_rejected_appeal) { Fabricate(:appeal, rejected_at: nil) }
|
||||||
|
|
||||||
|
@ -37,7 +102,7 @@ RSpec.describe Appeal do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'pending' do
|
describe '.pending' do
|
||||||
let(:approved_appeal) { Fabricate(:appeal, approved_at: 10.days.ago) }
|
let(:approved_appeal) { Fabricate(:appeal, approved_at: 10.days.ago) }
|
||||||
let(:rejected_appeal) { Fabricate(:appeal, rejected_at: 10.days.ago) }
|
let(:rejected_appeal) { Fabricate(:appeal, rejected_at: 10.days.ago) }
|
||||||
let(:pending_appeal) { Fabricate(:appeal, rejected_at: nil, approved_at: nil) }
|
let(:pending_appeal) { Fabricate(:appeal, rejected_at: nil, approved_at: nil) }
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Account::Suspensions do
|
||||||
|
subject { Fabricate(:account) }
|
||||||
|
|
||||||
|
describe '.suspended' do
|
||||||
|
let!(:suspended_account) { Fabricate :account, suspended: true }
|
||||||
|
|
||||||
|
before { Fabricate :account, suspended: false }
|
||||||
|
|
||||||
|
it 'returns accounts that are suspended' do
|
||||||
|
expect(Account.suspended)
|
||||||
|
.to contain_exactly(suspended_account)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#suspended_locally?' do
|
||||||
|
context 'when the account is not suspended' do
|
||||||
|
it { is_expected.to_not be_suspended_locally }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the account is suspended locally' do
|
||||||
|
before { subject.update!(suspended_at: 1.day.ago, suspension_origin: :local) }
|
||||||
|
|
||||||
|
it { is_expected.to be_suspended_locally }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the account is suspended remotely' do
|
||||||
|
before { subject.update!(suspended_at: 1.day.ago, suspension_origin: :remote) }
|
||||||
|
|
||||||
|
it { is_expected.to_not be_suspended_locally }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#suspend!' do
|
||||||
|
it 'marks the account as suspended and creates a deletion request' do
|
||||||
|
expect { subject.suspend! }
|
||||||
|
.to change(subject, :suspended?).from(false).to(true)
|
||||||
|
.and change(subject, :suspended_locally?).from(false).to(true)
|
||||||
|
.and(change { AccountDeletionRequest.exists?(account: subject) }.from(false).to(true))
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the account is of a local user' do
|
||||||
|
subject { local_user_account }
|
||||||
|
|
||||||
|
let!(:local_user_account) { Fabricate(:user, email: 'foo+bar@domain.org').account }
|
||||||
|
|
||||||
|
it 'creates a canonical domain block' do
|
||||||
|
expect { subject.suspend! }
|
||||||
|
.to change { CanonicalEmailBlock.block?(subject.user_email) }.from(false).to(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a canonical domain block already exists for that email' do
|
||||||
|
before { Fabricate(:canonical_email_block, email: subject.user_email) }
|
||||||
|
|
||||||
|
it 'does not raise an error' do
|
||||||
|
expect { subject.suspend! }
|
||||||
|
.to_not raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -6,11 +6,10 @@ RSpec.describe DomainAllow do
|
||||||
describe 'Validations' do
|
describe 'Validations' do
|
||||||
it { is_expected.to validate_presence_of(:domain) }
|
it { is_expected.to validate_presence_of(:domain) }
|
||||||
|
|
||||||
it 'is invalid if the same normalized domain already exists' do
|
context 'when a normalized domain exists' do
|
||||||
_domain_allow = Fabricate(:domain_allow, domain: 'にゃん')
|
before { Fabricate(:domain_allow, domain: 'にゃん') }
|
||||||
domain_allow_with_normalized_value = Fabricate.build(:domain_allow, domain: 'xn--r9j5b5b')
|
|
||||||
domain_allow_with_normalized_value.valid?
|
it { is_expected.to_not allow_value('xn--r9j5b5b').for(:domain) }
|
||||||
expect(domain_allow_with_normalized_value).to model_have_error_on_field(:domain)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,27 +3,26 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe Follow do
|
RSpec.describe Follow do
|
||||||
let(:alice) { Fabricate(:account, username: 'alice') }
|
describe 'Associations' do
|
||||||
let(:bob) { Fabricate(:account, username: 'bob') }
|
|
||||||
|
|
||||||
describe 'validations' do
|
|
||||||
subject { described_class.new(account: alice, target_account: bob, rate_limit: true) }
|
|
||||||
|
|
||||||
it { is_expected.to belong_to(:account).required }
|
it { is_expected.to belong_to(:account).required }
|
||||||
it { is_expected.to belong_to(:target_account).required }
|
it { is_expected.to belong_to(:target_account).required }
|
||||||
|
|
||||||
it 'is invalid if account already follows too many people' do
|
|
||||||
alice.update(following_count: FollowLimitValidator::LIMIT)
|
|
||||||
|
|
||||||
expect(subject).to_not be_valid
|
|
||||||
expect(subject).to model_have_error_on_field(:base)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is valid if account is only on the brink of following too many people' do
|
describe 'Validations' do
|
||||||
alice.update(following_count: FollowLimitValidator::LIMIT - 1)
|
subject { Fabricate.build :follow, rate_limit: true }
|
||||||
|
|
||||||
expect(subject).to be_valid
|
let(:account) { Fabricate(:account) }
|
||||||
expect(subject).to_not model_have_error_on_field(:base)
|
|
||||||
|
context 'when account follows too many people' do
|
||||||
|
before { account.update(following_count: FollowLimitValidator::LIMIT) }
|
||||||
|
|
||||||
|
it { is_expected.to_not allow_value(account).for(:account).against(:base) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when account is on brink of following too many people' do
|
||||||
|
before { account.update(following_count: FollowLimitValidator::LIMIT - 1) }
|
||||||
|
|
||||||
|
it { is_expected.to allow_value(account).for(:account).against(:base) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -54,4 +53,58 @@ RSpec.describe Follow do
|
||||||
expect(account.requested?(target_account)).to be true
|
expect(account.requested?(target_account)).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#local?' do
|
||||||
|
it { is_expected.to_not be_local }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Callbacks' do
|
||||||
|
describe 'Setting a URI' do
|
||||||
|
context 'when URI exists' do
|
||||||
|
subject { Fabricate.build :follow, uri: 'https://uri/value' }
|
||||||
|
|
||||||
|
it 'does not change' do
|
||||||
|
expect { subject.save }
|
||||||
|
.to not_change(subject, :uri)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when URI is blank' do
|
||||||
|
subject { Fabricate.build :follow, uri: nil }
|
||||||
|
|
||||||
|
it 'populates the value' do
|
||||||
|
expect { subject.save }
|
||||||
|
.to change(subject, :uri).to(be_present)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Maintaining counters' do
|
||||||
|
subject { Fabricate.build :follow, account:, target_account: }
|
||||||
|
|
||||||
|
let(:account) { Fabricate :account }
|
||||||
|
let(:target_account) { Fabricate :account }
|
||||||
|
|
||||||
|
before do
|
||||||
|
account.account_stat.update following_count: 123
|
||||||
|
target_account.account_stat.update followers_count: 123
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'saving the follow' do
|
||||||
|
it 'increments counters' do
|
||||||
|
expect { subject.save }
|
||||||
|
.to change(account, :following_count).by(1)
|
||||||
|
.and(change(target_account, :followers_count).by(1))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'destroying the follow' do
|
||||||
|
it 'decrements counters' do
|
||||||
|
expect { subject.destroy }
|
||||||
|
.to change(account, :following_count).by(-1)
|
||||||
|
.and(change(target_account, :followers_count).by(-1))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,33 +3,17 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe Form::AdminSettings do
|
RSpec.describe Form::AdminSettings do
|
||||||
describe 'validations' do
|
describe 'Validations' do
|
||||||
describe 'site_contact_username' do
|
describe 'site_contact_username' do
|
||||||
context 'with no accounts' do
|
context 'with no accounts' do
|
||||||
it 'is not valid' do
|
it { is_expected.to_not allow_value('Test').for(:site_contact_username) }
|
||||||
setting = described_class.new(site_contact_username: 'Test')
|
|
||||||
setting.valid?
|
|
||||||
|
|
||||||
expect(setting).to model_have_error_on_field(:site_contact_username)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with an account' do
|
context 'with an account' do
|
||||||
before { Fabricate(:account, username: 'Glorp') }
|
before { Fabricate(:account, username: 'Glorp') }
|
||||||
|
|
||||||
it 'is not valid when account doesnt match' do
|
it { is_expected.to_not allow_value('Test').for(:site_contact_username) }
|
||||||
setting = described_class.new(site_contact_username: 'Test')
|
it { is_expected.to allow_value('Glorp').for(:site_contact_username) }
|
||||||
setting.valid?
|
|
||||||
|
|
||||||
expect(setting).to model_have_error_on_field(:site_contact_username)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is valid when account matches' do
|
|
||||||
setting = described_class.new(site_contact_username: 'Glorp')
|
|
||||||
setting.valid?
|
|
||||||
|
|
||||||
expect(setting).to_not model_have_error_on_field(:site_contact_username)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,18 +3,13 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe IpBlock do
|
RSpec.describe IpBlock do
|
||||||
describe 'validations' do
|
describe 'Validations' do
|
||||||
|
subject { Fabricate.build :ip_block }
|
||||||
|
|
||||||
it { is_expected.to validate_presence_of(:ip) }
|
it { is_expected.to validate_presence_of(:ip) }
|
||||||
it { is_expected.to validate_presence_of(:severity) }
|
it { is_expected.to validate_presence_of(:severity) }
|
||||||
|
|
||||||
it 'validates ip uniqueness', :aggregate_failures do
|
it { is_expected.to validate_uniqueness_of(:ip) }
|
||||||
described_class.create!(ip: '127.0.0.1', severity: :no_access)
|
|
||||||
|
|
||||||
ip_block = described_class.new(ip: '127.0.0.1', severity: :no_access)
|
|
||||||
|
|
||||||
expect(ip_block).to_not be_valid
|
|
||||||
expect(ip_block).to model_have_error_on_field(:ip)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#to_log_human_identifier' do
|
describe '#to_log_human_identifier' do
|
||||||
|
|
|
@ -9,26 +9,10 @@ RSpec.describe PreviewCard do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'validations' do
|
describe 'Validations' do
|
||||||
describe 'urls' do
|
describe 'url' do
|
||||||
it 'allows http schemes' do
|
it { is_expected.to allow_values('http://example.host/path', 'https://example.host/path').for(:url) }
|
||||||
record = described_class.new(url: 'http://example.host/path')
|
it { is_expected.to_not allow_value('javascript:alert()').for(:url) }
|
||||||
|
|
||||||
expect(record).to be_valid
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows https schemes' do
|
|
||||||
record = described_class.new(url: 'https://example.host/path')
|
|
||||||
|
|
||||||
expect(record).to be_valid
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not allow javascript: schemes' do
|
|
||||||
record = described_class.new(url: 'javascript:alert()')
|
|
||||||
|
|
||||||
expect(record).to_not be_valid
|
|
||||||
expect(record).to model_have_error_on_field(:url)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe ReportNote do
|
RSpec.describe ReportNote do
|
||||||
describe 'chronological scope' do
|
describe 'Scopes' do
|
||||||
|
describe '.chronological' do
|
||||||
it 'returns report notes oldest to newest' do
|
it 'returns report notes oldest to newest' do
|
||||||
report = Fabricate(:report)
|
report = Fabricate(:report)
|
||||||
note1 = Fabricate(:report_note, report: report)
|
note1 = Fabricate(:report_note, report: report)
|
||||||
|
@ -12,20 +13,14 @@ RSpec.describe ReportNote do
|
||||||
expect(report.notes.chronological).to eq [note1, note2]
|
expect(report.notes.chronological).to eq [note1, note2]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'validations' do
|
|
||||||
it 'is invalid if the content is empty' do
|
|
||||||
report = Fabricate.build(:report_note, content: '')
|
|
||||||
expect(report.valid?).to be false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is invalid if content is longer than character limit' do
|
describe 'Validations' do
|
||||||
report = Fabricate.build(:report_note, content: comment_over_limit)
|
subject { Fabricate.build :report_note }
|
||||||
expect(report.valid?).to be false
|
|
||||||
end
|
|
||||||
|
|
||||||
def comment_over_limit
|
describe 'content' do
|
||||||
Faker::Lorem.paragraph_by_chars(number: described_class::CONTENT_SIZE_LIMIT * 2)
|
it { is_expected.to_not allow_value('').for(:content) }
|
||||||
|
it { is_expected.to validate_length_of(:content).is_at_most(described_class::CONTENT_SIZE_LIMIT) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,53 +3,17 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe WebauthnCredential do
|
RSpec.describe WebauthnCredential do
|
||||||
describe 'validations' do
|
describe 'Validations' do
|
||||||
|
subject { Fabricate.build :webauthn_credential }
|
||||||
|
|
||||||
it { is_expected.to validate_presence_of(:external_id) }
|
it { is_expected.to validate_presence_of(:external_id) }
|
||||||
it { is_expected.to validate_presence_of(:public_key) }
|
it { is_expected.to validate_presence_of(:public_key) }
|
||||||
it { is_expected.to validate_presence_of(:nickname) }
|
it { is_expected.to validate_presence_of(:nickname) }
|
||||||
it { is_expected.to validate_presence_of(:sign_count) }
|
it { is_expected.to validate_presence_of(:sign_count) }
|
||||||
|
|
||||||
it 'is invalid if already exist a webauthn credential with the same external id' do
|
it { is_expected.to validate_uniqueness_of(:external_id) }
|
||||||
Fabricate(:webauthn_credential, external_id: '_Typ0ygudDnk9YUVWLQayw')
|
it { is_expected.to validate_uniqueness_of(:nickname).scoped_to(:user_id) }
|
||||||
new_webauthn_credential = Fabricate.build(:webauthn_credential, external_id: '_Typ0ygudDnk9YUVWLQayw')
|
|
||||||
|
|
||||||
new_webauthn_credential.valid?
|
it { is_expected.to validate_numericality_of(:sign_count).only_integer.is_greater_than_or_equal_to(0).is_less_than_or_equal_to(described_class::SIGN_COUNT_LIMIT - 1) }
|
||||||
|
|
||||||
expect(new_webauthn_credential).to model_have_error_on_field(:external_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is invalid if user already registered a webauthn credential with the same nickname' do
|
|
||||||
user = Fabricate(:user)
|
|
||||||
Fabricate(:webauthn_credential, user_id: user.id, nickname: 'USB Key')
|
|
||||||
new_webauthn_credential = Fabricate.build(:webauthn_credential, user_id: user.id, nickname: 'USB Key')
|
|
||||||
|
|
||||||
new_webauthn_credential.valid?
|
|
||||||
|
|
||||||
expect(new_webauthn_credential).to model_have_error_on_field(:nickname)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is invalid if sign_count is not a number' do
|
|
||||||
webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: 'invalid sign_count')
|
|
||||||
|
|
||||||
webauthn_credential.valid?
|
|
||||||
|
|
||||||
expect(webauthn_credential).to model_have_error_on_field(:sign_count)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is invalid if sign_count is negative number' do
|
|
||||||
webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: -1)
|
|
||||||
|
|
||||||
webauthn_credential.valid?
|
|
||||||
|
|
||||||
expect(webauthn_credential).to model_have_error_on_field(:sign_count)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is invalid if sign_count is greater than the limit' do
|
|
||||||
webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: (described_class::SIGN_COUNT_LIMIT * 2))
|
|
||||||
|
|
||||||
webauthn_credential.valid?
|
|
||||||
|
|
||||||
expect(webauthn_credential).to model_have_error_on_field(:sign_count)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'ActivityPub Likes' do
|
||||||
|
let(:account) { Fabricate(:account) }
|
||||||
|
let(:status) { Fabricate :status, account: account }
|
||||||
|
|
||||||
|
before { Fabricate :favourite, status: status }
|
||||||
|
|
||||||
|
describe 'GET /accounts/:account_username/statuses/:status_id/likes' do
|
||||||
|
it 'returns http success and activity json types and correct items count' do
|
||||||
|
get account_status_likes_path(account, status)
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(200)
|
||||||
|
expect(response.media_type)
|
||||||
|
.to eq 'application/activity+json'
|
||||||
|
|
||||||
|
expect(response.parsed_body)
|
||||||
|
.to include(type: 'Collection')
|
||||||
|
.and include(totalItems: 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'ActivityPub Shares' do
|
||||||
|
let(:account) { Fabricate(:account) }
|
||||||
|
let(:status) { Fabricate :status, account: account }
|
||||||
|
|
||||||
|
before { Fabricate :status, reblog: status }
|
||||||
|
|
||||||
|
describe 'GET /accounts/:account_username/statuses/:status_id/shares' do
|
||||||
|
it 'returns http success and activity json types and correct items count' do
|
||||||
|
get account_status_shares_path(account, status)
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(200)
|
||||||
|
expect(response.media_type)
|
||||||
|
.to eq 'application/activity+json'
|
||||||
|
|
||||||
|
expect(response.parsed_body)
|
||||||
|
.to include(type: 'Collection')
|
||||||
|
.and include(totalItems: 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,36 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Domain Blocks Previews API' do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
|
||||||
|
let(:scopes) { 'write:blocks' }
|
||||||
|
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
|
||||||
|
let(:account) { Fabricate(:account, user: user) }
|
||||||
|
|
||||||
|
describe 'GET /api/v1/domain_blocks/preview' do
|
||||||
|
subject { get '/api/v1/domain_blocks/preview', params: { domain: domain }, headers: headers }
|
||||||
|
|
||||||
|
let(:domain) { 'host.example' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate :follow, account: account, target_account: Fabricate(:account, domain: domain)
|
||||||
|
Fabricate :follow, target_account: account, account: Fabricate(:account, domain: domain)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
|
||||||
|
|
||||||
|
it 'returns http success and follower counts' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(200)
|
||||||
|
expect(response.content_type)
|
||||||
|
.to start_with('application/json')
|
||||||
|
expect(response.parsed_body)
|
||||||
|
.to include(followers_count: 1)
|
||||||
|
.and include(following_count: 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,55 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'API Web Push Subscriptions' do
|
||||||
|
describe 'DELETE /api/web/push_subscriptions/:id' do
|
||||||
|
subject { delete api_web_push_subscription_path(token) }
|
||||||
|
|
||||||
|
context 'when the subscription exists' do
|
||||||
|
let!(:web_push_subscription) do
|
||||||
|
Fabricate(:web_push_subscription)
|
||||||
|
end
|
||||||
|
let(:token) do
|
||||||
|
web_push_subscription.generate_token_for(:unsubscribe)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deletes the subscription' do
|
||||||
|
expect { subject }
|
||||||
|
.to change(Web::PushSubscription, :count).by(-1)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the subscription does not exist' do
|
||||||
|
let(:web_push_subscription) do
|
||||||
|
Fabricate(:web_push_subscription)
|
||||||
|
end
|
||||||
|
let(:token) do
|
||||||
|
web_push_subscription.generate_token_for(:unsubscribe)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
token # memoize before destroying the record
|
||||||
|
web_push_subscription.destroy!
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does nothing' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the token is invalid' do
|
||||||
|
let(:token) { 'invalid--invalid' }
|
||||||
|
|
||||||
|
it 'does nothing' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ProfileStories
|
module ProfileStories
|
||||||
attr_reader :bob, :alice, :alice_bio
|
attr_reader :bob
|
||||||
|
|
||||||
def fill_in_auth_details(email, password)
|
def fill_in_auth_details(email, password)
|
||||||
fill_in 'user_email', with: email
|
fill_in 'user_email', with: email
|
||||||
|
@ -31,18 +31,6 @@ module ProfileStories
|
||||||
bob.update!(role: UserRole.find_by!(name: 'Admin'))
|
bob.update!(role: UserRole.find_by!(name: 'Admin'))
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_alice_as_local_user
|
|
||||||
@alice_bio = '@alice and @bob are fictional characters commonly used as' \
|
|
||||||
'placeholder names in #cryptology, as well as #science and' \
|
|
||||||
'engineering 📖 literature. Not affiliated with @pepe.'
|
|
||||||
|
|
||||||
@alice = Fabricate(
|
|
||||||
:user,
|
|
||||||
email: 'alice@example.com', password: password, confirmed_at: confirmed_at,
|
|
||||||
account: Fabricate(:account, username: 'alice', note: @alice_bio)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def confirmed_at
|
def confirmed_at
|
||||||
@confirmed_at ||= Time.zone.now
|
@confirmed_at ||= Time.zone.now
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,10 +11,10 @@ RSpec.describe 'Profile' do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
as_a_logged_in_user
|
as_a_logged_in_user
|
||||||
with_alice_as_local_user
|
Fabricate(:user, account: Fabricate(:account, username: 'alice'))
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'I can view Annes public account' do
|
it 'I can view public account page for Alice' do
|
||||||
visit account_path('alice')
|
visit account_path('alice')
|
||||||
|
|
||||||
expect(subject).to have_title("alice (@alice@#{local_domain})")
|
expect(subject).to have_title("alice (@alice@#{local_domain})")
|
||||||
|
|
|
@ -7,9 +7,7 @@ RSpec.describe AccountRefreshWorker do
|
||||||
let(:service) { instance_double(ResolveAccountService, call: true) }
|
let(:service) { instance_double(ResolveAccountService, call: true) }
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
before do
|
before { stub_service }
|
||||||
allow(ResolveAccountService).to receive(:new).and_return(service)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when account does not exist' do
|
context 'when account does not exist' do
|
||||||
it 'returns immediately without processing' do
|
it 'returns immediately without processing' do
|
||||||
|
@ -48,5 +46,11 @@ RSpec.describe AccountRefreshWorker do
|
||||||
(Account::BACKGROUND_REFRESH_INTERVAL + 3.days).ago
|
(Account::BACKGROUND_REFRESH_INTERVAL + 3.days).ago
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def stub_service
|
||||||
|
allow(ResolveAccountService)
|
||||||
|
.to receive(:new)
|
||||||
|
.and_return(service)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe ActivityPub::FollowersSynchronizationWorker do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
let(:service) { instance_double(ActivityPub::SynchronizeFollowersService, call: true) }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before { stub_service }
|
||||||
|
|
||||||
|
let(:account) { Fabricate(:account, domain: 'host.example') }
|
||||||
|
let(:url) { 'https://sync.url' }
|
||||||
|
|
||||||
|
it 'sends the status to the service' do
|
||||||
|
worker.perform(account.id, url)
|
||||||
|
|
||||||
|
expect(service).to have_received(:call).with(account, url)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nil for non-existent record' do
|
||||||
|
result = worker.perform(123_123_123, url)
|
||||||
|
|
||||||
|
expect(result).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def stub_service
|
||||||
|
allow(ActivityPub::SynchronizeFollowersService)
|
||||||
|
.to receive(:new)
|
||||||
|
.and_return(service)
|
||||||
|
end
|
||||||
|
end
|
|
@ -6,8 +6,30 @@ RSpec.describe PushConversationWorker do
|
||||||
let(:worker) { described_class.new }
|
let(:worker) { described_class.new }
|
||||||
|
|
||||||
describe 'perform' do
|
describe 'perform' do
|
||||||
it 'runs without error for missing record' do
|
context 'with missing values' do
|
||||||
expect { worker.perform(nil) }.to_not raise_error
|
it 'runs without error' do
|
||||||
|
expect { worker.perform(nil) }
|
||||||
|
.to_not raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with valid records' do
|
||||||
|
let(:account_conversation) { Fabricate :account_conversation }
|
||||||
|
|
||||||
|
before { allow(redis).to receive(:publish) }
|
||||||
|
|
||||||
|
it 'pushes message to timeline' do
|
||||||
|
expect { worker.perform(account_conversation.id) }
|
||||||
|
.to_not raise_error
|
||||||
|
|
||||||
|
expect(redis)
|
||||||
|
.to have_received(:publish)
|
||||||
|
.with(redis_key, anything)
|
||||||
|
end
|
||||||
|
|
||||||
|
def redis_key
|
||||||
|
"timeline:direct:#{account_conversation.account_id}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,11 +6,31 @@ RSpec.describe PushUpdateWorker do
|
||||||
let(:worker) { described_class.new }
|
let(:worker) { described_class.new }
|
||||||
|
|
||||||
describe 'perform' do
|
describe 'perform' do
|
||||||
it 'runs without error for missing record' do
|
context 'with missing values' do
|
||||||
account_id = nil
|
it 'runs without error' do
|
||||||
status_id = nil
|
expect { worker.perform(nil, nil) }
|
||||||
|
.to_not raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
expect { worker.perform(account_id, status_id) }.to_not raise_error
|
context 'with valid records' do
|
||||||
|
let(:account) { Fabricate :account }
|
||||||
|
let(:status) { Fabricate :status }
|
||||||
|
|
||||||
|
before { allow(redis).to receive(:publish) }
|
||||||
|
|
||||||
|
it 'pushes message to timeline' do
|
||||||
|
expect { worker.perform(account.id, status.id) }
|
||||||
|
.to_not raise_error
|
||||||
|
|
||||||
|
expect(redis)
|
||||||
|
.to have_received(:publish)
|
||||||
|
.with(redis_key, anything)
|
||||||
|
end
|
||||||
|
|
||||||
|
def redis_key
|
||||||
|
"timeline:#{account.id}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe RemoteAccountRefreshWorker do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
let(:service) { instance_double(ActivityPub::FetchRemoteAccountService, call: true) }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before { stub_service }
|
||||||
|
|
||||||
|
let(:account) { Fabricate(:account, domain: 'host.example') }
|
||||||
|
|
||||||
|
it 'sends the status to the service' do
|
||||||
|
worker.perform(account.id)
|
||||||
|
|
||||||
|
expect(service).to have_received(:call).with(account.uri)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nil for non-existent record' do
|
||||||
|
result = worker.perform(123_123_123)
|
||||||
|
|
||||||
|
expect(result).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nil for a local record' do
|
||||||
|
account = Fabricate :account, domain: nil
|
||||||
|
result = worker.perform(account)
|
||||||
|
expect(result).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def stub_service
|
||||||
|
allow(ActivityPub::FetchRemoteAccountService)
|
||||||
|
.to receive(:new)
|
||||||
|
.and_return(service)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,12 +4,35 @@ require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe RemoveFeaturedTagWorker do
|
RSpec.describe RemoveFeaturedTagWorker do
|
||||||
let(:worker) { described_class.new }
|
let(:worker) { described_class.new }
|
||||||
|
let(:service) { instance_double(RemoveFeaturedTagService, call: true) }
|
||||||
|
|
||||||
describe 'perform' do
|
describe 'perform' do
|
||||||
it 'runs without error for missing record' do
|
context 'with missing values' do
|
||||||
account_id = nil
|
it 'runs without error' do
|
||||||
featured_tag_id = nil
|
expect { worker.perform(nil, nil) }
|
||||||
expect { worker.perform(account_id, featured_tag_id) }.to_not raise_error
|
.to_not raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with real records' do
|
||||||
|
before { stub_service }
|
||||||
|
|
||||||
|
let(:account) { Fabricate :account }
|
||||||
|
let(:featured_tag) { Fabricate :featured_tag }
|
||||||
|
|
||||||
|
it 'calls the service for processing' do
|
||||||
|
worker.perform(account.id, featured_tag.id)
|
||||||
|
|
||||||
|
expect(service)
|
||||||
|
.to have_received(:call)
|
||||||
|
.with(be_an(Account), be_an(FeaturedTag))
|
||||||
|
end
|
||||||
|
|
||||||
|
def stub_service
|
||||||
|
allow(RemoveFeaturedTagService)
|
||||||
|
.to receive(:new)
|
||||||
|
.and_return(service)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,10 +4,34 @@ require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe ResolveAccountWorker do
|
RSpec.describe ResolveAccountWorker do
|
||||||
let(:worker) { described_class.new }
|
let(:worker) { described_class.new }
|
||||||
|
let(:service) { instance_double(ResolveAccountService, call: true) }
|
||||||
|
|
||||||
describe 'perform' do
|
describe 'perform' do
|
||||||
it 'runs without error for missing record' do
|
context 'with missing values' do
|
||||||
expect { worker.perform(nil) }.to_not raise_error
|
it 'runs without error' do
|
||||||
|
expect { worker.perform(nil) }
|
||||||
|
.to_not raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a URI' do
|
||||||
|
before { stub_service }
|
||||||
|
|
||||||
|
let(:uri) { 'https://host/path/value' }
|
||||||
|
|
||||||
|
it 'initiates account resolution' do
|
||||||
|
worker.perform(uri)
|
||||||
|
|
||||||
|
expect(service)
|
||||||
|
.to have_received(:call)
|
||||||
|
.with(uri)
|
||||||
|
end
|
||||||
|
|
||||||
|
def stub_service
|
||||||
|
allow(ResolveAccountService)
|
||||||
|
.to receive(:new)
|
||||||
|
.and_return(service)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -61,6 +61,7 @@ RSpec.describe Web::PushNotificationWorker do
|
||||||
'Ttl' => '172800',
|
'Ttl' => '172800',
|
||||||
'Urgency' => 'normal',
|
'Urgency' => 'normal',
|
||||||
'Authorization' => 'WebPush jwt.encoded.payload',
|
'Authorization' => 'WebPush jwt.encoded.payload',
|
||||||
|
'Unsubscribe-URL' => %r{/api/web/push_subscriptions/},
|
||||||
},
|
},
|
||||||
body: "+\xB8\xDBT}\u0013\xB6\xDD.\xF9\xB0\xA7\xC8Ҁ\xFD\x99#\xF7\xAC\x83\xA4\xDB,\u001F\xB5\xB9w\x85>\xF7\xADr"
|
body: "+\xB8\xDBT}\u0013\xB6\xDD.\xF9\xB0\xA7\xC8Ҁ\xFD\x99#\xF7\xAC\x83\xA4\xDB,\u001F\xB5\xB9w\x85>\xF7\xADr"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@mastodon/streaming",
|
"name": "@mastodon/streaming",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"packageManager": "yarn@4.5.0",
|
"packageManager": "yarn@4.5.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue