diff --git a/app/controllers/api/v1/statuses/bookmarks_controller.rb b/app/controllers/api/v1/statuses/bookmarks_controller.rb index 61afd7085..5759ea1f9 100644 --- a/app/controllers/api/v1/statuses/bookmarks_controller.rb +++ b/app/controllers/api/v1/statuses/bookmarks_controller.rb @@ -40,7 +40,7 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController end def curate_status(status) - return if status.curated || status.direct_visibility? || (status.reply? && status.in_reply_to_account_id != status.account_id) + return if status.curated || !status.distributable? || (status.reply? && status.in_reply_to_account_id != status.account_id) status.curated = true status.save FanOutOnWriteService.new.call(status) diff --git a/app/lib/bangtags.rb b/app/lib/bangtags.rb new file mode 100644 index 000000000..e8681073d --- /dev/null +++ b/app/lib/bangtags.rb @@ -0,0 +1,360 @@ +# frozen_string_literal: true + +class Bangtags + attr_reader :status, :account + + def initialize(status) + @status = status + @account = status.account + @parent_status = Status.find(status.in_reply_to_id) if status.reply? + + @prefix_ns = { + 'permalink' => ['link'], + 'cloudroot' => ['link'], + 'blogroot' => ['link'], + } + + @aliases = { + ['media', 'end'] => ['var', 'end'], + ['media', 'stop'] => ['var', 'end'], + ['media', 'endall'] => ['var', 'endall'], + ['media', 'stopall'] => ['var', 'endall'], + } + + # sections of the final status text + @chunks = [] + # list of transformation commands + @tf_cmds = [] + # list of post-processing commands + @post_cmds = [] + # hash of bangtag variables + @vars = {} + # keep track of what variables we're appending the value of between chunks + @vore_stack = [] + # keep track of what type of nested components are active so we can !end them in order + @component_stack = [] + end + + def process + status.text.gsub!('#!!', "#\u200c!") + + status.text.split(/(#!(?:.*:!#|{.*?}|[^\s#]+))/).each do |chunk| + if chunk.starts_with?("#!") + chunk.sub!(/(\\:)?+:+?!#\Z/, '\1') + chunk.sub!(/{(.*)}\Z/, '\1') + + if @vore_stack.last != '_comment' + cmd = chunk[2..-1].strip + next if cmd.blank? + cmd = cmd.split(':::') + cmd = cmd[0].split('::') + cmd[1..-1] + cmd = cmd[0].split(':') + cmd[1..-1] + + cmd.map! {|c| c.gsub(/\\:/, ':').gsub(/\\\\:/, '\:')} + + prefix = @prefix_ns[cmd[0]] + cmd = prefix + cmd unless prefix.nil? + + @aliases.each_key do |old_cmd| + cmd = aliases[old_cmd] + cmd.drop(old_cmd.length) if cmd.take(old_cmd.length) == old_cmd + end + elsif chunk.in?(['#!comment:end', '#!comment:stop', '#!comment:endall', '#!comment:stopall']) + @vore_stack.pop + @component_stack.pop + next + else + next + end + + case cmd[0] + when 'var' + chunk = nil + case cmd[1] + when 'end', 'stop' + @vore_stack.pop + @component_stack.pop + when 'endall', 'stopall' + @vore_stack = [] + @component_stack.reject! {|c| c == :var} + else + var = cmd[1] + next if var.nil? || var.starts_with?('_') + new_value = cmd[2..-1] + if new_value.blank? + chunk = @vars[var] + elsif new_value.length == 1 && new_value[0] == '-' + @vore_stack.push(var) + @component_stack.push(:var) + else + @vars[var] = new_value.join(':') + end + end + when 'tf' + chunk = nil + case cmd[1] + when 'end', 'stop' + @tf_cmds.pop + @component_stack.pop + when 'endall', 'stopall' + @tf_cmds = [] + @component_stack.reject! {|c| c == :tf} + else + @tf_cmds.push(cmd[1..-1]) + @component_stack.push(:tf) + end + when 'end', 'stop' + chunk = nil + case @component_stack.pop + when :tf + @tf_cmds.pop + when :var, :hide + @vore_stack.pop + end + when 'endall', 'stopall' + chunk = nil + @tf_cmds = [] + @vore_stack = [] + @component_stack = [] + when 'emojify' + chunk = nil + next if cmd[1].nil? + src_img = nil + shortcode = cmd[2] + case cmd[1] + when 'avatar' + src_img = status.account.avatar + when 'parent' + next unless cmd[3].present? && reply? + shortcode = cmd[3] + next if @parent_status.nil? + case cmd[2] + when 'avatar' + src_img = @parent_status.account.avatar + end + end + + next if src_img.nil? || shortcode.nil? || !shortcode.match?(/\A\w+\Z/) + + chunk = ":#{shortcode}:" + emoji = CustomEmoji.find_or_initialize_by(shortcode: shortcode, domain: nil) + if emoji.id.nil? + emoji.image = src_img + emoji.save + end + when 'emoji' + next if cmd[1].nil? + shortcode = cmd[1] + domain = (cmd[2].blank? ? nil : cmd[2].downcase) + chunk = ":#{shortcode}:" + ours = CustomEmoji.find_or_initialize_by(shortcode: shortcode, domain: nil) + if ours.id.nil? + if domain.nil? + theirs = CustomEmoji.find_by(shortcode: shortcode) + else + theirs = CustomEmoji.find_by(shortcode: shortcode, domain: domain) + end + unless theirs.nil? + ours.image = theirs.image + ours.save + end + end + when 'char' + chunk = nil + charmap = { + 'zws' => "\u200b", + 'zwnj' => "\u200c", + 'zwj' => "\u200d", + '\n' => "\n", + '\r' => "\r", + '\t' => "\t", + '\T' => ' ' + } + cmd[1..-1].each do |c| + next if c.nil? + if c.in?(charmap) + @chunks << charmap[cmd[1]] + elsif (/^\h{1,5}$/ =~ c) && c.to_i(16) > 0 + begin + @chunks << [c.to_i(16)].pack('U*') + rescue + @chunks << '?' + end + end + end + when 'link' + chunk = nil + case cmd[1] + when 'permalink', 'self' + chunk = TagManager.instance.url_for(status) + when 'cloudroot' + chunk = "https://monsterpit.cloud/~/#{account.username}" + when 'blogroot' + chunk = "https://monsterpit.blog/~/#{account.username}" + end + when 'ping' + mentions = [] + case cmd[1] + when 'admins' + mentions = User.admins.map { |u| "@#{u.account.username}" } + mentions.sort! + when 'mods' + mentions = User.moderators.map { |u| "@#{u.account.username}" } + mentions.sort! + when 'staff' + mentions = User.admins.map { |u| "@#{u.account.username}" } + mentions += User.moderators.map { |u| "@#{u.account.username}" } + mentions.uniq! + mentions.sort! + end + chunk = mentions.join(' ') + when 'tag' + chunk = nil + records = [] + valid_name = /^[[:word:]_\-]*[[:alpha:]_·\-][[:word:]_\-]*$/ + cmd[1..-1].select {|t| t.present? && valid_name.match?(t)}.uniq.each do |name| + next if status.tags.where(name: name).exists? + tag = Tag.where(name: name).first_or_create(name: name) + status.tags << tag + records << tag + TrendingTags.record_use!(tag, account, status.created_at) if status.distributable? + end + if status.distributable? + account.featured_tags.where(tag_id: records.map(&:id)).each do |featured_tag| + featured_tag.increment(status.created_at) + end + end + when 'thread' + chunk = nil + case cmd[1] + when 'reall' + if status.conversation_id.present? + mention_ids = Status.where(conversation_id: status.conversation_id).flat_map { |s| s.mentions.pluck(:account_id) } + mention_ids.uniq! + mentions = Account.where(id: mention_ids).map { |a| "@#{a.username}" } + chunk = mentions.join(' ') + end + end + when 'parent' + chunk = nil + next if @parent_status.nil? + case cmd[1] + when 'permalink' + chunk = TagManager.instance.url_for(@parent_status) + end + when 'media' + chunk = nil + + media_idx = cmd[1] + media_cmd = cmd[2] + media_args = cmd[3..-1] + + next unless media_cmd.present? && media_idx.present? && media_idx.scan(/\D/).empty? + media_idx = media_idx.to_i + next if status.media_attachments[media_idx-1].nil? + + case media_cmd + when 'desc' + if media_args.present? + @vars["media_#{media_idx}_desc"] = media_args.join(':') + else + @vore_stack.push("media_#{media_idx}_desc") + @component_stack.push(:var) + end + end + + @post_cmds.push(['media', media_idx, media_cmd]) + when 'bangtag' + chunk = chunk.sub('bangtag:', '').gsub(':', ":\u200c") + when 'join' + chunk = nil + next if cmd[1].nil? + charmap = { + 'zws' => "\u200b", + 'zwnj' => "\u200c", + 'zwj' => "\u200d", + '\n' => "\n", + '\r' => "\r", + '\t' => "\t", + '\T' => ' ' + } + sep = charmap[cmd[1]] + chunk = cmd[2..-1].join(sep.nil? ? cmd[1] : sep) + when 'hide' + chunk = nil + case cmd[1] + when 'end', 'stop', 'endall', 'stopall' + @vore_stack.reject! {|v| v == '_'} + @compontent_stack.reject! {|c| c == :hide} + else + if cmd[1].nil? && !'_'.in?(@vore_stack) + @vore_stack.push('_') + @component_stack.push(:hide) + end + end + when 'comment' + chunk = nil + if cmd[1].nil? + @vore_stack.push('_comment') + @component_stack.push(:var) + end + end + end + + if chunk.present? && @tf_cmds.present? + @tf_cmds.each do |tf_cmd| + next if chunk.nil? + case tf_cmd[0] + when 'replace', 'sub', 's' + tf_cmd[1..-1].in_groups_of(2) do |args| + chunk.sub!(*args) if args.all? + end + when 'replaceall', 'gsub', 'gs' + tf_cmd[1..-1].in_groups_of(2) do |args| + chunk.gsub!(*args) if args.all? + end + end + end + end + + unless chunk.blank? || @vore_stack.empty? + var = @vore_stack.last + next if var == '_' + if @vars[var].nil? + @vars[var] = chunk.lstrip + else + @vars[var] += chunk.rstrip + end + chunk = nil + end + + @chunks << chunk unless chunk.nil? + end + + @vars.transform_values! {|v| v.rstrip} + + postprocess + + status.text = @chunks.join('') + status.save + end + + private + + def postprocess + @post_cmds.each do |post_cmd| + case post_cmd[0] + when 'media' + media_idx = post_cmd[1] + media_cmd = post_cmd[2] + media_args = post_cmd[3..-1] + + case media_cmd + when 'desc' + status.media_attachments[media_idx-1].description = @vars["media_#{media_idx}_desc"] + status.media_attachments[media_idx-1].save + end + end + end + end + +end diff --git a/app/models/status.rb b/app/models/status.rb index cd35ca017..022296145 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -368,18 +368,12 @@ class Status < ApplicationRecord end def as_public_timeline(account = nil, local_only = false) - if local_only - query = Status.network - .with_public_visibility - .without_replies - .without_reblogs - elsif account.nil? || account&.user&.setting_rawr_federated + if local_only || account.nil? || account&.user&.setting_rawr_federated query = timeline_scope(local_only) - query = query.without_replies unless Setting.show_replies_in_public_timelines else - query = Status.curated.public_browsable - query = query.without_replies unless Setting.show_replies_in_public_timelines + query = Status.curated end + query = query.without_replies unless Setting.show_replies_in_public_timelines apply_timeline_filters(query, account, local_only) end @@ -557,354 +551,7 @@ class Status < ApplicationRecord end def process_bangtags - return if text&.nil? - return unless '#!'.in?(text) - text.gsub!('#!!', "#\u200c!") - - prefix_ns = { - 'permalink' => ['link'], - 'cloudroot' => ['link'], - 'blogroot' => ['link'], - } - - aliases = { - ['media', 'end'] => ['var', 'end'], - ['media', 'stop'] => ['var', 'end'], - ['media', 'endall'] => ['var', 'endall'], - ['media', 'stopall'] => ['var', 'endall'], - } - - # sections of the final status text - chunks = [] - # list of transformation commands - tf_cmds = [] - # list of post-processing commands - post_cmds = [] - # hash of bangtag variables - vars = {} - # keep track of what variables we're appending the value of between chunks - vore_stack = [] - # keep track of what type of nested components are active so we can !end them in order - component_stack = [] - - text.split(/(#!(?:.*:!#|{.*?}|[^\s#]+))/).each do |chunk| - if chunk.starts_with?("#!") - chunk.sub!(/(\\:)?+:+?!#\Z/, '\1') - chunk.sub!(/{(.*)}\Z/, '\1') - - if vore_stack.last != '_comment' - cmd = chunk[2..-1].strip - next if cmd.blank? - cmd = cmd.split(':::') - cmd = cmd[0].split('::') + cmd[1..-1] - cmd = cmd[0].split(':') + cmd[1..-1] - - cmd.map! {|c| c.gsub(/\\:/, ':').gsub(/\\\\:/, '\:')} - - prefix = prefix_ns[cmd[0]] - cmd = prefix + cmd unless prefix.nil? - - aliases.each_key do |old_cmd| - cmd = aliases[old_cmd] + cmd.drop(old_cmd.length) if cmd.take(old_cmd.length) == old_cmd - end - elsif chunk.in?(['#!comment:end', '#!comment:stop', '#!comment:endall', '#!comment:stopall']) - vore_stack.pop - component_stack.pop - next - else - next - end - - case cmd[0] - when 'var' - chunk = nil - case cmd[1] - when 'end', 'stop' - vore_stack.pop - component_stack.pop - when 'endall', 'stopall' - vore_stack = [] - component_stack.reject! {|c| c == :var} - else - var = cmd[1] - next if var.nil? || var.starts_with?('_') - new_value = cmd[2..-1] - if new_value.blank? - chunk = vars[var] - elsif new_value.length == 1 && new_value[0] == '-' - vore_stack.push(var) - component_stack.push(:var) - else - vars[var] = new_value.join(':') - end - end - when 'tf' - chunk = nil - case cmd[1] - when 'end', 'stop' - tf_cmds.pop - component_stack.pop - when 'endall', 'stopall' - tf_cmds = [] - component_stack.reject! {|c| c == :tf} - else - tf_cmds.push(cmd[1..-1]) - component_stack.push(:tf) - end - when 'end', 'stop' - chunk = nil - case component_stack.pop - when :tf - tf_cmds.pop - when :var, :hide - vore_stack.pop - end - when 'endall', 'stopall' - chunk = nil - tf_cmds = [] - vore_stack = [] - component_stack = [] - when 'emojify' - chunk = nil - next if cmd[1].nil? - src_img = nil - shortcode = cmd[2] - case cmd[1] - when 'avatar' - src_img = account.avatar - when 'parent' - next unless cmd[3].present? && reply? - shortcode = cmd[3] - parent_status = Status.where(id: in_reply_to_id).first - next if parent_status.nil? - case cmd[2] - when 'avatar' - src_img = parent_status.account.avatar - end - end - - next if src_img.nil? || shortcode.nil? || !shortcode.match?(/\A\w+\Z/) - - chunk = ":#{shortcode}:" - emoji = CustomEmoji.find_or_initialize_by(shortcode: shortcode, domain: nil) - if emoji.id.nil? - emoji.image = src_img - emoji.save - end - when 'emoji' - next if cmd[1].nil? - shortcode = cmd[1] - domain = (cmd[2].blank? ? nil : cmd[2].downcase) - chunk = ":#{shortcode}:" - ours = CustomEmoji.find_or_initialize_by(shortcode: shortcode, domain: nil) - if ours.id.nil? - if domain.nil? - theirs = CustomEmoji.find_by(shortcode: shortcode) - else - theirs = CustomEmoji.find_by(shortcode: shortcode, domain: domain) - end - unless theirs.nil? - ours.image = theirs.image - ours.save - end - end - when 'char' - chunk = nil - charmap = { - 'zws' => "\u200b", - 'zwnj' => "\u200c", - 'zwj' => "\u200d", - '\n' => "\n", - '\r' => "\r", - '\t' => "\t", - '\T' => ' ' - } - cmd[1..-1].each do |c| - next if c.nil? - if c.in?(charmap) - chunks << charmap[cmd[1]] - elsif (/^\h{1,5}$/ =~ c) && c.to_i(16) > 0 - begin - chunks << [c.to_i(16)].pack('U*') - rescue - chunks << '?' - end - end - end - when 'link' - chunk = nil - case cmd[1] - when 'permalink', 'self' - chunk = TagManager.instance.url_for(self) - when 'cloudroot' - chunk = "https://monsterpit.cloud/~/#{account.username}" - when 'blogroot' - chunk = "https://monsterpit.blog/~/#{account.username}" - end - when 'ping' - mentions = [] - case cmd[1] - when 'admins' - mentions = User.admins.map { |u| "@#{u.account.username}" } - mentions.sort! - when 'mods' - mentions = User.moderators.map { |u| "@#{u.account.username}" } - mentions.sort! - when 'staff' - mentions = User.admins.map { |u| "@#{u.account.username}" } - mentions += User.moderators.map { |u| "@#{u.account.username}" } - mentions.uniq! - mentions.sort! - end - chunk = mentions.join(' ') - when 'tag' - chunk = nil - records = [] - valid_name = /^[[:word:]_\-]*[[:alpha:]_·\-][[:word:]_\-]*$/ - cmd[1..-1].select {|t| t.present? && valid_name.match?(t)}.uniq.each do |name| - next if self.tags.where(name: name).exists? - tag = Tag.where(name: name).first_or_create(name: name) - self.tags << tag - records << tag - TrendingTags.record_use!(tag, account, created_at) if distributable? - end - if public_visibility? || unlisted_visibility? - account.featured_tags.where(tag_id: records.map(&:id)).each do |featured_tag| - featured_tag.increment(created_at) - end - end - when 'thread' - chunk = nil - case cmd[1] - when 'reall' - if conversation_id.present? - mention_ids = Status.where(conversation_id: conversation_id).flat_map { |s| s.mentions.pluck(:account_id) } - mention_ids.uniq! - mentions = Account.where(id: mention_ids).map { |a| "@#{a.username}" } - chunk = mentions.join(' ') - end - end - when 'parent' - chunk = nil - next unless reply? - parent_status = Status.where(id: in_reply_to_id).first - next if parent_status.nil? - case cmd[1] - when 'edit' - next unless reply? && in_reply_to_account_id == account_id - when 'permalink' - chunk = TagManager.instance.url_for(parent_status) - end - when 'media' - chunk = nil - - media_idx = cmd[1] - media_cmd = cmd[2] - media_args = cmd[3..-1] - - next unless media_cmd.present? && media_idx.present? && media_idx.scan(/\D/).empty? - media_idx = media_idx.to_i - next if media_attachments[media_idx-1].nil? - - case media_cmd - when 'desc' - if media_args.present? - vars["media_#{media_idx}_desc"] = media_args.join(':') - else - vore_stack.push("media_#{media_idx}_desc") - component_stack.push(:var) - end - end - - post_cmds.push(['media', media_idx, media_cmd]) - when 'bangtag' - chunk = chunk.sub('bangtag:', '').gsub(':', ":\u200c") - when 'join' - chunk = nil - next if cmd[1].nil? - charmap = { - 'zws' => "\u200b", - 'zwnj' => "\u200c", - 'zwj' => "\u200d", - '\n' => "\n", - '\r' => "\r", - '\t' => "\t", - '\T' => ' ' - } - sep = charmap[cmd[1]] - chunk = cmd[2..-1].join(sep.nil? ? cmd[1] : sep) - when 'hide' - chunk = nil - case cmd[1] - when 'end', 'stop', 'endall', 'stopall' - vore_stack.reject! {|v| v == '_'} - compontent_stack.reject! {|c| c == :hide} - else - if cmd[1].nil? && !'_'.in?(vore_stack) - vore_stack.push('_') - component_stack.push(:hide) - end - end - when 'comment' - chunk = nil - if cmd[1].nil? - vore_stack.push('_comment') - component_stack.push(:var) - end - end - end - - if chunk.present? && tf_cmds.present? - tf_cmds.each do |tf_cmd| - next if chunk.nil? - case tf_cmd[0] - when 'replace', 'sub', 's' - tf_cmd[1..-1].in_groups_of(2) do |args| - chunk.sub!(*args) if args.all? - end - when 'replaceall', 'gsub', 'gs' - tf_cmd[1..-1].in_groups_of(2) do |args| - chunk.gsub!(*args) if args.all? - end - end - end - end - - unless chunk.blank? || vore_stack.empty? - var = vore_stack.last - next if var == '_' - if vars[var].nil? - vars[var] = chunk.lstrip - else - vars[var] += chunk.rstrip - end - chunk = nil - end - - chunks << chunk unless chunk.nil? - end - - vars.transform_values! {|v| v.rstrip} - - if post_cmds.present? - post_cmds.each do |post_cmd| - case post_cmd[0] - when 'media' - media_idx = post_cmd[1] - media_cmd = post_cmd[2] - media_args = post_cmd[3..-1] - - case media_cmd - when 'desc' - media_attachments[media_idx-1].description = vars["media_#{media_idx}_desc"] - media_attachments[media_idx-1].save - end - end - end - end - - self.text = chunks.join('') - save + Bangtags.new(self).process if text&.present? && '#!'.in?(text) end def set_conversation diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb index 2573bde0c..98275b37e 100644 --- a/app/services/favourite_service.rb +++ b/app/services/favourite_service.rb @@ -56,7 +56,7 @@ class FavouriteService < BaseService end def curate_status(status) - return if status.curated || status.direct_visibility? || (status.reply? && status.in_reply_to_account_id != status.account_id) + return if status.curated || !status.distributable? || (status.reply? && status.in_reply_to_account_id != status.account_id) status.curated = true status.save FanOutOnWriteService.new.call(status) diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index a9b662b0c..1a39c6c95 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -65,7 +65,7 @@ class ReblogService < BaseService end def curate_status(status) - return if status.curated || status.direct_visibility? || (status.reply? && status.in_reply_to_account_id != status.account_id) + return if status.curated || !status.distributable? || (status.reply? && status.in_reply_to_account_id != status.account_id) status.curated = true status.save FanOutOnWriteService.new.call(status)