Skip to content

Commit

Permalink
Merge pull request #965 from kmycode/kb-draft-16.2
Browse files Browse the repository at this point in the history
Release: 16.2
  • Loading branch information
kmycode authored Jan 17, 2025
2 parents 6df5dfe + f03a5ab commit 66b57ae
Show file tree
Hide file tree
Showing 23 changed files with 249 additions and 50 deletions.
20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

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

### Added
Expand Down Expand Up @@ -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)\
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.\
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)\
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.
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ RUN \
libpq-dev \
libssl-dev \
libtool \
libyaml-dev \
meson \
nasm \
pkg-config \
Expand Down
23 changes: 22 additions & 1 deletion app/controllers/api/v2/notifications_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,31 @@ def load_grouped_notifications
return [] if @notifications.empty?

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

def incomplete_page?
@notifications.size < limit_param(DEFAULT_NOTIFICATIONS_LIMIT)
end

def paginating_up?
params[:min_id].present?
end

def browserable_account_notifications
current_account.notifications.without_suspended.browserable(
types: Array(browserable_params[:types]),
Expand Down
2 changes: 2 additions & 0 deletions app/lib/delivery_failure_tracker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ def without_unavailable(urls)
urls.reject do |url|
host = Addressable::URI.parse(url).normalized_host
unavailable_domains_map[host]
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
true
end
end

Expand Down
2 changes: 1 addition & 1 deletion app/models/featured_tag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def increment(timestamp)
def decrement(deleted_status)
if statuses_count <= 1
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)
else
# Fetching the latest status creation time can be expensive, so only perform it
Expand Down
2 changes: 2 additions & 0 deletions app/models/instance_info.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ class InstanceInfo < ApplicationRecord
firefish
hollo
iceshrimp
Iceshrimp.NET
meisskey
misskey
pleroma
sharkey
tanukey
yojo-art
).freeze

