about summary refs log tree commit diff
path: root/app/lib/command_tag/processor.rb
blob: d9e49c84a9f87823e4423b6d4b0074832ac27506 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# frozen_string_literal: true

class CommandTag::Processor
  include Redisable
  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

  def initialize(account, status)
    @account      = account
    @status       = status
    @parent       = status.thread
    @conversation = status.conversation
    @run_once     = Set[]
    @vars         = {}
    @text         = prepare_input(status.text)

    return unless @account.present? && @account.local? && @status.present?
  end

  def process!
    reset_status_caches

    @statements = parse_statements
    @text = @text.gsub(STATEMENT_STRIP_RE, '').split("\n")

    %w(at_start once_at_start).each { |suffix| execute_statements(suffix) }
    @text = @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) }
    elsif @status.update(text: @text)
      %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

    %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_statements
    @text.scan(STATEMENT_RE).flatten.map do |statement|
      next if statement.blank? || statement[0]&.strip.blank?

      statement = statement.scan(/^(.*) (?:start|begin|do)$(.*)|'([^']*)'|"([^"]*)"|(\S+)/im).flatten.compact
      statement[0] = statement[0].strip.tr(':.\- ', '_').gsub(/__+/, '_').downcase
      statement
    end.compact
  end

  def execute_statements(suffix)
    @statements.each do |statement|
      name = "handle_#{statement[0]}_#{suffix}"
      is_run_once = suffix.start_with?('once_')
      public_send(name, *statement[1..-1]) if respond_to?(name) && !(is_run_once && @run_once.include?(name))
      @run_once << name if is_run_once
    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
end