Compare commits
26 commits
kb_develop
...
releases/1
Author | SHA1 | Date | |
---|---|---|---|
|
66b57ae84a | ||
|
f03a5abc21 | ||
|
b803e5d4f1 | ||
|
92e529834d | ||
|
0805b13a94 | ||
|
d936349a02 | ||
|
af2f6597cc | ||
|
fd8ca6fc29 | ||
|
da9f9a74af | ||
|
3867cc8504 | ||
|
cc25d99330 | ||
|
94d9a1930e | ||
|
4e1262f8bd | ||
|
775f2b8624 | ||
|
4ab134ae50 | ||
|
e6b5b61559 | ||
|
6df5dfeebd | ||
|
4a0bd8a0fd | ||
|
0c3fab40df | ||
|
c5a7a70355 | ||
|
2472c85096 | ||
|
dc97219d37 | ||
|
8c2519832e | ||
|
49ca570448 | ||
|
d1e6027cf6 | ||
|
7348de7e41 |
32 changed files with 367 additions and 79 deletions
62
CHANGELOG.md
62
CHANGELOG.md
|
@ -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.
|
||||||
|
|
|
@ -153,6 +153,7 @@ RUN \
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
libtool \
|
libtool \
|
||||||
|
libyaml-dev \
|
||||||
meson \
|
meson \
|
||||||
nasm \
|
nasm \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
|
|
|
@ -80,10 +80,31 @@ class Api::V2::NotificationsController < Api::BaseController
|
||||||
return [] if @notifications.empty?
|
return [] if @notifications.empty?
|
||||||
|
|
||||||
MastodonOTELTracer.in_span('Api::V2::NotificationsController#load_grouped_notifications') do
|
MastodonOTELTracer.in_span('Api::V2::NotificationsController#load_grouped_notifications') do
|
||||||
NotificationGroup.from_notifications(@notifications, pagination_range: (@notifications.last.id)..(@notifications.first.id), grouped_types: params[:grouped_types])
|
pagination_range = (@notifications.last.id)..@notifications.first.id
|
||||||
|
|
||||||
|
# If the page is incomplete, we know we are on the last page
|
||||||
|
if incomplete_page?
|
||||||
|
if paginating_up?
|
||||||
|
pagination_range = @notifications.last.id...(params[:max_id]&.to_i)
|
||||||
|
else
|
||||||
|
range_start = params[:since_id]&.to_i
|
||||||
|
range_start += 1 unless range_start.nil?
|
||||||
|
pagination_range = range_start..(@notifications.first.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
NotificationGroup.from_notifications(@notifications, pagination_range: pagination_range, grouped_types: params[:grouped_types])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def incomplete_page?
|
||||||
|
@notifications.size < limit_param(DEFAULT_NOTIFICATIONS_LIMIT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def paginating_up?
|
||||||
|
params[:min_id].present?
|
||||||
|
end
|
||||||
|
|
||||||
def browserable_account_notifications
|
def browserable_account_notifications
|
||||||
current_account.notifications.without_suspended.browserable(
|
current_account.notifications.without_suspended.browserable(
|
||||||
types: Array(browserable_params[:types]),
|
types: Array(browserable_params[:types]),
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -25,11 +25,13 @@ class InstanceInfo < ApplicationRecord
|
||||||
firefish
|
firefish
|
||||||
hollo
|
hollo
|
||||||
iceshrimp
|
iceshrimp
|
||||||
|
Iceshrimp.NET
|
||||||
meisskey
|
meisskey
|
||||||
misskey
|
misskey
|
||||||
pleroma
|
pleroma
|
||||||
sharkey
|
sharkey
|
||||||
tanukey
|
tanukey
|
||||||
|
yojo-art
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
QUOTE_AVAILABLE_SOFTWARES = EMOJI_REACTION_AVAILABLE_SOFTWARES + %w(bridgy-fed).freeze
|
QUOTE_AVAILABLE_SOFTWARES = EMOJI_REACTION_AVAILABLE_SOFTWARES + %w(bridgy-fed).freeze
|
||||||
|
|
|
@ -89,22 +89,32 @@ class NotificationGroup < ActiveModelSerializers::Model
|
||||||
binds = [
|
binds = [
|
||||||
account_id,
|
account_id,
|
||||||
SAMPLE_ACCOUNTS_SIZE,
|
SAMPLE_ACCOUNTS_SIZE,
|
||||||
pagination_range.begin,
|
|
||||||
pagination_range.end,
|
|
||||||
ActiveRecord::Relation::QueryAttribute.new('group_keys', group_keys, ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::String.new)),
|
ActiveRecord::Relation::QueryAttribute.new('group_keys', group_keys, ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::String.new)),
|
||||||
|
pagination_range.begin || 0,
|
||||||
]
|
]
|
||||||
|
binds << pagination_range.end unless pagination_range.end.nil?
|
||||||
|
|
||||||
|
upper_bound_cond = begin
|
||||||
|
if pagination_range.end.nil?
|
||||||
|
''
|
||||||
|
elsif pagination_range.exclude_end?
|
||||||
|
'AND id < $5'
|
||||||
|
else
|
||||||
|
'AND id <= $5'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
ActiveRecord::Base.connection.select_all(<<~SQL.squish, 'grouped_notifications', binds).cast_values.to_h { |k, *values| [k, values] }
|
ActiveRecord::Base.connection.select_all(<<~SQL.squish, 'grouped_notifications', binds).cast_values.to_h { |k, *values| [k, values] }
|
||||||
SELECT
|
SELECT
|
||||||
groups.group_key,
|
groups.group_key,
|
||||||
(SELECT id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4 ORDER BY id DESC LIMIT 1),
|
(SELECT id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key #{upper_bound_cond} ORDER BY id DESC LIMIT 1),
|
||||||
array(SELECT from_account_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4 ORDER BY id DESC LIMIT $2),
|
array(SELECT from_account_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key #{upper_bound_cond} ORDER BY id DESC LIMIT $2),
|
||||||
(SELECT count(*) FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4) AS notifications_count,
|
(SELECT count(*) FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key #{upper_bound_cond}) AS notifications_count,
|
||||||
array(SELECT activity_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4 AND activity_type = 'EmojiReaction'),
|
array(SELECT activity_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key #{upper_bound_cond} AND activity_type = 'EmojiReaction'),
|
||||||
(SELECT id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id >= $3 ORDER BY id ASC LIMIT 1) AS min_id,
|
(SELECT id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id >= $4 ORDER BY id ASC LIMIT 1) AS min_id,
|
||||||
(SELECT created_at FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4 ORDER BY id DESC LIMIT 1)
|
(SELECT created_at FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key #{upper_bound_cond} ORDER BY id DESC LIMIT 1)
|
||||||
FROM
|
FROM
|
||||||
unnest($5::text[]) AS groups(group_key);
|
unnest($3::text[]) AS groups(group_key);
|
||||||
SQL
|
SQL
|
||||||
else
|
else
|
||||||
binds = [
|
binds = [
|
||||||
|
|
|
@ -134,7 +134,7 @@ class PreviewCard < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def authors
|
def authors
|
||||||
@authors ||= [PreviewCard::Author.new(self)]
|
@authors ||= Array(serialized_authors)
|
||||||
end
|
end
|
||||||
|
|
||||||
class Author < ActiveModelSerializers::Model
|
class Author < ActiveModelSerializers::Model
|
||||||
|
@ -169,6 +169,13 @@ class PreviewCard < ApplicationRecord
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def serialized_authors
|
||||||
|
if author_name? || author_url? || author_account_id?
|
||||||
|
PreviewCard::Author
|
||||||
|
.new(self)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def extract_dimensions
|
def extract_dimensions
|
||||||
file = image.queued_for_write[:original]
|
file = image.queued_for_write[:original]
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,8 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||||
SCAN_SEARCHABILITY_RE = /\[searchability:(public|followers|reactors|private)\]/
|
SCAN_SEARCHABILITY_RE = /\[searchability:(public|followers|reactors|private)\]/
|
||||||
SCAN_SEARCHABILITY_FEDIBIRD_RE = /searchable_by_(all_users|followers_only|reacted_users_only|nobody)/
|
SCAN_SEARCHABILITY_FEDIBIRD_RE = /searchable_by_(all_users|followers_only|reacted_users_only|nobody)/
|
||||||
|
|
||||||
|
VALID_URI_SCHEMES = %w(http https).freeze
|
||||||
|
|
||||||
# Should be called with confirmed valid JSON
|
# Should be called with confirmed valid JSON
|
||||||
# and WebFinger-resolved username and domain
|
# and WebFinger-resolved username and domain
|
||||||
def call(username, domain, json, options = {}) # rubocop:disable Metrics/PerceivedComplexity
|
def call(username, domain, json, options = {}) # rubocop:disable Metrics/PerceivedComplexity
|
||||||
|
@ -113,16 +115,28 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_immediate_protocol_attributes!
|
def set_immediate_protocol_attributes!
|
||||||
@account.inbox_url = @json['inbox'] || ''
|
@account.inbox_url = valid_collection_uri(@json['inbox'])
|
||||||
@account.outbox_url = @json['outbox'] || ''
|
@account.outbox_url = valid_collection_uri(@json['outbox'])
|
||||||
@account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || ''
|
@account.shared_inbox_url = valid_collection_uri(@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox'])
|
||||||
@account.followers_url = @json['followers'] || ''
|
@account.followers_url = valid_collection_uri(@json['followers'])
|
||||||
@account.url = url || @uri
|
@account.url = url || @uri
|
||||||
@account.uri = @uri
|
@account.uri = @uri
|
||||||
@account.actor_type = actor_type
|
@account.actor_type = actor_type
|
||||||
@account.created_at = @json['published'] if @json['published'].present?
|
@account.created_at = @json['published'] if @json['published'].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def valid_collection_uri(uri)
|
||||||
|
uri = uri.first if uri.is_a?(Array)
|
||||||
|
uri = uri['id'] if uri.is_a?(Hash)
|
||||||
|
return '' unless uri.is_a?(String)
|
||||||
|
|
||||||
|
parsed_uri = Addressable::URI.parse(uri)
|
||||||
|
|
||||||
|
VALID_URI_SCHEMES.include?(parsed_uri.scheme) && parsed_uri.host.present? ? parsed_uri : ''
|
||||||
|
rescue Addressable::URI::InvalidURIError
|
||||||
|
''
|
||||||
|
end
|
||||||
|
|
||||||
def set_immediate_attributes!
|
def set_immediate_attributes!
|
||||||
@account.featured_collection_url = @json['featured'] || ''
|
@account.featured_collection_url = @json['featured'] || ''
|
||||||
@account.display_name = @json['name'] || ''
|
@account.display_name = @json['name'] || ''
|
||||||
|
@ -398,10 +412,11 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def collection_info(type)
|
def collection_info(type)
|
||||||
return [nil, nil] if @json[type].blank?
|
collection_uri = valid_collection_uri(@json[type])
|
||||||
|
return [nil, nil] if collection_uri.blank?
|
||||||
return @collections[type] if @collections.key?(type)
|
return @collections[type] if @collections.key?(type)
|
||||||
|
|
||||||
collection = fetch_resource_without_id_validation(@json[type])
|
collection = fetch_resource_without_id_validation(collection_uri)
|
||||||
|
|
||||||
total_items = collection.is_a?(Hash) && collection['totalItems'].present? && collection['totalItems'].is_a?(Numeric) ? collection['totalItems'] : nil
|
total_items = collection.is_a?(Hash) && collection['totalItems'].present? && collection['totalItems'].is_a?(Numeric) ? collection['totalItems'] : nil
|
||||||
has_first_page = collection.is_a?(Hash) && collection['first'].present?
|
has_first_page = collection.is_a?(Hash) && collection['first'].present?
|
||||||
|
|
|
@ -259,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!
|
||||||
|
|
|
@ -15,7 +15,7 @@ class ProcessMentionsService < BaseService
|
||||||
|
|
||||||
return unless @status.local?
|
return unless @status.local?
|
||||||
|
|
||||||
@previous_mentions = @status.active_mentions.includes(:account).to_a
|
@previous_mentions = @status.mentions.includes(:account).to_a
|
||||||
@current_mentions = []
|
@current_mentions = []
|
||||||
|
|
||||||
Status.transaction do
|
Status.transaction do
|
||||||
|
@ -63,6 +63,8 @@ class ProcessMentionsService < BaseService
|
||||||
mention ||= @current_mentions.find { |x| x.account_id == mentioned_account.id }
|
mention ||= @current_mentions.find { |x| x.account_id == mentioned_account.id }
|
||||||
mention ||= @status.mentions.new(account: mentioned_account)
|
mention ||= @status.mentions.new(account: mentioned_account)
|
||||||
|
|
||||||
|
mention.silent = false
|
||||||
|
|
||||||
@current_mentions << mention
|
@current_mentions << mention
|
||||||
|
|
||||||
"@#{mentioned_account.acct}"
|
"@#{mentioned_account.acct}"
|
||||||
|
@ -87,7 +89,7 @@ class ProcessMentionsService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
@current_mentions.each do |mention|
|
@current_mentions.each do |mention|
|
||||||
mention.save if mention.new_record? && @save_records
|
mention.save if (mention.new_record? || mention.silent_changed?) && @save_records
|
||||||
end
|
end
|
||||||
|
|
||||||
# If previous mentions are no longer contained in the text, convert them
|
# If previous mentions are no longer contained in the text, convert them
|
||||||
|
@ -95,7 +97,7 @@ class ProcessMentionsService < BaseService
|
||||||
# received a notification might be more confusing
|
# received a notification might be more confusing
|
||||||
removed_mentions = @previous_mentions - @current_mentions
|
removed_mentions = @previous_mentions - @current_mentions
|
||||||
|
|
||||||
Mention.where(id: removed_mentions.map(&:id)).update_all(silent: true) unless removed_mentions.empty?
|
Mention.where(id: removed_mentions.map(&:id), silent: false).update_all(silent: true) unless removed_mentions.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def mention_undeliverable?(mentioned_account)
|
def mention_undeliverable?(mentioned_account)
|
||||||
|
|
|
@ -16,7 +16,7 @@ class MentionResolveWorker
|
||||||
|
|
||||||
return if account.nil?
|
return if account.nil?
|
||||||
|
|
||||||
status.mentions.create!(account: account, silent: false)
|
status.mentions.upsert({ account_id: account.id, silent: false }, unique_by: %w(status_id account_id))
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
# Do nothing
|
# Do nothing
|
||||||
rescue Mastodon::UnexpectedResponseError => e
|
rescue Mastodon::UnexpectedResponseError => e
|
||||||
|
|
|
@ -19,6 +19,7 @@ class Scheduler::UserCleanupScheduler
|
||||||
User.unconfirmed.where(confirmation_sent_at: ..UNCONFIRMED_ACCOUNTS_MAX_AGE_DAYS.days.ago).find_in_batches do |batch|
|
User.unconfirmed.where(confirmation_sent_at: ..UNCONFIRMED_ACCOUNTS_MAX_AGE_DAYS.days.ago).find_in_batches do |batch|
|
||||||
# We have to do it separately because of missing database constraints
|
# We have to do it separately because of missing database constraints
|
||||||
AccountModerationNote.where(target_account_id: batch.map(&:account_id)).delete_all
|
AccountModerationNote.where(target_account_id: batch.map(&:account_id)).delete_all
|
||||||
|
WebauthnCredential.where(user_id: batch.map(&:id)).delete_all
|
||||||
Account.where(id: batch.map(&:account_id)).delete_all
|
Account.where(id: batch.map(&:account_id)).delete_all
|
||||||
User.where(id: batch.map(&:id)).delete_all
|
User.where(id: batch.map(&:id)).delete_all
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,7 +9,7 @@ class MigrateNotificationsPolicyV2 < ActiveRecord::Migration[7.1]
|
||||||
def up
|
def up
|
||||||
NotificationPolicy.in_batches.update_all(<<~SQL.squish)
|
NotificationPolicy.in_batches.update_all(<<~SQL.squish)
|
||||||
for_not_following = CASE filter_not_following WHEN true THEN 1 ELSE 0 END,
|
for_not_following = CASE filter_not_following WHEN true THEN 1 ELSE 0 END,
|
||||||
for_not_followers = CASE filter_not_following WHEN true THEN 1 ELSE 0 END,
|
for_not_followers = CASE filter_not_followers WHEN true THEN 1 ELSE 0 END,
|
||||||
for_new_accounts = CASE filter_new_accounts WHEN true THEN 1 ELSE 0 END,
|
for_new_accounts = CASE filter_new_accounts WHEN true THEN 1 ELSE 0 END,
|
||||||
for_private_mentions = CASE filter_private_mentions WHEN true THEN 1 ELSE 0 END
|
for_private_mentions = CASE filter_private_mentions WHEN true THEN 1 ELSE 0 END
|
||||||
SQL
|
SQL
|
||||||
|
@ -18,7 +18,7 @@ class MigrateNotificationsPolicyV2 < ActiveRecord::Migration[7.1]
|
||||||
def down
|
def down
|
||||||
NotificationPolicy.in_batches.update_all(<<~SQL.squish)
|
NotificationPolicy.in_batches.update_all(<<~SQL.squish)
|
||||||
filter_not_following = CASE for_not_following WHEN 0 THEN false ELSE true END,
|
filter_not_following = CASE for_not_following WHEN 0 THEN false ELSE true END,
|
||||||
filter_not_following = CASE for_not_followers WHEN 0 THEN false ELSE true END,
|
filter_not_followers = CASE for_not_followers WHEN 0 THEN false ELSE true END,
|
||||||
filter_new_accounts = CASE for_new_accounts WHEN 0 THEN false ELSE true END,
|
filter_new_accounts = CASE for_new_accounts WHEN 0 THEN false ELSE true END,
|
||||||
filter_private_mentions = CASE for_private_mentions WHEN 0 THEN false ELSE true END
|
filter_private_mentions = CASE for_private_mentions WHEN 0 THEN false ELSE true END
|
||||||
SQL
|
SQL
|
||||||
|
|
|
@ -9,7 +9,7 @@ class PostDeploymentMigrateNotificationsPolicyV2 < ActiveRecord::Migration[7.1]
|
||||||
def up
|
def up
|
||||||
NotificationPolicy.in_batches.update_all(<<~SQL.squish)
|
NotificationPolicy.in_batches.update_all(<<~SQL.squish)
|
||||||
for_not_following = CASE filter_not_following WHEN true THEN 1 ELSE 0 END,
|
for_not_following = CASE filter_not_following WHEN true THEN 1 ELSE 0 END,
|
||||||
for_not_followers = CASE filter_not_following WHEN true THEN 1 ELSE 0 END,
|
for_not_followers = CASE filter_not_followers WHEN true THEN 1 ELSE 0 END,
|
||||||
for_new_accounts = CASE filter_new_accounts WHEN true THEN 1 ELSE 0 END,
|
for_new_accounts = CASE filter_new_accounts WHEN true THEN 1 ELSE 0 END,
|
||||||
for_private_mentions = CASE filter_private_mentions WHEN true THEN 1 ELSE 0 END
|
for_private_mentions = CASE filter_private_mentions WHEN true THEN 1 ELSE 0 END
|
||||||
SQL
|
SQL
|
||||||
|
@ -18,7 +18,7 @@ class PostDeploymentMigrateNotificationsPolicyV2 < ActiveRecord::Migration[7.1]
|
||||||
def down
|
def down
|
||||||
NotificationPolicy.in_batches.update_all(<<~SQL.squish)
|
NotificationPolicy.in_batches.update_all(<<~SQL.squish)
|
||||||
filter_not_following = CASE for_not_following WHEN 0 THEN false ELSE true END,
|
filter_not_following = CASE for_not_following WHEN 0 THEN false ELSE true END,
|
||||||
filter_not_following = CASE for_not_followers WHEN 0 THEN false ELSE true END,
|
filter_not_followers = CASE for_not_followers WHEN 0 THEN false ELSE true END,
|
||||||
filter_new_accounts = CASE for_new_accounts WHEN 0 THEN false ELSE true END,
|
filter_new_accounts = CASE for_new_accounts WHEN 0 THEN false ELSE true END,
|
||||||
filter_private_mentions = CASE for_private_mentions WHEN 0 THEN false ELSE true END
|
filter_private_mentions = CASE for_private_mentions WHEN 0 THEN false ELSE true END
|
||||||
SQL
|
SQL
|
||||||
|
|
|
@ -59,7 +59,7 @@ services:
|
||||||
web:
|
web:
|
||||||
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
||||||
build: .
|
build: .
|
||||||
image: kmyblue:16.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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(' ') : []);
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -143,6 +143,55 @@ RSpec.describe 'Notifications' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when there are numerous notifications for the same final group' do
|
||||||
|
before do
|
||||||
|
user.account.notifications.destroy_all
|
||||||
|
5.times.each { FavouriteService.new.call(Fabricate(:account), user.account.statuses.first) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with no options' do
|
||||||
|
it 'returns a notification group covering all notifications' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
notification_ids = user.account.notifications.reload.pluck(:id)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(response.content_type)
|
||||||
|
.to start_with('application/json')
|
||||||
|
expect(response.parsed_body[:notification_groups]).to contain_exactly(
|
||||||
|
a_hash_including(
|
||||||
|
type: 'favourite',
|
||||||
|
sample_account_ids: have_attributes(size: 5),
|
||||||
|
page_min_id: notification_ids.first.to_s,
|
||||||
|
page_max_id: notification_ids.last.to_s
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with min_id param' do
|
||||||
|
let(:params) { { min_id: user.account.notifications.reload.first.id - 1 } }
|
||||||
|
|
||||||
|
it 'returns a notification group covering all notifications' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
notification_ids = user.account.notifications.reload.pluck(:id)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(response.content_type)
|
||||||
|
.to start_with('application/json')
|
||||||
|
expect(response.parsed_body[:notification_groups]).to contain_exactly(
|
||||||
|
a_hash_including(
|
||||||
|
type: 'favourite',
|
||||||
|
sample_account_ids: have_attributes(size: 5),
|
||||||
|
page_min_id: notification_ids.first.to_s,
|
||||||
|
page_max_id: notification_ids.last.to_s
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with no options' do
|
context 'with no options' do
|
||||||
it 'returns expected notification types', :aggregate_failures do
|
it 'returns expected notification types', :aggregate_failures do
|
||||||
subject
|
subject
|
||||||
|
|
58
spec/serializers/rest/preview_card_serializer_spec.rb
Normal file
58
spec/serializers/rest/preview_card_serializer_spec.rb
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe REST::PreviewCardSerializer do
|
||||||
|
subject do
|
||||||
|
serialized_record_json(
|
||||||
|
preview_card,
|
||||||
|
described_class
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when preview card does not have author data' do
|
||||||
|
let(:preview_card) { Fabricate.build :preview_card }
|
||||||
|
|
||||||
|
it 'includes empty authors array' do
|
||||||
|
expect(subject.deep_symbolize_keys)
|
||||||
|
.to include(
|
||||||
|
authors: be_an(Array).and(be_empty)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when preview card has fediverse author data' do
|
||||||
|
let(:preview_card) { Fabricate.build :preview_card, author_account: Fabricate(:account) }
|
||||||
|
|
||||||
|
it 'includes populated authors array' do
|
||||||
|
expect(subject.deep_symbolize_keys)
|
||||||
|
.to include(
|
||||||
|
authors: be_an(Array).and(
|
||||||
|
contain_exactly(
|
||||||
|
include(
|
||||||
|
account: be_present
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when preview card has non-fediverse author data' do
|
||||||
|
let(:preview_card) { Fabricate.build :preview_card, author_name: 'Name', author_url: 'https://host.example/123' }
|
||||||
|
|
||||||
|
it 'includes populated authors array' do
|
||||||
|
expect(subject.deep_symbolize_keys)
|
||||||
|
.to include(
|
||||||
|
authors: be_an(Array).and(
|
||||||
|
contain_exactly(
|
||||||
|
include(
|
||||||
|
name: 'Name',
|
||||||
|
url: 'https://host.example/123'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -36,7 +36,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
||||||
let(:media_attachments) { [] }
|
let(:media_attachments) { [] }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
mentions.each { |a| Fabricate(:mention, status: status, account: a) }
|
mentions.each { |(account, silent)| Fabricate(:mention, status: status, account: account, silent: silent) }
|
||||||
tags.each { |t| status.tags << t }
|
tags.each { |t| status.tags << t }
|
||||||
media_attachments.each { |m| status.media_attachments << m }
|
media_attachments.each { |m| status.media_attachments << m }
|
||||||
end
|
end
|
||||||
|
@ -320,7 +320,19 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when originally with mentions' do
|
context 'when originally with mentions' do
|
||||||
let(:mentions) { [alice, bob] }
|
let(:mentions) { [[alice, false], [bob, false]] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.call(status, json, json)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates mentions' do
|
||||||
|
expect(status.active_mentions.reload.map(&:account_id)).to eq [alice.id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when originally with silent mentions' do
|
||||||
|
let(:mentions) { [[alice, true], [bob, true]] }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
subject.call(status, json, json)
|
subject.call(status, json, json)
|
||||||
|
|
|
@ -199,6 +199,14 @@ RSpec.describe UpdateStatusService do
|
||||||
.to eq [bob.id]
|
.to eq [bob.id]
|
||||||
expect(status.mentions.pluck(:account_id))
|
expect(status.mentions.pluck(:account_id))
|
||||||
.to contain_exactly(alice.id, bob.id)
|
.to contain_exactly(alice.id, bob.id)
|
||||||
|
|
||||||
|
# Going back when a mention was switched to silence should still be possible
|
||||||
|
subject.call(status, status.account_id, text: 'Hello @alice')
|
||||||
|
|
||||||
|
expect(status.active_mentions.pluck(:account_id))
|
||||||
|
.to eq [alice.id]
|
||||||
|
expect(status.mentions.pluck(:account_id))
|
||||||
|
.to contain_exactly(alice.id, bob.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ RSpec.describe Scheduler::UserCleanupScheduler do
|
||||||
let!(:old_unconfirmed_user) { Fabricate(:user) }
|
let!(:old_unconfirmed_user) { Fabricate(:user) }
|
||||||
let!(:confirmed_user) { Fabricate(:user) }
|
let!(:confirmed_user) { Fabricate(:user) }
|
||||||
let!(:moderation_note) { Fabricate(:account_moderation_note, account: Fabricate(:account), target_account: old_unconfirmed_user.account) }
|
let!(:moderation_note) { Fabricate(:account_moderation_note, account: Fabricate(:account), target_account: old_unconfirmed_user.account) }
|
||||||
|
let!(:webauthn_credential) { Fabricate(:webauthn_credential, user_id: old_unconfirmed_user.id) }
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
before do
|
before do
|
||||||
|
@ -26,6 +27,8 @@ RSpec.describe Scheduler::UserCleanupScheduler do
|
||||||
.from(true).to(false)
|
.from(true).to(false)
|
||||||
expect { moderation_note.reload }
|
expect { moderation_note.reload }
|
||||||
.to raise_error(ActiveRecord::RecordNotFound)
|
.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
expect { webauthn_credential.reload }
|
||||||
|
.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
expect_preservation_of(new_unconfirmed_user)
|
expect_preservation_of(new_unconfirmed_user)
|
||||||
expect_preservation_of(confirmed_user)
|
expect_preservation_of(confirmed_user)
|
||||||
end
|
end
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue