diff options
Diffstat (limited to 'app/lib')
-rw-r--r-- | app/lib/activitypub/activity/create.rb | 4 | ||||
-rw-r--r-- | app/lib/activitypub/parser/status_parser.rb | 6 | ||||
-rw-r--r-- | app/lib/advanced_text_formatter.rb | 133 | ||||
-rw-r--r-- | app/lib/feed_manager.rb | 58 | ||||
-rw-r--r-- | app/lib/html_aware_formatter.rb | 6 | ||||
-rw-r--r-- | app/lib/settings/scoped_settings.rb | 3 | ||||
-rw-r--r-- | app/lib/status_cache_hydrator.rb | 4 | ||||
-rw-r--r-- | app/lib/themes.rb | 80 | ||||
-rw-r--r-- | app/lib/user_settings_decorator.rb | 35 | ||||
-rw-r--r-- | app/lib/vacuum/feeds_vacuum.rb | 7 |
10 files changed, 323 insertions, 13 deletions
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index b15e66ca2..4dfbfc665 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -154,7 +154,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity # If there is at least one silent mention, then the status can be considered # as a limited-audience status, and not strictly a direct message, but only # if we considered a direct message in the first place - @params[:visibility] = :limited if @params[:visibility] == :direct + @params[:visibility] = :limited if @params[:visibility] == :direct && !@object['directMessage'] end # Accounts that are tagged but are not in the audience are not @@ -166,7 +166,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity return if @status.mentions.find_by(account_id: @options[:delivered_to_account_id]) @status.mentions.create(account: delivered_to_account, silent: true) - @status.update(visibility: :limited) if @status.direct_visibility? + @status.update(visibility: :limited) if @status.direct_visibility? && !@object['directMessage'] return unless delivered_to_account.following?(@account) diff --git a/app/lib/activitypub/parser/status_parser.rb b/app/lib/activitypub/parser/status_parser.rb index 3ba154d01..75b8f3d5c 100644 --- a/app/lib/activitypub/parser/status_parser.rb +++ b/app/lib/activitypub/parser/status_parser.rb @@ -79,6 +79,8 @@ class ActivityPub::Parser::StatusParser :unlisted elsif audience_to.include?(@magic_values[:followers_collection]) :private + elsif direct_message == false + :limited else :direct end @@ -94,6 +96,10 @@ class ActivityPub::Parser::StatusParser end end + def direct_message + @object['directMessage'] + end + private def audience_to diff --git a/app/lib/advanced_text_formatter.rb b/app/lib/advanced_text_formatter.rb new file mode 100644 index 000000000..21e81d4d1 --- /dev/null +++ b/app/lib/advanced_text_formatter.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +class AdvancedTextFormatter < TextFormatter + class HTMLRenderer < Redcarpet::Render::HTML + def initialize(options, &block) + super(options) + @format_link = block + end + + def block_code(code, _language) + <<~HTML + <pre><code>#{ERB::Util.h(code).gsub("\n", '<br/>')}</code></pre> + HTML + end + + def autolink(link, link_type) + return link if link_type == :email + @format_link.call(link) + end + end + + attr_reader :content_type + + # @param [String] text + # @param [Hash] options + # @option options [Boolean] :multiline + # @option options [Boolean] :with_domains + # @option options [Boolean] :with_rel_me + # @option options [Array<Account>] :preloaded_accounts + # @option options [String] :content_type + def initialize(text, options = {}) + @content_type = options.delete(:content_type) + super(text, options) + + @text = format_markdown(text) if content_type == 'text/markdown' + end + + # Differs from TextFormatter by not messing with newline after parsing + def to_s + return ''.html_safe if text.blank? + + html = rewrite do |entity| + if entity[:url] + link_to_url(entity) + elsif entity[:hashtag] + link_to_hashtag(entity) + elsif entity[:screen_name] + link_to_mention(entity) + end + end + + html.html_safe # rubocop:disable Rails/OutputSafety + end + + # Differs from TextFormatter by operating on the parsed HTML tree + def rewrite + if @tree.nil? + src = text.gsub(Sanitize::REGEX_UNSUITABLE_CHARS, '') + @tree = Nokogiri::HTML5.fragment(src) + document = @tree.document + + @tree.xpath('.//text()[not(ancestor::a | ancestor::code)]').each do |text_node| + # Iterate over text elements and build up their replacements. + content = text_node.content + replacement = Nokogiri::XML::NodeSet.new(document) + processed_index = 0 + Extractor.extract_entities_with_indices( + content, + extract_url_without_protocol: false + ) do |entity| + # Iterate over entities in this text node. + advance = entity[:indices].first - processed_index + if advance.positive? + # Text node for content which precedes entity. + replacement << Nokogiri::XML::Text.new( + content[processed_index, advance], + document + ) + end + replacement << Nokogiri::HTML5.fragment(yield(entity)) + processed_index = entity[:indices].last + end + if processed_index < content.size + # Text node for remaining content. + replacement << Nokogiri::XML::Text.new( + content[processed_index, content.size - processed_index], + document + ) + end + text_node.replace(replacement) + end + end + + Sanitize.node!(@tree, Sanitize::Config::MASTODON_OUTGOING).to_html + end + + private + + def format_markdown(html) + html = markdown_formatter.render(html) + html.delete("\r").delete("\n") + end + + def markdown_formatter + extensions = { + autolink: true, + no_intra_emphasis: true, + fenced_code_blocks: true, + disable_indented_code_blocks: true, + strikethrough: true, + lax_spacing: true, + space_after_headers: true, + superscript: true, + underline: true, + highlight: true, + footnotes: false, + } + + renderer = HTMLRenderer.new({ + filter_html: false, + escape_html: false, + no_images: true, + no_styles: true, + safe_links_only: true, + hard_wrap: true, + link_attributes: { target: '_blank', rel: 'nofollow noopener' }, + }) do |url| + link_to_url({ url: url }) + end + + Redcarpet::Markdown.new(renderer, extensions) + end +end diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 510667558..9fe9ec346 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -45,6 +45,8 @@ class FeedManager filter_from_list?(status, receiver) || filter_from_home?(status, receiver.account_id, build_crutches(receiver.account_id, [status])) when :mentions filter_from_mentions?(status, receiver.id) + when :direct + filter_from_direct?(status, receiver.id) when :tags filter_from_tags?(status, receiver.id, build_crutches(receiver.id, [status])) else @@ -102,6 +104,29 @@ class FeedManager true end + # Add a status to a linear direct message feed and send a streaming API update + # @param [Account] account + # @param [Status] status + # @return [Boolean] + def push_to_direct(account, status, update: false) + return false unless add_to_feed(:direct, account.id, status) + + trim(:direct, account.id) + PushUpdateWorker.perform_async(account.id, status.id, "timeline:direct:#{account.id}") unless update + true + end + + # Remove a status from a linear direct message feed and send a streaming API update + # @param [List] list + # @param [Status] status + # @return [Boolean] + def unpush_from_direct(account, status, update: false) + return false unless remove_from_feed(:direct, account.id, status) + + redis.publish("timeline:direct:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s)) unless update + true + end + # Fill a home feed with an account's statuses # @param [Account] from_account # @param [Account] into_account @@ -266,6 +291,30 @@ class FeedManager end end + # Populate direct feed of account from scratch + # @param [Account] account + # @return [void] + def populate_direct_feed(account) + added = 0 + limit = FeedManager::MAX_ITEMS / 2 + max_id = nil + + loop do + statuses = Status.as_direct_timeline(account, limit, max_id) + + break if statuses.empty? + + statuses.each do |status| + next if filter_from_direct?(status, account) + added += 1 if add_to_feed(:direct, account.id, status) + end + + break unless added.zero? + + max_id = statuses.last.id + end + end + # Completely clear multiple feeds at once # @param [Symbol] type # @param [Array<Integer>] ids @@ -403,6 +452,15 @@ class FeedManager should_filter end + # Check if status should not be added to the linear direct message feed + # @param [Status] status + # @param [Integer] receiver_id + # @return [Boolean] + def filter_from_direct?(status, receiver_id) + return false if receiver_id == status.account_id + filter_from_mentions?(status, receiver_id) + end + # Check if status should not be added to the list feed # @param [Status] status # @param [List] list diff --git a/app/lib/html_aware_formatter.rb b/app/lib/html_aware_formatter.rb index 64edba09b..7a1cd0340 100644 --- a/app/lib/html_aware_formatter.rb +++ b/app/lib/html_aware_formatter.rb @@ -33,6 +33,10 @@ class HtmlAwareFormatter end def linkify - TextFormatter.new(text, options).to_s + if %w(text/markdown text/html).include?(@options[:content_type]) + AdvancedTextFormatter.new(text, options).to_s + else + TextFormatter.new(text, options).to_s + end end end diff --git a/app/lib/settings/scoped_settings.rb b/app/lib/settings/scoped_settings.rb index 1e18d6d46..796de1113 100644 --- a/app/lib/settings/scoped_settings.rb +++ b/app/lib/settings/scoped_settings.rb @@ -3,7 +3,8 @@ module Settings class ScopedSettings DEFAULTING_TO_UNSCOPED = %w( - theme + flavour + skin noindex ).freeze diff --git a/app/lib/status_cache_hydrator.rb b/app/lib/status_cache_hydrator.rb index 298d7851a..a84d25694 100644 --- a/app/lib/status_cache_hydrator.rb +++ b/app/lib/status_cache_hydrator.rb @@ -11,7 +11,7 @@ class StatusCacheHydrator # If we're delivering to the author who disabled the display of the application used to create the # status, we need to hydrate the application, since it was not rendered for the basic payload - payload[:application] = ActiveModelSerializers::SerializableResource.new(@status.application, serializer: REST::StatusSerializer::ApplicationSerializer).as_json if payload[:application].nil? && @status.account_id == account_id && @status.application.present? + payload[:application] = @status.application.present? ? ActiveModelSerializers::SerializableResource.new(@status.application, serializer: REST::StatusSerializer::ApplicationSerializer).as_json : nil if payload[:application].nil? && @status.account_id == account_id # We take advantage of the fact that some relationships can only occur with an original status, not # the reblog that wraps it, so we can assume that some values are always false @@ -23,7 +23,7 @@ class StatusCacheHydrator # If the reblogged status is being delivered to the author who disabled the display of the application # used to create the status, we need to hydrate it here too - payload[:reblog][:application] = ActiveModelSerializers::SerializableResource.new(@status.reblog.application, serializer: REST::StatusSerializer::ApplicationSerializer).as_json if payload[:reblog][:application].nil? && @status.reblog.account_id == account_id && @status.reblog.application.present? + payload[:reblog][:application] = @status.reblog.application.present? ? ActiveModelSerializers::SerializableResource.new(@status.reblog.application, serializer: REST::StatusSerializer::ApplicationSerializer).as_json : nil if payload[:reblog][:application].nil? && @status.reblog.account_id == account_id payload[:reblog][:favourited] = Favourite.where(account_id: account_id, status_id: @status.reblog_of_id).exists? payload[:reblog][:reblogged] = Status.where(account_id: account_id, reblog_of_id: @status.reblog_of_id).exists? diff --git a/app/lib/themes.rb b/app/lib/themes.rb index 243ffb9ab..81e016d4a 100644 --- a/app/lib/themes.rb +++ b/app/lib/themes.rb @@ -7,10 +7,86 @@ class Themes include Singleton def initialize - @conf = YAML.load_file(Rails.root.join('config', 'themes.yml')) + + core = YAML.load_file(Rails.root.join('app', 'javascript', 'core', 'theme.yml')) + core['pack'] = Hash.new unless core['pack'] + + result = Hash.new + Dir.glob(Rails.root.join('app', 'javascript', 'flavours', '*', 'theme.yml')) do |path| + data = YAML.load_file(path) + next unless data['pack'] + + dir = File.dirname(path) + name = File.basename(dir) + locales = [] + screenshots = [] + + if data['locales'] + Dir.glob(File.join(dir, data['locales'], '*.{js,json}')) do |locale| + localeName = File.basename(locale, File.extname(locale)) + locales.push(localeName) unless localeName.match(/defaultMessages|whitelist|index/) + end + end + + if data['screenshot'] + if data['screenshot'].is_a? Array + screenshots = data['screenshot'] + else + screenshots.push(data['screenshot']) + end + end + + data['name'] = name + data['locales'] = locales + data['screenshot'] = screenshots + data['skin'] = { 'default' => [] } + result[name] = data + end + + Dir.glob(Rails.root.join('app', 'javascript', 'skins', '*', '*')) do |path| + ext = File.extname(path) + skin = File.basename(path) + name = File.basename(File.dirname(path)) + next unless result[name] + + if File.directory?(path) + pack = [] + Dir.glob(File.join(path, '*.{css,scss}')) do |sheet| + pack.push(File.basename(sheet, File.extname(sheet))) + end + elsif ext.match(/^\.s?css$/i) + skin = File.basename(path, ext) + pack = ['common'] + end + + if skin != 'default' + result[name]['skin'][skin] = pack + end + end + + @core = core + @conf = result + end + + def core + @core + end + + def flavour(name) + @conf[name] end - def names + def flavours @conf.keys end + + def skins_for(name) + @conf[name]['skin'].keys + end + + def flavours_and_skins + flavours.map do |flavour| + [flavour, skins_for(flavour).map{ |skin| [flavour, skin] }] + end + end end diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 5fb7655a9..260077a1c 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -22,6 +22,7 @@ class UserSettingsDecorator user.settings['default_language'] = default_language_preference if change?('setting_default_language') user.settings['unfollow_modal'] = unfollow_modal_preference if change?('setting_unfollow_modal') user.settings['boost_modal'] = boost_modal_preference if change?('setting_boost_modal') + user.settings['favourite_modal'] = favourite_modal_preference if change?('setting_favourite_modal') user.settings['delete_modal'] = delete_modal_preference if change?('setting_delete_modal') user.settings['auto_play_gif'] = auto_play_gif_preference if change?('setting_auto_play_gif') user.settings['display_media'] = display_media_preference if change?('setting_display_media') @@ -29,11 +30,15 @@ class UserSettingsDecorator user.settings['reduce_motion'] = reduce_motion_preference if change?('setting_reduce_motion') user.settings['disable_swiping'] = disable_swiping_preference if change?('setting_disable_swiping') user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui') + user.settings['system_emoji_font'] = system_emoji_font_preference if change?('setting_system_emoji_font') user.settings['noindex'] = noindex_preference if change?('setting_noindex') - user.settings['theme'] = theme_preference if change?('setting_theme') + user.settings['hide_followers_count'] = hide_followers_count_preference if change?('setting_hide_followers_count') + user.settings['flavour'] = flavour_preference if change?('setting_flavour') + user.settings['skin'] = skin_preference if change?('setting_skin') user.settings['aggregate_reblogs'] = aggregate_reblogs_preference if change?('setting_aggregate_reblogs') user.settings['show_application'] = show_application_preference if change?('setting_show_application') user.settings['advanced_layout'] = advanced_layout_preference if change?('setting_advanced_layout') + user.settings['default_content_type']= default_content_type_preference if change?('setting_default_content_type') user.settings['use_blurhash'] = use_blurhash_preference if change?('setting_use_blurhash') user.settings['use_pending_items'] = use_pending_items_preference if change?('setting_use_pending_items') user.settings['trends'] = trends_preference if change?('setting_trends') @@ -65,6 +70,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_boost_modal' end + def favourite_modal_preference + boolean_cast_setting 'setting_favourite_modal' + end + def delete_modal_preference boolean_cast_setting 'setting_delete_modal' end @@ -73,6 +82,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_system_font_ui' end + def system_emoji_font_preference + boolean_cast_setting 'setting_system_emoji_font' + end + def auto_play_gif_preference boolean_cast_setting 'setting_auto_play_gif' end @@ -97,12 +110,20 @@ class UserSettingsDecorator boolean_cast_setting 'setting_noindex' end - def show_application_preference - boolean_cast_setting 'setting_show_application' + def flavour_preference + settings['setting_flavour'] + end + + def skin_preference + settings['setting_skin'] end - def theme_preference - settings['setting_theme'] + def hide_followers_count_preference + boolean_cast_setting 'setting_hide_followers_count' + end + + def show_application_preference + boolean_cast_setting 'setting_show_application' end def default_language_preference @@ -117,6 +138,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_advanced_layout' end + def default_content_type_preference + settings['setting_default_content_type'] + end + def use_blurhash_preference boolean_cast_setting 'setting_use_blurhash' end diff --git a/app/lib/vacuum/feeds_vacuum.rb b/app/lib/vacuum/feeds_vacuum.rb index f46bcf75f..00b9fd646 100644 --- a/app/lib/vacuum/feeds_vacuum.rb +++ b/app/lib/vacuum/feeds_vacuum.rb @@ -4,6 +4,7 @@ class Vacuum::FeedsVacuum def perform vacuum_inactive_home_feeds! vacuum_inactive_list_feeds! + vacuum_inactive_direct_feeds! end private @@ -20,6 +21,12 @@ class Vacuum::FeedsVacuum end end + def vacuum_inactive_direct_feeds! + inactive_users_lists.select(:id).find_in_batches do |lists| + feed_manager.clean_feeds!(:direct, lists.map(&:id)) + end + end + def inactive_users User.confirmed.inactive end |