QUOTE_AVAILABLE_SOFTWARES = EMOJI_REACTION_AVAILABLE_SOFTWARES + %w(bridgy-fed).freeze
Expand Down
28 changes: 19 additions & 9 deletions app/models/notification_group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,22 +89,32 @@ def load_groups_data(account_id, group_keys, pagination_range: nil)
binds = [
account_id,
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)),
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] }
SELECT
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),
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),
(SELECT count(*) FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4) 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'),
(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 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 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 #{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 #{upper_bound_cond}) AS notifications_count,
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 >= $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 #{upper_bound_cond} ORDER BY id DESC LIMIT 1)
FROM
unnest($5::text[]) AS groups(group_key);
unnest($3::text[]) AS groups(group_key);
SQL
else
binds = [
Expand Down
9 changes: 8 additions & 1 deletion app/models/preview_card.rb
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def history
end

def authors
@authors ||= [PreviewCard::Author.new(self)]
@authors ||= Array(serialized_authors)
end

class Author < ActiveModelSerializers::Model
Expand Down Expand Up @@ -169,6 +169,13 @@ def image_styles(file)

private

def serialized_authors
if author_name? || author_url? || author_account_id?
PreviewCard::Author
.new(self)
end
end

def extract_dimensions
file = image.queued_for_write[:original]

Expand Down
27 changes: 21 additions & 6 deletions app/services/activitypub/process_account_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class ActivityPub::ProcessAccountService < BaseService
SCAN_SEARCHABILITY_RE = /\[searchability:(public|followers|reactors|private)\]/
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
# and WebFinger-resolved username and domain
def call(username, domain, json, options = {}) # rubocop:disable Metrics/PerceivedComplexity
Expand Down Expand Up @@ -113,16 +115,28 @@ def update_account
end

def set_immediate_protocol_attributes!
@account.inbox_url = @json['inbox'] || ''
@account.outbox_url = @json['outbox'] || ''
@account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || ''
@account.followers_url = @json['followers'] || ''
@account.inbox_url = valid_collection_uri(@json['inbox'])
@account.outbox_url = valid_collection_uri(@json['outbox'])
@account.shared_inbox_url = valid_collection_uri(@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox'])
@account.followers_url = valid_collection_uri(@json['followers'])
@account.url = url || @uri
@account.uri = @uri
@account.actor_type = actor_type
@account.created_at = @json['published'] if @json['published'].present?
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!
@account.featured_collection_url = @json['featured'] || ''
@account.display_name = @json['name'] || ''
Expand Down Expand Up @@ -398,10 +412,11 @@ def followers_private?
end

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)

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
has_first_page = collection.is_a?(Hash) && collection['first'].present?
Expand Down
20 changes: 5 additions & 15 deletions app/services/activitypub/process_status_update_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -259,40 +259,30 @@ def update_tags!
end

def update_mentions!
previous_mentions = @status.active_mentions.includes(:account).to_a
current_mentions = []
unresolved_mentions = []

@raw_mentions.each do |href|
currently_mentioned_account_ids = @raw_mentions.filter_map do |href|
next if href.blank?

account = ActivityPub::TagManager.instance.uri_to_resource(href, Account)
account ||= ActivityPub::FetchRemoteAccountService.new.call(href, request_id: @request_id)

next if account.nil?

mention = previous_mentions.find { |x| x.account_id == account.id }
mention ||= account.mentions.new(status: @status)

current_mentions << mention
account&.id
rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS
# Since previous mentions are about already-known accounts,
# 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
# affect `removed_mentions` so they can safely be retried asynchronously
unresolved_mentions << href
nil
end

current_mentions.each do |mention|
mention.save if mention.new_record?
end
@status.mentions.upsert_all(currently_mentioned_account_ids.map { |id| { account_id: id, silent: false } }, unique_by: %w(status_id account_id))

# If previous mentions are no longer contained in the text, convert them
# to silent mentions, since withdrawing access from someone who already
# received a notification might be more confusing
removed_mentions = previous_mentions - current_mentions

Mention.where(id: removed_mentions.map(&:id)).update_all(silent: true) unless removed_mentions.empty?
@status.mentions.where.not(account_id: currently_mentioned_account_ids).update_all(silent: true)

# Queue unresolved mentions for later
unresolved_mentions.uniq.each do |uri|
Expand Down
8 changes: 5 additions & 3 deletions app/services/process_mentions_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def call(status, limited_type: '', circle: nil, save_records: true)

return unless @status.local?

@previous_mentions = @status.active_mentions.includes(:account).to_a
@previous_mentions = @status.mentions.includes(:account).to_a
@current_mentions = []

Status.transaction do
Expand Down Expand Up @@ -63,6 +63,8 @@ def scan_text!
mention ||= @current_mentions.find { |x| x.account_id == mentioned_account.id }
mention ||= @status.mentions.new(account: mentioned_account)

mention.silent = false

@current_mentions << mention

"@#{mentioned_account.acct}"
Expand All @@ -87,15 +89,15 @@ def assign_mentions!
end

@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

# If previous mentions are no longer contained in the text, convert them
# to silent mentions, since withdrawing access from someone who already
# received a notification might be more confusing
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

def mention_undeliverable?(mentioned_account)
Expand Down
2 changes: 1 addition & 1 deletion app/workers/mention_resolve_worker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def perform(status_id, uri, options = {})

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
# Do nothing
rescue Mastodon::UnexpectedResponseError => e
Expand Down
1 change: 1 addition & 0 deletions app/workers/scheduler/user_cleanup_scheduler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def clean_unconfirmed_accounts!
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
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
User.where(id: batch.map(&:id)).delete_all
end
Expand Down
4 changes: 2 additions & 2 deletions db/migrate/20240808124338_migrate_notifications_policy_v2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class NotificationPolicy < ApplicationRecord; end
def up
NotificationPolicy.in_batches.update_all(<<~SQL.squish)
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_private_mentions = CASE filter_private_mentions WHEN true THEN 1 ELSE 0 END
SQL
Expand All @@ -18,7 +18,7 @@ def up
def down
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_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_private_mentions = CASE for_private_mentions WHEN 0 THEN false ELSE true END
SQL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class NotificationPolicy < ApplicationRecord; end
def up
NotificationPolicy.in_batches.update_all(<<~SQL.squish)
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_private_mentions = CASE filter_private_mentions WHEN true THEN 1 ELSE 0 END
SQL
Expand All @@ -18,7 +18,7 @@ def up
def down
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_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_private_mentions = CASE for_private_mentions WHEN 0 THEN false ELSE true END
SQL
Expand Down
6 changes: 3 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ services:
web:
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
build: .
image: kmyblue:16.1
image: kmyblue:16.2
restart: always
env_file: .env.production
command: bundle exec puma -C config/puma.rb
Expand All @@ -83,7 +83,7 @@ services:
build:
dockerfile: ./streaming/Dockerfile
context: .
image: kmyblue-streaming:16.1
image: kmyblue-streaming:16.2
restart: always
env_file: .env.production
command: node ./streaming/index.js
Expand All @@ -101,7 +101,7 @@ services:

sidekiq:
build: .
image: kmyblue:16.1
image: kmyblue:16.2
restart: always
env_file: .env.production
command: bundle exec sidekiq
Expand Down
2 changes: 1 addition & 1 deletion lib/mastodon/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def kmyblue_major
end

def kmyblue_minor
1
2
end

def kmyblue_flag
Expand Down
Loading

0 comments on commit 66b57ae

Please sign in to comment.