# frozen_string_literal: true # .~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~. # ################### Cthulhu Code! ################### # `~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~` # # - Interprets and executes user input. THIS CAN BE VERY DANGEROUS! # # - Has a high complexity level and needs tests. # # - May destroy objects passed to it. # # - Incurs a high performance penalty. # # # ############################################################################### class CommandTag::Processor include Redisable include ImgProxyHelper include CommandTag::Commands STATEMENT_RE = /^\s*#!\s*([^\n]+ (?:start|begin|do)$.*)\n\s*#!\s*(?:end|stop|done)\s*$|^\s*#!\s*(.*?)\s*$/im.freeze STATEMENT_STRIP_RE = /^\s*#!\s*(?:[^\n]+ (?:start|begin|do)$.*)\n\s*#!\s*(?:end|stop|done)\s*$\n?|^\s*#!\s*(?:.*?)\s*$\n?/im.freeze TEMPLATE_RE = /%%\s*(.*?)\s*%%/.freeze ESCAPE_MAP = { '\n' => "\n", '\r' => "\r", '\t' => "\t", '\\\\' => '\\', }.freeze def initialize(account, status) @account = account @status = status @parent = status.thread @conversation = status.conversation @run_once = Set[] @vars = {} @text = prepare_input(status.text) @statements = [] return unless @account.present? && @account.local? && @status.present? end def process! reset_status_caches parse_statements @text = @text.gsub(STATEMENT_STRIP_RE, '').split("\n") %w(at_start once_at_start).each { |suffix| execute_statements(suffix) } @text = parse_templates(@text.join("\n")).rstrip %w(before_save once_before_save).each { |suffix| execute_statements(suffix) } if @text.blank? || @text.gsub(Account::MENTION_RE, '').strip.blank? %w(when_blank once_when_blank).each { |suffix| execute_statements(suffix) } unless (@status.published? && !@status.edited.zero?) || @text.present? %w(before_destroy once_before_destroy).each { |suffix| execute_statements(suffix) } @status.destroy %w(after_destroy once_after_destroy).each { |suffix| execute_statements(suffix) } end elsif @status.destroyed? %w(after_destroy once_after_destroy).each { |suffix| execute_statements(suffix) } else @status.text = @text process_inline_images! if @status.save %w(after_save once_after_save).each { |suffix| execute_statements(suffix) } else %w(after_save_fail once_after_save_fail).each { |suffix| execute_statements(suffix) } end end %w(at_end once_at_end).each { |suffix| execute_statements(suffix) } reset_status_caches end private def prepare_input(text) text.gsub(/\r\n|\n\r|\r/, "\n") .gsub(/^\s*((?:(?:#{Account::MENTION_RE}|#{Tag::HASHTAG_RE})\s*)+)#!/, "\\1\n#!") end def parse_templates(text) text.gsub(TEMPLATE_RE) do return if Regexp.last_match(1).blank? template = Regexp.last_match(1).scan(/'([^']*)'|"([^"]*)"|(\S+)/).flatten.compact return if template[0].blank? name = normalize(template[0]) separator = "\n" if template.count > 2 if %w(by with using sep separator delim delimiter).include?(template[-2].downcase) separator = unescape_literals(template[-1]) template = template[0..-3] elsif !template[-1].match?(/\A[-+]?[0-9]+\z/) separator = unescape_literals(template[-1]) template.pop end end index_start = to_integer(template[1]) index_end = to_integer(template[2]) if ['all', '[]'].include?(template[1]) var(name).join(separator) elsif index_end.zero? var(name)[index_start].presence || '' else var(name)[index_start..index_end].presence || '' end end end def parse_statements @text.scan(STATEMENT_RE).flatten.compact.each do |statement| next if statement.blank? statement = statement.scan(/'([^']*)'|"([^"]*)"|(\S+)|\s+(?:start|begin|do)\s*$[\r\n]+(.*)/im).flatten.compact statement[0] = statement[0].strip.tr(':.\- ', '_').gsub(/__+/, '_').downcase statement[-1].rstrip! if statement.count > 1 add_statement_handlers_for(statement) if statement[0].present? end end def potential_handlers_for(name) ['_once', ''].each_with_index do |count_affix, index| %w(at_start when_blank at_end).each do |when_affix| yield ["handle_#{name}#{count_affix}_#{when_affix}", index.zero?] end %w(destroy save postprocess save_fail).each do |event_affix| %w(before after).each do |when_affix| yield ["handle_#{name}#{count_affix}_#{when_affix}_#{event_affix}", index.zero?] end end end end def add_statement_handlers_for(statement_array) potential_handlers_for(statement_array[0]) do |handler, once| (@statements << [handler, statement_array[1..-1]]) if respond_to?(handler) && !(once && @run_once.include?(handler)) @run_once << handler if once end end def execute_statements(event) @statements.select { |handler, _| handler.end_with?("_#{event}") }.each do |handler, arguments| public_send(handler, arguments) end end def reset_status_caches [@status, @parent].each do |status| next unless @account.id == status&.account_id Rails.cache.delete_matched("statuses/#{status.id}-*") Rails.cache.delete("statuses/#{status.id}") Rails.cache.delete(status) redis.zremrangebyscore("spam_check:#{status.account.id}", status.id, status.id) end end def author_of_status? @account.id == @status.account_id end def author_of_parent? @account.id == @parent&.account_id end def destroy_status! @status.destroy unless @status.destroyed? end def normalize(text) text.to_s.strip.downcase end def to_integer(text) text&.strip.to_i end def unescape_literals(text) ESCAPE_MAP.each { |escaped, unescaped| text.gsub!(escaped, unescaped) } text end def html_encode(text) (@html_entities ||= HTMLEntities.new).encode(text) end def var(name) @vars[name].presence || [] end def read_visibility_from(arg) return if arg.strip.blank? arg = case arg.strip when 'p', 'pu', 'all', 'world' 'public' when 'u', 'ul' 'unlisted' when 'f', 'follower', 'followers', 'packmates', 'follower-only', 'followers-only', 'packmates-only' 'private' when 'd', 'dm', 'pm', 'directmessage' 'direct' when 'default', 'reset' @account.user.setting_default_privacy else arg.strip end %w(public unlisted private limited direct).include?(arg) ? arg : nil end def normalize_domain(domain) return if domain&.strip.blank? || !domain.include?('.') domain.split('.').map(&:strip).reject(&:blank?).join('.').downcase end def federating_with_domain?(domain) return false if domain.blank? DomainAllow.where(domain: domain).exists? || Account.where(domain: domain, suspended_at: nil).exists? end end