diff options
Diffstat (limited to 'app/lib')
-rw-r--r-- | app/lib/feed_manager.rb | 43 | ||||
-rw-r--r-- | app/lib/formatter.rb | 108 | ||||
-rw-r--r-- | app/lib/sanitize_config.rb | 66 | ||||
-rw-r--r-- | app/lib/settings/scoped_settings.rb | 3 | ||||
-rw-r--r-- | app/lib/themes.rb | 77 | ||||
-rw-r--r-- | app/lib/user_settings_decorator.rb | 35 |
6 files changed, 288 insertions, 44 deletions
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 13b7aafdf..3ce182809 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -26,6 +26,8 @@ class FeedManager filter_from_home?(status, receiver_id, build_crutches(receiver_id, [status])) elsif timeline_type == :mentions filter_from_mentions?(status, receiver_id) + elsif timeline_type == :direct + filter_from_direct?(status, receiver_id) else false end @@ -49,7 +51,8 @@ class FeedManager def push_to_list(list, status) if status.reply? && status.in_reply_to_account_id != status.account_id should_filter = status.in_reply_to_account_id != list.account_id - should_filter &&= !ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists? + should_filter &&= !list.show_all_replies? + should_filter &&= !(list.show_list_replies? && ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists?) return false if should_filter end @@ -67,6 +70,18 @@ class FeedManager true end + def push_to_direct(account, status) + 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}") + true + end + + def unpush_from_direct(account, status) + 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)) + end + def trim(type, account_id) timeline_key = key(type, account_id) reblog_key = key(type, account_id, 'reblogs') @@ -166,6 +181,27 @@ class FeedManager end end + 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 + private def push_update_required?(timeline_id) @@ -225,6 +261,11 @@ class FeedManager should_filter end + def filter_from_direct?(status, receiver_id) + return false if receiver_id == status.account_id + filter_from_mentions?(status, receiver_id) + end + def phrase_filtered?(status, receiver_id, context) active_filters = Rails.cache.fetch("filters:#{receiver_id}") { CustomFilter.where(account_id: receiver_id).active_irreversible.to_a }.to_a diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index e6f5d7a63..051f27408 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -3,6 +3,27 @@ require 'singleton' require_relative './sanitize_config' +class HTMLRenderer < Redcarpet::Render::HTML + def block_code(code, language) + "<pre><code>#{encode(code).gsub("\n", "<br/>")}</code></pre>" + end + + def autolink(link, link_type) + return link if link_type == :email + Formatter.instance.link_url(link) + end + + private + + def html_entities + @html_entities ||= HTMLEntities.new + end + + def encode(html) + html_entities.encode(html) + end +end + class Formatter include Singleton include RoutingHelper @@ -36,16 +57,26 @@ class Formatter html = raw_content html = "RT @#{prepend_reblog} #{html}" if prepend_reblog - html = encode_and_link_urls(html, linkable_accounts) + html = format_markdown(html) if status.content_type == 'text/markdown' + html = encode_and_link_urls(html, linkable_accounts, keep_html: %w(text/markdown text/html).include?(status.content_type)) + html = reformat(html, true) if %w(text/markdown text/html).include?(status.content_type) html = encode_custom_emojis(html, status.emojis, options[:autoplay]) if options[:custom_emojify] - html = simple_format(html, {}, sanitize: false) - html = html.delete("\n") + + unless %w(text/markdown text/html).include?(status.content_type) + html = simple_format(html, {}, sanitize: false) + html = html.delete("\n") + end html.html_safe # rubocop:disable Rails/OutputSafety end - def reformat(html) - sanitize(html, Sanitize::Config::MASTODON_STRICT) + def format_markdown(html) + html = markdown_formatter.render(html) + html.delete("\r").delete("\n") + end + + def reformat(html, outgoing = false) + sanitize(html, Sanitize::Config::MASTODON_STRICT.merge(outgoing: outgoing)) rescue ArgumentError '' end @@ -99,8 +130,40 @@ class Formatter html.html_safe # rubocop:disable Rails/OutputSafety end + def link_url(url) + "<a href=\"#{encode(url)}\" target=\"blank\" rel=\"nofollow noopener noreferrer\">#{link_html(url)}</a>" + end + private + 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' }, + }) + + Redcarpet::Markdown.new(renderer, extensions) + end + def html_entities @html_entities ||= HTMLEntities.new end @@ -110,14 +173,14 @@ class Formatter end def encode_and_link_urls(html, accounts = nil, options = {}) - entities = utf8_friendly_extractor(html, extract_url_without_protocol: false) - if accounts.is_a?(Hash) options = accounts accounts = nil end - rewrite(html.dup, entities) do |entity| + entities = options[:keep_html] ? html_friendly_extractor(html) : utf8_friendly_extractor(html, extract_url_without_protocol: false) + + rewrite(html.dup, entities, options[:keep_html]) do |entity| if entity[:url] link_to_url(entity, options) elsif entity[:hashtag] @@ -190,7 +253,7 @@ class Formatter html end - def rewrite(text, entities) + def rewrite(text, entities, keep_html = false) text = text.to_s # Sort by start index @@ -203,12 +266,12 @@ class Formatter last_index = entities.reduce(0) do |index, entity| indices = entity.respond_to?(:indices) ? entity.indices : entity[:indices] - result << encode(text[index...indices.first]) + result << (keep_html ? text[index...indices.first] : encode(text[index...indices.first])) result << yield(entity) indices.last end - result << encode(text[last_index..-1]) + result << (keep_html ? text[last_index..-1] : encode(text[last_index..-1])) result.flatten.join end @@ -252,6 +315,29 @@ class Formatter Extractor.remove_overlapping_entities(special + standard + extra) end + def html_friendly_extractor(html, options = {}) + gaps = [] + total_offset = 0 + + escaped = html.gsub(/<[^>]*>|&#[0-9]+;/) do |match| + total_offset += match.length - 1 + end_offset = Regexp.last_match.end(0) + gaps << [end_offset - total_offset, total_offset] + "\u200b" + end + + entities = Extractor.extract_hashtags_with_indices(escaped, :check_url_overlap => false) + + Extractor.extract_mentions_or_lists_with_indices(escaped) + Extractor.remove_overlapping_entities(entities).map do |extract| + pos = extract[:indices].first + offset_idx = gaps.rindex { |gap| gap.first <= pos } + offset = offset_idx.nil? ? 0 : gaps[offset_idx].last + next extract.merge( + :indices => [extract[:indices].first + offset, extract[:indices].last + offset] + ) + end + end + def link_to_url(entity, options = {}) url = Addressable::URI.parse(entity[:url]) html_attrs = { target: '_blank', rel: 'nofollow noopener noreferrer' } diff --git a/app/lib/sanitize_config.rb b/app/lib/sanitize_config.rb index 4ad1199a6..34793ed93 100644 --- a/app/lib/sanitize_config.rb +++ b/app/lib/sanitize_config.rb @@ -35,6 +35,37 @@ class Sanitize node['class'] = class_list.join(' ') end + IMG_TAG_TRANSFORMER = lambda do |env| + node = env[:node] + + return unless env[:node_name] == 'img' + + node.name = 'a' + + node['href'] = node['src'] + if node['alt'].present? + node.content = "[🖼 #{node['alt']}]" + else + url = node['href'] + prefix = url.match(/\Ahttps?:\/\/(www\.)?/).to_s + text = url[prefix.length, 30] + text = text + "…" if url[prefix.length..-1].length > 30 + node.content = "[🖼 #{text}]" + end + end + + LINK_REL_TRANSFORMER = lambda do |env| + return unless env[:node_name] == 'a' + + node = env[:node] + + rel = (node['rel'] || '').split(' ') & ['tag'] + unless env[:config][:outgoing] && TagManager.instance.local_url?(node['href']) + rel += ['nofollow', 'noopener', 'noreferrer'] + end + node['rel'] = rel.join(' ') + end + UNSUPPORTED_HREF_TRANSFORMER = lambda do |env| return unless env[:node_name] == 'a' @@ -51,45 +82,32 @@ class Sanitize current_node.replace(current_node.text) unless LINK_PROTOCOLS.include?(scheme) end - UNSUPPORTED_ELEMENTS_TRANSFORMER = lambda do |env| - return unless %w(h1 h2 h3 h4 h5 h6 blockquote pre ul ol li).include?(env[:node_name]) - - current_node = env[:node] - - case env[:node_name] - when 'li' - current_node.traverse do |node| - next unless %w(p ul ol li).include?(node.name) - - node.add_next_sibling('<br>') if node.next_sibling - node.replace(node.children) unless node.text? - end - else - current_node.name = 'p' - end - end - MASTODON_STRICT ||= freeze_config( - elements: %w(p br span a), + elements: %w(p br span a abbr del pre blockquote code b strong u sub sup i em h1 h2 h3 h4 h5 ul ol li), attributes: { - 'a' => %w(href rel class), - 'span' => %w(class), + 'a' => %w(href rel class title), + 'span' => %w(class), + 'abbr' => %w(title), + 'blockquote' => %w(cite), }, add_attributes: { 'a' => { - 'rel' => 'nofollow noopener noreferrer', 'target' => '_blank', }, }, - protocols: {}, + protocols: { + 'a' => { 'href' => LINK_PROTOCOLS }, + 'blockquote' => { 'cite' => LINK_PROTOCOLS }, + }, transformers: [ CLASS_WHITELIST_TRANSFORMER, - UNSUPPORTED_ELEMENTS_TRANSFORMER, + IMG_TAG_TRANSFORMER, UNSUPPORTED_HREF_TRANSFORMER, + LINK_REL_TRANSFORMER, ] ) diff --git a/app/lib/settings/scoped_settings.rb b/app/lib/settings/scoped_settings.rb index 9ca39510a..4d21e0de7 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/themes.rb b/app/lib/themes.rb index 243ffb9ab..2147904e4 100644 --- a/app/lib/themes.rb +++ b/app/lib/themes.rb @@ -7,10 +7,83 @@ 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) + 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 + if data['pack'] + data['name'] = name + data['locales'] = locales + data['screenshot'] = screenshots + data['skin'] = { 'default' => [] } + result[name] = data + end + 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)) + if 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 + end + + @core = core + @conf = result + + end + + def core + @core end - def names + def flavour(name) + @conf[name] + end + + 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 fa8255faa..2f9cfe3ad 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -22,18 +22,23 @@ 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') user.settings['expand_spoilers'] = expand_spoilers_preference if change?('setting_expand_spoilers') user.settings['reduce_motion'] = reduce_motion_preference if change?('setting_reduce_motion') 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['hide_network'] = hide_network_preference if change?('setting_hide_network') 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') @@ -64,6 +69,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 @@ -72,6 +81,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 @@ -92,6 +105,18 @@ class UserSettingsDecorator boolean_cast_setting 'setting_noindex' end + def hide_followers_count_preference + boolean_cast_setting 'setting_hide_followers_count' + end + + def flavour_preference + settings['setting_flavour'] + end + + def skin_preference + settings['setting_skin'] + end + def hide_network_preference boolean_cast_setting 'setting_hide_network' end @@ -100,10 +125,6 @@ class UserSettingsDecorator boolean_cast_setting 'setting_show_application' end - def theme_preference - settings['setting_theme'] - end - def default_language_preference settings['setting_default_language'] end @@ -116,6 +137,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 |