diff --git a/Gemfile.lock b/Gemfile.lock
index 8547e4fba1..7c92629114 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -416,7 +416,7 @@ GEM
mutex_m (0.3.0)
net-http (0.6.0)
uri
- net-imap (0.5.5)
+ net-imap (0.5.6)
date
net-protocol
net-ldap (0.19.0)
@@ -424,10 +424,10 @@ GEM
net-protocol
net-protocol (0.2.2)
timeout
- net-smtp (0.5.0)
+ net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
- nokogiri (1.18.2)
+ nokogiri (1.18.3)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
oj (3.16.9)
@@ -597,7 +597,7 @@ GEM
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
- rack (2.2.10)
+ rack (2.2.11)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-cors (2.0.2)
@@ -748,7 +748,7 @@ GEM
ruby-saml (1.17.0)
nokogiri (>= 1.13.10)
rexml
- ruby-vips (2.2.2)
+ ruby-vips (2.2.3)
ffi (~> 1.12)
logger
rubyzip (2.4.1)
diff --git a/app/controllers/admin/ng_words/keywords_controller.rb b/app/controllers/admin/ng_words/keywords_controller.rb
index 9af38fab7b..10969204e8 100644
--- a/app/controllers/admin/ng_words/keywords_controller.rb
+++ b/app/controllers/admin/ng_words/keywords_controller.rb
@@ -21,6 +21,10 @@ module Admin
false
end
+ def avoid_save?
+ true
+ end
+
private
def after_update_redirect_path
diff --git a/app/controllers/admin/ng_words_controller.rb b/app/controllers/admin/ng_words_controller.rb
index f052843475..9e437f8c8b 100644
--- a/app/controllers/admin/ng_words_controller.rb
+++ b/app/controllers/admin/ng_words_controller.rb
@@ -13,6 +13,12 @@ module Admin
return unless validate
+ if avoid_save?
+ flash[:notice] = I18n.t('generic.changes_saved_msg')
+ redirect_to after_update_redirect_path
+ return
+ end
+
@admin_settings = Form::AdminSettings.new(settings_params)
if @admin_settings.save
@@ -33,6 +39,10 @@ module Admin
admin_ng_words_path
end
+ def avoid_save?
+ false
+ end
+
private
def settings_params
@@ -40,7 +50,7 @@ module Admin
end
def settings_params_test
- params.require(:form_admin_settings)[:ng_words_test]
+ params.expect(form_admin_settings: [ng_words_test: [keywords: [], regexps: [], strangers: [], temporary_ids: []]])['ng_words_test']
end
end
end
diff --git a/app/controllers/api/v1/instances/domain_blocks_controller.rb b/app/controllers/api/v1/instances/domain_blocks_controller.rb
index 7ec94312f4..bf96fbaaa8 100644
--- a/app/controllers/api/v1/instances/domain_blocks_controller.rb
+++ b/app/controllers/api/v1/instances/domain_blocks_controller.rb
@@ -31,7 +31,7 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr
end
def show_domain_blocks_to_user?
- Setting.show_domain_blocks == 'users' && user_signed_in?
+ Setting.show_domain_blocks == 'users' && user_signed_in? && current_user.functional_or_moved?
end
def set_domain_blocks
@@ -47,6 +47,6 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr
end
def show_rationale_for_user?
- Setting.show_domain_blocks_rationale == 'users' && user_signed_in?
+ Setting.show_domain_blocks_rationale == 'users' && user_signed_in? && current_user.functional_or_moved?
end
end
diff --git a/app/controllers/api/v2/notifications_controller.rb b/app/controllers/api/v2/notifications_controller.rb
index cc38b95114..848c361cfc 100644
--- a/app/controllers/api/v2/notifications_controller.rb
+++ b/app/controllers/api/v2/notifications_controller.rb
@@ -46,7 +46,7 @@ class Api::V2::NotificationsController < Api::BaseController
end
def show
- @notification = current_account.notifications.without_suspended.find_by!(group_key: params[:group_key])
+ @notification = current_account.notifications.without_suspended.by_group_key(params[:group_key]).take!
presenter = GroupedNotificationsPresenter.new(NotificationGroup.from_notifications([@notification]))
render json: presenter, serializer: REST::DedupNotificationGroupSerializer
end
@@ -57,7 +57,7 @@ class Api::V2::NotificationsController < Api::BaseController
end
def dismiss
- current_account.notifications.where(group_key: params[:group_key]).destroy_all
+ current_account.notifications.by_group_key(params[:group_key]).destroy_all
render_empty
end
diff --git a/app/controllers/system_css_controller.rb b/app/controllers/system_css_controller.rb
index a19728bbfd..dd90491894 100644
--- a/app/controllers/system_css_controller.rb
+++ b/app/controllers/system_css_controller.rb
@@ -1,16 +1,8 @@
# frozen_string_literal: true
class SystemCssController < ActionController::Base # rubocop:disable Rails/ApplicationController
- before_action :set_user_roles
-
def show
expires_in 3.minutes, public: true
render content_type: 'text/css'
end
-
- private
-
- def set_user_roles
- @user_roles = UserRole.providing_styles
- end
end
diff --git a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
index 022c9baaf7..35804de82a 100644
--- a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
+++ b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
@@ -22,23 +22,23 @@ describe('emoji', () => {
it('does unicode', () => {
expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
- '
');
+ '
');
expect(emojify('๐จโ๐ฉโ๐งโ๐ง')).toEqual(
- '
');
- expect(emojify('๐ฉโ๐ฉโ๐ฆ')).toEqual('
');
+ '
');
+ expect(emojify('๐ฉโ๐ฉโ๐ฆ')).toEqual('
');
expect(emojify('\u2757')).toEqual(
- '
');
+ '
');
});
it('does multiple unicode', () => {
expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual(
- '
');
+ '
');
expect(emojify('\u2757#\uFE0F\u20E3')).toEqual(
- '
');
+ '
');
expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual(
- '
');
+ '
');
expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual(
- 'foo
bar');
+ 'foo
bar');
});
it('ignores unicode inside of tags', () => {
@@ -46,16 +46,16 @@ describe('emoji', () => {
});
it('does multiple emoji properly (issue 5188)', () => {
- expect(emojify('๐๐๐')).toEqual('

');
- expect(emojify('๐ ๐ ๐')).toEqual('
');
+ expect(emojify('๐๐๐')).toEqual('

');
+ expect(emojify('๐ ๐ ๐')).toEqual('
');
});
it('does an emoji that has no shortcode', () => {
- expect(emojify('๐โ๐จ')).toEqual('
');
+ expect(emojify('๐โ๐จ')).toEqual('
');
});
it('does an emoji whose filename is irregular', () => {
- expect(emojify('โ๏ธ')).toEqual('
');
+ expect(emojify('โ๏ธ')).toEqual('
');
});
it('avoid emojifying on invisible text', () => {
@@ -67,11 +67,11 @@ describe('emoji', () => {
it('avoid emojifying on invisible text with nested tags', () => {
expect(emojify('๐bar๐ด๐'))
- .toEqual('๐bar๐ด
');
+ .toEqual('๐bar๐ด
');
expect(emojify('๐๐๐ด๐'))
- .toEqual('๐๐๐ด
');
+ .toEqual('๐๐๐ด
');
expect(emojify('๐
๐ด๐'))
- .toEqual('๐
๐ด
');
+ .toEqual('๐
๐ด
');
});
it('does not emojify emojis with textual presentation VS15 character', () => {
@@ -81,17 +81,17 @@ describe('emoji', () => {
it('does a simple emoji properly', () => {
expect(emojify('โโ'))
- .toEqual('
');
+ .toEqual('
');
});
it('does an emoji containing ZWJ properly', () => {
expect(emojify('๐โโ๏ธ๐โโ๏ธ'))
- .toEqual('
');
+ .toEqual('
');
});
it('keeps ordering as expected (issue fixed by PR 20677)', () => {
expect(emojify('
๐ #foo test: foo.
'))
- .toEqual('
#foo test: foo.
');
+ .toEqual('
#foo test: foo.
');
});
});
});
diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js
index 1f469aced7..66dcd89488 100644
--- a/app/javascript/mastodon/features/emoji/emoji.js
+++ b/app/javascript/mastodon/features/emoji/emoji.js
@@ -97,30 +97,30 @@ const emojifyTextNode = (node, customEmojis) => {
const { filename, shortCode } = unicodeMapping[unicode_emoji];
const title = shortCode ? `:${shortCode}:` : '';
- replacement = document.createElement('picture');
-
const isSystemTheme = !!document.body?.classList.contains('theme-system');
- if(isSystemTheme) {
- let source = document.createElement('source');
- source.setAttribute('media', '(prefers-color-scheme: dark)');
- source.setAttribute('srcset', `${assetHost}/emoji/${emojiFilename(filename, "dark")}.svg`);
- replacement.appendChild(source);
- }
+ const theme = (isSystemTheme || document.body?.classList.contains('theme-mastodon-light')) ? 'light' : 'dark';
- let img = document.createElement('img');
+ const imageFilename = emojiFilename(filename, theme);
+
+ const img = document.createElement('img');
img.setAttribute('draggable', 'false');
img.setAttribute('class', 'emojione');
img.setAttribute('alt', unicode_emoji);
img.setAttribute('title', title);
+ img.setAttribute('src', `${assetHost}/emoji/${imageFilename}.svg`);
- let theme = "light";
+ if (isSystemTheme && imageFilename !== emojiFilename(filename, 'dark')) {
+ replacement = document.createElement('picture');
- if(!isSystemTheme && !document.body?.classList.contains('theme-mastodon-light'))
- theme = "dark";
-
- img.setAttribute('src', `${assetHost}/emoji/${emojiFilename(filename, theme)}.svg`);
- replacement.appendChild(img);
+ const source = document.createElement('source');
+ source.setAttribute('media', '(prefers-color-scheme: dark)');
+ source.setAttribute('srcset', `${assetHost}/emoji/${emojiFilename(filename, 'dark')}.svg`);
+ replacement.appendChild(source);
+ replacement.appendChild(img);
+ } else {
+ replacement = img;
+ }
}
// Add the processed-up-to-now string and the emoji replacement
@@ -135,7 +135,7 @@ const emojifyTextNode = (node, customEmojis) => {
};
const emojifyNode = (node, customEmojis) => {
- for (const child of node.childNodes) {
+ for (const child of Array.from(node.childNodes)) {
switch(child.nodeType) {
case Node.TEXT_NODE:
emojifyTextNode(child, customEmojis);
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index df4fc17908..1a558cacbe 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -42,7 +42,7 @@ class FeedManager
when :home
filter_from_home(status, receiver.id, build_crutches(receiver.id, [status]), :home)
when :list
- (filter_from_list?(status, receiver) ? :filter : nil) || filter_from_home(status, receiver.account_id, build_crutches(receiver.account_id, [status]), :list, stl_home: stl_home)
+ (filter_from_list?(status, receiver) ? :filter : nil) || filter_from_home(status, receiver.account_id, build_crutches(receiver.account_id, [status], list: receiver), :list, stl_home: stl_home)
when :mentions
filter_from_mentions?(status, receiver.id) ? :filter : nil
when :tags
@@ -136,7 +136,7 @@ class FeedManager
timeline_key = key(:home, into_account.id)
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(reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4
oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i
@@ -164,7 +164,7 @@ class FeedManager
timeline_key = key(:list, list.id)
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(reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4
oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i
@@ -172,10 +172,10 @@ class FeedManager
end
statuses = query.to_a
- crutches = build_crutches(list.account_id, statuses)
+ crutches = build_crutches(list.account_id, statuses, list: list)
statuses.each do |status|
- next if filter_from_home(status, list.account_id, crutches) || filter_from_list?(status, list)
+ next if filter_from_home(status, list.account_id, crutches, :list)
add_to_feed(:list, list.id, status, aggregate_reblogs: aggregate)
end
@@ -309,23 +309,32 @@ class FeedManager
limit = FeedManager::MAX_ITEMS / 2
aggregate = account.user&.aggregates_reblogs?
timeline_key = key(:home, account.id)
+ over_limit = false
account.statuses.limit(limit).each do |status|
add_to_feed(:home, account.id, status, aggregate_reblogs: aggregate)
end
account.following.includes(:account_stat).reorder(nil).find_each do |target_account|
- if redis.zcard(timeline_key) >= limit
+ query = target_account.statuses.list_eligible_visibility.includes(reblog: :account).limit(limit)
+
+ over_limit ||= redis.zcard(timeline_key) >= limit
+ if over_limit
oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i
- last_status_score = Mastodon::Snowflake.id_at(target_account.last_status_at)
+ last_status_score = Mastodon::Snowflake.id_at(target_account.last_status_at, with_random: false)
# If the feed is full and this account has not posted more recently
# than the last item on the feed, then we can skip the whole account
# because none of its statuses would stay on the feed anyway
next if last_status_score < oldest_home_score
+
+ # No need to get older statuses
+ query = query.where(id: oldest_home_score...)
end
- statuses = target_account.statuses.list_eligible_visibility.includes(:preloadable_poll, :media_attachments, :account, reblog: :account).limit(limit)
+ statuses = query.to_a
+ next if statuses.empty?
+
crutches = build_crutches(account.id, statuses)
statuses.each do |status|
@@ -345,23 +354,32 @@ class FeedManager
limit = FeedManager::MAX_ITEMS / 2
aggregate = list.account.user&.aggregates_reblogs?
timeline_key = key(:list, list.id)
+ over_limit = false
list.active_accounts.includes(:account_stat).reorder(nil).find_each do |target_account|
- if redis.zcard(timeline_key) >= limit
+ query = target_account.statuses.list_eligible_visibility.includes(reblog: :account).limit(limit)
+
+ over_limit ||= redis.zcard(timeline_key) >= limit
+ if over_limit
oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i
- last_status_score = Mastodon::Snowflake.id_at(target_account.last_status_at)
+ last_status_score = Mastodon::Snowflake.id_at(target_account.last_status_at, with_random: false)
# If the feed is full and this account has not posted more recently
# than the last item on the feed, then we can skip the whole account
# because none of its statuses would stay on the feed anyway
next if last_status_score < oldest_home_score
+
+ # No need to get older statuses
+ query = query.where(id: oldest_home_score...)
end
- statuses = target_account.statuses.list_eligible_visibility.includes(:preloadable_poll, :media_attachments, :account, reblog: :account).limit(limit)
- crutches = build_crutches(list.account_id, statuses)
+ statuses = query.to_a
+ next if statuses.empty?
+
+ crutches = build_crutches(list.account_id, statuses, list: list)
statuses.each do |status|
- next if filter_from_home(status, list.account_id, crutches) || filter_from_list?(status, list)
+ next if filter_from_home(status, list.account_id, crutches, :list)
add_to_feed(:list, list.id, status, aggregate_reblogs: aggregate)
end
@@ -632,8 +650,9 @@ class FeedManager
# are going to be checked by the filtering methods
# @param [Integer] receiver_id
# @param [Array] statuses
+ # @param [List] list
# @return [Hash]
- def build_crutches(receiver_id, statuses) # rubocop:disable Metrics/AbcSize
+ def build_crutches(receiver_id, statuses, list: nil)
crutches = {}
crutches[:active_mentions] = crutches_active_mentions(statuses)
@@ -650,25 +669,43 @@ class FeedManager
arr
end
- lists = List.where(account_id: receiver_id, exclusive: true)
- antennas = Antenna.where(list: lists, insert_feeds: true)
-
- replied_accounts = statuses.filter_map(&:in_reply_to_account_id)
- replied_accounts += statuses.filter { |status| status.limited_visibility? && status.thread.present? }.map { |status| status.thread.account_id }
-
- crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: replied_accounts).pluck(:target_account_id).index_with(true)
+ crutches[:following] = crutches_following(receiver_id, statuses, list)
crutches[:languages] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:account_id)).pluck(:target_account_id, :languages).to_h
crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map { |s| s.account_id if s.reblog? }, show_reblogs: false).pluck(:target_account_id).index_with(true)
crutches[:blocking] = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.flat_map { |s| [s.account.domain, s.reblog&.account&.domain] }.compact).pluck(:domain).index_with(true)
crutches[:blocked_by] = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| [s.account_id, s.reblog&.account_id] }.flatten.compact).pluck(:account_id).index_with(true)
- crutches[:exclusive_list_users] = ListAccount.where(list: lists, account_id: statuses.map(&:account_id)).pluck(:account_id).index_with(true)
- crutches[:exclusive_antenna_users] = AntennaAccount.where(antenna: antennas, account_id: statuses.map(&:account_id)).pluck(:account_id).index_with(true)
+ crutches[:exclusive_list_users] = crutches_exclusive_list_users(receiver_id, statuses) if list.blank?
+ crutches[:exclusive_antenna_users] = crutches_exclusive_antenna_users(receiver_id, statuses)
crutches
end
+ def crutches_exclusive_list_users(recipient_id, statuses)
+ lists = List.where(account_id: recipient_id, exclusive: true)
+ ListAccount.where(list: lists, account_id: statuses.map(&:account_id)).pluck(:account_id).index_with(true)
+ end
+
+ def crutches_exclusive_antenna_users(recipient_id, statuses)
+ lists = List.where(account_id: recipient_id, exclusive: true)
+ antennas = Antenna.where(list: lists, insert_feeds: true)
+ AntennaAccount.where(antenna: antennas, account_id: statuses.map(&:account_id)).pluck(:account_id).index_with(true)
+ end
+
+ def crutches_following(recipient_id, statuses, list)
+ if list.blank? || list.show_followed?
+ replied_accounts = statuses.filter_map(&:in_reply_to_account_id)
+ replied_accounts += statuses.filter { |status| status.limited_visibility? && status.thread.present? }.map { |status| status.thread.account_id }
+
+ Follow.where(account_id: recipient_id, target_account_id: replied_accounts).pluck(:target_account_id).index_with(true)
+ elsif list.show_list?
+ ListAccount.where(list_id: list.id, account_id: statuses.filter_map(&:in_reply_to_account_id)).pluck(:account_id).index_with(true)
+ else
+ {}
+ end
+ end
+
def crutches_active_mentions(statuses)
Mention
.active
diff --git a/app/models/concerns/notification/groups.rb b/app/models/concerns/notification/groups.rb
index e064df8502..c678ff9f50 100644
--- a/app/models/concerns/notification/groups.rb
+++ b/app/models/concerns/notification/groups.rb
@@ -7,6 +7,10 @@ module Notification::Groups
GROUPABLE_NOTIFICATION_TYPES = %i(favourite reblog follow emoji_reaction).freeze
MAXIMUM_GROUP_SPAN_HOURS = 12
+ included do
+ scope :by_group_key, ->(group_key) { group_key&.start_with?('ungrouped-') ? where(id: group_key.delete_prefix('ungrouped-')) : where(group_key: group_key) }
+ end
+
def set_group_key!
return if filtered? || GROUPABLE_NOTIFICATION_TYPES.exclude?(type)
diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb
index 1be39aaedf..10224f4d7e 100644
--- a/app/services/activitypub/process_status_update_service.rb
+++ b/app/services/activitypub/process_status_update_service.rb
@@ -296,7 +296,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
nil
end
- @status.mentions.upsert_all(currently_mentioned_account_ids.map { |id| { account_id: id, silent: false } }, unique_by: %w(status_id account_id))
+ @status.mentions.upsert_all(currently_mentioned_account_ids.uniq.map { |id| { account_id: id, silent: false } }, unique_by: %w(status_id account_id))
# If previous mentions are no longer contained in the text, convert them
# to silent mentions, since withdrawing access from someone who already
diff --git a/app/views/custom_css/show_system.css.erb b/app/views/custom_css/show_system.css.erb
index 72f0281aa1..e69de29bb2 100644
--- a/app/views/custom_css/show_system.css.erb
+++ b/app/views/custom_css/show_system.css.erb
@@ -1,6 +0,0 @@
-<%- @user_roles.each do |role| %>
-.user-role-<%= role.id %> {
- --user-role-accent: <%= role.color %>;
-}
-
-<%- end %>
diff --git a/app/views/system_css/show.css.erb b/app/views/system_css/show.css.erb
index 72f0281aa1..e69de29bb2 100644
--- a/app/views/system_css/show.css.erb
+++ b/app/views/system_css/show.css.erb
@@ -1,6 +0,0 @@
-<%- @user_roles.each do |role| %>
-.user-role-<%= role.id %> {
- --user-role-accent: <%= role.color %>;
-}
-
-<%- end %>
diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb
index b4eaab1daa..f558ee5fe0 100644
--- a/config/initializers/rack_attack.rb
+++ b/config/initializers/rack_attack.rb
@@ -122,7 +122,7 @@ class Rack::Attack
end
throttle('throttle_email_confirmations/ip', limit: 25, period: 5.minutes) do |req|
- req.throttleable_remote_ip if req.post? && (req.path_matches?('/auth/confirmation') || req.path == '/api/v1/emails/confirmations')
+ req.throttleable_remote_ip if (req.post? && (req.path_matches?('/auth/confirmation') || req.path == '/api/v1/emails/confirmations')) || ((req.put? || req.patch?) && req.path_matches?('/auth/setup'))
end
throttle('throttle_email_confirmations/email', limit: 5, period: 30.minutes) do |req|
@@ -133,6 +133,14 @@ class Rack::Attack
end
end
+ throttle('throttle_auth_setup/email', limit: 5, period: 10.minutes) do |req|
+ req.params.dig('user', 'email').presence if (req.put? || req.patch?) && req.path_matches?('/auth/setup')
+ end
+
+ throttle('throttle_auth_setup/account', limit: 5, period: 10.minutes) do |req|
+ req.warden_user_id if (req.put? || req.patch?) && req.path_matches?('/auth/setup')
+ end
+
throttle('throttle_login_attempts/ip', limit: 25, period: 5.minutes) do |req|
req.throttleable_remote_ip if req.post? && req.path_matches?('/auth/sign_in')
end
diff --git a/docker-compose.yml b/docker-compose.yml
index 61deb51ab6..dbbb430acd 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -59,7 +59,7 @@ services:
web:
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
build: .
- image: kmyblue:17.0-dev
+ image: kmyblue:17.1
restart: always
env_file: .env.production
command: bundle exec puma -C config/puma.rb
@@ -83,7 +83,7 @@ services:
build:
dockerfile: ./streaming/Dockerfile
context: .
- image: kmyblue-streaming:17.0-dev
+ image: kmyblue-streaming:17.1
restart: always
env_file: .env.production
command: node ./streaming/index.js
@@ -101,7 +101,7 @@ services:
sidekiq:
build: .
- image: kmyblue:17.0-dev
+ image: kmyblue:17.1
restart: always
env_file: .env.production
command: bundle exec sidekiq
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index f09f32ab30..688ad8b158 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -13,13 +13,13 @@ module Mastodon
end
def kmyblue_minor
- 0
+ 1
end
def kmyblue_flag
# 'LTS'
- 'dev'
- # nil
+ # 'dev'
+ nil
end
def major
diff --git a/lib/sanitize_ext/sanitize_config.rb b/lib/sanitize_ext/sanitize_config.rb
index 7815d9ed52..410a58b07e 100644
--- a/lib/sanitize_ext/sanitize_config.rb
+++ b/lib/sanitize_ext/sanitize_config.rb
@@ -155,18 +155,16 @@ class Sanitize
)
MASTODON_OEMBED = freeze_config(
- elements: %w(audio embed iframe source video),
+ elements: %w(audio iframe source video),
attributes: {
'audio' => %w(controls),
- 'embed' => %w(height src type width),
'iframe' => %w(allowfullscreen frameborder height scrolling src width),
'source' => %w(src type),
'video' => %w(controls height loop width),
},
protocols: {
- 'embed' => { 'src' => HTTP_PROTOCOLS },
'iframe' => { 'src' => HTTP_PROTOCOLS },
'source' => { 'src' => HTTP_PROTOCOLS },
},
diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb
index f5528f13a7..108111c06b 100644
--- a/spec/lib/activitypub/activity/create_spec.rb
+++ b/spec/lib/activitypub/activity/create_spec.rb
@@ -48,10 +48,16 @@ RSpec.describe ActivityPub::Activity::Create do
content: '@bob lorem ipsum',
published: 1.hour.ago.utc.iso8601,
updated: 1.hour.ago.utc.iso8601,
- tag: {
- type: 'Mention',
- href: ActivityPub::TagManager.instance.uri_for(follower),
- },
+ tag: [
+ {
+ type: 'Mention',
+ href: ActivityPub::TagManager.instance.uri_for(follower),
+ },
+ {
+ type: 'Mention',
+ href: ActivityPub::TagManager.instance.uri_for(follower),
+ },
+ ],
}
end
diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb
index ad7913c758..48defa1f83 100644
--- a/spec/lib/feed_manager_spec.rb
+++ b/spec/lib/feed_manager_spec.rb
@@ -233,6 +233,28 @@ RSpec.describe FeedManager do
end
end
+ context 'with list feed' do
+ let(:list) { Fabricate(:list, account: bob) }
+
+ before do
+ bob.follow!(alice)
+ list.list_accounts.create!(account: alice)
+ end
+
+ it "returns false for followee's status" do
+ status = Fabricate(:status, text: 'Hello world', account: alice)
+
+ expect(subject.filter?(:list, status, list)).to be false
+ end
+
+ it 'returns false for reblog by followee' do
+ status = Fabricate(:status, text: 'Hello world', account: jeff)
+ reblog = Fabricate(:status, reblog: status, account: alice)
+
+ expect(subject.filter?(:list, reblog, list)).to be false
+ end
+ end
+
context 'with mentions feed' do
it 'returns true for status that mentions blocked account' do
bob.block!(jeff)
diff --git a/spec/requests/api/v1/instances/domain_blocks_spec.rb b/spec/requests/api/v1/instances/domain_blocks_spec.rb
index d0707d784c..40b79c9691 100644
--- a/spec/requests/api/v1/instances/domain_blocks_spec.rb
+++ b/spec/requests/api/v1/instances/domain_blocks_spec.rb
@@ -4,14 +4,15 @@ require 'rails_helper'
RSpec.describe 'Domain Blocks' do
let(:user) { Fabricate(:user) }
- let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+ let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes).token }
let(:scopes) { 'read' }
- let(:headers) { { Authorization: "Bearer #{token.token}" } }
+ let(:headers) { { Authorization: "Bearer #{token}" } }
describe 'GET /api/v1/instance/domain_blocks' do
- before do
- Fabricate(:domain_block)
- end
+ let(:user) { Fabricate(:user) }
+ let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id).token }
+
+ before { Fabricate(:domain_block) }
context 'with domain blocks set to all' do
before { Setting.show_domain_blocks = 'all' }
@@ -45,11 +46,95 @@ RSpec.describe 'Domain Blocks' do
context 'with domain blocks set to users' do
before { Setting.show_domain_blocks = 'users' }
- it 'returns http not found' do
- get api_v1_instance_domain_blocks_path
+ context 'without authentication token' do
+ it 'returns http not found' do
+ get api_v1_instance_domain_blocks_path
- expect(response)
- .to have_http_status(404)
+ expect(response)
+ .to have_http_status(404)
+ end
+ end
+
+ context 'with authentication token' do
+ context 'with unapproved user' do
+ before { user.update(approved: false) }
+
+ it 'returns http not found' do
+ get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token}" }
+
+ expect(response)
+ .to have_http_status(404)
+ end
+ end
+
+ context 'with unconfirmed user' do
+ before { user.update(confirmed_at: nil) }
+
+ it 'returns http not found' do
+ get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token}" }
+
+ expect(response)
+ .to have_http_status(404)
+ end
+ end
+
+ context 'with disabled user' do
+ before { user.update(disabled: true) }
+
+ it 'returns http not found' do
+ get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token}" }
+
+ expect(response)
+ .to have_http_status(404)
+ end
+ end
+
+ context 'with suspended user' do
+ before { user.account.update(suspended_at: Time.zone.now) }
+
+ it 'returns http not found' do
+ get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token}" }
+
+ expect(response)
+ .to have_http_status(403)
+ end
+ end
+
+ context 'with moved user' do
+ before { user.account.update(moved_to_account_id: Fabricate(:account).id) }
+
+ it 'returns http success' do
+ get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token}" }
+
+ expect(response)
+ .to have_http_status(200)
+
+ expect(response.content_type)
+ .to start_with('application/json')
+
+ expect(response.parsed_body)
+ .to be_present
+ .and(be_an(Array))
+ .and(have_attributes(size: 1))
+ end
+ end
+
+ context 'with normal user' do
+ it 'returns http success' do
+ get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token}" }
+
+ expect(response)
+ .to have_http_status(200)
+
+ expect(response.content_type)
+ .to start_with('application/json')
+
+ expect(response.parsed_body)
+ .to be_present
+ .and(be_an(Array))
+ .and(have_attributes(size: 1))
+ end
+ end
end
end
diff --git a/spec/requests/api/v2/notifications_spec.rb b/spec/requests/api/v2/notifications_spec.rb
index b2f6d71b51..a7608e1419 100644
--- a/spec/requests/api/v2/notifications_spec.rb
+++ b/spec/requests/api/v2/notifications_spec.rb
@@ -365,6 +365,18 @@ RSpec.describe 'Notifications' do
.to start_with('application/json')
end
+ context 'with an ungrouped notification' do
+ let(:notification) { Fabricate(:notification, account: user.account, type: :favourite) }
+
+ it 'returns http success' do
+ get "/api/v2/notifications/ungrouped-#{notification.id}", headers: headers
+
+ expect(response).to have_http_status(200)
+ expect(response.content_type)
+ .to start_with('application/json')
+ end
+ end
+
context 'when notification belongs to someone else' do
let(:notification) { Fabricate(:notification, group_key: 'foobar') }
@@ -396,6 +408,19 @@ RSpec.describe 'Notifications' do
expect { notification.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
+ context 'with an ungrouped notification' do
+ let(:notification) { Fabricate(:notification, account: user.account, type: :favourite) }
+
+ it 'destroys the notification' do
+ post "/api/v2/notifications/ungrouped-#{notification.id}/dismiss", headers: headers
+
+ expect(response).to have_http_status(200)
+ expect(response.content_type)
+ .to start_with('application/json')
+ expect { notification.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
context 'when notification belongs to someone else' do
let(:notification) { Fabricate(:notification, group_key: 'foobar') }
diff --git a/spec/requests/auth/setup_spec.rb b/spec/requests/auth/setup_spec.rb
index 72413e1740..fa3c196805 100644
--- a/spec/requests/auth/setup_spec.rb
+++ b/spec/requests/auth/setup_spec.rb
@@ -24,15 +24,4 @@ RSpec.describe 'Auth Setup' do
end
end
end
-
- describe 'PUT /auth/setup' do
- before { sign_in Fabricate(:user, confirmed_at: nil) }
-
- it 'gracefully handles invalid nested params' do
- put '/auth/setup?user=invalid'
-
- expect(response)
- .to have_http_status(400)
- end
- end
end
diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb
index 49d496e295..6d008840be 100644
--- a/spec/services/activitypub/process_status_update_service_spec.rb
+++ b/spec/services/activitypub/process_status_update_service_spec.rb
@@ -12,6 +12,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
[
{ 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