commit
b7c7b2afb2
23 changed files with 289 additions and 120 deletions
10
Gemfile.lock
10
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)
|
||||
|
|
|
@ -21,6 +21,10 @@ module Admin
|
|||
false
|
||||
end
|
||||
|
||||
def avoid_save?
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def after_update_redirect_path
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -22,23 +22,23 @@ describe('emoji', () => {
|
|||
|
||||
it('does unicode', () => {
|
||||
expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
|
||||
'<picture><img draggable="false" class="emojione" alt="👩👩👦👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg"></picture>');
|
||||
'<img draggable="false" class="emojione" alt="👩👩👦👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg">');
|
||||
expect(emojify('👨👩👧👧')).toEqual(
|
||||
'<picture><img draggable="false" class="emojione" alt="👨👩👧👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg"></picture>');
|
||||
expect(emojify('👩👩👦')).toEqual('<picture><img draggable="false" class="emojione" alt="👩👩👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg"></picture>');
|
||||
'<img draggable="false" class="emojione" alt="👨👩👧👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg">');
|
||||
expect(emojify('👩👩👦')).toEqual('<img draggable="false" class="emojione" alt="👩👩👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg">');
|
||||
expect(emojify('\u2757')).toEqual(
|
||||
'<picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture>');
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg">');
|
||||
});
|
||||
|
||||
it('does multiple unicode', () => {
|
||||
expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual(
|
||||
'<picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture> <picture><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"></picture>');
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg">');
|
||||
expect(emojify('\u2757#\uFE0F\u20E3')).toEqual(
|
||||
'<picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture><picture><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"></picture>');
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg">');
|
||||
expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual(
|
||||
'<picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture> <picture><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"></picture> <picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture>');
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg">');
|
||||
expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual(
|
||||
'foo <picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture> <picture><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"></picture> bar');
|
||||
'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"> bar');
|
||||
});
|
||||
|
||||
it('ignores unicode inside of tags', () => {
|
||||
|
@ -46,16 +46,16 @@ describe('emoji', () => {
|
|||
});
|
||||
|
||||
it('does multiple emoji properly (issue 5188)', () => {
|
||||
expect(emojify('👌🌈💕')).toEqual('<picture><img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"></picture><picture><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"></picture><picture><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"></picture>');
|
||||
expect(emojify('👌 🌈 💕')).toEqual('<picture><img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"></picture> <picture><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"></picture> <picture><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"></picture>');
|
||||
expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg">');
|
||||
expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg">');
|
||||
});
|
||||
|
||||
it('does an emoji that has no shortcode', () => {
|
||||
expect(emojify('👁🗨')).toEqual('<picture><img draggable="false" class="emojione" alt="👁🗨" title="" src="/emoji/1f441-200d-1f5e8.svg"></picture>');
|
||||
expect(emojify('👁🗨')).toEqual('<img draggable="false" class="emojione" alt="👁🗨" title="" src="/emoji/1f441-200d-1f5e8.svg">');
|
||||
});
|
||||
|
||||
it('does an emoji whose filename is irregular', () => {
|
||||
expect(emojify('↙️')).toEqual('<picture><img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg"></picture>');
|
||||
expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg">');
|
||||
});
|
||||
|
||||
it('avoid emojifying on invisible text', () => {
|
||||
|
@ -67,11 +67,11 @@ describe('emoji', () => {
|
|||
|
||||
it('avoid emojifying on invisible text with nested tags', () => {
|
||||
expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇'))
|
||||
.toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><picture><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg"></picture>');
|
||||
.toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
|
||||
expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇'))
|
||||
.toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><picture><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg"></picture>');
|
||||
.toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
|
||||
expect(emojify('<span class="invisible">😄<br>😴</span>😇'))
|
||||
.toEqual('<span class="invisible">😄<br>😴</span><picture><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg"></picture>');
|
||||
.toEqual('<span class="invisible">😄<br>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
|
||||
});
|
||||
|
||||
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('<picture><img draggable="false" class="emojione" alt="♀" title=":female_sign:" src="/emoji/2640.svg"></picture><picture><img draggable="false" class="emojione" alt="♂" title=":male_sign:" src="/emoji/2642.svg"></picture>');
|
||||
.toEqual('<img draggable="false" class="emojione" alt="♀" title=":female_sign:" src="/emoji/2640.svg"><img draggable="false" class="emojione" alt="♂" title=":male_sign:" src="/emoji/2642.svg">');
|
||||
});
|
||||
|
||||
it('does an emoji containing ZWJ properly', () => {
|
||||
expect(emojify('💂♀️💂♂️'))
|
||||
.toEqual('<picture><img draggable="false" class="emojione" alt="💂\u200D♀️" title=":female-guard:" src="/emoji/1f482-200d-2640-fe0f_border.svg"></picture><picture><img draggable="false" class="emojione" alt="💂\u200D♂️" title=":male-guard:" src="/emoji/1f482-200d-2642-fe0f_border.svg"></picture>');
|
||||
.toEqual('<img draggable="false" class="emojione" alt="💂\u200D♀️" title=":female-guard:" src="/emoji/1f482-200d-2640-fe0f_border.svg"><img draggable="false" class="emojione" alt="💂\u200D♂️" title=":male-guard:" src="/emoji/1f482-200d-2642-fe0f_border.svg">');
|
||||
});
|
||||
|
||||
it('keeps ordering as expected (issue fixed by PR 20677)', () => {
|
||||
expect(emojify('<p>💕 <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener" target="_blank">#<span>foo</span></a> test: foo.</p>'))
|
||||
.toEqual('<p><picture><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"></picture> <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener" target="_blank">#<span>foo</span></a> test: foo.</p>');
|
||||
.toEqual('<p><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"> <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener" target="_blank">#<span>foo</span></a> test: foo.</p>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<Status>] 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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
<%- @user_roles.each do |role| %>
|
||||
.user-role-<%= role.id %> {
|
||||
--user-role-accent: <%= role.color %>;
|
||||
}
|
||||
|
||||
<%- end %>
|
|
@ -1,6 +0,0 @@
|
|||
<%- @user_roles.each do |role| %>
|
||||
.user-role-<%= role.id %> {
|
||||
--user-role-accent: <%= role.color %>;
|
||||
}
|
||||
|
||||
<%- end %>
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
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
|
||||
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
|
||||
image: kmyblue:17.1
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bundle exec sidekiq
|
||||
|
|
|
@ -13,7 +13,7 @@ module Mastodon
|
|||
end
|
||||
|
||||
def kmyblue_minor
|
||||
0
|
||||
1
|
||||
end
|
||||
|
||||
def kmyblue_flag
|
||||
|
|
|
@ -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 },
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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') }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue