mirror of
https://github.com/glitch-soc/mastodon.git
synced 2024-11-27 02:24:03 -05:00
Merge pull request #2889 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 77cd16f4ee
This commit is contained in:
commit
eb68d81013
2
.github/workflows/check-i18n.yml
vendored
2
.github/workflows/check-i18n.yml
vendored
@ -18,7 +18,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
check-i18n:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- 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"]
|
||||
# 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"]
|
||||
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 'link_header', '~> 0.0'
|
||||
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 'oj', '~> 3.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-net_http', '~> 0.22.4', require: false
|
||||
gem 'opentelemetry-instrumentation-pg', '~> 0.29.0', require: false
|
||||
gem 'opentelemetry-instrumentation-rack', '~> 0.24.1', require: false
|
||||
gem 'opentelemetry-instrumentation-rails', '~> 0.31.0', require: false
|
||||
gem 'opentelemetry-instrumentation-rack', '~> 0.25.0', require: false
|
||||
gem 'opentelemetry-instrumentation-rails', '~> 0.32.0', require: false
|
||||
gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false
|
||||
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', 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)
|
||||
awrence (1.2.1)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.983.0)
|
||||
aws-sdk-core (3.209.1)
|
||||
aws-partitions (1.992.0)
|
||||
aws-sdk-core (3.210.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.94.0)
|
||||
aws-sdk-core (~> 3, >= 3.207.0)
|
||||
aws-sdk-kms (1.95.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.167.0)
|
||||
aws-sdk-core (~> 3, >= 3.207.0)
|
||||
aws-sdk-s3 (1.169.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.10.0)
|
||||
@ -137,7 +137,7 @@ GEM
|
||||
blurhash (0.1.8)
|
||||
bootsnap (1.18.4)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (6.2.1)
|
||||
brakeman (6.2.2)
|
||||
racc
|
||||
browser (5.3.1)
|
||||
brpoplpush-redis_script (0.1.3)
|
||||
@ -233,7 +233,7 @@ GEM
|
||||
tzinfo
|
||||
excon (0.111.0)
|
||||
fabrication (2.31.0)
|
||||
faker (3.4.2)
|
||||
faker (3.5.1)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (1.10.3)
|
||||
faraday-em_http (~> 1.0)
|
||||
@ -429,9 +429,10 @@ GEM
|
||||
azure-storage-blob (~> 2.0.1)
|
||||
hashie (~> 5.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.2024.0820)
|
||||
mime-types-data (3.2024.1001)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.7)
|
||||
minitest (5.25.1)
|
||||
@ -503,7 +504,7 @@ GEM
|
||||
opentelemetry-semantic_conventions
|
||||
opentelemetry-helpers-sql-obfuscation (0.2.0)
|
||||
opentelemetry-common (~> 0.21)
|
||||
opentelemetry-instrumentation-action_mailer (0.1.0)
|
||||
opentelemetry-instrumentation-action_mailer (0.2.0)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-active_support (~> 0.1)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
@ -515,13 +516,13 @@ GEM
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-active_support (~> 0.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-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-active_model_serializers (0.20.2)
|
||||
opentelemetry-api (~> 1.0)
|
||||
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-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-active_support (0.6.0)
|
||||
@ -553,16 +554,16 @@ GEM
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-helpers-sql-obfuscation
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-rack (0.24.6)
|
||||
opentelemetry-instrumentation-rack (0.25.0)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-rails (0.31.2)
|
||||
opentelemetry-instrumentation-rails (0.32.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_view (~> 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-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-redis (0.25.7)
|
||||
@ -590,7 +591,7 @@ GEM
|
||||
parslet (2.0.0)
|
||||
pastel (0.8.0)
|
||||
tty-color (~> 0.5)
|
||||
pg (1.5.8)
|
||||
pg (1.5.9)
|
||||
pghero (3.6.1)
|
||||
activerecord (>= 6.1)
|
||||
premailer (1.27.0)
|
||||
@ -761,7 +762,7 @@ GEM
|
||||
rubocop-rspec_rails (2.30.0)
|
||||
rubocop (~> 1.61)
|
||||
rubocop-rspec (~> 3, >= 3.0.1)
|
||||
ruby-prof (1.7.0)
|
||||
ruby-prof (1.7.1)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-saml (1.17.0)
|
||||
nokogiri (>= 1.13.10)
|
||||
@ -970,7 +971,7 @@ DEPENDENCIES
|
||||
mario-redis-lock (~> 1.2)
|
||||
md-paperclip-azure (~> 2.2)
|
||||
memory_profiler
|
||||
mime-types (~> 3.5.0)
|
||||
mime-types (~> 3.6.0)
|
||||
net-http (~> 0.4.0)
|
||||
net-ldap (~> 0.18)
|
||||
nokogiri (~> 1.15)
|
||||
@ -991,8 +992,8 @@ DEPENDENCIES
|
||||
opentelemetry-instrumentation-http_client (~> 0.22.3)
|
||||
opentelemetry-instrumentation-net_http (~> 0.22.4)
|
||||
opentelemetry-instrumentation-pg (~> 0.29.0)
|
||||
opentelemetry-instrumentation-rack (~> 0.24.1)
|
||||
opentelemetry-instrumentation-rails (~> 0.31.0)
|
||||
opentelemetry-instrumentation-rack (~> 0.25.0)
|
||||
opentelemetry-instrumentation-rails (~> 0.32.0)
|
||||
opentelemetry-instrumentation-redis (~> 0.25.3)
|
||||
opentelemetry-instrumentation-sidekiq (~> 0.25.2)
|
||||
opentelemetry-sdk (~> 1.4)
|
||||
@ -1057,7 +1058,7 @@ DEPENDENCIES
|
||||
xorcist (~> 1.1)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.3.4p94
|
||||
ruby 3.3.5p100
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.18
|
||||
2.5.22
|
||||
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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 :destroy_previous_subscriptions, only: :create, if: :prior_subscriptions?
|
||||
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
|
||||
end
|
||||
|
||||
def destroy
|
||||
push_subscription = ::Web::PushSubscription.find_by_token_for(:unsubscribe, params[:id])
|
||||
push_subscription&.destroy
|
||||
|
||||
head 200
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def active_session
|
||||
|
@ -10,7 +10,7 @@ module Auth::CaptchaConcern
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def captcha_enabled?
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
module Admin::SettingsHelper
|
||||
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
|
||||
|
||||
def login_activity_title(activity)
|
||||
|
@ -120,18 +120,6 @@ module ApplicationHelper
|
||||
inline_svg_tag 'check.svg'
|
||||
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)
|
||||
if relationships.following[account_id] && relationships.followed_by[account_id]
|
||||
material_symbol('sync_alt', title: I18n.t('relationships.mutual'), class: 'active passive')
|
||||
|
@ -162,7 +162,7 @@ module LanguagesHelper
|
||||
th: ['Thai', 'ไทย'].freeze,
|
||||
ti: ['Tigrinya', 'ትግርኛ'].freeze,
|
||||
tk: ['Turkmen', 'Türkmen'].freeze,
|
||||
tl: ['Tagalog', 'Wikang Tagalog'].freeze,
|
||||
tl: ['Tagalog', 'Tagalog'].freeze,
|
||||
tn: ['Tswana', 'Setswana'].freeze,
|
||||
to: ['Tonga', 'faka Tonga'].freeze,
|
||||
tr: ['Turkish', 'Türkçe'].freeze,
|
||||
|
@ -327,31 +327,24 @@ Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
|
||||
|
||||
if (!input) return;
|
||||
|
||||
const oldReadOnly = input.readOnly;
|
||||
|
||||
input.readOnly = false;
|
||||
input.focus();
|
||||
input.select();
|
||||
input.setSelectionRange(0, input.value.length);
|
||||
|
||||
try {
|
||||
if (document.execCommand('copy')) {
|
||||
input.blur();
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(input.value)
|
||||
.then(() => {
|
||||
const parent = target.parentElement;
|
||||
|
||||
if (!parent) return;
|
||||
if (parent) {
|
||||
parent.classList.add('copied');
|
||||
|
||||
setTimeout(() => {
|
||||
parent.classList.remove('copied');
|
||||
}, 700);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
input.readOnly = oldReadOnly;
|
||||
return true;
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
|
||||
const toggleSidebar = () => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import type { PropsWithChildren, JSX } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
@ -17,9 +17,15 @@ export const ContentWarning: React.FC<{
|
||||
aria-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}
|
||||
</button>
|
||||
|
@ -10,13 +10,16 @@ export const FilterWarning: React.FC<{
|
||||
<StatusBanner
|
||||
expanded={expanded}
|
||||
onClick={onClick}
|
||||
variant={BannerVariant.Blue}
|
||||
variant={BannerVariant.Filter}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='filter_warning.matches_filter'
|
||||
defaultMessage='Matches filter “{title}”'
|
||||
values={{ title }}
|
||||
defaultMessage='Matches filter “<span>{title}</span>”'
|
||||
values={{
|
||||
title,
|
||||
span: (chunks) => <span className='filter-name'>{chunks}</span>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</StatusBanner>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { memo } from 'react';
|
||||
import type { JSX } from 'react';
|
||||
|
||||
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||
|
||||
|
@ -648,7 +648,7 @@ class Status extends ImmutablePureComponent {
|
||||
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(
|
||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
||||
{Component => (
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export enum BannerVariant {
|
||||
Yellow = 'yellow',
|
||||
Blue = 'blue',
|
||||
Warning = 'warning',
|
||||
Filter = 'filter',
|
||||
}
|
||||
|
||||
export const StatusBanner: React.FC<{
|
||||
@ -11,9 +11,9 @@ export const StatusBanner: React.FC<{
|
||||
expanded?: boolean;
|
||||
onClick?: () => void;
|
||||
}> = ({ children, variant, expanded, onClick }) => (
|
||||
<div
|
||||
<label
|
||||
className={
|
||||
variant === BannerVariant.Yellow
|
||||
variant === BannerVariant.Warning
|
||||
? 'content-warning'
|
||||
: 'content-warning content-warning--filter'
|
||||
}
|
||||
@ -26,6 +26,11 @@ export const StatusBanner: React.FC<{
|
||||
id='content_warning.hide'
|
||||
defaultMessage='Hide post'
|
||||
/>
|
||||
) : variant === BannerVariant.Warning ? (
|
||||
<FormattedMessage
|
||||
id='content_warning.show_more'
|
||||
defaultMessage='Show more'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='content_warning.show'
|
||||
@ -33,5 +38,5 @@ export const StatusBanner: React.FC<{
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
|
@ -327,31 +327,24 @@ Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
|
||||
|
||||
if (!input) return;
|
||||
|
||||
const oldReadOnly = input.readOnly;
|
||||
|
||||
input.readOnly = false;
|
||||
input.focus();
|
||||
input.select();
|
||||
input.setSelectionRange(0, input.value.length);
|
||||
|
||||
try {
|
||||
if (document.execCommand('copy')) {
|
||||
input.blur();
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(input.value)
|
||||
.then(() => {
|
||||
const parent = target.parentElement;
|
||||
|
||||
if (!parent) return;
|
||||
if (parent) {
|
||||
parent.classList.add('copied');
|
||||
|
||||
setTimeout(() => {
|
||||
parent.classList.remove('copied');
|
||||
}, 700);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
input.readOnly = oldReadOnly;
|
||||
return true;
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
|
||||
const toggleSidebar = () => {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import type { JSX } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { JSX } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
@ -154,7 +154,5 @@
|
||||
"status.is_poll": "This toot is a poll",
|
||||
"status.local_only": "Only visible from your instance",
|
||||
"status.show_filter_reason": "Show anyway",
|
||||
"status.show_less": "Show less",
|
||||
"status.show_more": "Show more",
|
||||
"status.uncollapse": "Uncollapse"
|
||||
}
|
||||
|
@ -57,7 +57,10 @@ export const accountsReducer: Reducer<typeof initialState> = (
|
||||
return state.setIn([action.payload.id, 'hidden'], false);
|
||||
else if (importAccounts.match(action))
|
||||
return normalizeAccounts(state, action.payload.accounts);
|
||||
else if (followAccountSuccess.match(action)) {
|
||||
else if (
|
||||
followAccountSuccess.match(action) &&
|
||||
!action.payload.alreadyFollowing
|
||||
) {
|
||||
return state
|
||||
.update(action.payload.relationship.id, (account) =>
|
||||
account?.update('followers_count', (n) => n + 1),
|
||||
|
@ -11648,19 +11648,21 @@ noscript {
|
||||
}
|
||||
|
||||
.content-warning {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
background: rgba($ui-highlight-color, 0.05);
|
||||
color: $secondary-text-color;
|
||||
border-top: 1px solid;
|
||||
border-bottom: 1px solid;
|
||||
border-color: rgba($ui-highlight-color, 0.15);
|
||||
border: 1px solid rgba($ui-highlight-color, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 8px (5px + 8px);
|
||||
position: relative;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
cursor: pointer;
|
||||
|
||||
p {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.link-button {
|
||||
@ -11669,31 +11671,16 @@ noscript {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
background: url('~images/warning-stripes.svg') repeat-y;
|
||||
width: 5px;
|
||||
top: 0;
|
||||
&--filter {
|
||||
color: $darker-text-color;
|
||||
|
||||
p {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&::before {
|
||||
border-start-start-radius: 4px;
|
||||
border-end-start-radius: 4px;
|
||||
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');
|
||||
.filter-name {
|
||||
font-weight: 500;
|
||||
color: $secondary-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -76,4 +76,7 @@ body {
|
||||
--background-color-tint: rgba(255, 255, 255, 80%);
|
||||
--background-filter: blur(10px);
|
||||
--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,
|
||||
.edit-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,
|
||||
blockquote {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 22px;
|
||||
white-space: pre-wrap;
|
||||
unicode-bidi: plaintext;
|
||||
|
||||
@ -14,19 +34,45 @@
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding-inline-start: 10px;
|
||||
border-inline-start: 3px solid $darker-text-color;
|
||||
color: $darker-text-color;
|
||||
padding-inline-start: 32px;
|
||||
color: var(--rich-text-text-color);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
& > ul,
|
||||
& > ol {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 22px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
@ -76,7 +122,15 @@
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-inline-start: 2em;
|
||||
padding-inline-start: 24px;
|
||||
|
||||
li {
|
||||
padding-inline-start: 8px;
|
||||
|
||||
&::marker {
|
||||
text-align: end;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
@ -84,7 +138,11 @@
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
list-style-type: '•';
|
||||
|
||||
li::marker {
|
||||
text-align: start;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
|
@ -122,4 +122,7 @@ $dismiss-overlay-width: 4rem;
|
||||
--error-background-color: #{darken($error-red, 16%)};
|
||||
--error-active-background-color: #{darken($error-red, 12%)};
|
||||
--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 |
3
app/javascript/images/quote.svg
Normal file
3
app/javascript/images/quote.svg
Normal file
@ -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 classNames from 'classnames';
|
||||
|
@ -8,7 +8,7 @@ export const ContentWarning: React.FC<{
|
||||
<StatusBanner
|
||||
expanded={expanded}
|
||||
onClick={onClick}
|
||||
variant={BannerVariant.Yellow}
|
||||
variant={BannerVariant.Warning}
|
||||
>
|
||||
<p dangerouslySetInnerHTML={{ __html: text }} />
|
||||
</StatusBanner>
|
||||
|
@ -10,13 +10,16 @@ export const FilterWarning: React.FC<{
|
||||
<StatusBanner
|
||||
expanded={expanded}
|
||||
onClick={onClick}
|
||||
variant={BannerVariant.Blue}
|
||||
variant={BannerVariant.Filter}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='filter_warning.matches_filter'
|
||||
defaultMessage='Matches filter “{title}”'
|
||||
values={{ title }}
|
||||
defaultMessage='Matches filter “<span>{title}</span>”'
|
||||
values={{
|
||||
title,
|
||||
span: (chunks) => <span className='filter-name'>{chunks}</span>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</StatusBanner>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { memo } from 'react';
|
||||
import type { JSX } from 'react';
|
||||
|
||||
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||
|
||||
|
@ -449,7 +449,7 @@ class Status extends ImmutablePureComponent {
|
||||
} else if (status.get('media_attachments').size > 0) {
|
||||
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 = (
|
||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
||||
{Component => (
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export enum BannerVariant {
|
||||
Yellow = 'yellow',
|
||||
Blue = 'blue',
|
||||
Warning = 'warning',
|
||||
Filter = 'filter',
|
||||
}
|
||||
|
||||
export const StatusBanner: React.FC<{
|
||||
@ -11,9 +11,9 @@ export const StatusBanner: React.FC<{
|
||||
expanded?: boolean;
|
||||
onClick?: () => void;
|
||||
}> = ({ children, variant, expanded, onClick }) => (
|
||||
<div
|
||||
<label
|
||||
className={
|
||||
variant === BannerVariant.Yellow
|
||||
variant === BannerVariant.Warning
|
||||
? 'content-warning'
|
||||
: 'content-warning content-warning--filter'
|
||||
}
|
||||
@ -26,6 +26,11 @@ export const StatusBanner: React.FC<{
|
||||
id='content_warning.hide'
|
||||
defaultMessage='Hide post'
|
||||
/>
|
||||
) : variant === BannerVariant.Warning ? (
|
||||
<FormattedMessage
|
||||
id='content_warning.show_more'
|
||||
defaultMessage='Show more'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='content_warning.show'
|
||||
@ -33,5 +38,5 @@ export const StatusBanner: React.FC<{
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
|
@ -1,3 +1,5 @@
|
||||
import type { JSX } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { JSX } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
@ -197,6 +197,7 @@
|
||||
"confirmations.unfollow.title": "Unfollow user?",
|
||||
"content_warning.hide": "Hide post",
|
||||
"content_warning.show": "Show anyway",
|
||||
"content_warning.show_more": "Show more",
|
||||
"conversation.delete": "Delete conversation",
|
||||
"conversation.mark_as_read": "Mark as read",
|
||||
"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.title": "Filter this 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.title": "Filtered notifications",
|
||||
"firehose.all": "All",
|
||||
|
@ -57,7 +57,10 @@ export const accountsReducer: Reducer<typeof initialState> = (
|
||||
return state.setIn([action.payload.id, 'hidden'], false);
|
||||
else if (importAccounts.match(action))
|
||||
return normalizeAccounts(state, action.payload.accounts);
|
||||
else if (followAccountSuccess.match(action)) {
|
||||
else if (
|
||||
followAccountSuccess.match(action) &&
|
||||
!action.payload.alreadyFollowing
|
||||
) {
|
||||
return state
|
||||
.update(action.payload.relationship.id, (account) =>
|
||||
account?.update('followers_count', (n) => n + 1),
|
||||
|
@ -76,4 +76,7 @@ body {
|
||||
--background-color-tint: rgba(255, 255, 255, 80%);
|
||||
--background-filter: blur(10px);
|
||||
--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 {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
background: rgba($ui-highlight-color, 0.05);
|
||||
color: $secondary-text-color;
|
||||
border-top: 1px solid;
|
||||
border-bottom: 1px solid;
|
||||
border-color: rgba($ui-highlight-color, 0.15);
|
||||
border: 1px solid rgba($ui-highlight-color, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 8px (5px + 8px);
|
||||
position: relative;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
cursor: pointer;
|
||||
|
||||
p {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.link-button {
|
||||
@ -11130,31 +11132,16 @@ noscript {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
background: url('~images/warning-stripes.svg') repeat-y;
|
||||
width: 5px;
|
||||
top: 0;
|
||||
&--filter {
|
||||
color: $darker-text-color;
|
||||
|
||||
p {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&::before {
|
||||
border-start-start-radius: 4px;
|
||||
border-end-start-radius: 4px;
|
||||
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');
|
||||
.filter-name {
|
||||
font-weight: 500;
|
||||
color: $secondary-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,29 @@
|
||||
.e-content,
|
||||
.edit-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,
|
||||
blockquote {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 22px;
|
||||
white-space: pre-wrap;
|
||||
unicode-bidi: plaintext;
|
||||
|
||||
@ -14,19 +34,45 @@
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding-inline-start: 10px;
|
||||
border-inline-start: 3px solid $darker-text-color;
|
||||
color: $darker-text-color;
|
||||
padding-inline-start: 32px;
|
||||
color: var(--rich-text-text-color);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
& > ul,
|
||||
& > ol {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 22px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
b,
|
||||
@ -41,7 +87,15 @@
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-inline-start: 2em;
|
||||
padding-inline-start: 24px;
|
||||
|
||||
li {
|
||||
padding-inline-start: 8px;
|
||||
|
||||
&::marker {
|
||||
text-align: end;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
@ -49,7 +103,11 @@
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
list-style-type: '•';
|
||||
|
||||
li::marker {
|
||||
text-align: start;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
|
@ -116,4 +116,7 @@ $font-monospace: 'mastodon-font-monospace' !default;
|
||||
--error-background-color: #{darken($error-red, 16%)};
|
||||
--error-active-background-color: #{darken($error-red, 12%)};
|
||||
--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
|
||||
|
||||
def self.configured
|
||||
if ENV['DEEPL_API_KEY'].present?
|
||||
TranslationService::DeepL.new(ENV.fetch('DEEPL_PLAN', 'free'), ENV['DEEPL_API_KEY'])
|
||||
elsif ENV['LIBRE_TRANSLATE_ENDPOINT'].present?
|
||||
TranslationService::LibreTranslate.new(ENV['LIBRE_TRANSLATE_ENDPOINT'], ENV['LIBRE_TRANSLATE_API_KEY'])
|
||||
if configuration.deepl[:api_key].present?
|
||||
TranslationService::DeepL.new(
|
||||
configuration.deepl[:plan],
|
||||
configuration.deepl[:api_key]
|
||||
)
|
||||
elsif configuration.libre_translate[:endpoint].present?
|
||||
TranslationService::LibreTranslate.new(
|
||||
configuration.libre_translate[:endpoint],
|
||||
configuration.libre_translate[:api_key]
|
||||
)
|
||||
else
|
||||
raise NotConfiguredError
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def languages
|
||||
|
@ -65,6 +65,8 @@ class Account < ApplicationRecord
|
||||
)
|
||||
|
||||
BACKGROUND_REFRESH_INTERVAL = 1.week.freeze
|
||||
REFRESH_DEADLINE = 6.hours
|
||||
STALE_THRESHOLD = 1.day
|
||||
DEFAULT_FIELDS_SIZE = (ENV['MAX_PROFILE_FIELDS'] || 4).to_i
|
||||
INSTANCE_ACTOR_ID = -99
|
||||
|
||||
@ -229,13 +231,13 @@ class Account < ApplicationRecord
|
||||
end
|
||||
|
||||
def possibly_stale?
|
||||
last_webfingered_at.nil? || last_webfingered_at <= 1.day.ago
|
||||
last_webfingered_at.nil? || last_webfingered_at <= STALE_THRESHOLD.ago
|
||||
end
|
||||
|
||||
def schedule_refresh_if_stale!
|
||||
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
|
||||
|
||||
def refresh!
|
||||
|
@ -36,9 +36,14 @@ class IpBlock < ApplicationRecord
|
||||
|
||||
class << self
|
||||
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)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def blocked_ips_map
|
||||
Rails.cache.fetch(CACHE_KEY) { FastIpMap.new(severity_no_access.pluck(:ip)) }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -19,6 +19,8 @@ class LinkFeed < PublicFeed
|
||||
|
||||
scope.merge!(discoverable)
|
||||
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)
|
||||
end
|
||||
|
@ -29,6 +29,8 @@ class Web::PushSubscription < ApplicationRecord
|
||||
|
||||
delegate :locale, to: :associated_user
|
||||
|
||||
generates_token_for :unsubscribe, expires_in: Web::PushNotificationWorker::TTL
|
||||
|
||||
def pushable?(notification)
|
||||
policy_allows_notification?(notification) && alert_enabled_for_notification_type?(notification)
|
||||
end
|
||||
|
@ -2,10 +2,11 @@
|
||||
|
||||
class Web::PushNotificationWorker
|
||||
include Sidekiq::Worker
|
||||
include RoutingHelper
|
||||
|
||||
sidekiq_options queue: 'push', retry: 5
|
||||
|
||||
TTL = 48.hours.to_s
|
||||
TTL = 48.hours
|
||||
URGENCY = 'normal'
|
||||
|
||||
def perform(subscription_id, notification_id)
|
||||
@ -23,12 +24,13 @@ class Web::PushNotificationWorker
|
||||
|
||||
request.add_headers(
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Ttl' => TTL,
|
||||
'Ttl' => TTL.to_s,
|
||||
'Urgency' => URGENCY,
|
||||
'Content-Encoding' => 'aesgcm',
|
||||
'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}",
|
||||
'Authorization' => web_push_request.authorization_header
|
||||
'Authorization' => web_push_request.authorization_header,
|
||||
'Unsubscribe-URL' => subscription_url
|
||||
)
|
||||
|
||||
request.perform do |response|
|
||||
@ -72,4 +74,8 @@ class Web::PushNotificationWorker
|
||||
def request_pool
|
||||
RequestPool.current
|
||||
end
|
||||
|
||||
def subscription_url
|
||||
api_web_push_subscription_url(id: @subscription.generate_token_for(:unsubscribe))
|
||||
end
|
||||
end
|
||||
|
@ -109,6 +109,9 @@ module Mastodon
|
||||
end
|
||||
end
|
||||
|
||||
config.x.captcha = config_for(:captcha)
|
||||
config.x.translation = config_for(:translation)
|
||||
|
||||
config.to_prepare do
|
||||
Doorkeeper::AuthorizationsController.layout 'modal'
|
||||
Doorkeeper::AuthorizedApplicationsController.layout 'admin'
|
||||
|
3
config/captcha.yml
Normal file
3
config/captcha.yml
Normal file
@ -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.
|
||||
config.consider_all_requests_local = true
|
||||
|
||||
# Enable server timing
|
||||
# Enable server timing.
|
||||
config.server_timing = true
|
||||
|
||||
# Enable/disable caching. By default caching is disabled.
|
||||
@ -77,9 +77,6 @@ Rails.application.configure do
|
||||
# Annotate rendered view with file names.
|
||||
# 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' }
|
||||
|
||||
# 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
|
||||
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
|
||||
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).
|
||||
# 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.
|
||||
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.
|
||||
config.force_ssl = true
|
||||
# Skip http-to-https redirect for the default health check endpoint.
|
||||
config.ssl_options = {
|
||||
redirect: {
|
||||
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
|
||||
|
||||
# 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`.
|
||||
# Some locales are missing translation entries and would have errors:
|
||||
# https://github.com/mastodon/mastodon/pull/24727
|
||||
|
@ -26,7 +26,7 @@ Rails.application.configure do
|
||||
config.action_controller.perform_caching = false
|
||||
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
|
||||
|
||||
# Disable request forgery protection in test environment.
|
||||
@ -70,7 +70,7 @@ Rails.application.configure do
|
||||
# Annotate rendered view with file names.
|
||||
# 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
|
||||
end
|
||||
|
||||
|
@ -348,7 +348,7 @@ namespace :api, format: false do
|
||||
namespace :web do
|
||||
resource :settings, only: [:update]
|
||||
resources :embeds, only: [:show]
|
||||
resources :push_subscriptions, only: [:create] do
|
||||
resources :push_subscriptions, only: [:create, :destroy] do
|
||||
member do
|
||||
put :update
|
||||
end
|
||||
|
7
config/translation.yml
Normal file
7
config/translation.yml
Normal file
@ -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) %>
|
10
db/migrate/20241014010506_remove_duplicate_indexes.rb
Normal file
10
db/migrate/20241014010506_remove_duplicate_indexes.rb
Normal file
@ -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.
|
||||
|
||||
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
|
||||
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 "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"], name: "index_account_aliases_on_account_id"
|
||||
end
|
||||
|
||||
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 "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"], name: "index_account_relationship_severance_events_on_account_id"
|
||||
t.index ["relationship_severance_event_id"], name: "idx_on_relationship_severance_event_id_403f53e707"
|
||||
end
|
||||
|
||||
@ -397,7 +395,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_10_07_071624) do
|
||||
t.datetime "updated_at", null: false
|
||||
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"], name: "index_custom_filter_statuses_on_status_id"
|
||||
end
|
||||
|
||||
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.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"], name: "index_webauthn_credentials_on_user_id"
|
||||
end
|
||||
|
||||
create_table "webhooks", force: :cascade do |t|
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mastodon/mastodon",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"packageManager": "yarn@4.5.0",
|
||||
"packageManager": "yarn@4.5.1",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@ -180,13 +180,13 @@
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-define-config": "^2.0.0",
|
||||
"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-jsdoc": "^50.0.0",
|
||||
"eslint-plugin-jsx-a11y": "~6.10.0",
|
||||
"eslint-plugin-promise": "~7.1.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"husky": "^9.0.11",
|
||||
"jest": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
|
7
spec/fabricators/account_conversation_fabricator.rb
Normal file
7
spec/fabricators/account_conversation_fabricator.rb
Normal file
@ -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
|
||||
|
||||
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
|
||||
it 'returns site title on production environment' do
|
||||
Setting.site_title = 'site title'
|
||||
|
61
spec/helpers/database_helper_spec.rb
Normal file
61
spec/helpers/database_helper_spec.rb
Normal file
@ -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'
|
||||
|
||||
RSpec.describe FeedManager do
|
||||
subject { described_class.instance }
|
||||
|
||||
before do |example|
|
||||
unless example.metadata[:skip_stub]
|
||||
stub_const 'FeedManager::MAX_ITEMS', 10
|
||||
@ -32,26 +34,26 @@ RSpec.describe FeedManager do
|
||||
it 'returns false for followee\'s status' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: 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
|
||||
|
||||
it 'returns false for reblog by followee' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||
reblog = Fabricate(:status, reblog: status, account: 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
|
||||
|
||||
it 'returns true for post from account who blocked me' do
|
||||
status = Fabricate(:status, text: 'Hello, World', account: alice)
|
||||
alice.block!(bob)
|
||||
expect(described_class.instance.filter?(:home, status, bob)).to be true
|
||||
expect(subject.filter?(:home, status, bob)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for post from blocked account' do
|
||||
status = Fabricate(:status, text: 'Hello, World', account: 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
|
||||
|
||||
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)
|
||||
bob.follow!(alice)
|
||||
bob.block!(jeff)
|
||||
expect(described_class.instance.filter?(:home, reblog, bob)).to be true
|
||||
expect(subject.filter?(:home, reblog, bob)).to be true
|
||||
end
|
||||
|
||||
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)
|
||||
bob.follow!(alice)
|
||||
bob.mute!(jeff)
|
||||
expect(described_class.instance.filter?(:home, reblog, bob)).to be true
|
||||
expect(subject.filter?(:home, reblog, bob)).to be true
|
||||
end
|
||||
|
||||
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)
|
||||
bob.follow!(alice)
|
||||
jeff.block!(bob)
|
||||
expect(described_class.instance.filter?(:home, reblog, bob)).to be true
|
||||
expect(subject.filter?(:home, reblog, bob)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for reblog from account with reblogs disabled' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||
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
|
||||
|
||||
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)
|
||||
bob.follow!(alice)
|
||||
bob.follow!(jeff)
|
||||
expect(described_class.instance.filter?(:home, reply, bob)).to be false
|
||||
expect(subject.filter?(:home, reply, bob)).to be false
|
||||
end
|
||||
|
||||
it 'returns false for reply by followee to recipient' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: bob)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: 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
|
||||
|
||||
it 'returns false for reply by followee to self' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: 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
|
||||
|
||||
it 'returns true for reply by followee to non-followed account' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: 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
|
||||
|
||||
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)
|
||||
second_reply = Fabricate(:status, text: 'Reply 2', thread: reply, account: 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
|
||||
|
||||
it 'returns false for status by followee mentioning another account' do
|
||||
bob.follow!(alice)
|
||||
jeff.follow!(alice)
|
||||
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
|
||||
|
||||
it 'returns true for status by followee mentioning blocked account' do
|
||||
bob.block!(jeff)
|
||||
bob.follow!(alice)
|
||||
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
|
||||
|
||||
it 'returns true for status by followee mentioning muted account' do
|
||||
@ -147,19 +149,19 @@ RSpec.describe FeedManager do
|
||||
alice.follow!(jeff)
|
||||
status = Fabricate(:status, text: 'Hello world', account: bob)
|
||||
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
|
||||
|
||||
it 'returns true for German post when follow is set to English only' do
|
||||
alice.follow!(bob, languages: %w(en))
|
||||
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
|
||||
|
||||
it 'returns false for German post when follow is set to German' do
|
||||
alice.follow!(bob, languages: %w(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
|
||||
|
||||
it 'returns true for post from followee on exclusive list' do
|
||||
@ -168,7 +170,7 @@ RSpec.describe FeedManager do
|
||||
list.accounts << bob
|
||||
allow(List).to receive(:where).and_return(list)
|
||||
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
|
||||
|
||||
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)
|
||||
status = Fabricate(:status, text: 'I post a lot', account: bob)
|
||||
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
|
||||
|
||||
it 'returns false for post from followee on non-exclusive list' do
|
||||
@ -186,7 +188,7 @@ RSpec.describe FeedManager do
|
||||
alice.follow!(bob)
|
||||
list.accounts << 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
|
||||
|
||||
it 'returns false for reblog from followee on non-exclusive list' do
|
||||
@ -195,7 +197,7 @@ RSpec.describe FeedManager do
|
||||
list.accounts << jeff
|
||||
status = Fabricate(:status, text: 'I post a lot', account: bob)
|
||||
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
|
||||
|
||||
@ -203,27 +205,27 @@ RSpec.describe FeedManager do
|
||||
it 'returns true for status that mentions blocked account' do
|
||||
bob.block!(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
|
||||
|
||||
it 'returns true for status that replies to a blocked account' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||
bob.block!(jeff)
|
||||
expect(described_class.instance.filter?(:mentions, reply, bob)).to be true
|
||||
expect(subject.filter?(:mentions, reply, bob)).to be true
|
||||
end
|
||||
|
||||
it 'returns false for status by limited account who recipient is not following' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||
alice.silence!
|
||||
expect(described_class.instance.filter?(:mentions, status, bob)).to be false
|
||||
expect(subject.filter?(:mentions, status, bob)).to be false
|
||||
end
|
||||
|
||||
it 'returns false for status by followed limited account' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||
alice.silence!
|
||||
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
|
||||
@ -235,7 +237,7 @@ RSpec.describe FeedManager do
|
||||
members = Array.new(described_class::MAX_ITEMS) { |count| [count, count] }
|
||||
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
|
||||
end
|
||||
@ -246,7 +248,7 @@ RSpec.describe FeedManager do
|
||||
reblogged = Fabricate(:status)
|
||||
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
|
||||
|
||||
it 'does not save a new reblog of a recent status' do
|
||||
@ -254,9 +256,9 @@ RSpec.describe FeedManager do
|
||||
reblogged = Fabricate(:status)
|
||||
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
|
||||
|
||||
it 'saves a new reblog of an old status' do
|
||||
@ -264,14 +266,14 @@ RSpec.describe FeedManager do
|
||||
reblogged = Fabricate(:status)
|
||||
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
|
||||
described_class::REBLOG_FALLOFF.times do
|
||||
described_class.instance.push_to_home(account, Fabricate(:status))
|
||||
subject.push_to_home(account, Fabricate(:status))
|
||||
end
|
||||
|
||||
expect(described_class.instance.push_to_home(account, reblog)).to be true
|
||||
expect(subject.push_to_home(account, reblog)).to be true
|
||||
end
|
||||
|
||||
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) }
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
# 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
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
||||
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) }
|
||||
|
||||
# Accept the reblogs
|
||||
described_class.instance.push_to_home(account, reblogs[0])
|
||||
described_class.instance.push_to_home(account, reblogs[1])
|
||||
subject.push_to_home(account, reblogs[0])
|
||||
subject.push_to_home(account, reblogs[1])
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
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) }
|
||||
|
||||
# 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
|
||||
described_class::REBLOG_FALLOFF.times do
|
||||
described_class.instance.push_to_home(account, Fabricate(:status))
|
||||
subject.push_to_home(account, Fabricate(:status))
|
||||
end
|
||||
|
||||
# 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
|
||||
|
||||
@ -341,9 +343,9 @@ RSpec.describe FeedManager do
|
||||
account = Fabricate(:account)
|
||||
reblog = Fabricate(:status)
|
||||
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
|
||||
|
||||
@ -366,9 +368,9 @@ RSpec.describe FeedManager do
|
||||
it "does not push when the given status's reblog is already inserted" do
|
||||
reblog = Fabricate(:status)
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
it 'pushes statuses that are replies to list owner' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: owner)
|
||||
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
|
||||
|
||||
it 'does not push replies to another member of the list' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||
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
|
||||
|
||||
@ -401,25 +403,25 @@ RSpec.describe FeedManager do
|
||||
|
||||
it 'pushes statuses that are not replies' do
|
||||
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
|
||||
|
||||
it 'pushes statuses that are replies to list owner' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: owner)
|
||||
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
|
||||
|
||||
it 'pushes replies to another member of the list' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||
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
|
||||
|
||||
it 'does not push replies to someone not a member of the list' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: eve)
|
||||
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
|
||||
|
||||
@ -430,25 +432,25 @@ RSpec.describe FeedManager do
|
||||
|
||||
it 'pushes statuses that are not replies' do
|
||||
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
|
||||
|
||||
it 'pushes statuses that are replies to list owner' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: owner)
|
||||
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
|
||||
|
||||
it 'pushes replies to another member of the list' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||
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
|
||||
|
||||
it 'pushes replies to someone not a member of the list' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: eve)
|
||||
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
|
||||
@ -458,9 +460,9 @@ RSpec.describe FeedManager do
|
||||
account = Fabricate(:account, id: 0)
|
||||
reblog = Fabricate(:status)
|
||||
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
|
||||
end
|
||||
@ -473,14 +475,14 @@ RSpec.describe FeedManager do
|
||||
reblogged = Fabricate(:status)
|
||||
status = Fabricate(:status, reblog: reblogged)
|
||||
|
||||
described_class.instance.push_to_home(receiver, reblogged)
|
||||
described_class::REBLOG_FALLOFF.times { described_class.instance.push_to_home(receiver, Fabricate(:status)) }
|
||||
described_class.instance.push_to_home(receiver, status)
|
||||
subject.push_to_home(receiver, reblogged)
|
||||
described_class::REBLOG_FALLOFF.times { subject.push_to_home(receiver, Fabricate(:status)) }
|
||||
subject.push_to_home(receiver, status)
|
||||
|
||||
# The reblogging status should show up under normal conditions.
|
||||
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
|
||||
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)
|
||||
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.
|
||||
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
|
||||
end
|
||||
@ -506,14 +508,14 @@ RSpec.describe FeedManager do
|
||||
reblogs = Array.new(3) { Fabricate(:status, reblog: reblogged) }
|
||||
|
||||
reblogs.each do |reblog|
|
||||
described_class.instance.push_to_home(receiver, reblog)
|
||||
subject.push_to_home(receiver, reblog)
|
||||
end
|
||||
|
||||
# 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]
|
||||
|
||||
reblogs[0...-1].each do |reblog|
|
||||
described_class.instance.unpush_from_home(receiver, reblog)
|
||||
subject.unpush_from_home(receiver, reblog)
|
||||
end
|
||||
|
||||
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
|
||||
status = Fabricate(:status)
|
||||
|
||||
described_class.instance.push_to_home(receiver, status)
|
||||
subject.push_to_home(receiver, status)
|
||||
|
||||
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)
|
||||
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
|
||||
status = Fabricate(:status)
|
||||
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)
|
||||
end
|
||||
@ -552,9 +554,9 @@ RSpec.describe FeedManager do
|
||||
|
||||
status = Fabricate(:status, account: followee)
|
||||
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)
|
||||
end
|
||||
@ -562,9 +564,9 @@ RSpec.describe FeedManager do
|
||||
it 'remains a tagged status written by receiver' do
|
||||
status = Fabricate(:status, account: receiver)
|
||||
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)
|
||||
end
|
||||
@ -595,7 +597,7 @@ RSpec.describe FeedManager do
|
||||
end
|
||||
|
||||
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]
|
||||
end
|
||||
|
@ -8,4 +8,26 @@ RSpec.describe AccountAlias do
|
||||
it { is_expected.to normalize(:acct).from(' @username@domain ').to('username@domain') }
|
||||
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
|
||||
|
@ -9,8 +9,8 @@ RSpec.describe AccountMigration do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
subject { described_class.new(account: source_account, acct: target_acct) }
|
||||
describe 'Validations' do
|
||||
subject { Fabricate.build :account_migration, account: source_account }
|
||||
|
||||
let(:source_account) { Fabricate(:account) }
|
||||
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)
|
||||
end
|
||||
|
||||
it 'passes validations' do
|
||||
expect(subject).to be_valid
|
||||
end
|
||||
it { is_expected.to allow_value(target_account.acct).for(:acct) }
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
it 'has errors on acct field' do
|
||||
expect(subject).to model_have_error_on_field(:acct)
|
||||
end
|
||||
it { is_expected.to_not allow_value(target_acct).for(:acct) }
|
||||
end
|
||||
|
||||
context 'with a space in the domain part' do
|
||||
let(:target_acct) { 'target@remote. org' }
|
||||
|
||||
it 'has errors on acct field' do
|
||||
expect(subject).to model_have_error_on_field(:acct)
|
||||
end
|
||||
it { is_expected.to_not allow_value(target_acct).for(:acct) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -3,7 +3,8 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AccountModerationNote do
|
||||
describe 'chronological scope' do
|
||||
describe 'Scopes' do
|
||||
describe '.chronological' do
|
||||
it 'returns account moderation notes oldest to newest' do
|
||||
account = Fabricate(: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]
|
||||
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
|
||||
|
||||
it 'is invalid if content is longer than character limit' do
|
||||
report = Fabricate.build(:account_moderation_note, content: comment_over_limit)
|
||||
expect(report.valid?).to be false
|
||||
end
|
||||
describe 'Validations' do
|
||||
subject { Fabricate.build :account_moderation_note }
|
||||
|
||||
def comment_over_limit
|
||||
Faker::Lorem.paragraph_by_chars(number: described_class::CONTENT_SIZE_LIMIT * 2)
|
||||
describe 'content' do
|
||||
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
|
||||
|
@ -10,64 +10,6 @@ RSpec.describe Account do
|
||||
|
||||
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
|
||||
it 'creates a follow' do
|
||||
follow = subject.follow!(bob)
|
||||
@ -208,16 +150,16 @@ RSpec.describe Account do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when last_webfingered_at is more than 24 hours before' do
|
||||
let(:last_webfingered_at) { 25.hours.ago }
|
||||
context 'when last_webfingered_at is before the threshold' do
|
||||
let(:last_webfingered_at) { (described_class::STALE_THRESHOLD + 1.hour).ago }
|
||||
|
||||
it 'returns true' do
|
||||
expect(account.possibly_stale?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when last_webfingered_at is less than 24 hours before' do
|
||||
let(:last_webfingered_at) { 23.hours.ago }
|
||||
context 'when last_webfingered_at is after the threshold' do
|
||||
let(:last_webfingered_at) { (described_class::STALE_THRESHOLD - 1.hour).ago }
|
||||
|
||||
it 'returns false' do
|
||||
expect(account.possibly_stale?).to be false
|
||||
@ -752,26 +694,42 @@ RSpec.describe Account do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#prepare_contents' do
|
||||
subject { Fabricate.build :account, domain: domain, note: ' padded note ', display_name: ' padded name ' }
|
||||
describe 'Callbacks' do
|
||||
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
|
||||
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
|
||||
it 'preserves content' do
|
||||
expect { subject.valid? }
|
||||
.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
|
||||
@ -826,22 +784,19 @@ RSpec.describe Account do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
describe 'Validations' do
|
||||
it { is_expected.to validate_presence_of(:username) }
|
||||
|
||||
context 'when is local' do
|
||||
it 'is invalid if the username is not unique in case-insensitive comparison among local accounts' do
|
||||
_account = Fabricate(:account, username: 'the_doctor')
|
||||
non_unique_account = Fabricate.build(:account, username: 'the_Doctor')
|
||||
non_unique_account.valid?
|
||||
expect(non_unique_account).to model_have_error_on_field(:username)
|
||||
context 'when account is local' do
|
||||
subject { Fabricate.build :account, domain: nil }
|
||||
|
||||
context 'with an existing differently-cased username account' do
|
||||
before { Fabricate :account, username: 'the_doctor' }
|
||||
|
||||
it { is_expected.to_not allow_value('the_Doctor').for(:username) }
|
||||
end
|
||||
|
||||
it 'is invalid if the username is reserved' do
|
||||
account = Fabricate.build(:account, username: 'support')
|
||||
account.valid?
|
||||
expect(account).to model_have_error_on_field(:username)
|
||||
end
|
||||
it { is_expected.to_not allow_value('support').for(:username) }
|
||||
|
||||
it 'is valid when username is reserved but record has already been created' do
|
||||
account = Fabricate.build(:account, username: 'support')
|
||||
@ -849,9 +804,10 @@ RSpec.describe Account do
|
||||
expect(account.valid?).to be true
|
||||
end
|
||||
|
||||
it 'is valid if we are creating an instance actor account with a period' do
|
||||
account = Fabricate.build(:account, id: described_class::INSTANCE_ACTOR_ID, actor_type: 'Application', locked: true, username: 'example.com')
|
||||
expect(account.valid?).to be true
|
||||
context 'with the instance actor' do
|
||||
subject { Fabricate.build :account, id: described_class::INSTANCE_ACTOR_ID, actor_type: 'Application', locked: true }
|
||||
|
||||
it { is_expected.to allow_value('example.com').for(:username) }
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
it 'is invalid if the username doesn\'t only contains letters, numbers and underscores' do
|
||||
account = Fabricate.build(:account, username: 'the-doctor')
|
||||
account.valid?
|
||||
expect(account).to model_have_error_on_field(:username)
|
||||
it { is_expected.to_not allow_values('the-doctor', 'the.doctor').for(:username) }
|
||||
|
||||
it { is_expected.to validate_length_of(:username).is_at_most(described_class::USERNAME_LENGTH_LIMIT) }
|
||||
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
|
||||
|
||||
it 'is invalid if the username contains a period' do
|
||||
account = Fabricate.build(:account, username: 'the.doctor')
|
||||
account.valid?
|
||||
expect(account).to model_have_error_on_field(:username)
|
||||
context 'when account is remote' do
|
||||
subject { Fabricate.build :account, domain: 'host.example' }
|
||||
|
||||
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
|
||||
|
||||
it 'is invalid if the username is longer than the character limit' do
|
||||
account = Fabricate.build(:account, username: username_over_limit)
|
||||
account.valid?
|
||||
expect(account).to model_have_error_on_field(:username)
|
||||
end
|
||||
it { is_expected.to allow_values('the-doctor', username_over_limit).for(:username) }
|
||||
it { is_expected.to_not allow_values('the doctor').for(:username) }
|
||||
|
||||
it 'is invalid if the display name is longer than the character limit' do
|
||||
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_expected.to allow_values(display_name_over_limit).for(:display_name) }
|
||||
|
||||
it 'is invalid if the note is longer than the character limit' do
|
||||
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
|
||||
it { is_expected.to allow_values(account_note_over_limit).for(:note) }
|
||||
end
|
||||
|
||||
def username_over_limit
|
||||
@ -1085,14 +991,6 @@ RSpec.describe Account do
|
||||
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
|
||||
let!(:suspended_local) { Fabricate(:account, suspended: true, username: 'suspended_local') }
|
||||
let!(:suspended_remote) { Fabricate(:account, suspended: true, domain: 'example.org', username: 'suspended_remote') }
|
||||
|
@ -5,13 +5,12 @@ require 'rails_helper'
|
||||
RSpec.describe AccountStatusesCleanupPolicy do
|
||||
let(:account) { Fabricate(:account, username: 'alice', domain: nil) }
|
||||
|
||||
describe 'validation' do
|
||||
it 'disallow remote accounts' do
|
||||
account.update(domain: 'example.com')
|
||||
account_statuses_cleanup_policy = Fabricate.build(:account_statuses_cleanup_policy, account: account)
|
||||
account_statuses_cleanup_policy.valid?
|
||||
expect(account_statuses_cleanup_policy).to model_have_error_on_field(:account)
|
||||
end
|
||||
describe 'Validations' do
|
||||
subject { Fabricate.build :account_statuses_cleanup_policy }
|
||||
|
||||
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
|
||||
|
||||
it { is_expected.to_not allow_value(remote_account).for(:account) }
|
||||
end
|
||||
|
||||
describe 'save hooks' do
|
||||
@ -339,14 +338,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
|
||||
end
|
||||
|
||||
context 'when policy is set to keep DMs and reject everything else' do
|
||||
before do
|
||||
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
|
||||
before { establish_policy(keep_direct: true) }
|
||||
|
||||
it 'returns every old status except does not return the old direct message for deletion' do
|
||||
expect(subject.pluck(:id))
|
||||
@ -356,14 +348,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
|
||||
end
|
||||
|
||||
context 'when policy is set to keep self-bookmarked toots and reject everything else' do
|
||||
before do
|
||||
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
|
||||
before { establish_policy(keep_self_bookmark: true) }
|
||||
|
||||
it 'returns every old status but does not return the old self-bookmarked message for deletion' do
|
||||
expect(subject.pluck(:id))
|
||||
@ -373,14 +358,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
|
||||
end
|
||||
|
||||
context 'when policy is set to keep self-faved toots and reject everything else' do
|
||||
before do
|
||||
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
|
||||
before { establish_policy(keep_self_fav: true) }
|
||||
|
||||
it 'returns every old status but does not return the old self-faved message for deletion' do
|
||||
expect(subject.pluck(:id))
|
||||
@ -390,14 +368,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
|
||||
end
|
||||
|
||||
context 'when policy is set to keep toots with media and reject everything else' do
|
||||
before do
|
||||
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
|
||||
before { establish_policy(keep_media: true) }
|
||||
|
||||
it 'returns every old status but does not return the old message with media for deletion' do
|
||||
expect(subject.pluck(:id))
|
||||
@ -407,14 +378,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
|
||||
end
|
||||
|
||||
context 'when policy is set to keep toots with polls and reject everything else' do
|
||||
before do
|
||||
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
|
||||
before { establish_policy(keep_polls: true) }
|
||||
|
||||
it 'returns every old status but does not return the old poll message for deletion' do
|
||||
expect(subject.pluck(:id))
|
||||
@ -424,14 +388,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
|
||||
end
|
||||
|
||||
context 'when policy is set to keep pinned toots and reject everything else' do
|
||||
before do
|
||||
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
|
||||
before { establish_policy(keep_pinned: true) }
|
||||
|
||||
it 'returns every old status but does not return the old pinned message for deletion' do
|
||||
expect(subject.pluck(:id))
|
||||
@ -441,14 +398,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
|
||||
end
|
||||
|
||||
context 'when policy is to not keep any special messages' do
|
||||
before do
|
||||
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
|
||||
before { establish_policy }
|
||||
|
||||
it 'returns every old status but does not return the recent or unrelated statuses' do
|
||||
expect(subject.pluck(:id))
|
||||
@ -459,14 +409,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
|
||||
end
|
||||
|
||||
context 'when policy is set to keep every category of toots' do
|
||||
before do
|
||||
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
|
||||
before { establish_policy(keep_direct: true, keep_pinned: true, keep_polls: true, keep_media: true, keep_self_fav: true, keep_self_bookmark: true) }
|
||||
|
||||
it 'returns normal statuses and does not return unrelated old status' do
|
||||
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)
|
||||
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
|
||||
|
@ -67,18 +67,30 @@ RSpec.describe Announcement do
|
||||
it { is_expected.to validate_presence_of(:text) }
|
||||
|
||||
describe 'ends_at' do
|
||||
it 'validates presence when starts_at is present' do
|
||||
record = Fabricate.build(:announcement, starts_at: 1.day.ago)
|
||||
context 'when starts_at is present' do
|
||||
subject { Fabricate.build :announcement, starts_at: 1.day.ago }
|
||||
|
||||
expect(record).to_not be_valid
|
||||
expect(record.errors[:ends_at]).to be_present
|
||||
it { is_expected.to validate_presence_of(:ends_at) }
|
||||
end
|
||||
|
||||
it 'does not validate presence when starts_at is missing' do
|
||||
record = Fabricate.build(:announcement, starts_at: nil)
|
||||
context 'when starts_at is missing' do
|
||||
subject { Fabricate.build :announcement, starts_at: nil }
|
||||
|
||||
expect(record).to be_valid
|
||||
expect(record.errors[:ends_at]).to_not be_present
|
||||
it { is_expected.to_not validate_presence_of(:ends_at) }
|
||||
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
|
||||
|
@ -4,20 +4,85 @@ require 'rails_helper'
|
||||
|
||||
RSpec.describe Appeal do
|
||||
describe 'Validations' do
|
||||
it 'validates text length is under limit' do
|
||||
appeal = Fabricate.build(
|
||||
:appeal,
|
||||
strike: Fabricate(:account_warning),
|
||||
text: 'a' * described_class::TEXT_LENGTH_LIMIT * 2
|
||||
)
|
||||
subject { Fabricate.build :appeal, strike: Fabricate(:account_warning) }
|
||||
|
||||
expect(appeal).to_not be_valid
|
||||
expect(appeal).to model_have_error_on_field(:text)
|
||||
it { is_expected.to validate_length_of(:text).is_at_most(described_class::TEXT_LENGTH_LIMIT) }
|
||||
|
||||
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
|
||||
|
||||
describe 'scopes' do
|
||||
describe 'approved' do
|
||||
describe 'Query methods' 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(:not_approved_appeal) { Fabricate(:appeal, approved_at: nil) }
|
||||
|
||||
@ -27,7 +92,7 @@ RSpec.describe Appeal do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'rejected' do
|
||||
describe '.rejected' do
|
||||
let(:rejected_appeal) { Fabricate(:appeal, rejected_at: 10.days.ago) }
|
||||
let(:not_rejected_appeal) { Fabricate(:appeal, rejected_at: nil) }
|
||||
|
||||
@ -37,7 +102,7 @@ RSpec.describe Appeal do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'pending' do
|
||||
describe '.pending' do
|
||||
let(:approved_appeal) { Fabricate(:appeal, approved_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) }
|
||||
|
65
spec/models/concerns/account/suspensions_spec.rb
Normal file
65
spec/models/concerns/account/suspensions_spec.rb
Normal file
@ -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
|
||||
it { is_expected.to validate_presence_of(:domain) }
|
||||
|
||||
it 'is invalid if the same normalized domain already exists' do
|
||||
_domain_allow = Fabricate(:domain_allow, domain: 'にゃん')
|
||||
domain_allow_with_normalized_value = Fabricate.build(:domain_allow, domain: 'xn--r9j5b5b')
|
||||
domain_allow_with_normalized_value.valid?
|
||||
expect(domain_allow_with_normalized_value).to model_have_error_on_field(:domain)
|
||||
context 'when a normalized domain exists' do
|
||||
before { Fabricate(:domain_allow, domain: 'にゃん') }
|
||||
|
||||
it { is_expected.to_not allow_value('xn--r9j5b5b').for(:domain) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -3,27 +3,26 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Follow do
|
||||
let(:alice) { Fabricate(:account, username: 'alice') }
|
||||
let(:bob) { Fabricate(:account, username: 'bob') }
|
||||
|
||||
describe 'validations' do
|
||||
subject { described_class.new(account: alice, target_account: bob, rate_limit: true) }
|
||||
|
||||
describe 'Associations' do
|
||||
it { is_expected.to belong_to(: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
|
||||
|
||||
it 'is valid if account is only on the brink of following too many people' do
|
||||
alice.update(following_count: FollowLimitValidator::LIMIT - 1)
|
||||
describe 'Validations' do
|
||||
subject { Fabricate.build :follow, rate_limit: true }
|
||||
|
||||
expect(subject).to be_valid
|
||||
expect(subject).to_not model_have_error_on_field(:base)
|
||||
let(:account) { Fabricate(:account) }
|
||||
|
||||
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
|
||||
|
||||
@ -54,4 +53,58 @@ RSpec.describe Follow do
|
||||
expect(account.requested?(target_account)).to be true
|
||||
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
|
||||
|
@ -3,33 +3,17 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Form::AdminSettings do
|
||||
describe 'validations' do
|
||||
describe 'Validations' do
|
||||
describe 'site_contact_username' do
|
||||
context 'with no accounts' do
|
||||
it 'is not valid' do
|
||||
setting = described_class.new(site_contact_username: 'Test')
|
||||
setting.valid?
|
||||
|
||||
expect(setting).to model_have_error_on_field(:site_contact_username)
|
||||
end
|
||||
it { is_expected.to_not allow_value('Test').for(:site_contact_username) }
|
||||
end
|
||||
|
||||
context 'with an account' do
|
||||
before { Fabricate(:account, username: 'Glorp') }
|
||||
|
||||
it 'is not valid when account doesnt match' do
|
||||
setting = described_class.new(site_contact_username: 'Test')
|
||||
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
|
||||
it { is_expected.to_not allow_value('Test').for(:site_contact_username) }
|
||||
it { is_expected.to allow_value('Glorp').for(:site_contact_username) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -3,18 +3,13 @@
|
||||
require 'rails_helper'
|
||||
|
||||
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(:severity) }
|
||||
|
||||
it 'validates ip uniqueness', :aggregate_failures do
|
||||
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
|
||||
it { is_expected.to validate_uniqueness_of(:ip) }
|
||||
end
|
||||
|
||||
describe '#to_log_human_identifier' do
|
||||
|
@ -9,26 +9,10 @@ RSpec.describe PreviewCard do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
describe 'urls' do
|
||||
it 'allows http schemes' do
|
||||
record = described_class.new(url: 'http://example.host/path')
|
||||
|
||||
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
|
||||
describe 'Validations' do
|
||||
describe 'url' do
|
||||
it { is_expected.to allow_values('http://example.host/path', 'https://example.host/path').for(:url) }
|
||||
it { is_expected.to_not allow_value('javascript:alert()').for(:url) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -3,7 +3,8 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ReportNote do
|
||||
describe 'chronological scope' do
|
||||
describe 'Scopes' do
|
||||
describe '.chronological' do
|
||||
it 'returns report notes oldest to newest' do
|
||||
report = Fabricate(:report)
|
||||
note1 = Fabricate(:report_note, report: report)
|
||||
@ -12,20 +13,14 @@ RSpec.describe ReportNote do
|
||||
expect(report.notes.chronological).to eq [note1, note2]
|
||||
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
|
||||
|
||||
it 'is invalid if content is longer than character limit' do
|
||||
report = Fabricate.build(:report_note, content: comment_over_limit)
|
||||
expect(report.valid?).to be false
|
||||
end
|
||||
describe 'Validations' do
|
||||
subject { Fabricate.build :report_note }
|
||||
|
||||
def comment_over_limit
|
||||
Faker::Lorem.paragraph_by_chars(number: described_class::CONTENT_SIZE_LIMIT * 2)
|
||||
describe 'content' do
|
||||
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
|
||||
|
@ -3,53 +3,17 @@
|
||||
require 'rails_helper'
|
||||
|
||||
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(:public_key) }
|
||||
it { is_expected.to validate_presence_of(:nickname) }
|
||||
it { is_expected.to validate_presence_of(:sign_count) }
|
||||
|
||||
it 'is invalid if already exist a webauthn credential with the same external id' do
|
||||
Fabricate(:webauthn_credential, external_id: '_Typ0ygudDnk9YUVWLQayw')
|
||||
new_webauthn_credential = Fabricate.build(:webauthn_credential, external_id: '_Typ0ygudDnk9YUVWLQayw')
|
||||
it { is_expected.to validate_uniqueness_of(:external_id) }
|
||||
it { is_expected.to validate_uniqueness_of(:nickname).scoped_to(:user_id) }
|
||||
|
||||
new_webauthn_credential.valid?
|
||||
|
||||
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
|
||||
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) }
|
||||
end
|
||||
end
|
||||
|
25
spec/requests/activitypub/likes_spec.rb
Normal file
25
spec/requests/activitypub/likes_spec.rb
Normal file
@ -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
|
25
spec/requests/activitypub/shares_spec.rb
Normal file
25
spec/requests/activitypub/shares_spec.rb
Normal file
@ -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
|
36
spec/requests/api/v1/domain_blocks/previews_spec.rb
Normal file
36
spec/requests/api/v1/domain_blocks/previews_spec.rb
Normal file
@ -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
|
55
spec/requests/api/web/push_subscriptions_spec.rb
Normal file
55
spec/requests/api/web/push_subscriptions_spec.rb
Normal file
@ -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
|
||||
|
||||
module ProfileStories
|
||||
attr_reader :bob, :alice, :alice_bio
|
||||
attr_reader :bob
|
||||
|
||||
def fill_in_auth_details(email, password)
|
||||
fill_in 'user_email', with: email
|
||||
@ -31,18 +31,6 @@ module ProfileStories
|
||||
bob.update!(role: UserRole.find_by!(name: 'Admin'))
|
||||
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
|
||||
@confirmed_at ||= Time.zone.now
|
||||
end
|
||||
|
@ -11,10 +11,10 @@ RSpec.describe 'Profile' do
|
||||
|
||||
before do
|
||||
as_a_logged_in_user
|
||||
with_alice_as_local_user
|
||||
Fabricate(:user, account: Fabricate(:account, username: 'alice'))
|
||||
end
|
||||
|
||||
it 'I can view Annes public account' do
|
||||
it 'I can view public account page for Alice' do
|
||||
visit account_path('alice')
|
||||
|
||||
expect(subject).to have_title("alice (@alice@#{local_domain})")
|
||||
|
@ -7,9 +7,7 @@ RSpec.describe AccountRefreshWorker do
|
||||
let(:service) { instance_double(ResolveAccountService, call: true) }
|
||||
|
||||
describe '#perform' do
|
||||
before do
|
||||
allow(ResolveAccountService).to receive(:new).and_return(service)
|
||||
end
|
||||
before { stub_service }
|
||||
|
||||
context 'when account does not exist' do
|
||||
it 'returns immediately without processing' do
|
||||
@ -48,5 +46,11 @@ RSpec.describe AccountRefreshWorker do
|
||||
(Account::BACKGROUND_REFRESH_INTERVAL + 3.days).ago
|
||||
end
|
||||
end
|
||||
|
||||
def stub_service
|
||||
allow(ResolveAccountService)
|
||||
.to receive(:new)
|
||||
.and_return(service)
|
||||
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 }
|
||||
|
||||
describe 'perform' do
|
||||
it 'runs without error for missing record' do
|
||||
expect { worker.perform(nil) }.to_not raise_error
|
||||
context 'with missing values' do
|
||||
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
|
||||
|
@ -6,11 +6,31 @@ RSpec.describe PushUpdateWorker do
|
||||
let(:worker) { described_class.new }
|
||||
|
||||
describe 'perform' do
|
||||
it 'runs without error for missing record' do
|
||||
account_id = nil
|
||||
status_id = nil
|
||||
context 'with missing values' do
|
||||
it 'runs without error' do
|
||||
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
|
||||
|
38
spec/workers/remote_account_refresh_worker_spec.rb
Normal file
38
spec/workers/remote_account_refresh_worker_spec.rb
Normal file
@ -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
|
||||
let(:worker) { described_class.new }
|
||||
let(:service) { instance_double(RemoveFeaturedTagService, call: true) }
|
||||
|
||||
describe 'perform' do
|
||||
it 'runs without error for missing record' do
|
||||
account_id = nil
|
||||
featured_tag_id = nil
|
||||
expect { worker.perform(account_id, featured_tag_id) }.to_not raise_error
|
||||
context 'with missing values' do
|
||||
it 'runs without error' do
|
||||
expect { worker.perform(nil, nil) }
|
||||
.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
|
||||
|
@ -4,10 +4,34 @@ require 'rails_helper'
|
||||
|
||||
RSpec.describe ResolveAccountWorker do
|
||||
let(:worker) { described_class.new }
|
||||
let(:service) { instance_double(ResolveAccountService, call: true) }
|
||||
|
||||
describe 'perform' do
|
||||
it 'runs without error for missing record' do
|
||||
expect { worker.perform(nil) }.to_not raise_error
|
||||
context 'with missing values' do
|
||||
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
|
||||
|
@ -61,6 +61,7 @@ RSpec.describe Web::PushNotificationWorker do
|
||||
'Ttl' => '172800',
|
||||
'Urgency' => 'normal',
|
||||
'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"
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mastodon/streaming",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"packageManager": "yarn@4.5.0",
|
||||
"packageManager": "yarn@4.5.1",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user