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/feed_manager.rb | 58 | ||||
-rw-r--r-- | app/lib/formatter.rb | 110 | ||||
-rw-r--r-- | app/lib/settings/scoped_settings.rb | 3 | ||||
-rw-r--r-- | app/lib/tag_manager.rb | 3 | ||||
-rw-r--r-- | app/lib/themes.rb | 80 | ||||
-rw-r--r-- | app/lib/user_settings_decorator.rb | 35 |
8 files changed, 278 insertions, 21 deletions
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index ea8d146d4..cf31b6ff6 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -152,7 +152,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 @@ -164,7 +164,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/feed_manager.rb b/app/lib/feed_manager.rb index 46a55c7a4..efc9da34b 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) else false end @@ -100,6 +102,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 @@ -264,6 +289,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 @@ -402,6 +451,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/formatter.rb b/app/lib/formatter.rb index b6a13163d..dfa493ed5 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -2,6 +2,29 @@ require 'singleton' +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) + rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError + encode(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 @@ -35,16 +58,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 @@ -100,8 +133,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 @@ -111,14 +176,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] @@ -193,7 +258,7 @@ class Formatter end # rubocop:enable Metrics/BlockNesting - def rewrite(text, entities) + def rewrite(text, entities, keep_html = false) text = text.to_s # Sort by start index @@ -206,12 +271,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 @@ -226,6 +291,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/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/tag_manager.rb b/app/lib/tag_manager.rb index 39a98c3eb..a1d12a654 100644 --- a/app/lib/tag_manager.rb +++ b/app/lib/tag_manager.rb @@ -24,8 +24,11 @@ class TagManager def local_url?(url) uri = Addressable::URI.parse(url).normalize + return false unless uri.host domain = uri.host + (uri.port ? ":#{uri.port}" : '') TagManager.instance.web_domain?(domain) + rescue Addressable::URI::InvalidURIError + false end end 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 de054e403..d8015e50d 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['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 @@ -96,12 +109,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_network_preference + boolean_cast_setting 'setting_hide_network' + end + + def show_application_preference + boolean_cast_setting 'setting_show_application' end def default_language_preference @@ -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 |