commit
66b57ae84a
23 changed files with 249 additions and 50 deletions
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -2,6 +2,24 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [4.3.3] - 2025-01-16
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix insufficient validation of account URIs ([GHSA-5wxh-3p65-r4g6](https://github.com/mastodon/mastodon/security/advisories/GHSA-5wxh-3p65-r4g6))
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix `libyaml` missing from `Dockerfile` build stage (#33591 by @vmstan)
|
||||||
|
- Fix incorrect notification settings migration for non-followers (#33348 by @ClearlyClaire)
|
||||||
|
- Fix down clause for notification policy v2 migrations (#33340 by @jesseplusplus)
|
||||||
|
- Fix error decrementing status count when `FeaturedTags#last_status_at` is `nil` (#33320 by @ClearlyClaire)
|
||||||
|
- Fix last paginated notification group only including data on a single notification (#33271 by @ClearlyClaire)
|
||||||
|
- Fix processing of mentions for post edits with an existing corresponding silent mention (#33227 by @ClearlyClaire)
|
||||||
|
- Fix deletion of unconfirmed users with Webauthn set (#33186 by @ClearlyClaire)
|
||||||
|
- Fix empty authors preview card serialization (#33151, #33466 by @mjankowski and @ClearlyClaire)
|
||||||
|
|
||||||
## [4.3.2] - 2024-12-03
|
## [4.3.2] - 2024-12-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -135,7 +153,7 @@ The following changelog entries focus on changes visible to users, administrator
|
||||||
- **Add notifications of severed relationships** (#27511, #29665, #29668, #29670, #29700, #29714, #29712, and #29731 by @ClearlyClaire and @Gargron)\
|
- **Add notifications of severed relationships** (#27511, #29665, #29668, #29670, #29700, #29714, #29712, and #29731 by @ClearlyClaire and @Gargron)\
|
||||||
Notify local users when they lose relationships as a result of a local moderator blocking a remote account or server, allowing the affected user to retrieve the list of broken relationships.\
|
Notify local users when they lose relationships as a result of a local moderator blocking a remote account or server, allowing the affected user to retrieve the list of broken relationships.\
|
||||||
Note that this does not notify remote users.\
|
Note that this does not notify remote users.\
|
||||||
This adds the `severed_relationships` notification type to the REST API and streaming, with a new [`relationship_severance_event` attribute](https://docs.joinmastodon.org/entities/Notification/#relationship_severance_event).
|
This adds the `severed_relationships` notification type to the REST API and streaming, with a new [`event` attribute](https://docs.joinmastodon.org/entities/Notification/#relationship_severance_event).
|
||||||
- **Add hover cards in web UI** (#30754, #30864, #30850, #30879, #30928, #30949, #30948, #30931, and #31300 by @ClearlyClaire, @Gargron, and @renchap)\
|
- **Add hover cards in web UI** (#30754, #30864, #30850, #30879, #30928, #30949, #30948, #30931, and #31300 by @ClearlyClaire, @Gargron, and @renchap)\
|
||||||
Hovering over an avatar or username will now display a hover card with the first two lines of the user's description and their first two profile fields.\
|
Hovering over an avatar or username will now display a hover card with the first two lines of the user's description and their first two profile fields.\
|
||||||
This can be disabled in the “Animations and accessibility” section of the preferences.
|
This can be disabled in the “Animations and accessibility” section of the preferences.
|
||||||
|
|
|
@ -153,6 +153,7 @@ RUN \
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
libtool \
|
libtool \
|
||||||
|
libyaml-dev \
|
||||||
meson \
|
meson \
|
||||||
nasm \
|
nasm \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
|
|
|
@ -80,10 +80,31 @@ class Api::V2::NotificationsController < Api::BaseController
|
||||||
return [] if @notifications.empty?
|
return [] if @notifications.empty?
|
||||||
|
|
||||||
MastodonOTELTracer.in_span('Api::V2::NotificationsController#load_grouped_notifications') do
|
MastodonOTELTracer.in_span('Api::V2::NotificationsController#load_grouped_notifications') do
|
||||||
NotificationGroup.from_notifications(@notifications, pagination_range: (@notifications.last.id)..(@notifications.first.id), grouped_types: params[:grouped_types])
|
pagination_range = (@notifications.last.id)..@notifications.first.id
|
||||||
|
|
||||||
|
# If the page is incomplete, we know we are on the last page
|
||||||
|
if incomplete_page?
|
||||||
|
if paginating_up?
|
||||||
|
pagination_range = @notifications.last.id...(params[:max_id]&.to_i)
|
||||||
|
else
|
||||||
|
range_start = params[:since_id]&.to_i
|
||||||
|
range_start += 1 unless range_start.nil?
|
||||||
|
pagination_range = range_start..(@notifications.first.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
NotificationGroup.from_notifications(@notifications, pagination_range: pagination_range, grouped_types: params[:grouped_types])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def incomplete_page?
|
||||||
|
@notifications.size < limit_param(DEFAULT_NOTIFICATIONS_LIMIT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def paginating_up?
|
||||||
|
params[:min_id].present?
|
||||||
|
end
|
||||||
|
|
||||||
def browserable_account_notifications
|
def browserable_account_notifications
|
||||||
current_account.notifications.without_suspended.browserable(
|
current_account.notifications.without_suspended.browserable(
|
||||||
types: Array(browserable_params[:types]),
|
types: Array(browserable_params[:types]),
|
||||||
|
|
|
@ -46,6 +46,8 @@ class DeliveryFailureTracker
|
||||||
urls.reject do |url|
|
urls.reject do |url|
|
||||||
host = Addressable::URI.parse(url).normalized_host
|
host = Addressable::URI.parse(url).normalized_host
|
||||||
unavailable_domains_map[host]
|
unavailable_domains_map[host]
|
||||||
|
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
|
||||||
|
true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ class FeaturedTag < ApplicationRecord
|
||||||
def decrement(deleted_status)
|
def decrement(deleted_status)
|
||||||
if statuses_count <= 1
|
if statuses_count <= 1
|
||||||
update(statuses_count: 0, last_status_at: nil)
|
update(statuses_count: 0, last_status_at: nil)
|
||||||
elsif last_status_at > deleted_status.created_at
|
elsif last_status_at.present? && last_status_at > deleted_status.created_at
|
||||||
update(statuses_count: statuses_count - 1)
|
update(statuses_count: statuses_count - 1)
|
||||||
else
|
else
|
||||||
# Fetching the latest status creation time can be expensive, so only perform it
|
# Fetching the latest status creation time can be expensive, so only perform it
|
||||||
|
|
|
@ -25,11 +25,13 @@ class InstanceInfo < ApplicationRecord
|
||||||
firefish
|
firefish
|
||||||
hollo
|
hollo
|
||||||
iceshrimp
|
iceshrimp
|
||||||
|
Iceshrimp.NET
|
||||||
meisskey
|
meisskey
|
||||||
misskey
|
misskey
|
||||||
pleroma
|
pleroma
|
||||||
sharkey
|
sharkey
|
||||||
tanukey
|
tanukey
|
||||||
|
yojo-art
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
QUOTE_AVAILABLE_SOFTWARES = EMOJI_REACTION_AVAILABLE_SOFTWARES + %w(bridgy-fed).freeze
|
QUOTE_AVAILABLE_SOFTWARES = EMOJI_REACTION_AVAILABLE_SOFTWARES + %w(bridgy-fed).freeze
|
||||||
|
|
|
@ -89,22 +89,32 @@ class NotificationGroup < ActiveModelSerializers::Model
|
||||||
binds = [
|
binds = [
|
||||||
account_id,
|
account_id,
|
||||||
SAMPLE_ACCOUNTS_SIZE,
|
SAMPLE_ACCOUNTS_SIZE,
|
||||||
pagination_range.begin,
|
|
||||||
pagination_range.end,
|
|
||||||
ActiveRecord::Relation::QueryAttribute.new('group_keys', group_keys, ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::String.new)),
|
ActiveRecord::Relation::QueryAttribute.new('group_keys', group_keys, ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::String.new)),
|
||||||
|
pagination_range.begin || 0,
|
||||||
]
|
]
|
||||||
|
binds << pagination_range.end unless pagination_range.end.nil?
|
||||||
|
|
||||||
|
upper_bound_cond = begin
|
||||||
|
if pagination_range.end.nil?
|
||||||
|
''
|
||||||
|
elsif pagination_range.exclude_end?
|
||||||
|
'AND id < $5'
|
||||||
|
else
|
||||||
|
'AND id <= $5'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
ActiveRecord::Base.connection.select_all(<<~SQL.squish, 'grouped_notifications', binds).cast_values.to_h { |k, *values| [k, values] }
|
ActiveRecord::Base.connection.select_all(<<~SQL.squish, 'grouped_notifications', binds).cast_values.to_h { |k, *values| [k, values] }
|
||||||
SELECT
|
SELECT
|
||||||
groups.group_key,
|
groups.group_key,
|
||||||
(SELECT id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4 ORDER BY id DESC LIMIT 1),
|
(SELECT id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key #{upper_bound_cond} ORDER BY id DESC LIMIT 1),
|
||||||
array(SELECT from_account_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4 ORDER BY id DESC LIMIT $2),
|
array(SELECT from_account_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key #{upper_bound_cond} ORDER BY id DESC LIMIT $2),
|
||||||
(SELECT count(*) FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4) AS notifications_count,
|
(SELECT count(*) FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key #{upper_bound_cond}) AS notifications_count,
|
||||||
array(SELECT activity_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4 AND activity_type = 'EmojiReaction'),
|
array(SELECT activity_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key #{upper_bound_cond} AND activity_type = 'EmojiReaction'),
|
||||||
(SELECT id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id >= $3 ORDER BY id ASC LIMIT 1) AS min_id,
|
(SELECT id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id >= $4 ORDER BY id ASC LIMIT 1) AS min_id,
|
||||||
(SELECT created_at FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4 ORDER BY id DESC LIMIT 1)
|
(SELECT created_at FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key #{upper_bound_cond} ORDER BY id DESC LIMIT 1)
|
||||||
FROM
|
FROM
|
||||||
unnest($5::text[]) AS groups(group_key);
|
unnest($3::text[]) AS groups(group_key);
|
||||||
SQL
|
SQL
|
||||||
else
|
else
|
||||||
binds = [
|
binds = [
|
||||||
|
|
|
@ -134,7 +134,7 @@ class PreviewCard < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def authors
|
def authors
|
||||||
@authors ||= [PreviewCard::Author.new(self)]
|
@authors ||= Array(serialized_authors)
|
||||||
end
|
end
|
||||||
|
|
||||||
class Author < ActiveModelSerializers::Model
|
class Author < ActiveModelSerializers::Model
|
||||||
|
@ -169,6 +169,13 @@ class PreviewCard < ApplicationRecord
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def serialized_authors
|
||||||
|
if author_name? || author_url? || author_account_id?
|
||||||
|
PreviewCard::Author
|
||||||
|
.new(self)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def extract_dimensions
|
def extract_dimensions
|
||||||
file = image.queued_for_write[:original]
|
file = image.queued_for_write[:original]
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,8 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||||
SCAN_SEARCHABILITY_RE = /\[searchability:(public|followers|reactors|private)\]/
|
SCAN_SEARCHABILITY_RE = /\[searchability:(public|followers|reactors|private)\]/
|
||||||
SCAN_SEARCHABILITY_FEDIBIRD_RE = /searchable_by_(all_users|followers_only|reacted_users_only|nobody)/
|
SCAN_SEARCHABILITY_FEDIBIRD_RE = /searchable_by_(all_users|followers_only|reacted_users_only|nobody)/
|
||||||
|
|
||||||
|
VALID_URI_SCHEMES = %w(http https).freeze
|
||||||
|
|
||||||
# Should be called with confirmed valid JSON
|
# Should be called with confirmed valid JSON
|
||||||
# and WebFinger-resolved username and domain
|
# and WebFinger-resolved username and domain
|
||||||
def call(username, domain, json, options = {}) # rubocop:disable Metrics/PerceivedComplexity
|
def call(username, domain, json, options = {}) # rubocop:disable Metrics/PerceivedComplexity
|
||||||
|
@ -113,16 +115,28 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_immediate_protocol_attributes!
|
def set_immediate_protocol_attributes!
|
||||||
@account.inbox_url = @json['inbox'] || ''
|
@account.inbox_url = valid_collection_uri(@json['inbox'])
|
||||||
@account.outbox_url = @json['outbox'] || ''
|
@account.outbox_url = valid_collection_uri(@json['outbox'])
|
||||||
@account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || ''
|
@account.shared_inbox_url = valid_collection_uri(@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox'])
|
||||||
@account.followers_url = @json['followers'] || ''
|
@account.followers_url = valid_collection_uri(@json['followers'])
|
||||||
@account.url = url || @uri
|
@account.url = url || @uri
|
||||||
@account.uri = @uri
|
@account.uri = @uri
|
||||||
@account.actor_type = actor_type
|
@account.actor_type = actor_type
|
||||||
@account.created_at = @json['published'] if @json['published'].present?
|
@account.created_at = @json['published'] if @json['published'].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def valid_collection_uri(uri)
|
||||||
|
uri = uri.first if uri.is_a?(Array)
|
||||||
|
uri = uri['id'] if uri.is_a?(Hash)
|
||||||
|
return '' unless uri.is_a?(String)
|
||||||
|
|
||||||
|
parsed_uri = Addressable::URI.parse(uri)
|
||||||
|
|
||||||
|
VALID_URI_SCHEMES.include?(parsed_uri.scheme) && parsed_uri.host.present? ? parsed_uri : ''
|
||||||
|
rescue Addressable::URI::InvalidURIError
|
||||||
|
''
|
||||||
|
end
|
||||||
|
|
||||||
def set_immediate_attributes!
|
def set_immediate_attributes!
|
||||||
@account.featured_collection_url = @json['featured'] || ''
|
@account.featured_collection_url = @json['featured'] || ''
|
||||||
@account.display_name = @json['name'] || ''
|
@account.display_name = @json['name'] || ''
|
||||||
|
@ -398,10 +412,11 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def collection_info(type)
|
def collection_info(type)
|
||||||
return [nil, nil] if @json[type].blank?
|
collection_uri = valid_collection_uri(@json[type])
|
||||||
|
return [nil, nil] if collection_uri.blank?
|
||||||
return @collections[type] if @collections.key?(type)
|
return @collections[type] if @collections.key?(type)
|
||||||
|
|
||||||
collection = fetch_resource_without_id_validation(@json[type])
|
collection = fetch_resource_without_id_validation(collection_uri)
|
||||||
|
|
||||||
total_items = collection.is_a?(Hash) && collection['totalItems'].present? && collection['totalItems'].is_a?(Numeric) ? collection['totalItems'] : nil
|
total_items = collection.is_a?(Hash) && collection['totalItems'].present? && collection['totalItems'].is_a?(Numeric) ? collection['totalItems'] : nil
|
||||||
has_first_page = collection.is_a?(Hash) && collection['first'].present?
|
has_first_page = collection.is_a?(Hash) && collection['first'].present?
|
||||||
|
|
|
@ -259,40 +259,30 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_mentions!
|
def update_mentions!
|
||||||
previous_mentions = @status.active_mentions.includes(:account).to_a
|
|
||||||
current_mentions = []
|
|
||||||
unresolved_mentions = []
|
unresolved_mentions = []
|
||||||
|
|
||||||
@raw_mentions.each do |href|
|
currently_mentioned_account_ids = @raw_mentions.filter_map do |href|
|
||||||
next if href.blank?
|
next if href.blank?
|
||||||
|
|
||||||
account = ActivityPub::TagManager.instance.uri_to_resource(href, Account)
|
account = ActivityPub::TagManager.instance.uri_to_resource(href, Account)
|
||||||
account ||= ActivityPub::FetchRemoteAccountService.new.call(href, request_id: @request_id)
|
account ||= ActivityPub::FetchRemoteAccountService.new.call(href, request_id: @request_id)
|
||||||
|
|
||||||
next if account.nil?
|
account&.id
|
||||||
|
|
||||||
mention = previous_mentions.find { |x| x.account_id == account.id }
|
|
||||||
mention ||= account.mentions.new(status: @status)
|
|
||||||
|
|
||||||
current_mentions << mention
|
|
||||||
rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS
|
rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS
|
||||||
# Since previous mentions are about already-known accounts,
|
# Since previous mentions are about already-known accounts,
|
||||||
# they don't try to resolve again and won't fall into this case.
|
# they don't try to resolve again and won't fall into this case.
|
||||||
# In other words, this failure case is only for new mentions and won't
|
# In other words, this failure case is only for new mentions and won't
|
||||||
# affect `removed_mentions` so they can safely be retried asynchronously
|
# affect `removed_mentions` so they can safely be retried asynchronously
|
||||||
unresolved_mentions << href
|
unresolved_mentions << href
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
current_mentions.each do |mention|
|
@status.mentions.upsert_all(currently_mentioned_account_ids.map { |id| { account_id: id, silent: false } }, unique_by: %w(status_id account_id))
|
||||||
mention.save if mention.new_record?
|
|
||||||
end
|
|
||||||
|
|
||||||
# If previous mentions are no longer contained in the text, convert them
|
# If previous mentions are no longer contained in the text, convert them
|
||||||
# to silent mentions, since withdrawing access from someone who already
|
# to silent mentions, since withdrawing access from someone who already
|
||||||
# received a notification might be more confusing
|
# received a notification might be more confusing
|
||||||
removed_mentions = previous_mentions - current_mentions
|
@status.mentions.where.not(account_id: currently_mentioned_account_ids).update_all(silent: true)
|
||||||
|
|
||||||
Mention.where(id: removed_mentions.map(&:id)).update_all(silent: true) unless removed_mentions.empty?
|
|
||||||
|
|
||||||
# Queue unresolved mentions for later
|
# Queue unresolved mentions for later
|
||||||
unresolved_mentions.uniq.each do |uri|
|
unresolved_mentions.uniq.each do |uri|
|
||||||
|
|
|
@ -15,7 +15,7 @@ class ProcessMentionsService < BaseService
|
||||||
|
|
||||||
return unless @status.local?
|
return unless @status.local?
|
||||||
|
|
||||||
@previous_mentions = @status.active_mentions.includes(:account).to_a
|
@previous_mentions = @status.mentions.includes(:account).to_a
|
||||||
@current_mentions = []
|
@current_mentions = []
|
||||||
|
|
||||||
Status.transaction do
|
Status.transaction do
|
||||||
|
@ -63,6 +63,8 @@ class ProcessMentionsService < BaseService
|
||||||
mention ||= @current_mentions.find { |x| x.account_id == mentioned_account.id }
|
mention ||= @current_mentions.find { |x| x.account_id == mentioned_account.id }
|
||||||
mention ||= @status.mentions.new(account: mentioned_account)
|
mention ||= @status.mentions.new(account: mentioned_account)
|
||||||
|
|
||||||
|
mention.silent = false
|
||||||
|
|
||||||
@current_mentions << mention
|
@current_mentions << mention
|
||||||
|
|
||||||
"@#{mentioned_account.acct}"
|
"@#{mentioned_account.acct}"
|
||||||
|
@ -87,7 +89,7 @@ class ProcessMentionsService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
@current_mentions.each do |mention|
|
@current_mentions.each do |mention|
|
||||||
mention.save if mention.new_record? && @save_records
|
mention.save if (mention.new_record? || mention.silent_changed?) && @save_records
|
||||||
end
|
end
|
||||||
|
|
||||||
# If previous mentions are no longer contained in the text, convert them
|
# If previous mentions are no longer contained in the text, convert them
|
||||||
|
@ -95,7 +97,7 @@ class ProcessMentionsService < BaseService
|
||||||
# received a notification might be more confusing
|
# received a notification might be more confusing
|
||||||
removed_mentions = @previous_mentions - @current_mentions
|
removed_mentions = @previous_mentions - @current_mentions
|
||||||
|
|
||||||
Mention.where(id: removed_mentions.map(&:id)).update_all(silent: true) unless removed_mentions.empty?
|
Mention.where(id: removed_mentions.map(&:id), silent: false).update_all(silent: true) unless removed_mentions.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def mention_undeliverable?(mentioned_account)
|
def mention_undeliverable?(mentioned_account)
|
||||||
|
|
|
@ -16,7 +16,7 @@ class MentionResolveWorker
|
||||||
|
|
||||||
return if account.nil?
|
return if account.nil?
|
||||||
|
|
||||||
status.mentions.create!(account: account, silent: false)
|
status.mentions.upsert({ account_id: account.id, silent: false }, unique_by: %w(status_id account_id))
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
# Do nothing
|
# Do nothing
|
||||||
rescue Mastodon::UnexpectedResponseError => e
|
rescue Mastodon::UnexpectedResponseError => e
|
||||||
|
|
|
@ -19,6 +19,7 @@ class Scheduler::UserCleanupScheduler
|
||||||
User.unconfirmed.where(confirmation_sent_at: ..UNCONFIRMED_ACCOUNTS_MAX_AGE_DAYS.days.ago).find_in_batches do |batch|
|
User.unconfirmed.where(confirmation_sent_at: ..UNCONFIRMED_ACCOUNTS_MAX_AGE_DAYS.days.ago).find_in_batches do |batch|
|
||||||
# We have to do it separately because of missing database constraints
|
# We have to do it separately because of missing database constraints
|
||||||
AccountModerationNote.where(target_account_id: batch.map(&:account_id)).delete_all
|
AccountModerationNote.where(target_account_id: batch.map(&:account_id)).delete_all
|
||||||
|
WebauthnCredential.where(user_id: batch.map(&:id)).delete_all
|
||||||
Account.where(id: batch.map(&:account_id)).delete_all
|
Account.where(id: batch.map(&:account_id)).delete_all
|
||||||
User.where(id: batch.map(&:id)).delete_all
|
User.where(id: batch.map(&:id)).delete_all
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,7 +9,7 @@ class MigrateNotificationsPolicyV2 < ActiveRecord::Migration[7.1]
|
||||||
def up
|
def up
|
||||||
NotificationPolicy.in_batches.update_all(<<~SQL.squish)
|
NotificationPolicy.in_batches.update_all(<<~SQL.squish)
|
||||||
for_not_following = CASE filter_not_following WHEN true THEN 1 ELSE 0 END,
|
for_not_following = CASE filter_not_following WHEN true THEN 1 ELSE 0 END,
|
||||||
for_not_followers = CASE filter_not_following WHEN true THEN 1 ELSE 0 END,
|
for_not_followers = CASE filter_not_followers WHEN true THEN 1 ELSE 0 END,
|
||||||
for_new_accounts = CASE filter_new_accounts WHEN true THEN 1 ELSE 0 END,
|
for_new_accounts = CASE filter_new_accounts WHEN true THEN 1 ELSE 0 END,
|
||||||
for_private_mentions = CASE filter_private_mentions WHEN true THEN 1 ELSE 0 END
|
for_private_mentions = CASE filter_private_mentions WHEN true THEN 1 ELSE 0 END
|
||||||
SQL
|
SQL
|
||||||
|
@ -18,7 +18,7 @@ class MigrateNotificationsPolicyV2 < ActiveRecord::Migration[7.1]
|
||||||
def down
|
def down
|
||||||
NotificationPolicy.in_batches.update_all(<<~SQL.squish)
|
NotificationPolicy.in_batches.update_all(<<~SQL.squish)
|
||||||
filter_not_following = CASE for_not_following WHEN 0 THEN false ELSE true END,
|
filter_not_following = CASE for_not_following WHEN 0 THEN false ELSE true END,
|
||||||
filter_not_following = CASE for_not_followers WHEN 0 THEN false ELSE true END,
|
filter_not_followers = CASE for_not_followers WHEN 0 THEN false ELSE true END,
|
||||||
filter_new_accounts = CASE for_new_accounts WHEN 0 THEN false ELSE true END,
|
filter_new_accounts = CASE for_new_accounts WHEN 0 THEN false ELSE true END,
|
||||||
filter_private_mentions = CASE for_private_mentions WHEN 0 THEN false ELSE true END
|
filter_private_mentions = CASE for_private_mentions WHEN 0 THEN false ELSE true END
|
||||||
SQL
|
SQL
|
||||||
|
|
|
@ -9,7 +9,7 @@ class PostDeploymentMigrateNotificationsPolicyV2 < ActiveRecord::Migration[7.1]
|
||||||
def up
|
def up
|
||||||
NotificationPolicy.in_batches.update_all(<<~SQL.squish)
|
NotificationPolicy.in_batches.update_all(<<~SQL.squish)
|
||||||
for_not_following = CASE filter_not_following WHEN true THEN 1 ELSE 0 END,
|
for_not_following = CASE filter_not_following WHEN true THEN 1 ELSE 0 END,
|
||||||
for_not_followers = CASE filter_not_following WHEN true THEN 1 ELSE 0 END,
|
for_not_followers = CASE filter_not_followers WHEN true THEN 1 ELSE 0 END,
|
||||||
for_new_accounts = CASE filter_new_accounts WHEN true THEN 1 ELSE 0 END,
|
for_new_accounts = CASE filter_new_accounts WHEN true THEN 1 ELSE 0 END,
|
||||||
for_private_mentions = CASE filter_private_mentions WHEN true THEN 1 ELSE 0 END
|
for_private_mentions = CASE filter_private_mentions WHEN true THEN 1 ELSE 0 END
|
||||||
SQL
|
SQL
|
||||||
|
@ -18,7 +18,7 @@ class PostDeploymentMigrateNotificationsPolicyV2 < ActiveRecord::Migration[7.1]
|
||||||
def down
|
def down
|
||||||
NotificationPolicy.in_batches.update_all(<<~SQL.squish)
|
NotificationPolicy.in_batches.update_all(<<~SQL.squish)
|
||||||
filter_not_following = CASE for_not_following WHEN 0 THEN false ELSE true END,
|
filter_not_following = CASE for_not_following WHEN 0 THEN false ELSE true END,
|
||||||
filter_not_following = CASE for_not_followers WHEN 0 THEN false ELSE true END,
|
filter_not_followers = CASE for_not_followers WHEN 0 THEN false ELSE true END,
|
||||||
filter_new_accounts = CASE for_new_accounts WHEN 0 THEN false ELSE true END,
|
filter_new_accounts = CASE for_new_accounts WHEN 0 THEN false ELSE true END,
|
||||||
filter_private_mentions = CASE for_private_mentions WHEN 0 THEN false ELSE true END
|
filter_private_mentions = CASE for_private_mentions WHEN 0 THEN false ELSE true END
|
||||||
SQL
|
SQL
|
||||||
|
|
|
@ -59,7 +59,7 @@ services:
|
||||||
web:
|
web:
|
||||||
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
||||||
build: .
|
build: .
|
||||||
image: kmyblue:16.1
|
image: kmyblue:16.2
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bundle exec puma -C config/puma.rb
|
command: bundle exec puma -C config/puma.rb
|
||||||
|
@ -83,7 +83,7 @@ services:
|
||||||
build:
|
build:
|
||||||
dockerfile: ./streaming/Dockerfile
|
dockerfile: ./streaming/Dockerfile
|
||||||
context: .
|
context: .
|
||||||
image: kmyblue-streaming:16.1
|
image: kmyblue-streaming:16.2
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: node ./streaming/index.js
|
command: node ./streaming/index.js
|
||||||
|
@ -101,7 +101,7 @@ services:
|
||||||
|
|
||||||
sidekiq:
|
sidekiq:
|
||||||
build: .
|
build: .
|
||||||
image: kmyblue:16.1
|
image: kmyblue:16.2
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bundle exec sidekiq
|
command: bundle exec sidekiq
|
||||||
|
|
|
@ -13,7 +13,7 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
def kmyblue_minor
|
def kmyblue_minor
|
||||||
1
|
2
|
||||||
end
|
end
|
||||||
|
|
||||||
def kmyblue_flag
|
def kmyblue_flag
|
||||||
|
|
|
@ -42,8 +42,8 @@ RSpec.describe DeliveryFailureTracker do
|
||||||
Fabricate(:unavailable_domain, domain: 'foo.bar')
|
Fabricate(:unavailable_domain, domain: 'foo.bar')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'removes URLs that are unavailable' do
|
it 'removes URLs that are bogus or unavailable' do
|
||||||
results = described_class.without_unavailable(['http://example.com/good/inbox', 'http://foo.bar/unavailable/inbox'])
|
results = described_class.without_unavailable(['http://example.com/good/inbox', 'http://foo.bar/unavailable/inbox', '{foo:'])
|
||||||
|
|
||||||
expect(results).to include('http://example.com/good/inbox')
|
expect(results).to include('http://example.com/good/inbox')
|
||||||
expect(results).to_not include('http://foo.bar/unavailable/inbox')
|
expect(results).to_not include('http://foo.bar/unavailable/inbox')
|
||||||
|
|
|
@ -143,6 +143,55 @@ RSpec.describe 'Notifications' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when there are numerous notifications for the same final group' do
|
||||||
|
before do
|
||||||
|
user.account.notifications.destroy_all
|
||||||
|
5.times.each { FavouriteService.new.call(Fabricate(:account), user.account.statuses.first) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with no options' do
|
||||||
|
it 'returns a notification group covering all notifications' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
notification_ids = user.account.notifications.reload.pluck(:id)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(response.content_type)
|
||||||
|
.to start_with('application/json')
|
||||||
|
expect(response.parsed_body[:notification_groups]).to contain_exactly(
|
||||||
|
a_hash_including(
|
||||||
|
type: 'favourite',
|
||||||
|
sample_account_ids: have_attributes(size: 5),
|
||||||
|
page_min_id: notification_ids.first.to_s,
|
||||||
|
page_max_id: notification_ids.last.to_s
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with min_id param' do
|
||||||
|
let(:params) { { min_id: user.account.notifications.reload.first.id - 1 } }
|
||||||
|
|
||||||
|
it 'returns a notification group covering all notifications' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
notification_ids = user.account.notifications.reload.pluck(:id)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(response.content_type)
|
||||||
|
.to start_with('application/json')
|
||||||
|
expect(response.parsed_body[:notification_groups]).to contain_exactly(
|
||||||
|
a_hash_including(
|
||||||
|
type: 'favourite',
|
||||||
|
sample_account_ids: have_attributes(size: 5),
|
||||||
|
page_min_id: notification_ids.first.to_s,
|
||||||
|
page_max_id: notification_ids.last.to_s
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with no options' do
|
context 'with no options' do
|
||||||
it 'returns expected notification types', :aggregate_failures do
|
it 'returns expected notification types', :aggregate_failures do
|
||||||
subject
|
subject
|
||||||
|
|
58
spec/serializers/rest/preview_card_serializer_spec.rb
Normal file
58
spec/serializers/rest/preview_card_serializer_spec.rb
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe REST::PreviewCardSerializer do
|
||||||
|
subject do
|
||||||
|
serialized_record_json(
|
||||||
|
preview_card,
|
||||||
|
described_class
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when preview card does not have author data' do
|
||||||
|
let(:preview_card) { Fabricate.build :preview_card }
|
||||||
|
|
||||||
|
it 'includes empty authors array' do
|
||||||
|
expect(subject.deep_symbolize_keys)
|
||||||
|
.to include(
|
||||||
|
authors: be_an(Array).and(be_empty)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when preview card has fediverse author data' do
|
||||||
|
let(:preview_card) { Fabricate.build :preview_card, author_account: Fabricate(:account) }
|
||||||
|
|
||||||
|
it 'includes populated authors array' do
|
||||||
|
expect(subject.deep_symbolize_keys)
|
||||||
|
.to include(
|
||||||
|
authors: be_an(Array).and(
|
||||||
|
contain_exactly(
|
||||||
|
include(
|
||||||
|
account: be_present
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when preview card has non-fediverse author data' do
|
||||||
|
let(:preview_card) { Fabricate.build :preview_card, author_name: 'Name', author_url: 'https://host.example/123' }
|
||||||
|
|
||||||
|
it 'includes populated authors array' do
|
||||||
|
expect(subject.deep_symbolize_keys)
|
||||||
|
.to include(
|
||||||
|
authors: be_an(Array).and(
|
||||||
|
contain_exactly(
|
||||||
|
include(
|
||||||
|
name: 'Name',
|
||||||
|
url: 'https://host.example/123'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -36,7 +36,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
||||||
let(:media_attachments) { [] }
|
let(:media_attachments) { [] }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
mentions.each { |a| Fabricate(:mention, status: status, account: a) }
|
mentions.each { |(account, silent)| Fabricate(:mention, status: status, account: account, silent: silent) }
|
||||||
tags.each { |t| status.tags << t }
|
tags.each { |t| status.tags << t }
|
||||||
media_attachments.each { |m| status.media_attachments << m }
|
media_attachments.each { |m| status.media_attachments << m }
|
||||||
end
|
end
|
||||||
|
@ -320,7 +320,19 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when originally with mentions' do
|
context 'when originally with mentions' do
|
||||||
let(:mentions) { [alice, bob] }
|
let(:mentions) { [[alice, false], [bob, false]] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.call(status, json, json)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates mentions' do
|
||||||
|
expect(status.active_mentions.reload.map(&:account_id)).to eq [alice.id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when originally with silent mentions' do
|
||||||
|
let(:mentions) { [[alice, true], [bob, true]] }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
subject.call(status, json, json)
|
subject.call(status, json, json)
|
||||||
|
|
|
@ -199,6 +199,14 @@ RSpec.describe UpdateStatusService do
|
||||||
.to eq [bob.id]
|
.to eq [bob.id]
|
||||||
expect(status.mentions.pluck(:account_id))
|
expect(status.mentions.pluck(:account_id))
|
||||||
.to contain_exactly(alice.id, bob.id)
|
.to contain_exactly(alice.id, bob.id)
|
||||||
|
|
||||||
|
# Going back when a mention was switched to silence should still be possible
|
||||||
|
subject.call(status, status.account_id, text: 'Hello @alice')
|
||||||
|
|
||||||
|
expect(status.active_mentions.pluck(:account_id))
|
||||||
|
.to eq [alice.id]
|
||||||
|
expect(status.mentions.pluck(:account_id))
|
||||||
|
.to contain_exactly(alice.id, bob.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ RSpec.describe Scheduler::UserCleanupScheduler do
|
||||||
let!(:old_unconfirmed_user) { Fabricate(:user) }
|
let!(:old_unconfirmed_user) { Fabricate(:user) }
|
||||||
let!(:confirmed_user) { Fabricate(:user) }
|
let!(:confirmed_user) { Fabricate(:user) }
|
||||||
let!(:moderation_note) { Fabricate(:account_moderation_note, account: Fabricate(:account), target_account: old_unconfirmed_user.account) }
|
let!(:moderation_note) { Fabricate(:account_moderation_note, account: Fabricate(:account), target_account: old_unconfirmed_user.account) }
|
||||||
|
let!(:webauthn_credential) { Fabricate(:webauthn_credential, user_id: old_unconfirmed_user.id) }
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
before do
|
before do
|
||||||
|
@ -26,6 +27,8 @@ RSpec.describe Scheduler::UserCleanupScheduler do
|
||||||
.from(true).to(false)
|
.from(true).to(false)
|
||||||
expect { moderation_note.reload }
|
expect { moderation_note.reload }
|
||||||
.to raise_error(ActiveRecord::RecordNotFound)
|
.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
expect { webauthn_credential.reload }
|
||||||
|
.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
expect_preservation_of(new_unconfirmed_user)
|
expect_preservation_of(new_unconfirmed_user)
|
||||||
expect_preservation_of(confirmed_user)
|
expect_preservation_of(confirmed_user)
|
||||||
end
|
end
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue