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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
|
# 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
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 = @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_statements
@text.scan(STATEMENT_RE).flatten.each 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
add_statement_handlers_for(statement)
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 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
|