mirror of
https://github.com/glitch-soc/mastodon
synced 2025-04-24 14:14:51 +00:00
Merge commit '678c8dfeec2c221cf609b924161dc03cebfa41e1' into glitch-soc/merge-upstream
This commit is contained in:
commit
4e6c7fba78
29 changed files with 220 additions and 719 deletions
|
@ -395,7 +395,7 @@ GEM
|
|||
rexml
|
||||
link_header (0.0.8)
|
||||
lint_roller (1.1.0)
|
||||
linzer (0.6.4)
|
||||
linzer (0.6.5)
|
||||
openssl (~> 3.0, >= 3.0.0)
|
||||
rack (>= 2.2, < 4.0)
|
||||
starry (~> 0.2)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
|
@ -12,6 +12,7 @@ import {
|
|||
muteAccount,
|
||||
unmuteAccount,
|
||||
} from 'mastodon/actions/accounts';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { initMuteModal } from 'mastodon/actions/mutes';
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
|
@ -23,7 +24,7 @@ import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
|||
import { ShortNumber } from 'mastodon/components/short_number';
|
||||
import { Skeleton } from 'mastodon/components/skeleton';
|
||||
import { VerifiedBadge } from 'mastodon/components/verified_badge';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import type { MenuItem } from 'mastodon/models/dropdown_menu';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -46,6 +47,14 @@ const messages = defineMessages({
|
|||
mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
|
||||
block: { id: 'account.block_short', defaultMessage: 'Block' },
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
addToLists: {
|
||||
id: 'account.add_or_remove_from_list',
|
||||
defaultMessage: 'Add or Remove from lists',
|
||||
},
|
||||
openOriginalPage: {
|
||||
id: 'account.open_original_page',
|
||||
defaultMessage: 'Open original page',
|
||||
},
|
||||
});
|
||||
|
||||
export const Account: React.FC<{
|
||||
|
@ -60,6 +69,7 @@ export const Account: React.FC<{
|
|||
const account = useAppSelector((state) => state.accounts.get(id));
|
||||
const relationship = useAppSelector((state) => state.relationships.get(id));
|
||||
const dispatch = useAppDispatch();
|
||||
const accountUrl = account?.url;
|
||||
|
||||
const handleBlock = useCallback(() => {
|
||||
if (relationship?.blocking) {
|
||||
|
@ -77,13 +87,62 @@ export const Account: React.FC<{
|
|||
}
|
||||
}, [dispatch, id, account, relationship]);
|
||||
|
||||
const handleMuteNotifications = useCallback(() => {
|
||||
dispatch(muteAccount(id, true));
|
||||
}, [dispatch, id]);
|
||||
const menu = useMemo(() => {
|
||||
let arr: MenuItem[] = [];
|
||||
|
||||
const handleUnmuteNotifications = useCallback(() => {
|
||||
dispatch(muteAccount(id, false));
|
||||
}, [dispatch, id]);
|
||||
if (defaultAction === 'mute') {
|
||||
const handleMuteNotifications = () => {
|
||||
dispatch(muteAccount(id, true));
|
||||
};
|
||||
|
||||
const handleUnmuteNotifications = () => {
|
||||
dispatch(muteAccount(id, false));
|
||||
};
|
||||
|
||||
arr = [
|
||||
{
|
||||
text: intl.formatMessage(
|
||||
relationship?.muting_notifications
|
||||
? messages.unmute_notifications
|
||||
: messages.mute_notifications,
|
||||
),
|
||||
action: relationship?.muting_notifications
|
||||
? handleUnmuteNotifications
|
||||
: handleMuteNotifications,
|
||||
},
|
||||
];
|
||||
} else if (defaultAction !== 'block') {
|
||||
const handleAddToLists = () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'LIST_ADDER',
|
||||
modalProps: {
|
||||
accountId: id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
arr = [
|
||||
{
|
||||
text: intl.formatMessage(messages.addToLists),
|
||||
action: handleAddToLists,
|
||||
},
|
||||
];
|
||||
|
||||
if (accountUrl) {
|
||||
arr.unshift(
|
||||
{
|
||||
text: intl.formatMessage(messages.openOriginalPage),
|
||||
href: accountUrl,
|
||||
},
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return arr;
|
||||
}, [dispatch, intl, id, accountUrl, relationship, defaultAction]);
|
||||
|
||||
if (hidden) {
|
||||
return (
|
||||
|
@ -94,68 +153,42 @@ export const Account: React.FC<{
|
|||
);
|
||||
}
|
||||
|
||||
let buttons;
|
||||
let button: React.ReactNode, dropdown: React.ReactNode;
|
||||
|
||||
if (account && account.id !== me && relationship) {
|
||||
const { requested, blocking, muting } = relationship;
|
||||
|
||||
if (requested) {
|
||||
buttons = <FollowButton accountId={id} />;
|
||||
} else if (blocking) {
|
||||
buttons = (
|
||||
<Button
|
||||
text={intl.formatMessage(messages.unblock)}
|
||||
onClick={handleBlock}
|
||||
/>
|
||||
);
|
||||
} else if (muting) {
|
||||
const menu = [
|
||||
{
|
||||
text: intl.formatMessage(
|
||||
relationship.muting_notifications
|
||||
? messages.unmute_notifications
|
||||
: messages.mute_notifications,
|
||||
),
|
||||
action: relationship.muting_notifications
|
||||
? handleUnmuteNotifications
|
||||
: handleMuteNotifications,
|
||||
},
|
||||
];
|
||||
|
||||
buttons = (
|
||||
<>
|
||||
<Dropdown
|
||||
items={menu}
|
||||
icon='ellipsis-h'
|
||||
iconComponent={MoreHorizIcon}
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
text={intl.formatMessage(messages.unmute)}
|
||||
onClick={handleMute}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else if (defaultAction === 'mute') {
|
||||
buttons = (
|
||||
<Button text={intl.formatMessage(messages.mute)} onClick={handleMute} />
|
||||
);
|
||||
} else if (defaultAction === 'block') {
|
||||
buttons = (
|
||||
<Button
|
||||
text={intl.formatMessage(messages.block)}
|
||||
onClick={handleBlock}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
buttons = <FollowButton accountId={id} />;
|
||||
}
|
||||
} else {
|
||||
buttons = <FollowButton accountId={id} />;
|
||||
if (menu.length > 0) {
|
||||
dropdown = (
|
||||
<Dropdown
|
||||
items={menu}
|
||||
icon='ellipsis-h'
|
||||
iconComponent={MoreHorizIcon}
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let muteTimeRemaining;
|
||||
if (defaultAction === 'block') {
|
||||
button = (
|
||||
<Button
|
||||
text={intl.formatMessage(
|
||||
relationship?.blocking ? messages.unblock : messages.block,
|
||||
)}
|
||||
onClick={handleBlock}
|
||||
/>
|
||||
);
|
||||
} else if (defaultAction === 'mute') {
|
||||
button = (
|
||||
<Button
|
||||
text={intl.formatMessage(
|
||||
relationship?.muting ? messages.unmute : messages.mute,
|
||||
)}
|
||||
onClick={handleMute}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
button = <FollowButton accountId={id} />;
|
||||
}
|
||||
|
||||
let muteTimeRemaining: React.ReactNode;
|
||||
|
||||
if (account?.mute_expires_at) {
|
||||
muteTimeRemaining = (
|
||||
|
@ -165,7 +198,7 @@ export const Account: React.FC<{
|
|||
);
|
||||
}
|
||||
|
||||
let verification;
|
||||
let verification: React.ReactNode;
|
||||
|
||||
const firstVerifiedField = account?.fields.find((item) => !!item.verified_at);
|
||||
|
||||
|
@ -211,7 +244,12 @@ export const Account: React.FC<{
|
|||
</div>
|
||||
</Link>
|
||||
|
||||
{!minimal && <div className='account__relationship'>{buttons}</div>}
|
||||
{!minimal && (
|
||||
<div className='account__relationship'>
|
||||
{dropdown}
|
||||
{button}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{account &&
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
"account.statuses_counter": "{count, plural, one {{counter} فرسته} other {{counter} فرسته}}",
|
||||
"account.unblock": "رفع مسدودیت @{name}",
|
||||
"account.unblock_domain": "رفع مسدودیت دامنهٔ {domain}",
|
||||
"account.unblock_domain_short": "آنبلاک",
|
||||
"account.unblock_short": "رفع مسدودیت",
|
||||
"account.unendorse": "معرّفی نکردن در نمایه",
|
||||
"account.unfollow": "پینگرفتن",
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
"account.statuses_counter": "{count, plural, one {{counter} publicação} other {{counter} publicações}}",
|
||||
"account.unblock": "Desbloquear @{name}",
|
||||
"account.unblock_domain": "Desbloquear domínio {domain}",
|
||||
"account.unblock_domain_short": "Desbloquear",
|
||||
"account.unblock_short": "Desbloquear",
|
||||
"account.unendorse": "Remover",
|
||||
"account.unfollow": "Deixar de seguir",
|
||||
|
|
|
@ -45,7 +45,6 @@ class Admin::Metrics::Dimension::SpaceUsageDimension < Admin::Metrics::Dimension
|
|||
PreviewCard.sum(:image_file_size),
|
||||
Account.sum(Arel.sql('COALESCE(avatar_file_size, 0) + COALESCE(header_file_size, 0)')),
|
||||
Backup.sum(:dump_file_size),
|
||||
Import.sum(:data_file_size),
|
||||
SiteUpload.sum(:file_file_size),
|
||||
].sum
|
||||
|
||||
|
|
|
@ -26,12 +26,7 @@ class StatusCacheHydrator
|
|||
|
||||
def hydrate_non_reblog_payload(empty_payload, account_id)
|
||||
empty_payload.tap do |payload|
|
||||
payload[:favourited] = Favourite.exists?(account_id: account_id, status_id: @status.id)
|
||||
payload[:reblogged] = Status.exists?(account_id: account_id, reblog_of_id: @status.id)
|
||||
payload[:muted] = ConversationMute.exists?(account_id: account_id, conversation_id: @status.conversation_id)
|
||||
payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: @status.id)
|
||||
payload[:pinned] = StatusPin.exists?(account_id: account_id, status_id: @status.id) if @status.account_id == account_id
|
||||
payload[:filtered] = mapped_applied_custom_filter(account_id, @status)
|
||||
fill_status_payload(payload, @status, account_id)
|
||||
|
||||
if payload[:poll]
|
||||
payload[:poll][:voted] = @status.account_id == account_id
|
||||
|
@ -45,18 +40,12 @@ class StatusCacheHydrator
|
|||
payload[:muted] = false
|
||||
payload[:bookmarked] = false
|
||||
payload[:pinned] = false if @status.account_id == account_id
|
||||
payload[:filtered] = mapped_applied_custom_filter(account_id, @status.reblog)
|
||||
|
||||
# If the reblogged status is being delivered to the author who disabled the display of the application
|
||||
# used to create the status, we need to hydrate it here too
|
||||
payload[:reblog][:application] = payload_reblog_application if payload[:reblog][:application].nil? && @status.reblog.account_id == account_id
|
||||
|
||||
payload[:reblog][:favourited] = Favourite.exists?(account_id: account_id, status_id: @status.reblog_of_id)
|
||||
payload[:reblog][:reblogged] = Status.exists?(account_id: account_id, reblog_of_id: @status.reblog_of_id)
|
||||
payload[:reblog][:muted] = ConversationMute.exists?(account_id: account_id, conversation_id: @status.reblog.conversation_id)
|
||||
payload[:reblog][:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: @status.reblog_of_id)
|
||||
payload[:reblog][:pinned] = StatusPin.exists?(account_id: account_id, status_id: @status.reblog_of_id) if @status.reblog.account_id == account_id
|
||||
payload[:reblog][:filtered] = payload[:filtered]
|
||||
fill_status_payload(payload[:reblog], @status.reblog, account_id)
|
||||
|
||||
if payload[:reblog][:poll]
|
||||
if @status.reblog.account_id == account_id
|
||||
|
@ -69,11 +58,21 @@ class StatusCacheHydrator
|
|||
end
|
||||
end
|
||||
|
||||
payload[:filtered] = payload[:reblog][:filtered]
|
||||
payload[:favourited] = payload[:reblog][:favourited]
|
||||
payload[:reblogged] = payload[:reblog][:reblogged]
|
||||
end
|
||||
end
|
||||
|
||||
def fill_status_payload(payload, status, account_id)
|
||||
payload[:favourited] = Favourite.exists?(account_id: account_id, status_id: status.id)
|
||||
payload[:reblogged] = Status.exists?(account_id: account_id, reblog_of_id: status.id)
|
||||
payload[:muted] = ConversationMute.exists?(account_id: account_id, conversation_id: status.conversation_id)
|
||||
payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: status.id)
|
||||
payload[:pinned] = StatusPin.exists?(account_id: account_id, status_id: status.id) if status.account_id == account_id
|
||||
payload[:filtered] = mapped_applied_custom_filter(account_id, status)
|
||||
end
|
||||
|
||||
def mapped_applied_custom_filter(account_id, status)
|
||||
CustomFilter
|
||||
.apply_cached_filters(CustomFilter.cached_filters_for(account_id), status)
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: imports
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# type :integer not null
|
||||
# approved :boolean default(FALSE), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# data_file_name :string
|
||||
# data_content_type :string
|
||||
# data_file_size :integer
|
||||
# data_updated_at :datetime
|
||||
# account_id :bigint(8) not null
|
||||
# overwrite :boolean default(FALSE), not null
|
||||
#
|
||||
|
||||
# NOTE: This is a deprecated model, only kept to not break ongoing imports
|
||||
# on upgrade. See `BulkImport` and `Form::Import` for its replacements.
|
||||
|
||||
class Import < ApplicationRecord
|
||||
FILE_TYPES = %w(text/plain text/csv application/csv).freeze
|
||||
MODES = %i(merge overwrite).freeze
|
||||
|
||||
self.inheritance_column = false
|
||||
|
||||
belongs_to :account
|
||||
|
||||
enum :type, { following: 0, blocking: 1, muting: 2, domain_blocking: 3, bookmarks: 4 }
|
||||
|
||||
validates :type, presence: true
|
||||
|
||||
has_attached_file :data
|
||||
validates_attachment_content_type :data, content_type: FILE_TYPES
|
||||
validates_attachment_presence :data
|
||||
|
||||
def mode
|
||||
overwrite? ? :overwrite : :merge
|
||||
end
|
||||
|
||||
def mode=(str)
|
||||
self.overwrite = str.to_sym == :overwrite
|
||||
end
|
||||
end
|
|
@ -1,144 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'csv'
|
||||
|
||||
# NOTE: This is a deprecated service, only kept to not break ongoing imports
|
||||
# on upgrade. See `BulkImportService` for its replacement.
|
||||
|
||||
class ImportService < BaseService
|
||||
ROWS_PROCESSING_LIMIT = 20_000
|
||||
|
||||
def call(import)
|
||||
@import = import
|
||||
@account = @import.account
|
||||
|
||||
case @import.type
|
||||
when 'following'
|
||||
import_follows!
|
||||
when 'blocking'
|
||||
import_blocks!
|
||||
when 'muting'
|
||||
import_mutes!
|
||||
when 'domain_blocking'
|
||||
import_domain_blocks!
|
||||
when 'bookmarks'
|
||||
import_bookmarks!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def import_follows!
|
||||
parse_import_data!(['Account address'])
|
||||
import_relationships!('follow', 'unfollow', @account.following, ROWS_PROCESSING_LIMIT, reblogs: { header: 'Show boosts', default: true }, notify: { header: 'Notify on new posts', default: false }, languages: { header: 'Languages', default: nil })
|
||||
end
|
||||
|
||||
def import_blocks!
|
||||
parse_import_data!(['Account address'])
|
||||
import_relationships!('block', 'unblock', @account.blocking, ROWS_PROCESSING_LIMIT)
|
||||
end
|
||||
|
||||
def import_mutes!
|
||||
parse_import_data!(['Account address'])
|
||||
import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT, notifications: { header: 'Hide notifications', default: true })
|
||||
end
|
||||
|
||||
def import_domain_blocks!
|
||||
parse_import_data!(['#domain'])
|
||||
items = @data.take(ROWS_PROCESSING_LIMIT).map { |row| row['#domain'].strip }
|
||||
|
||||
if @import.overwrite?
|
||||
presence_hash = items.index_with(true)
|
||||
|
||||
@account.domain_blocks.find_each do |domain_block|
|
||||
if presence_hash[domain_block.domain]
|
||||
items.delete(domain_block.domain)
|
||||
else
|
||||
@account.unblock_domain!(domain_block.domain)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
items.each do |domain|
|
||||
@account.block_domain!(domain)
|
||||
end
|
||||
|
||||
AfterAccountDomainBlockWorker.push_bulk(items) do |domain|
|
||||
[@account.id, domain]
|
||||
end
|
||||
end
|
||||
|
||||
def import_relationships!(action, undo_action, overwrite_scope, limit, extra_fields = {})
|
||||
local_domain_suffix = "@#{Rails.configuration.x.local_domain}"
|
||||
items = @data.take(limit).map { |row| [row['Account address']&.strip&.delete_suffix(local_domain_suffix), extra_fields.to_h { |key, field_settings| [key, row[field_settings[:header]]&.strip || field_settings[:default]] }] }.reject { |(id, _)| id.blank? }
|
||||
|
||||
if @import.overwrite?
|
||||
presence_hash = items.each_with_object({}) { |(id, extra), mapping| mapping[id] = [true, extra] }
|
||||
|
||||
overwrite_scope.reorder(nil).find_each do |target_account|
|
||||
if presence_hash[target_account.acct]
|
||||
items.delete(target_account.acct)
|
||||
extra = presence_hash[target_account.acct][1]
|
||||
Import::RelationshipWorker.perform_async(@account.id, target_account.acct, action, extra.stringify_keys)
|
||||
else
|
||||
Import::RelationshipWorker.perform_async(@account.id, target_account.acct, undo_action)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
head_items = items.uniq { |acct, _| acct.split('@')[1] }
|
||||
tail_items = items - head_items
|
||||
|
||||
Import::RelationshipWorker.push_bulk(head_items + tail_items) do |acct, extra|
|
||||
[@account.id, acct, action, extra.stringify_keys]
|
||||
end
|
||||
end
|
||||
|
||||
def import_bookmarks!
|
||||
parse_import_data!(['#uri'])
|
||||
items = @data.take(ROWS_PROCESSING_LIMIT).map { |row| row['#uri'].strip }
|
||||
|
||||
if @import.overwrite?
|
||||
presence_hash = items.index_with(true)
|
||||
|
||||
@account.bookmarks.find_each do |bookmark|
|
||||
if presence_hash[bookmark.status.uri]
|
||||
items.delete(bookmark.status.uri)
|
||||
else
|
||||
bookmark.destroy!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
statuses = items.filter_map do |uri|
|
||||
status = ActivityPub::TagManager.instance.uri_to_resource(uri, Status)
|
||||
next if status.nil? && ActivityPub::TagManager.instance.local_uri?(uri)
|
||||
|
||||
status || ActivityPub::FetchRemoteStatusService.new.call(uri)
|
||||
rescue *Mastodon::HTTP_CONNECTION_ERRORS, Mastodon::UnexpectedResponseError
|
||||
nil
|
||||
rescue => e
|
||||
Rails.logger.warn "Unexpected error when importing bookmark: #{e}"
|
||||
nil
|
||||
end
|
||||
|
||||
account_ids = statuses.map(&:account_id)
|
||||
preloaded_relations = @account.relations_map(account_ids, skip_blocking_and_muting: true)
|
||||
|
||||
statuses.keep_if { |status| StatusPolicy.new(@account, status, preloaded_relations).show? }
|
||||
|
||||
statuses.each do |status|
|
||||
@account.bookmarks.find_or_create_by!(account: @account, status: status)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_import_data!(default_headers)
|
||||
data = CSV.parse(import_data, headers: true)
|
||||
data = CSV.parse(import_data, headers: default_headers) unless data.headers&.first&.strip&.include?(' ')
|
||||
@data = data.compact_blank
|
||||
end
|
||||
|
||||
def import_data
|
||||
Paperclip.io_adapters.for(@import.data).read.force_encoding(Encoding::UTF_8)
|
||||
end
|
||||
end
|
|
@ -1,57 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# NOTE: This is a deprecated worker, only kept to not break ongoing imports
|
||||
# on upgrade. See `Import::RowWorker` for its replacement.
|
||||
|
||||
class Import::RelationshipWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'pull', retry: 8, dead: false
|
||||
|
||||
def perform(account_id, target_account_uri, relationship, options)
|
||||
from_account = Account.find(account_id)
|
||||
target_domain = domain(target_account_uri)
|
||||
target_account = stoplight_wrapper(target_domain).run { ResolveAccountService.new.call(target_account_uri, { check_delivery_availability: true }) }
|
||||
options.symbolize_keys!
|
||||
|
||||
return if target_account.nil?
|
||||
|
||||
case relationship
|
||||
when 'follow'
|
||||
begin
|
||||
FollowService.new.call(from_account, target_account, **options)
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
raise if FollowLimitValidator.limit_for_account(from_account) < from_account.following_count
|
||||
end
|
||||
when 'unfollow'
|
||||
UnfollowService.new.call(from_account, target_account)
|
||||
when 'block'
|
||||
BlockService.new.call(from_account, target_account)
|
||||
when 'unblock'
|
||||
UnblockService.new.call(from_account, target_account)
|
||||
when 'mute'
|
||||
MuteService.new.call(from_account, target_account, **options)
|
||||
when 'unmute'
|
||||
UnmuteService.new.call(from_account, target_account)
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
|
||||
def domain(uri)
|
||||
domain = uri.is_a?(Account) ? uri.domain : uri.split('@')[1]
|
||||
TagManager.instance.local_domain?(domain) ? nil : TagManager.instance.normalize_domain(domain)
|
||||
end
|
||||
|
||||
def stoplight_wrapper(domain)
|
||||
if domain.present?
|
||||
Stoplight("source:#{domain}")
|
||||
.with_fallback { nil }
|
||||
.with_threshold(1)
|
||||
.with_cool_off_time(5.minutes.seconds)
|
||||
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) }
|
||||
else
|
||||
Stoplight('domain-blank')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,17 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# NOTE: This is a deprecated worker, only kept to not break ongoing imports
|
||||
# on upgrade. See `ImportWorker` for its replacement.
|
||||
|
||||
class ImportWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'pull', retry: false
|
||||
|
||||
def perform(import_id)
|
||||
import = Import.find(import_id)
|
||||
ImportService.new.call(import)
|
||||
ensure
|
||||
import&.destroy
|
||||
end
|
||||
end
|
|
@ -4,25 +4,14 @@ class UnfilterNotificationsWorker
|
|||
include Sidekiq::Worker
|
||||
include Redisable
|
||||
|
||||
# Earlier versions of the feature passed a `notification_request` ID
|
||||
# If `to_account_id` is passed, the first argument is an account ID
|
||||
# TODO for after 4.3.0: drop the single-argument case
|
||||
def perform(notification_request_or_account_id, from_account_id = nil)
|
||||
if from_account_id.present?
|
||||
@notification_request = nil
|
||||
@from_account = Account.find_by(id: from_account_id)
|
||||
@recipient = Account.find_by(id: notification_request_or_account_id)
|
||||
else
|
||||
@notification_request = NotificationRequest.find_by(id: notification_request_or_account_id)
|
||||
@from_account = @notification_request&.from_account
|
||||
@recipient = @notification_request&.account
|
||||
end
|
||||
def perform(account_id, from_account_id)
|
||||
@from_account = Account.find_by(id: from_account_id)
|
||||
@recipient = Account.find_by(id: account_id)
|
||||
|
||||
return if @from_account.nil? || @recipient.nil?
|
||||
|
||||
push_to_conversations!
|
||||
unfilter_notifications!
|
||||
remove_request!
|
||||
decrement_worker_count!
|
||||
end
|
||||
|
||||
|
@ -36,10 +25,6 @@ class UnfilterNotificationsWorker
|
|||
filtered_notifications.in_batches.update_all(filtered: false)
|
||||
end
|
||||
|
||||
def remove_request!
|
||||
@notification_request&.destroy!
|
||||
end
|
||||
|
||||
def filtered_notifications
|
||||
Notification.where(account: @recipient, from_account: @from_account, filtered: true)
|
||||
end
|
||||
|
|
|
@ -169,7 +169,7 @@ else
|
|||
end
|
||||
|
||||
Rails.application.reloader.to_prepare do
|
||||
Paperclip.options[:content_type_mappings] = { csv: Import::FILE_TYPES }
|
||||
Paperclip.options[:content_type_mappings] = { csv: %w(text/plain text/csv application/csv) }
|
||||
end
|
||||
|
||||
# In some places in the code, we rescue this exception, but we don't always
|
||||
|
|
|
@ -479,6 +479,22 @@ fa:
|
|||
new:
|
||||
title: درونریزی انسدادهای دامنه
|
||||
no_file: هیچ پروندهای گزیده نشده
|
||||
fasp:
|
||||
debug:
|
||||
callbacks:
|
||||
delete: حذف
|
||||
providers:
|
||||
active: فعال
|
||||
delete: حذف
|
||||
finish_registration: تکمیل ثبتنام
|
||||
name: نام
|
||||
providers: ارائه دهندگان
|
||||
registrations:
|
||||
confirm: تایید
|
||||
reject: رد کردن
|
||||
save: ذخیره
|
||||
sign_in: ورود
|
||||
status: وضعیت
|
||||
follow_recommendations:
|
||||
description_html: "<strong>پیشنهادات پیگیری به کاربران جدید کک میکند تا سریعتر محتوای جالب را پیدا کنند</strong>. زمانی که کاربری هنوز به اندازه کافی با دیگران تعامل نداشته است تا پیشنهادات پیگیری شخصیسازیشده دریافت کند، این حسابها را به جای آن فهرست مشاهده خواهد کرد. این حسابها به صورت روزانه و در ترکیب با بیشتری تعاملات و بالاترین دنبالکنندگان محلی برای یک زبان مشخص بازمحاسبه میشوند."
|
||||
language: برای زبان
|
||||
|
|
|
@ -259,6 +259,7 @@ lv:
|
|||
create_user_role_html: "%{name} nomainīja %{target} lomu"
|
||||
demote_user_html: "%{name} pazemināja lietotāju %{target}"
|
||||
destroy_announcement_html: "%{name} izdzēsa paziņojumu %{target}"
|
||||
destroy_canonical_email_block_html: "%{name} atcēla e-pasta adreses liegumu ar jaucējvērtību %{target}"
|
||||
destroy_custom_emoji_html: "%{name} izdzēsa emocijzīmi %{target}"
|
||||
destroy_domain_allow_html: "%{name} neatļāva federāciju ar domēnu %{target}"
|
||||
destroy_domain_block_html: "%{name} atbloķēja domēnu %{target}"
|
||||
|
@ -409,7 +410,7 @@ lv:
|
|||
permanent_action: Apturēšanas atsaukšana neatjaunos nekādus datus vai attiecības.
|
||||
preamble_html: Tu gatavojies apturēt domēna <strong>%{domain}</strong> un tā apakšdomēnu darbību.
|
||||
remove_all_data: Tādējādi no tava servera tiks noņemts viss šī domēna kontu saturs, multivide un profila dati.
|
||||
stop_communication: Jūsu serveris pārtrauks sazināties ar šiem serveriem.
|
||||
stop_communication: Tavs serveris pārtrauks sazināties ar šiem serveriem.
|
||||
title: Apstiprināt domēna %{domain} bloķēšanu
|
||||
undo_relationships: Tādējādi tiks atsauktas jebkuras sekošanas attiecības starp šo un tavu serveru kontiem.
|
||||
created_msg: Domēna bloķēšana tagad tiek apstrādāta
|
||||
|
@ -949,11 +950,13 @@ lv:
|
|||
message_html: "<strong>Tava objektu krātuve ir nepareizi konfigurēta. Tavu lietotāju privātums ir apdraudēts.</strong>"
|
||||
tags:
|
||||
moderation:
|
||||
not_trendable: Nav izplatīts
|
||||
not_usable: Nav izmantojams
|
||||
pending_review: Gaida pārskatīšanu
|
||||
review_requested: Pieprasīta pārskatīšana
|
||||
reviewed: Pārskatīts
|
||||
title: Stāvoklis
|
||||
trendable: Izplatīts
|
||||
unreviewed: Nepārskatīts
|
||||
usable: Izmantojams
|
||||
name: Nosaukums
|
||||
|
@ -1312,7 +1315,7 @@ lv:
|
|||
appeal_approved: Šis brīdinājums tika sekmīgi pārsūdzēts un vairs nav spēkā
|
||||
appeal_rejected: Apelācija ir noraidīta
|
||||
appeal_submitted_at: Apelācija iesniegta
|
||||
appealed_msg: Jūsu apelācija ir iesniegta. Ja tā tiks apstiprināta, jums tiks paziņots.
|
||||
appealed_msg: Tava pārsūdzība ir iesniegta. Ja tā tiks apstiprināta, Tev tiks paziņots.
|
||||
appeals:
|
||||
submit: Iesniegt apelāciju
|
||||
approve_appeal: Apstiprināt apelāciju
|
||||
|
@ -1332,9 +1335,9 @@ lv:
|
|||
sensitive: Konta atzīmēšana kā jūtīgu
|
||||
silence: Konta ierobežošana
|
||||
suspend: Konta apturēšana
|
||||
your_appeal_approved: Jūsu apelācija ir apstiprināta
|
||||
your_appeal_approved: Tava pārsūdzība tika apstiprināta
|
||||
your_appeal_pending: Jūs esat iesniedzis apelāciju
|
||||
your_appeal_rejected: Jūsu apelācija ir noraidīta
|
||||
your_appeal_rejected: Tava pārsūdzība tika noraidīta
|
||||
edit_profile:
|
||||
basic_information: Pamata informācija
|
||||
hint_html: "<strong>Pielāgo, ko cilvēki redz Tavā publiskajā profilā un blakus Taviem ierakstiem.</strong> Ir lielāka iespējamība, ka citi clivēki sekos Tev un mijiedarbosies ar Tevi, ja Tev ir aizpildīts profils un profila attēls."
|
||||
|
@ -1929,7 +1932,7 @@ lv:
|
|||
'7889238': 3 mēneši
|
||||
min_age_label: Vecuma slieksnis
|
||||
min_favs: Saglabāt ziņas izlsasē vismaz
|
||||
min_favs_hint: Nedzēš nevienu jūsu ziņu, kas ir saņēmusi vismaz tik daudz izcēlumu. Atstājiet tukšu, lai dzēstu ziņas neatkarīgi no to izcēlumu skaita
|
||||
min_favs_hint: Neizdzēš nevienu no Taviem ierakstiem, kas ir pievienoti šādā daudzumā izlašu. Atstāt tukšu, lai izdzēstu ierakstus neatkarīgi no tā, cik izlasēs tie ir pievienoti
|
||||
min_reblogs: Saglabāt ziņas izceltas vismaz
|
||||
min_reblogs_hint: Neizdzēš nevienu no tavām ziņām, kas ir izceltas vismaz tik reižu. Atstāj tukšu, lai dzēstu ziņas neatkarīgi no to izcēlumu skaita
|
||||
stream_entries:
|
||||
|
@ -1978,12 +1981,13 @@ lv:
|
|||
title: "%{domain} pakalpojuma paziņojums"
|
||||
appeal_approved:
|
||||
action: Konta iestatījumi
|
||||
explanation: Apelācija par brīdinājumu jūsu kontam %{strike_date}, ko iesniedzāt %{appeal_date}, ir apstiprināta. Jūsu konts atkal ir labā stāvoklī.
|
||||
subject: Jūsu %{date} apelācija ir apstiprināta
|
||||
explanation: Pārsūdzība par brīdinājumu Tavam kontam %{strike_date}, ko iesniedzi %{appeal_date}, ir apstiprināta. Tavs konts atkal ir labā stāvoklī.
|
||||
subject: Tava %{date} iesniegtā pārsūdzība tika apstiprināta
|
||||
title: Apelācija apstiprināta
|
||||
appeal_rejected:
|
||||
explanation: Apelācija par brīdinājumu jūsu kontam %{strike_date}, ko iesniedzāt %{appeal_date}, ir noraidīta.
|
||||
subject: Jūsu %{date} apelācija ir noraidīta
|
||||
explanation: Pārsūdzība par brīdinājumu Tavam kontam %{strike_date}, ko iesniedzi %{appeal_date}, tika noraidīta.
|
||||
subject: Tava %{date} iesniegta pārsūdzība tika noraidīta
|
||||
subtitle: Tava pārsūdzība tika noraidīta.
|
||||
title: Apelācija noraidīta
|
||||
backup_ready:
|
||||
explanation: Tu pieprasīji pilnu sava Mastodon konta rezerves kopiju.
|
||||
|
@ -1993,6 +1997,9 @@ lv:
|
|||
failed_2fa:
|
||||
details: 'Šeit ir informācija par pieteikšanās mēģinājumu:'
|
||||
explanation: Kāds mēģināja pieteikties Tavā kontā, bet norādīja nederīgu otro autentificēšanās soli.
|
||||
further_actions_html: Ja tas nebiji Tu, mēs iesakām nekavējoties %{action}, jo var būt noticis drošības pārkāpums.
|
||||
subject: Otrās pakāpes autentificēšanās atteice
|
||||
title: Neizdevās otrās pakāpes autentificēšanās
|
||||
suspicious_sign_in:
|
||||
change_password: mainīt paroli
|
||||
details: 'Šeit ir pieteikšanās izvērsums:'
|
||||
|
|
|
@ -320,6 +320,7 @@ pt-BR:
|
|||
title: Novo anúncio
|
||||
preview:
|
||||
explanation_html: 'Esse e-mail será enviado a <strong>%{display_count} usuários</strong>. O texto a seguir será incluído ao e-mail:'
|
||||
title: Visualizar anúncio
|
||||
publish: Publicar
|
||||
published_msg: Anúncio publicado!
|
||||
scheduled_for: Agendado para %{time}
|
||||
|
@ -484,19 +485,30 @@ pt-BR:
|
|||
created_at: Criado em
|
||||
delete: Apagar
|
||||
ip: Endereço de IP
|
||||
request_body: Corpo da solicitação
|
||||
title: Depurar Callbacks
|
||||
providers:
|
||||
active: Ativo
|
||||
base_url: URL Base
|
||||
callback: Callback
|
||||
delete: Apagar
|
||||
edit: Editar provedor
|
||||
finish_registration: Finalizar o cadastro
|
||||
name: Nome
|
||||
providers: Provedores
|
||||
public_key_fingerprint: Impressão digital de chave pública
|
||||
registration_requested: Cadastro solicitado
|
||||
registrations:
|
||||
confirm: Confirmar
|
||||
description: Você recebeu um registro de um FASP. Rejeite se você não tiver iniciado isso. Se você iniciou isso, compare cuidadosamente o nome e a impressão digital da chave antes de confirmar o registro.
|
||||
reject: Rejeitar
|
||||
title: Confirmar o registro FASP
|
||||
save: Salvar
|
||||
select_capabilities: Selecionar recursos
|
||||
sign_in: Entrar
|
||||
status: Estado
|
||||
title: Provedores de serviços auxiliares do Fediverso
|
||||
title: FASP
|
||||
follow_recommendations:
|
||||
description_html: "<strong>A recomendação de contas ajuda os novos usuários a encontrar rapidamente conteúdo interessante</strong>. Quando um usuário ainda não tiver interagido o suficiente para gerar recomendações de contas, essas contas serão recomendadas. Essas recomendações são recalculadas diariamente a partir de uma lista de contas com alto engajamento e maior número de seguidores locais em uma dada língua."
|
||||
language: Na língua
|
||||
|
@ -1927,6 +1939,10 @@ pt-BR:
|
|||
recovery_instructions_html: Se você perder acesso ao seu celular, você pode usar um dos códigos de recuperação abaixo para acessar a sua conta. <strong>Mantenha os códigos de recuperação em um local seguro</strong>. Por exemplo, você pode imprimi-los e guardá-los junto a outros documentos importantes.
|
||||
webauthn: Chaves de segurança
|
||||
user_mailer:
|
||||
announcement_published:
|
||||
description: 'Os administradores do %{domain} estão fazendo um anúncio:'
|
||||
subject: Anúncio de serviço
|
||||
title: Anúncio de serviço de %{domain}
|
||||
appeal_approved:
|
||||
action: Configurações da conta
|
||||
explanation: A revisão da punição na sua conta em %{strike_date} que você enviou em %{appeal_date} foi aprovada. Sua conta está novamente em situação regular.
|
||||
|
|
|
@ -349,6 +349,9 @@ fa:
|
|||
jurisdiction: صلاحیت قانونی
|
||||
min_age: کمینهٔ زمان
|
||||
user:
|
||||
date_of_birth_1i: روز
|
||||
date_of_birth_2i: ماه
|
||||
date_of_birth_3i: سال
|
||||
role: نقش
|
||||
time_zone: منطقهٔ زمانی
|
||||
user_role:
|
||||
|
|
11
db/migrate/20250410144908_drop_imports.rb
Normal file
11
db/migrate/20250410144908_drop_imports.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DropImports < ActiveRecord::Migration[7.1]
|
||||
def up
|
||||
drop_table :imports
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
16
db/schema.rb
16
db/schema.rb
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_03_13_123400) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_04_10_144908) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
|
||||
|
@ -555,19 +555,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_13_123400) do
|
|||
t.index ["user_id"], name: "index_identities_on_user_id"
|
||||
end
|
||||
|
||||
create_table "imports", force: :cascade do |t|
|
||||
t.integer "type", null: false
|
||||
t.boolean "approved", default: false, null: false
|
||||
t.datetime "created_at", precision: nil, null: false
|
||||
t.datetime "updated_at", precision: nil, null: false
|
||||
t.string "data_file_name"
|
||||
t.string "data_content_type"
|
||||
t.integer "data_file_size"
|
||||
t.datetime "data_updated_at", precision: nil
|
||||
t.bigint "account_id", null: false
|
||||
t.boolean "overwrite", default: false, null: false
|
||||
end
|
||||
|
||||
create_table "invites", force: :cascade do |t|
|
||||
t.bigint "user_id", null: false
|
||||
t.string "code", default: "", null: false
|
||||
|
@ -1332,7 +1319,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_13_123400) do
|
|||
add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade
|
||||
add_foreign_key "generated_annual_reports", "accounts"
|
||||
add_foreign_key "identities", "users", name: "fk_bea040f377", on_delete: :cascade
|
||||
add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade
|
||||
add_foreign_key "invites", "users", on_delete: :cascade
|
||||
add_foreign_key "list_accounts", "accounts", on_delete: :cascade
|
||||
add_foreign_key "list_accounts", "follow_requests", on_delete: :cascade
|
||||
|
|
|
@ -293,7 +293,6 @@ module Mastodon::CLI
|
|||
Account
|
||||
Backup
|
||||
CustomEmoji
|
||||
Import
|
||||
MediaAttachment
|
||||
PreviewCard
|
||||
SiteUpload
|
||||
|
@ -309,7 +308,6 @@ module Mastodon::CLI
|
|||
[:headers, Account.sum(:header_file_size), Account.local.sum(:header_file_size)],
|
||||
[:preview_cards, PreviewCard.sum(:image_file_size), nil],
|
||||
[:backups, Backup.sum(:dump_file_size), nil],
|
||||
[:imports, Import.sum(:data_file_size), nil],
|
||||
[:settings, SiteUpload.sum(:file_file_size), nil],
|
||||
].map { |label, total, local| [label.to_s.titleize, number_to_human_size(total), local.present? ? number_to_human_size(local) : nil] }
|
||||
end
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Fabricator(:import) do
|
||||
account
|
||||
type :following
|
||||
data { attachment_fixture('imports.txt') }
|
||||
end
|
|
@ -1,10 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Import do
|
||||
describe 'Validations' do
|
||||
it { is_expected.to validate_presence_of(:type) }
|
||||
it { is_expected.to validate_presence_of(:data) }
|
||||
end
|
||||
end
|
|
@ -1,242 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ImportService, :inline_jobs do
|
||||
include RoutingHelper
|
||||
|
||||
let!(:account) { Fabricate(:account, locked: false) }
|
||||
let!(:bob) { Fabricate(:account, username: 'bob', locked: false) }
|
||||
let!(:eve) { Fabricate(:account, username: 'eve', domain: 'example.com', locked: false, protocol: :activitypub, inbox_url: 'https://example.com/inbox') }
|
||||
|
||||
before do
|
||||
stub_request(:post, 'https://example.com/inbox').to_return(status: 200)
|
||||
end
|
||||
|
||||
context 'when importing old-style list of muted users' do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:csv) { attachment_fixture('mute-imports.txt') }
|
||||
|
||||
describe 'when no accounts are muted' do
|
||||
let(:import) { Import.create(account: account, type: 'muting', data: csv) }
|
||||
|
||||
it 'mutes the listed accounts, including notifications' do
|
||||
subject.call(import)
|
||||
expect(account.muting.count).to eq 2
|
||||
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when some accounts are muted and overwrite is not set' do
|
||||
let(:import) { Import.create(account: account, type: 'muting', data: csv) }
|
||||
|
||||
it 'mutes the listed accounts, including notifications' do
|
||||
account.mute!(bob, notifications: false)
|
||||
subject.call(import)
|
||||
expect(account.muting.count).to eq 2
|
||||
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when some accounts are muted and overwrite is set' do
|
||||
let(:import) { Import.create(account: account, type: 'muting', data: csv, overwrite: true) }
|
||||
|
||||
it 'mutes the listed accounts, including notifications' do
|
||||
account.mute!(bob, notifications: false)
|
||||
subject.call(import)
|
||||
expect(account.muting.count).to eq 2
|
||||
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when importing new-style list of muted users' do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:csv) { attachment_fixture('new-mute-imports.txt') }
|
||||
|
||||
describe 'when no accounts are muted' do
|
||||
let(:import) { Import.create(account: account, type: 'muting', data: csv) }
|
||||
|
||||
it 'mutes the listed accounts, respecting notifications' do
|
||||
subject.call(import)
|
||||
expect(account.muting.count).to eq 2
|
||||
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
|
||||
expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when some accounts are muted and overwrite is not set' do
|
||||
let(:import) { Import.create(account: account, type: 'muting', data: csv) }
|
||||
|
||||
it 'mutes the listed accounts, respecting notifications' do
|
||||
account.mute!(bob, notifications: true)
|
||||
subject.call(import)
|
||||
expect(account.muting.count).to eq 2
|
||||
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
|
||||
expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when some accounts are muted and overwrite is set' do
|
||||
let(:import) { Import.create(account: account, type: 'muting', data: csv, overwrite: true) }
|
||||
|
||||
it 'mutes the listed accounts, respecting notifications' do
|
||||
account.mute!(bob, notifications: true)
|
||||
subject.call(import)
|
||||
expect(account.muting.count).to eq 2
|
||||
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
|
||||
expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when importing old-style list of followed users' do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:csv) { attachment_fixture('mute-imports.txt') }
|
||||
|
||||
describe 'when no accounts are followed' do
|
||||
let(:import) { Import.create(account: account, type: 'following', data: csv) }
|
||||
|
||||
it 'follows the listed accounts, including boosts' do
|
||||
subject.call(import)
|
||||
|
||||
expect(account.following.count).to eq 1
|
||||
expect(account.follow_requests.count).to eq 1
|
||||
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when some accounts are already followed and overwrite is not set' do
|
||||
let(:import) { Import.create(account: account, type: 'following', data: csv) }
|
||||
|
||||
it 'follows the listed accounts, including notifications' do
|
||||
account.follow!(bob, reblogs: false)
|
||||
subject.call(import)
|
||||
expect(account.following.count).to eq 1
|
||||
expect(account.follow_requests.count).to eq 1
|
||||
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when some accounts are already followed and overwrite is set' do
|
||||
let(:import) { Import.create(account: account, type: 'following', data: csv, overwrite: true) }
|
||||
|
||||
it 'mutes the listed accounts, including notifications' do
|
||||
account.follow!(bob, reblogs: false)
|
||||
subject.call(import)
|
||||
expect(account.following.count).to eq 1
|
||||
expect(account.follow_requests.count).to eq 1
|
||||
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when importing new-style list of followed users' do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:csv) { attachment_fixture('new-following-imports.txt') }
|
||||
|
||||
describe 'when no accounts are followed' do
|
||||
let(:import) { Import.create(account: account, type: 'following', data: csv) }
|
||||
|
||||
it 'follows the listed accounts, respecting boosts' do
|
||||
subject.call(import)
|
||||
expect(account.following.count).to eq 1
|
||||
expect(account.follow_requests.count).to eq 1
|
||||
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
||||
expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when some accounts are already followed and overwrite is not set' do
|
||||
let(:import) { Import.create(account: account, type: 'following', data: csv) }
|
||||
|
||||
it 'mutes the listed accounts, respecting notifications' do
|
||||
account.follow!(bob, reblogs: true)
|
||||
subject.call(import)
|
||||
expect(account.following.count).to eq 1
|
||||
expect(account.follow_requests.count).to eq 1
|
||||
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
||||
expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when some accounts are already followed and overwrite is set' do
|
||||
let(:import) { Import.create(account: account, type: 'following', data: csv, overwrite: true) }
|
||||
|
||||
it 'mutes the listed accounts, respecting notifications' do
|
||||
account.follow!(bob, reblogs: true)
|
||||
subject.call(import)
|
||||
expect(account.following.count).to eq 1
|
||||
expect(account.follow_requests.count).to eq 1
|
||||
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
||||
expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Based on the bug report 20571 where UTF-8 encoded domains were rejecting import of their users
|
||||
#
|
||||
# https://github.com/mastodon/mastodon/issues/20571
|
||||
context 'with a utf-8 encoded domains' do
|
||||
subject { described_class.new }
|
||||
|
||||
let!(:nare) { Fabricate(:account, username: 'nare', domain: 'թութ.հայ', locked: false, protocol: :activitypub, inbox_url: 'https://թութ.հայ/inbox') }
|
||||
let(:csv) { attachment_fixture('utf8-followers.txt') }
|
||||
let(:import) { Import.create(account: account, type: 'following', data: csv) }
|
||||
|
||||
# Make sure to not actually go to the remote server
|
||||
before do
|
||||
stub_request(:post, nare.inbox_url).to_return(status: 200)
|
||||
end
|
||||
|
||||
it 'follows the listed account' do
|
||||
expect(account.follow_requests.count).to eq 0
|
||||
subject.call(import)
|
||||
expect(account.follow_requests.count).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'when importing bookmarks' do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:csv) { attachment_fixture('bookmark-imports.txt') }
|
||||
let(:local_account) { Fabricate(:account, username: 'foo', domain: nil) }
|
||||
let!(:remote_status) { Fabricate(:status, uri: 'https://example.com/statuses/1312') }
|
||||
let!(:direct_status) { Fabricate(:status, uri: 'https://example.com/statuses/direct', visibility: :direct) }
|
||||
|
||||
around do |example|
|
||||
local_before = Rails.configuration.x.local_domain
|
||||
web_before = Rails.configuration.x.web_domain
|
||||
Rails.configuration.x.local_domain = 'local.com'
|
||||
Rails.configuration.x.web_domain = 'local.com'
|
||||
example.run
|
||||
Rails.configuration.x.web_domain = web_before
|
||||
Rails.configuration.x.local_domain = local_before
|
||||
end
|
||||
|
||||
before do
|
||||
service = instance_double(ActivityPub::FetchRemoteStatusService)
|
||||
allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service)
|
||||
allow(service).to receive(:call).with('https://unknown-remote.com/users/bar/statuses/1') do
|
||||
Fabricate(:status, uri: 'https://unknown-remote.com/users/bar/statuses/1')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when no bookmarks are set' do
|
||||
let(:import) { Import.create(account: account, type: 'bookmarks', data: csv) }
|
||||
|
||||
it 'adds the toots the user has access to to bookmarks' do
|
||||
local_status = Fabricate(:status, account: local_account, uri: 'https://local.com/users/foo/statuses/42', id: 42, local: true)
|
||||
subject.call(import)
|
||||
expect(account.bookmarks.map { |bookmark| bookmark.status.id }).to include(local_status.id)
|
||||
expect(account.bookmarks.map { |bookmark| bookmark.status.id }).to include(remote_status.id)
|
||||
expect(account.bookmarks.map { |bookmark| bookmark.status.id }).to_not include(direct_status.id)
|
||||
expect(account.bookmarks.count).to eq 3
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SystemHelpers
|
||||
FRONTEND_TRANSLATIONS = JSON.parse Rails.root.join('app', 'javascript', 'mastodon', 'locales', 'en.json').read
|
||||
|
||||
def submit_button
|
||||
I18n.t('generic.save_changes')
|
||||
end
|
||||
|
@ -16,4 +18,8 @@ module SystemHelpers
|
|||
def css_id(record)
|
||||
"##{dom_id(record)}"
|
||||
end
|
||||
|
||||
def frontend_translations(key)
|
||||
FRONTEND_TRANSLATIONS[key]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,7 +18,7 @@ RSpec.describe 'Account notes', :inline_jobs, :js, :streaming do
|
|||
visit_profile(other_account)
|
||||
|
||||
note_text = 'This is a personal note'
|
||||
fill_in 'Click to add note', with: note_text
|
||||
fill_in frontend_translations('account_note.placeholder'), with: note_text
|
||||
|
||||
# This is a bit awkward since there is no button to save the change
|
||||
# The easiest way is to send ctrl+enter ourselves
|
||||
|
|
|
@ -17,8 +17,9 @@ RSpec.describe 'Log out' do
|
|||
click_on 'Logout'
|
||||
end
|
||||
|
||||
expect(page).to have_title(I18n.t('auth.login'))
|
||||
expect(page).to have_current_path('/auth/sign_in')
|
||||
expect(page)
|
||||
.to have_title(I18n.t('auth.login'))
|
||||
.and have_current_path('/auth/sign_in')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -28,6 +29,8 @@ RSpec.describe 'Log out' do
|
|||
ignore_js_error(/Failed to load resource: the server responded with a status of 422/)
|
||||
|
||||
visit root_path
|
||||
expect(page)
|
||||
.to have_css('body', class: 'app-body')
|
||||
|
||||
within '.navigation-bar' do
|
||||
click_on 'Menu'
|
||||
|
@ -39,8 +42,9 @@ RSpec.describe 'Log out' do
|
|||
|
||||
click_on 'Log out'
|
||||
|
||||
expect(page).to have_title(I18n.t('auth.login'))
|
||||
expect(page).to have_current_path('/auth/sign_in')
|
||||
expect(page)
|
||||
.to have_title(I18n.t('auth.login'))
|
||||
.and have_current_path('/auth/sign_in')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,20 +17,7 @@ RSpec.describe 'NewStatuses', :inline_jobs, :js, :streaming do
|
|||
status_text = 'This is a new status!'
|
||||
|
||||
within('.compose-form') do
|
||||
fill_in "What's on your mind?", with: status_text
|
||||
click_on 'Post'
|
||||
end
|
||||
|
||||
expect(page)
|
||||
.to have_css('.status__content__text', text: status_text)
|
||||
end
|
||||
|
||||
it 'can be posted again' do
|
||||
visit_homepage
|
||||
status_text = 'This is a second status!'
|
||||
|
||||
within('.compose-form') do
|
||||
fill_in "What's on your mind?", with: status_text
|
||||
fill_in frontend_translations('compose_form.placeholder'), with: status_text
|
||||
click_on 'Post'
|
||||
end
|
||||
|
||||
|
|
|
@ -23,24 +23,14 @@ RSpec.describe 'Share page', :js, :streaming do
|
|||
fill_in_form
|
||||
|
||||
expect(page)
|
||||
.to have_css('.notification-bar-message', text: translations['compose.published.body'])
|
||||
.to have_css('.notification-bar-message', text: frontend_translations('compose.published.body'))
|
||||
end
|
||||
|
||||
def fill_in_form
|
||||
within('.compose-form') do
|
||||
fill_in translations['compose_form.placeholder'],
|
||||
fill_in frontend_translations('compose_form.placeholder'),
|
||||
with: 'This is a new status!'
|
||||
click_on translations['compose_form.publish']
|
||||
click_on frontend_translations('compose_form.publish')
|
||||
end
|
||||
end
|
||||
|
||||
def translations
|
||||
# TODO: Extract to system spec helper for re-use?
|
||||
JSON.parse(
|
||||
Rails
|
||||
.root
|
||||
.join('app', 'javascript', 'mastodon', 'locales', 'en.json')
|
||||
.read
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ImportWorker do
|
||||
let(:worker) { described_class.new }
|
||||
let(:service) { instance_double(ImportService, call: true) }
|
||||
|
||||
describe '#perform' do
|
||||
before do
|
||||
allow(ImportService).to receive(:new).and_return(service)
|
||||
end
|
||||
|
||||
let(:import) { Fabricate(:import) }
|
||||
|
||||
it 'sends the import to the service' do
|
||||
worker.perform(import.id)
|
||||
|
||||
expect(service).to have_received(:call).with(import)
|
||||
expect { import.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,6 +5,7 @@ require 'rails_helper'
|
|||
RSpec.describe UnfilterNotificationsWorker do
|
||||
let(:recipient) { Fabricate(:account) }
|
||||
let(:sender) { Fabricate(:account) }
|
||||
let(:worker) { described_class.new }
|
||||
|
||||
before do
|
||||
# Populate multiple kinds of filtered notifications
|
||||
|
@ -67,23 +68,22 @@ RSpec.describe UnfilterNotificationsWorker do
|
|||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'with single argument (prerelease behavior)' do
|
||||
subject { described_class.new.perform(notification_request.id) }
|
||||
|
||||
let(:notification_request) { Fabricate(:notification_request, from_account: sender, account: recipient) }
|
||||
context 'with recipient and sender' do
|
||||
subject { worker.perform(recipient.id, sender.id) }
|
||||
|
||||
it_behaves_like 'shared behavior'
|
||||
|
||||
it 'destroys the notification request' do
|
||||
expect { subject }
|
||||
.to change { NotificationRequest.exists?(notification_request.id) }.to(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with two arguments' do
|
||||
subject { described_class.new.perform(recipient.id, sender.id) }
|
||||
context 'with missing records' do
|
||||
it 'runs without error for missing sender' do
|
||||
expect { worker.perform(recipient.id, nil) }
|
||||
.to_not raise_error
|
||||
end
|
||||
|
||||
it_behaves_like 'shared behavior'
|
||||
it 'runs without error for missing recipient' do
|
||||
expect { worker.perform(nil, sender.id) }
|
||||
.to_not raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Reference in a new issue