1
0
Fork 0
forked from gitea/nas

Merge remote-tracking branch 'parent/main' into kb_migration

This commit is contained in:
KMY 2023-09-01 18:05:03 +09:00
commit 621a41b670
29 changed files with 640 additions and 300 deletions

View file

@ -53,7 +53,7 @@ class AccountsIndex < Chewy::Index
},
verbatim: {
tokenizer: 'uax_url_email',
tokenizer: 'standard',
filter: %w(lowercase asciifolding cjk_width),
},

View file

@ -1,6 +1,7 @@
# frozen_string_literal: true
class Api::V1::Timelines::TagController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :show, if: :require_auth?
before_action :load_tag
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
@ -12,6 +13,10 @@ class Api::V1::Timelines::TagController < Api::BaseController
private
def require_auth?
!Setting.timeline_preview
end
def load_tag
@tag = Tag.find_normalized(params[:id])
end

View file

@ -34,6 +34,7 @@ const messages = defineMessages({
about: { id: 'navigation_bar.about', defaultMessage: 'About' },
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface' },
openedInClassicInterface: { id: 'navigation_bar.opened_in_classic_interface', defaultMessage: 'Posts, accounts, and other specific pages are opened by default in the classic web interface.' },
});
class NavigationPanel extends Component {
@ -70,12 +71,17 @@ class NavigationPanel extends Component {
<div className='navigation-panel__logo'>
<Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link>
{transientSingleColumn && (
<a href={`/deck${location.pathname}`} className='button button--block'>
{intl.formatMessage(messages.advancedInterface)}
</a>
{transientSingleColumn ? (
<div class='switch-to-advanced'>
{intl.formatMessage(messages.openedInClassicInterface)}
{" "}
<a href={`/deck${location.pathname}`} class='switch-to-advanced__toggle'>
{intl.formatMessage(messages.advancedInterface)}
</a>
</div>
) : (
<hr />
)}
<hr />
</div>
{signedIn && (

View file

@ -421,6 +421,7 @@
"navigation_bar.lists": "Lists",
"navigation_bar.logout": "Logout",
"navigation_bar.mutes": "Muted users",
"navigation_bar.opened_in_classic_interface": "Posts, accounts, and other specific pages are opened by default in the classic web interface.",
"navigation_bar.personal": "Personal",
"navigation_bar.pins": "Pinned posts",
"navigation_bar.preferences": "Preferences",

View file

@ -409,6 +409,7 @@
"navigation_bar.lists": "Listes",
"navigation_bar.logout": "Déconnexion",
"navigation_bar.mutes": "Comptes masqués",
"navigation_bar.opened_in_classic_interface": "Les messages, les comptes et les pages spécifiques sont ouvertes dans linterface classique.",
"navigation_bar.personal": "Personnel",
"navigation_bar.pins": "Messages épinglés",
"navigation_bar.preferences": "Préférences",

View file

@ -2441,6 +2441,7 @@ $ui-header-height: 55px;
.filter-form {
display: flex;
flex-wrap: wrap;
}
.autosuggest-textarea__textarea {
@ -3330,6 +3331,22 @@ $ui-header-height: 55px;
border-color: $ui-highlight-color;
}
.switch-to-advanced {
color: $classic-primary-color;
background-color: $classic-base-color;
padding: 15px;
border-radius: 4px;
margin-top: 4px;
margin-bottom: 12px;
font-size: 13px;
line-height: 18px;
.switch-to-advanced__toggle {
color: $ui-button-tertiary-color;
font-weight: bold;
}
}
.column-link {
background: lighten($ui-base-color, 8%);
color: $primary-text-color;

View file

@ -1186,14 +1186,14 @@ code {
}
li:first-child .label {
left: auto;
inset-inline-start: 0;
inset-inline-end: auto;
text-align: start;
transform: none;
}
li:last-child .label {
left: auto;
inset-inline-start: auto;
inset-inline-end: 0;
text-align: end;
transform: none;

View file

@ -4,10 +4,10 @@ class Importer::AccountsIndexImporter < Importer::BaseImporter
def import!
scope.includes(:account_stat).find_in_batches(batch_size: @batch_size) do |tmp|
in_work_unit(tmp) do |accounts|
bulk = Chewy::Index::Import::BulkBuilder.new(index, to_index: accounts).bulk_body
bulk = build_bulk_body(accounts)
indexed = bulk.count { |entry| entry[:index] }
deleted = bulk.count { |entry| entry[:delete] }
indexed = bulk.size
deleted = 0
Chewy::Index::Import::BulkRequest.new(index).perform(bulk)

View file

@ -68,6 +68,14 @@ class Importer::BaseImporter
protected
def build_bulk_body(to_import)
# Specialize `Chewy::Index::Import::BulkBuilder#bulk_body` to avoid a few
# inefficiencies, as none of our fields or join fields and we do not need
# `BulkBuilder`'s versatility.
crutches = Chewy::Index::Crutch::Crutches.new index, to_import
to_import.map { |object| { index: { _id: object.id, data: index.compose(object, crutches, fields: []) } } }
end
def in_work_unit(...)
work_unit = Concurrent::Promises.future_on(@executor, ...)

View file

@ -4,10 +4,10 @@ class Importer::InstancesIndexImporter < Importer::BaseImporter
def import!
index.adapter.default_scope.find_in_batches(batch_size: @batch_size) do |tmp|
in_work_unit(tmp) do |instances|
bulk = Chewy::Index::Import::BulkBuilder.new(index, to_index: instances).bulk_body
bulk = build_bulk_body(instances)
indexed = bulk.count { |entry| entry[:index] }
deleted = bulk.count { |entry| entry[:delete] }
indexed = bulk.size
deleted = 0
Chewy::Index::Import::BulkRequest.new(index).perform(bulk)

View file

@ -5,11 +5,11 @@ class Importer::PublicStatusesIndexImporter < Importer::BaseImporter
scope.select(:id).find_in_batches(batch_size: @batch_size) do |batch|
in_work_unit(batch.pluck(:id)) do |status_ids|
bulk = ActiveRecord::Base.connection_pool.with_connection do
Chewy::Index::Import::BulkBuilder.new(index, to_index: Status.includes(:media_attachments, :preloadable_poll, :preview_cards).where(id: status_ids)).bulk_body
build_bulk_body(index.adapter.default_scope.where(id: status_ids))
end
indexed = bulk.count { |entry| entry[:index] }
deleted = bulk.count { |entry| entry[:delete] }
indexed = bulk.size
deleted = 0
Chewy::Index::Import::BulkRequest.new(index).perform(bulk)

View file

@ -13,32 +13,25 @@ class Importer::StatusesIndexImporter < Importer::BaseImporter
scope.find_in_batches(batch_size: @batch_size) do |tmp|
in_work_unit(tmp.map(&:status_id)) do |status_ids|
bulk = ActiveRecord::Base.connection_pool.with_connection do
Chewy::Index::Import::BulkBuilder.new(index, to_index: index.adapter.default_scope.where(id: status_ids)).bulk_body
end
indexed = 0
deleted = 0
# We can't use the delete_if proc to do the filtering because delete_if
# is called before rendering the data and we need to filter based
# on the results of the filter, so this filtering happens here instead
bulk.map! do |entry|
new_entry = if entry[:index] && entry.dig(:index, :data, 'searchable_by').blank?
{ delete: entry[:index].except(:data) }
else
entry
end
if new_entry[:index]
indexed += 1
else
deleted += 1
bulk = ActiveRecord::Base.connection_pool.with_connection do
to_index = index.adapter.default_scope.where(id: status_ids)
crutches = Chewy::Index::Crutch::Crutches.new index, to_index
to_index.map do |object|
# This is unlikely to happen, but the post may have been
# un-interacted with since it was queued for indexing
if object.searchable_by.empty?
deleted += 1
{ delete: { _id: object.id } }
else
{ index: { _id: object.id, data: index.compose(object, crutches, fields: []) } }
end
end
new_entry
end
indexed = bulk.size - deleted
Chewy::Index::Import::BulkRequest.new(index).perform(bulk)
[indexed, deleted]

View file

@ -4,10 +4,10 @@ class Importer::TagsIndexImporter < Importer::BaseImporter
def import!
index.adapter.default_scope.find_in_batches(batch_size: @batch_size) do |tmp|
in_work_unit(tmp) do |tags|
bulk = Chewy::Index::Import::BulkBuilder.new(index, to_index: tags).bulk_body
bulk = build_bulk_body(tags)
indexed = bulk.count { |entry| entry[:index] }
deleted = bulk.count { |entry| entry[:delete] }
indexed = bulk.size
deleted = 0
Chewy::Index::Import::BulkRequest.new(index).perform(bulk)

View file

@ -1,8 +1,6 @@
# frozen_string_literal: true
class PlainTextFormatter
include ActionView::Helpers::TextHelper
NEWLINE_TAGS_RE = %r{(<br />|<br>|</p>)+}
attr_reader :text, :local
@ -18,7 +16,10 @@ class PlainTextFormatter
if local?
text
else
html_entities.decode(strip_tags(insert_newlines)).chomp
node = Nokogiri::HTML.fragment(insert_newlines)
# Elements that are entirely removed with our Sanitize config
node.xpath('.//iframe|.//math|.//noembed|.//noframes|.//noscript|.//plaintext|.//script|.//style|.//svg|.//xmp').remove
node.text.chomp
end
end
@ -27,8 +28,4 @@ class PlainTextFormatter
def insert_newlines
text.gsub(NEWLINE_TAGS_RE) { |match| "#{match}\n" }
end
def html_entities
HTMLEntities.new
end
end

View file

@ -6,10 +6,10 @@ class SearchQueryParser < Parslet::Parser
rule(:colon) { str(':') }
rule(:space) { match('\s').repeat(1) }
rule(:operator) { (str('+') | str('-')).as(:operator) }
rule(:prefix) { (term >> colon).as(:prefix) }
rule(:prefix) { term >> colon }
rule(:shortcode) { (colon >> term >> colon.maybe).as(:shortcode) }
rule(:phrase) { (quote >> (term >> space.maybe).repeat >> quote).as(:phrase) }
rule(:clause) { (operator.maybe >> prefix.maybe >> (phrase | term | shortcode)).as(:clause) }
rule(:clause) { (operator.maybe >> prefix.maybe.as(:prefix) >> (phrase | term | shortcode)).as(:clause) | prefix.as(:clause) | quote.as(:junk) }
rule(:query) { (clause >> space.maybe).repeat.as(:query) }
root(:query)
end

View file

@ -1,50 +1,32 @@
# frozen_string_literal: true
class SearchQueryTransformer < Parslet::Transform
SUPPORTED_PREFIXES = %w(
has
is
language
from
before
after
during
).freeze
class Query
attr_reader :should_clauses, :must_not_clauses, :must_clauses, :filter_clauses
attr_reader :must_not_clauses, :must_clauses, :filter_clauses
def initialize(clauses)
grouped = clauses.chunk(&:operator).to_h
@should_clauses = grouped.fetch(:should, [])
grouped = clauses.compact.chunk(&:operator).to_h
@must_not_clauses = grouped.fetch(:must_not, [])
@must_clauses = grouped.fetch(:must, [])
@filter_clauses = grouped.fetch(:filter, [])
end
def apply(search)
should_clauses.each { |clause| search = search.query.should(clause_to_query(clause)) }
must_clauses.each { |clause| search = search.query.must(clause_to_query(clause)) }
must_not_clauses.each { |clause| search = search.query.must_not(clause_to_query(clause)) }
filter_clauses.each { |clause| search = search.filter(**clause_to_filter(clause)) }
must_clauses.each { |clause| search = search.query.must(clause.to_query) }
must_not_clauses.each { |clause| search = search.query.must_not(clause.to_query) }
filter_clauses.each { |clause| search = search.filter(**clause.to_query) }
search.query.minimum_should_match(1)
end
private
def clause_to_query(clause)
case clause
when TermClause
{ match_phrase: { text: { query: clause.term } } }
when PhraseClause
{ match_phrase: { text: { query: clause.phrase } } }
else
raise "Unexpected clause type: #{clause}"
end
end
def clause_to_filter(clause)
case clause
when PrefixClause
if clause.negated?
{ bool: { must_not: { clause.type => { clause.filter => clause.term } } } }
else
{ clause.type => { clause.filter => clause.term } }
end
else
raise "Unexpected clause type: #{clause}"
end
end
end
class Operator
@ -63,31 +45,39 @@ class SearchQueryTransformer < Parslet::Transform
end
class TermClause
attr_reader :prefix, :operator, :term
attr_reader :operator, :term
def initialize(prefix, operator, term)
@prefix = prefix
def initialize(operator, term)
@operator = Operator.symbol(operator)
@term = term
end
def to_query
# { multi_match: { type: 'most_fields', query: @term, fields: ['text', 'text.stemmed'], operator: 'and' } }
{ match_phrase: { text: { query: @phrase } } }
end
end
class PhraseClause
attr_reader :prefix, :operator, :phrase
attr_reader :operator, :phrase
def initialize(prefix, operator, phrase)
@prefix = prefix
def initialize(operator, phrase)
@operator = Operator.symbol(operator)
@phrase = phrase
end
def to_query
{ match_phrase: { text: { query: @phrase } } }
end
end
class PrefixClause
attr_reader :type, :filter, :operator, :term
attr_reader :operator, :prefix, :term
def initialize(prefix, operator, term, options = {})
@negated = operator == '-'
@options = options
@prefix = prefix
@negated = operator == '-'
@options = options
@operator = :filter
case prefix
@ -98,7 +88,7 @@ class SearchQueryTransformer < Parslet::Transform
when 'language'
@filter = :language
@type = :term
@term = term
@term = language_code_from_term(term)
when 'from'
@filter = :account_id
@type = :term
@ -120,12 +110,16 @@ class SearchQueryTransformer < Parslet::Transform
@type = :range
@term = { gte: term, lte: term, time_zone: @options[:current_account]&.user_time_zone || 'UTC' }
else
raise Mastodon::SyntaxError
raise "Unknown prefix: #{prefix}"
end
end
def negated?
@negated
def to_query
if @negated
{ bool: { must_not: { @type => { @filter => @term } } } }
else
{ @type => { @filter => @term } }
end
end
private
@ -147,24 +141,48 @@ class SearchQueryTransformer < Parslet::Transform
term
end
def language_code_from_term(term)
language_code = term
return language_code if LanguagesHelper::SUPPORTED_LOCALES.key?(language_code.to_sym)
language_code = term.downcase
return language_code if LanguagesHelper::SUPPORTED_LOCALES.key?(language_code.to_sym)
language_code = term.split(/[_-]/).first.downcase
return language_code if LanguagesHelper::SUPPORTED_LOCALES.key?(language_code.to_sym)
term
end
end
rule(clause: subtree(:clause)) do
prefix = clause[:prefix][:term].to_s if clause[:prefix]
operator = clause[:operator]&.to_s
if clause[:prefix]
if clause[:prefix] && SUPPORTED_PREFIXES.include?(prefix)
PrefixClause.new(prefix, operator, clause[:term].to_s, current_account: current_account)
elsif clause[:prefix]
TermClause.new(operator, "#{prefix} #{clause[:term]}")
elsif clause[:term]
TermClause.new(prefix, operator, clause[:term].to_s)
TermClause.new(operator, clause[:term].to_s)
elsif clause[:shortcode]
TermClause.new(prefix, operator, ":#{clause[:term]}:")
TermClause.new(operator, ":#{clause[:term]}:")
elsif clause[:phrase]
PhraseClause.new(prefix, operator, clause[:phrase].is_a?(Array) ? clause[:phrase].map { |p| p[:term].to_s }.join(' ') : clause[:phrase].to_s)
PhraseClause.new(operator, clause[:phrase].is_a?(Array) ? clause[:phrase].map { |p| p[:term].to_s }.join(' ') : clause[:phrase].to_s)
else
raise "Unexpected clause type: #{clause}"
end
end
rule(query: sequence(:clauses)) { Query.new(clauses) }
rule(junk: subtree(:junk)) do
nil
end
rule(query: sequence(:clauses)) do
Query.new(clauses)
end
end

View file

@ -105,6 +105,8 @@ class MediaAttachment < ApplicationRecord
output: {
'loglevel' => 'fatal',
'preset' => 'veryfast',
'movflags' => 'faststart', # Move metadata to start of file so playback can begin before download finishes
'pix_fmt' => 'yuv420p', # Ensure color space for cross-browser compatibility
'c:v' => 'h264',
'c:a' => 'aac',
'b:a' => '192k',

View file

@ -18,18 +18,31 @@ class WebfingerSerializer < ActiveModel::Serializer
end
def links
if object.instance_actor?
[
{ rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: about_more_url(instance_actor: true) },
{ rel: 'self', type: 'application/activity+json', href: instance_actor_url },
{ rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" },
]
else
[
{ rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) },
{ rel: 'self', type: 'application/activity+json', href: account_url(object) },
{ rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" },
]
[
{ rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: profile_page_href },
{ rel: 'self', type: 'application/activity+json', href: self_href },
{ rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" },
].tap do |x|
x << { rel: 'http://webfinger.net/rel/avatar', type: object.avatar.content_type, href: full_asset_url(object.avatar_original_url) } if show_avatar?
end
end
private
def show_avatar?
media_present = object.avatar.present? && object.avatar.content_type.present?
# Show avatar only if an instance shows profiles to logged out users
allowed_by_config = ENV['DISALLOW_UNAUTHENTICATED_API_ACCESS'] != 'true' && !Rails.configuration.x.limited_federation_mode
media_present && allowed_by_config
end
def profile_page_href
object.instance_actor? ? about_more_url(instance_actor: true) : short_account_url(object)
end
def self_href
object.instance_actor? ? instance_actor_url : account_url(object)
end
end

View file

@ -1,9 +1,10 @@
# frozen_string_literal: true
class SearchService < BaseService
def call(query, account, limit, options = {})
@query = query&.strip
QUOTE_EQUIVALENT_CHARACTERS = /[“”„«»「」『』《》]/
def call(query, account, limit, options = {})
@query = query&.strip&.gsub(QUOTE_EQUIVALENT_CHARACTERS, '"')
@account = account
@options = options
@limit = limit.to_i