Compare commits

...
Sign in to create a new pull request.

26 commits

Author SHA1 Message Date
KMY(雪あすか)
66b57ae84a
Merge pull request #965 from kmycode/kb-draft-16.2
Release: 16.2
2025-01-17 21:39:38 +09:00
KMY(雪あすか)
f03a5abc21
Fix test 2025-01-17 09:33:11 +09:00
KMY
b803e5d4f1 Bump version to kb16.2 2025-01-16 23:04:29 +09:00
KMY(雪あすか)
92e529834d Add: 絵文字リアクション対応サーバーにyojo-art (#957) 2025-01-16 23:04:29 +09:00
KMY(雪あすか)
0805b13a94 Add: 絵文字リアクション対応ソフトウェア名にIceshrimp.NET 2025-01-16 23:04:29 +09:00
Michael Stanclift
d936349a02 Fix libyaml missing from Dockerfile build stage (#33591) 2025-01-16 22:58:39 +09:00
Claire
af2f6597cc Fix incorrect relationship_severance_event attribute name in changelog (#33443) 2025-01-16 22:58:14 +09:00
Claire
fd8ca6fc29 Fix incorrect notification settings migration for non-followers (#33348) 2025-01-16 22:57:40 +09:00
Jesse Karmani
da9f9a74af Fix down clause for notification policy v2 migrations (#33340) 2025-01-16 22:57:21 +09:00
Claire
3867cc8504 Fix error decrementing status count when FeaturedTags#last_status_at is nil (#33320) 2025-01-16 22:57:02 +09:00
Claire
cc25d99330 Fix last paginated notification group only including data on a single notification (#33271) 2025-01-16 22:56:47 +09:00
Claire
94d9a1930e Fix processing of mentions for post edits with an existing corresponding silent mention (#33227) 2025-01-16 22:47:47 +09:00
Claire
4e1262f8bd Fix deletion of unconfirmed users with Webauthn set (#33186) 2025-01-16 22:47:19 +09:00
Claire
775f2b8624 Fix fediverse:creator metadata not showing up in REST API (#33466) 2025-01-16 22:46:51 +09:00
Matt Jankowski
4ab134ae50 Fix empty authors preview card serialization (#33151) 2025-01-16 22:46:19 +09:00
Claire
e6b5b61559 Merge commit from fork 2025-01-16 22:44:50 +09:00
KMY(雪あすか)
6df5dfeebd
Fix: サークル・ブックマーク分類で過去投稿が遡れない問題 (#940) 2024-12-06 12:25:37 +09:00
KMY(雪あすか)
4a0bd8a0fd
Merge pull request #937 from kmycode/kb-draft-16.1
Release: 16.1
2024-12-04 09:37:12 +09:00
Claire
0c3fab40df Bump version to v4.3.2 (#33136) 2024-12-04 08:44:51 +09:00
Claire
c5a7a70355 Prepare changelog 2024-12-04 08:43:01 +09:00
Claire
2472c85096 Fix processing incoming post edits with mentions to unresolvable accounts (#33129) 2024-12-04 08:42:56 +09:00
Yann
dc97219d37 Remove constant definition from global scope in embed.js (#33107) 2024-12-04 08:42:51 +09:00
Claire
8c2519832e Add tootctl feeds vacuum (#33065) 2024-12-04 08:42:46 +09:00
Claire
49ca570448 Fix inactive users' timelines being backfilled on follow and unsuspend (#33094) 2024-12-04 08:42:03 +09:00
KMY(雪あすか)
d1e6027cf6
Bump version to 16.0 (#928) 2024-12-04 08:16:00 +09:00
KMY(雪あすか)
7348de7e41
Fix: フレンドサーバー申請時、ドメインを偽装して無関係のInboxを指定できる脆弱性 (#934) 2024-12-04 08:15:18 +09:00
32 changed files with 367 additions and 79 deletions

View file

@ -2,6 +2,66 @@
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
### Added
- Add `tootctl feeds vacuum` (#33065 by @ClearlyClaire)
- Add error message when user tries to follow their own account (#31910 by @lenikadali)
- Add client_secret_expires_at to OAuth Applications (#30317 by @ThisIsMissEm)
### Changed
- Change design of Content Warnings and filters (#32543 by @ClearlyClaire)
### Fixed
- Fix processing incoming post edits with mentions to unresolvable accounts (#33129 by @ClearlyClaire)
- Fix error when including multiple instances of `embed.js` (#33107 by @YKWeyer)
- Fix inactive users' timelines being backfilled on follow and unsuspend (#33094 by @ClearlyClaire)
- Fix direct inbox delivery pushing posts into inactive followers' timelines (#33067 by @ClearlyClaire)
- Fix `TagFollow` records not being correctly handled in account operations (#33063 by @ClearlyClaire)
- Fix pushing hashtag-followed posts to feeds of inactive users (#33018 by @Gargron)
- Fix duplicate notifications in notification groups when using slow mode (#33014 by @ClearlyClaire)
- Fix posts made in the future being allowed to trend (#32996 by @ClearlyClaire)
- Fix uploading higher-than-wide GIF profile picture with libvips enabled (#32911 by @ClearlyClaire)
- Fix domain attribution field having autocorrect and autocapitalize enabled (#32903 by @ClearlyClaire)
- Fix titles being escaped twice (#32889 by @ClearlyClaire)
- Fix list creation limit check (#32869 by @ClearlyClaire)
- Fix error in `tootctl email_domain_blocks` when supplying `--with-dns-records` (#32863 by @mjankowski)
- Fix `min_id` and `max_id` causing error in search API (#32857 by @Gargron)
- Fix inefficiencies when processing removal of posts that use featured tags (#32787 by @ClearlyClaire)
- Fix alt-text pop-in not using the translated description (#32766 by @ClearlyClaire)
- Fix preview cards with long titles erroneously causing layout changes (#32678 by @ClearlyClaire)
- Fix embed modal layout on mobile (#32641 by @DismalShadowX)
- Fix and improve batch attachment deletion handling when using OpenStack Swift (#32637 by @hugogameiro)
- Fix blocks not being applied on link timeline (#32625 by @tribela)
- Fix follow counters being incorrectly changed (#32622 by @oneiros)
- Fix 'unknown' media attachment type rendering (#32613 and #32713 by @ThisIsMissEm and @renatolond)
- Fix tl language native name (#32606 by @seav)
### Security
- Update dependencies
## [4.3.1] - 2024-10-21 ## [4.3.1] - 2024-10-21
### Added ### Added
@ -93,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.

View file

@ -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 \

View file

@ -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]),

View file

@ -129,9 +129,9 @@ export const fetchBookmarkCategoryStatusesFail = (id, error) => ({
export function expandBookmarkCategoryStatuses(bookmarkCategoryId) { export function expandBookmarkCategoryStatuses(bookmarkCategoryId) {
return (dispatch, getState) => { return (dispatch, getState) => {
const url = getState().getIn(['bookmark_categories', bookmarkCategoryId, 'next'], null); const url = getState().getIn(['status_lists', 'bookmark_category_statuses', bookmarkCategoryId, 'next'], null);
if (url === null || getState().getIn(['bookmark_categories', bookmarkCategoryId, 'isLoading'])) { if (url === null || getState().getIn(['status_lists', 'bookmark_category_statuses', bookmarkCategoryId, 'isLoading'])) {
return; return;
} }

View file

@ -152,9 +152,9 @@ export function fetchCircleStatusesFail(id, error) {
export function expandCircleStatuses(circleId) { export function expandCircleStatuses(circleId) {
return (dispatch, getState) => { return (dispatch, getState) => {
const url = getState().getIn(['circles', circleId, 'next'], null); const url = getState().getIn(['status_lists', 'circle_statuses', circleId, 'next'], null);
if (url === null || getState().getIn(['circles', circleId, 'isLoading'])) { if (url === null || getState().getIn(['status_lists', 'circle_statuses', circleId, 'isLoading'])) {
return; return;
} }

View file

@ -60,19 +60,13 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
already_accepted = friend.accepted? already_accepted = friend.accepted?
friend.update!(passive_state: :pending, active_state: :idle, passive_follow_activity_id: @json['id']) friend.update!(passive_state: :pending, active_state: :idle, passive_follow_activity_id: @json['id'])
else else
@friend = FriendDomain.new(domain: @account.domain, passive_state: :pending, passive_follow_activity_id: @json['id']) @friend = FriendDomain.create!(domain: @account.domain, passive_state: :pending, passive_follow_activity_id: @json['id'], inbox_url: @account.preferred_inbox_url)
@friend.inbox_url = @json['inboxUrl'].presence || @friend.default_inbox_url
@friend.save!
end end
if already_accepted || Setting.unlocked_friend friend.accept! if already_accepted || Setting.unlocked_friend
friend.accept!
# Notify for admin even if unlocked # Notify for admin
notify_staff_about_pending_friend_server! unless already_accepted notify_staff_about_pending_friend_server! unless already_accepted
else
notify_staff_about_pending_friend_server!
end
end end
def friend def friend

View file

@ -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

View file

@ -125,6 +125,8 @@ class FeedManager
# @param [Account] into_account # @param [Account] into_account
# @return [void] # @return [void]
def merge_into_home(from_account, into_account) def merge_into_home(from_account, into_account)
return unless into_account.user&.signed_in_recently?
timeline_key = key(:home, into_account.id) timeline_key = key(:home, into_account.id)
aggregate = into_account.user&.aggregates_reblogs? aggregate = into_account.user&.aggregates_reblogs?
query = from_account.statuses.list_eligible_visibility.includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4) query = from_account.statuses.list_eligible_visibility.includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
@ -151,6 +153,8 @@ class FeedManager
# @param [List] list # @param [List] list
# @return [void] # @return [void]
def merge_into_list(from_account, list) def merge_into_list(from_account, list)
return unless list.account.user&.signed_in_recently?
timeline_key = key(:list, list.id) timeline_key = key(:list, list.id)
aggregate = list.account.user&.aggregates_reblogs? aggregate = list.account.user&.aggregates_reblogs?
query = from_account.statuses.list_eligible_visibility.includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4) query = from_account.statuses.list_eligible_visibility.includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)

View file

@ -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

View file

@ -116,6 +116,7 @@ class FriendDomain < ApplicationRecord
object: ActivityPub::TagManager::COLLECTIONS[:public], object: ActivityPub::TagManager::COLLECTIONS[:public],
# Cannot use inbox_url method because this model also has inbox_url column # Cannot use inbox_url method because this model also has inbox_url column
# This is deprecated property. Newer version's kmyblue will ignore it.
inboxUrl: "https://#{Rails.configuration.x.web_domain}/inbox", inboxUrl: "https://#{Rails.configuration.x.web_domain}/inbox",
} }
end end

View file

@ -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

View file

@ -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 = [

View file

@ -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]

View file

@ -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?

View file

@ -259,33 +259,35 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
end end
def update_mentions! def update_mentions!
previous_mentions = @status.active_mentions.includes(:account).to_a unresolved_mentions = []
current_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
rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS
mention = previous_mentions.find { |x| x.account_id == account.id } # Since previous mentions are about already-known accounts,
mention ||= account.mentions.new(status: @status) # 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
current_mentions << mention # affect `removed_mentions` so they can safely be retried asynchronously
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
unresolved_mentions.uniq.each do |uri|
MentionResolveWorker.perform_in(rand(30...600).seconds, @status.id, uri, { 'request_id' => @request_id })
end
end end
def update_emojis! def update_emojis!

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.0-dev 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.0-dev 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.0-dev image: kmyblue:16.2
restart: always restart: always
env_file: .env.production env_file: .env.production
command: bundle exec sidekiq command: bundle exec sidekiq

View file

@ -5,6 +5,7 @@ require_relative 'base'
module Mastodon::CLI module Mastodon::CLI
class Feeds < Base class Feeds < Base
include Redisable include Redisable
include DatabaseHelper
option :all, type: :boolean, default: false option :all, type: :boolean, default: false
option :concurrency, type: :numeric, default: 5, aliases: [:c] option :concurrency, type: :numeric, default: 5, aliases: [:c]
@ -59,6 +60,38 @@ module Mastodon::CLI
say('OK', :green) say('OK', :green)
end end
desc 'vacuum', 'Remove home feeds of inactive users from Redis'
long_desc <<-LONG_DESC
Running this task should not be needed in most cases, as Mastodon will
automatically clean up feeds from inactive accounts every day.
However, this task is more aggressive in order to clean up feeds that
may have been missed because of bugs or database mishaps.
LONG_DESC
def vacuum
with_read_replica do
say('Deleting orphaned home feeds…')
redis.scan_each(match: 'feed:home:*').each_slice(1000) do |keys|
ids = keys.map { |key| key.split(':')[2] }.compact_blank
known_ids = User.confirmed.signed_in_recently.where(account_id: ids).pluck(:account_id)
keys_to_delete = keys.filter { |key| known_ids.exclude?(key.split(':')[2]&.to_i) }
redis.del(keys_to_delete)
end
say('Deleting orphaned list feeds…')
redis.scan_each(match: 'feed:list:*').each_slice(1000) do |keys|
ids = keys.map { |key| key.split(':')[2] }.compact_blank
known_ids = List.where(account_id: User.confirmed.signed_in_recently.select(:account_id)).where(id: ids).pluck(:id)
keys_to_delete = keys.filter { |key| known_ids.exclude?(key.split(':')[2]&.to_i) }
redis.del(keys_to_delete)
end
end
end
private private
def active_user_accounts def active_user_accounts

View file

@ -13,13 +13,13 @@ module Mastodon
end end
def kmyblue_minor def kmyblue_minor
0 2
end end
def kmyblue_flag def kmyblue_flag
# 'LTS' # 'LTS'
'dev' # 'dev'
# nil nil
end end
def major def major

View file

@ -1,8 +1,5 @@
// @ts-check // @ts-check
(function (allowedPrefixes) {
const allowedPrefixes = (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT' && document.currentScript.dataset.allowedPrefixes) ? document.currentScript.dataset.allowedPrefixes.split(' ') : [];
(function () {
'use strict'; 'use strict';
/** /**
@ -127,4 +124,4 @@ const allowedPrefixes = (document.currentScript && document.currentScript.tagNam
container.appendChild(iframe); container.appendChild(iframe);
}); });
}); });
})(); })((document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT' && document.currentScript.dataset.allowedPrefixes) ? document.currentScript.dataset.allowedPrefixes.split(' ') : []);

View file

@ -380,11 +380,10 @@ RSpec.describe ActivityPub::Activity::Follow do
context 'when given a friend server' do context 'when given a friend server' do
subject { described_class.new(json, sender) } subject { described_class.new(json, sender) }
let(:sender) { Fabricate(:account, domain: 'abc.com', url: 'https://abc.com/#actor') } let(:sender) { Fabricate(:account, domain: 'abc.com', url: 'https://abc.com/#actor', shared_inbox_url: 'https://abc.com/shared_inbox') }
let!(:friend) { Fabricate(:friend_domain, domain: 'abc.com', inbox_url: 'https://example.com/inbox', passive_state: :idle) } let!(:friend) { Fabricate(:friend_domain, domain: 'abc.com', inbox_url: 'https://example.com/inbox', passive_state: :idle) }
let!(:owner_user) { Fabricate(:user, role: UserRole.find_by(name: 'Owner')) } let!(:owner_user) { Fabricate(:user, role: UserRole.find_by(name: 'Owner')) }
let!(:patch_user) { Fabricate(:user, role: Fabricate(:user_role, name: 'OhagiOps', permissions: UserRole::FLAGS[:manage_federation])) } let!(:patch_user) { Fabricate(:user, role: Fabricate(:user_role, name: 'OhagiOps', permissions: UserRole::FLAGS[:manage_federation])) }
let(:inbox_url) { nil }
let(:json) do let(:json) do
{ {
@ -393,7 +392,6 @@ RSpec.describe ActivityPub::Activity::Follow do
type: 'Follow', type: 'Follow',
actor: ActivityPub::TagManager.instance.uri_for(sender), actor: ActivityPub::TagManager.instance.uri_for(sender),
object: 'https://www.w3.org/ns/activitystreams#Public', object: 'https://www.w3.org/ns/activitystreams#Public',
inboxUrl: inbox_url,
}.with_indifferent_access }.with_indifferent_access
end end
@ -415,25 +413,34 @@ RSpec.describe ActivityPub::Activity::Follow do
expect(friend).to_not be_nil expect(friend).to_not be_nil
expect(friend.they_are_pending?).to be true expect(friend.they_are_pending?).to be true
expect(friend.passive_follow_activity_id).to eq 'foo' expect(friend.passive_follow_activity_id).to eq 'foo'
expect(friend.inbox_url).to eq 'https://abc.com/inbox' expect(friend.inbox_url).to eq 'https://abc.com/shared_inbox'
end end
end end
context 'when no record and inbox_url is specified' do context 'when old spec which no record and inbox_url is specified' do
let(:inbox_url) { 'https://ohagi.com/inbox' } let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Follow',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: 'https://www.w3.org/ns/activitystreams#Public',
inboxUrl: 'https://evil.org/bad_inbox',
}.with_indifferent_access
end
before do before do
friend.destroy! friend.destroy!
end end
it 'marks the friend as pending' do it 'marks the friend as pending but inboxUrl is not working' do
subject.perform subject.perform
friend = FriendDomain.find_by(domain: 'abc.com') friend = FriendDomain.find_by(domain: 'abc.com')
expect(friend).to_not be_nil expect(friend).to_not be_nil
expect(friend.they_are_pending?).to be true expect(friend.they_are_pending?).to be true
expect(friend.passive_follow_activity_id).to eq 'foo' expect(friend.passive_follow_activity_id).to eq 'foo'
expect(friend.inbox_url).to eq 'https://ohagi.com/inbox' expect(friend.inbox_url).to eq 'https://abc.com/shared_inbox'
end end
end end

View file

@ -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')

View file

@ -21,7 +21,6 @@ RSpec.describe FriendDomain do
type: 'Follow', type: 'Follow',
actor: 'https://cb6e6126.ngrok.io/actor', actor: 'https://cb6e6126.ngrok.io/actor',
object: 'https://www.w3.org/ns/activitystreams#Public', object: 'https://www.w3.org/ns/activitystreams#Public',
inboxUrl: 'https://cb6e6126.ngrok.io/inbox',
}))).to have_been_made.once }))).to have_been_made.once
end end
end end

View file

@ -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

View 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

View file

@ -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)

View file

@ -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

View file

@ -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