mirror of
https://git.kescher.at/CatCatNya/catstodon.git
synced 2025-01-18 19:44:05 +01:00
Merge branch 'upstream-stable/4.3' into stable/4.3
This commit is contained in:
commit
d798e6a380
24 changed files with 320 additions and 109 deletions
|
@ -4,10 +4,15 @@ All changes to Catstodon that aren't Mastodon or glitch-soc Mastodon changes wil
|
||||||
|
|
||||||
All release dates, as well as most other dates, are intended to be read as "within the day, in UTC time."
|
All release dates, as well as most other dates, are intended to be read as "within the day, in UTC time."
|
||||||
|
|
||||||
|
## [v4.3.3+cat+1.0.0] - 2025-01-16
|
||||||
|
|
||||||
|
- Upstream changes, including security changes. See https://github.com/glitch-soc/mastodon/releases/tag/v4.3.3.
|
||||||
|
|
||||||
## [v4.3.2+cat.1.0.1] - 2025-01-04
|
## [v4.3.2+cat.1.0.1] - 2025-01-04
|
||||||
|
|
||||||
- The character counter is now always below the text field, not somewhere among the action buttons
|
- The character counter is now always below the text field, not somewhere among the action buttons
|
||||||
- The standalone share page now has the correct amount of remaining characters. Previously, it would assume a maximum character count of 500.
|
- The standalone share page now has the correct amount of remaining characters. Previously, it would assume a maximum
|
||||||
|
character count of 500.
|
||||||
- Emoji reaction patch changes (removal of old, obsolete migration)
|
- Emoji reaction patch changes (removal of old, obsolete migration)
|
||||||
|
|
||||||
## [v4.3.2+cat.1.0.0] - 2024-12-30
|
## [v4.3.2+cat.1.0.0] - 2024-12-30
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -150,6 +150,7 @@ RUN \
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
libtool \
|
libtool \
|
||||||
|
libyaml-dev \
|
||||||
meson \
|
meson \
|
||||||
nasm \
|
nasm \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
|
|
117
Gemfile.lock
117
Gemfile.lock
|
@ -10,35 +10,35 @@ GIT
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (7.1.4.1)
|
actioncable (7.1.5.1)
|
||||||
actionpack (= 7.1.4.1)
|
actionpack (= 7.1.5.1)
|
||||||
activesupport (= 7.1.4.1)
|
activesupport (= 7.1.5.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
actionmailbox (7.1.4.1)
|
actionmailbox (7.1.5.1)
|
||||||
actionpack (= 7.1.4.1)
|
actionpack (= 7.1.5.1)
|
||||||
activejob (= 7.1.4.1)
|
activejob (= 7.1.5.1)
|
||||||
activerecord (= 7.1.4.1)
|
activerecord (= 7.1.5.1)
|
||||||
activestorage (= 7.1.4.1)
|
activestorage (= 7.1.5.1)
|
||||||
activesupport (= 7.1.4.1)
|
activesupport (= 7.1.5.1)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
actionmailer (7.1.4.1)
|
actionmailer (7.1.5.1)
|
||||||
actionpack (= 7.1.4.1)
|
actionpack (= 7.1.5.1)
|
||||||
actionview (= 7.1.4.1)
|
actionview (= 7.1.5.1)
|
||||||
activejob (= 7.1.4.1)
|
activejob (= 7.1.5.1)
|
||||||
activesupport (= 7.1.4.1)
|
activesupport (= 7.1.5.1)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (7.1.4.1)
|
actionpack (7.1.5.1)
|
||||||
actionview (= 7.1.4.1)
|
actionview (= 7.1.5.1)
|
||||||
activesupport (= 7.1.4.1)
|
activesupport (= 7.1.5.1)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
racc
|
racc
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
|
@ -46,15 +46,15 @@ GEM
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
actiontext (7.1.4.1)
|
actiontext (7.1.5.1)
|
||||||
actionpack (= 7.1.4.1)
|
actionpack (= 7.1.5.1)
|
||||||
activerecord (= 7.1.4.1)
|
activerecord (= 7.1.5.1)
|
||||||
activestorage (= 7.1.4.1)
|
activestorage (= 7.1.5.1)
|
||||||
activesupport (= 7.1.4.1)
|
activesupport (= 7.1.5.1)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (7.1.4.1)
|
actionview (7.1.5.1)
|
||||||
activesupport (= 7.1.4.1)
|
activesupport (= 7.1.5.1)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.11)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
|
@ -64,30 +64,33 @@ GEM
|
||||||
activemodel (>= 4.1)
|
activemodel (>= 4.1)
|
||||||
case_transform (>= 0.2)
|
case_transform (>= 0.2)
|
||||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||||
activejob (7.1.4.1)
|
activejob (7.1.5.1)
|
||||||
activesupport (= 7.1.4.1)
|
activesupport (= 7.1.5.1)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (7.1.4.1)
|
activemodel (7.1.5.1)
|
||||||
activesupport (= 7.1.4.1)
|
activesupport (= 7.1.5.1)
|
||||||
activerecord (7.1.4.1)
|
activerecord (7.1.5.1)
|
||||||
activemodel (= 7.1.4.1)
|
activemodel (= 7.1.5.1)
|
||||||
activesupport (= 7.1.4.1)
|
activesupport (= 7.1.5.1)
|
||||||
timeout (>= 0.4.0)
|
timeout (>= 0.4.0)
|
||||||
activestorage (7.1.4.1)
|
activestorage (7.1.5.1)
|
||||||
actionpack (= 7.1.4.1)
|
actionpack (= 7.1.5.1)
|
||||||
activejob (= 7.1.4.1)
|
activejob (= 7.1.5.1)
|
||||||
activerecord (= 7.1.4.1)
|
activerecord (= 7.1.5.1)
|
||||||
activesupport (= 7.1.4.1)
|
activesupport (= 7.1.5.1)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
activesupport (7.1.4.1)
|
activesupport (7.1.5.1)
|
||||||
base64
|
base64
|
||||||
|
benchmark (>= 0.3)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
connection_pool (>= 2.2.5)
|
connection_pool (>= 2.2.5)
|
||||||
drb
|
drb
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
|
logger (>= 1.4.2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
mutex_m
|
mutex_m
|
||||||
|
securerandom (>= 0.3)
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
addressable (2.8.7)
|
addressable (2.8.7)
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
public_suffix (>= 2.0.2, < 7.0)
|
||||||
|
@ -126,6 +129,7 @@ GEM
|
||||||
base64 (0.2.0)
|
base64 (0.2.0)
|
||||||
bcp47_spec (0.2.1)
|
bcp47_spec (0.2.1)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
|
benchmark (0.4.0)
|
||||||
better_errors (2.10.1)
|
better_errors (2.10.1)
|
||||||
erubi (>= 1.0.0)
|
erubi (>= 1.0.0)
|
||||||
rack (>= 0.9.0)
|
rack (>= 0.9.0)
|
||||||
|
@ -454,7 +458,7 @@ GEM
|
||||||
net-smtp (0.5.0)
|
net-smtp (0.5.0)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.3)
|
nio4r (2.7.3)
|
||||||
nokogiri (1.16.7)
|
nokogiri (1.16.8)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oj (3.16.6)
|
oj (3.16.6)
|
||||||
|
@ -638,20 +642,20 @@ GEM
|
||||||
rackup (1.0.0)
|
rackup (1.0.0)
|
||||||
rack (< 3)
|
rack (< 3)
|
||||||
webrick
|
webrick
|
||||||
rails (7.1.4.1)
|
rails (7.1.5.1)
|
||||||
actioncable (= 7.1.4.1)
|
actioncable (= 7.1.5.1)
|
||||||
actionmailbox (= 7.1.4.1)
|
actionmailbox (= 7.1.5.1)
|
||||||
actionmailer (= 7.1.4.1)
|
actionmailer (= 7.1.5.1)
|
||||||
actionpack (= 7.1.4.1)
|
actionpack (= 7.1.5.1)
|
||||||
actiontext (= 7.1.4.1)
|
actiontext (= 7.1.5.1)
|
||||||
actionview (= 7.1.4.1)
|
actionview (= 7.1.5.1)
|
||||||
activejob (= 7.1.4.1)
|
activejob (= 7.1.5.1)
|
||||||
activemodel (= 7.1.4.1)
|
activemodel (= 7.1.5.1)
|
||||||
activerecord (= 7.1.4.1)
|
activerecord (= 7.1.5.1)
|
||||||
activestorage (= 7.1.4.1)
|
activestorage (= 7.1.5.1)
|
||||||
activesupport (= 7.1.4.1)
|
activesupport (= 7.1.5.1)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 7.1.4.1)
|
railties (= 7.1.5.1)
|
||||||
rails-controller-testing (1.0.5)
|
rails-controller-testing (1.0.5)
|
||||||
actionpack (>= 5.0.1.rc1)
|
actionpack (>= 5.0.1.rc1)
|
||||||
actionview (>= 5.0.1.rc1)
|
actionview (>= 5.0.1.rc1)
|
||||||
|
@ -660,15 +664,15 @@ GEM
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.6.0)
|
rails-html-sanitizer (1.6.2)
|
||||||
loofah (~> 2.21)
|
loofah (~> 2.21)
|
||||||
nokogiri (~> 1.14)
|
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||||
rails-i18n (7.0.9)
|
rails-i18n (7.0.9)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 8)
|
railties (>= 6.0.0, < 8)
|
||||||
railties (7.1.4.1)
|
railties (7.1.5.1)
|
||||||
actionpack (= 7.1.4.1)
|
actionpack (= 7.1.5.1)
|
||||||
activesupport (= 7.1.4.1)
|
activesupport (= 7.1.5.1)
|
||||||
irb
|
irb
|
||||||
rackup (>= 1.0.0)
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
|
@ -781,6 +785,7 @@ GEM
|
||||||
scenic (1.8.0)
|
scenic (1.8.0)
|
||||||
activerecord (>= 4.0.0)
|
activerecord (>= 4.0.0)
|
||||||
railties (>= 4.0.0)
|
railties (>= 4.0.0)
|
||||||
|
securerandom (0.4.1)
|
||||||
selenium-webdriver (4.25.0)
|
selenium-webdriver (4.25.0)
|
||||||
base64 (~> 0.2)
|
base64 (~> 0.2)
|
||||||
logger (~> 1.4)
|
logger (~> 1.4)
|
||||||
|
|
|
@ -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
|
||||||
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
|
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
|
||||||
|
|
|
@ -63,21 +63,31 @@ 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,
|
||||||
(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]
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,8 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||||
SUBDOMAINS_RATELIMIT = 10
|
SUBDOMAINS_RATELIMIT = 10
|
||||||
DISCOVERIES_PER_REQUEST = 400
|
DISCOVERIES_PER_REQUEST = 400
|
||||||
|
|
||||||
|
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 = {})
|
def call(username, domain, json, options = {})
|
||||||
|
@ -96,16 +98,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'] || ''
|
||||||
|
@ -268,10 +282,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?
|
||||||
|
|
|
@ -188,40 +188,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
|
||||||
|
rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError
|
||||||
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
|
|
||||||
# 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|
|
||||||
|
|
|
@ -13,7 +13,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
|
||||||
|
@ -57,6 +57,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}"
|
||||||
|
@ -78,7 +80,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
|
||||||
|
@ -86,7 +88,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: ghcr.io/mastodon/mastodon:v4.3.2
|
image: ghcr.io/mastodon/mastodon:v4.3.3
|
||||||
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: ghcr.io/mastodon/mastodon-streaming:v4.3.2
|
image: ghcr.io/mastodon/mastodon-streaming:v4.3.3
|
||||||
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: ghcr.io/mastodon/mastodon:v4.3.2
|
image: ghcr.io/mastodon/mastodon:v4.3.3
|
||||||
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 patch
|
def patch
|
||||||
2
|
3
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_prerelease
|
def default_prerelease
|
||||||
|
@ -25,7 +25,7 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
def catstodon_revision
|
def catstodon_revision
|
||||||
'1.0.1'
|
'1.0.0'
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_metadata
|
def build_metadata
|
||||||
|
|
|
@ -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
|
|
@ -6,6 +6,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
||||||
subject { described_class.new }
|
subject { described_class.new }
|
||||||
|
|
||||||
let!(:status) { Fabricate(:status, text: 'Hello world', account: Fabricate(:account, domain: 'example.com')) }
|
let!(:status) { Fabricate(:status, text: 'Hello world', account: Fabricate(:account, domain: 'example.com')) }
|
||||||
|
let(:bogus_mention) { 'https://example.com/users/erroringuser' }
|
||||||
let(:payload) do
|
let(:payload) do
|
||||||
{
|
{
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
@ -17,6 +18,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
||||||
tag: [
|
tag: [
|
||||||
{ type: 'Hashtag', name: 'hoge' },
|
{ type: 'Hashtag', name: 'hoge' },
|
||||||
{ type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) },
|
{ type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) },
|
||||||
|
{ type: 'Mention', href: bogus_mention },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
@ -30,19 +32,21 @@ 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 }
|
||||||
|
stub_request(:get, bogus_mention).to_raise(HTTP::ConnectionError)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#call' do
|
describe '#call' do
|
||||||
it 'updates text and content warning' do
|
it 'updates text and content warning, and schedules re-fetching broken mention' do
|
||||||
subject.call(status, json, json)
|
subject.call(status, json, json)
|
||||||
expect(status.reload)
|
expect(status.reload)
|
||||||
.to have_attributes(
|
.to have_attributes(
|
||||||
text: eq('Hello universe'),
|
text: eq('Hello universe'),
|
||||||
spoiler_text: eq('Show more')
|
spoiler_text: eq('Show more')
|
||||||
)
|
)
|
||||||
|
expect(MentionResolveWorker).to have_enqueued_sidekiq_job(status.id, bogus_mention, anything)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the changes are only in sanitized-out HTML' do
|
context 'when the changes are only in sanitized-out HTML' do
|
||||||
|
@ -276,7 +280,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)
|
||||||
|
|
|
@ -150,6 +150,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…
Reference in a new issue