Files
discourse-hledger/app/controllers/hledger/reports_controller.rb
T

135 lines
3.9 KiB
Ruby

# frozen_string_literal: true
module ::Hledger
class ReportsController < ::ApplicationController
requires_plugin PLUGIN_NAME
RATE_LIMIT_MAX = 30
RATE_LIMIT_SECONDS = 60
CACHE_TTL = 5.minutes
before_action :ensure_enabled
before_action :find_topic
before_action :enforce_rate_limit
before_action :enforce_global_rate_limit
def show
report_type = params[:report_type].to_s
if !Hledger::ReportBuilder.supported?(report_type)
return render_json_error(I18n.t("hledger.errors.unsupported_report"), status: 422)
end
begin_date = parse_date(params[:begin])
end_date = parse_date(params[:end])
if begin_date == :invalid || end_date == :invalid
return render_json_error(I18n.t("hledger.errors.invalid_date"), status: 422)
end
first_post = @topic.first_post
runner = Hledger::Runner.new
cache_key = cache_key_for(runner, first_post, report_type, begin_date, end_date)
if (cached = Discourse.cache.read(cache_key))
return render(json: cached)
end
Hledger::BuildReport.call(
params: {
raw: first_post&.raw,
report_type: report_type,
begin_date: begin_date,
end_date: end_date,
},
runner: runner,
) do
on_success do |report:|
Discourse.cache.write(cache_key, report, expires_in: CACHE_TTL)
render json: report
end
on_failed_step(:extract_journal) { |step| render_journal_error(step.error) }
on_failed_contract do |contract|
render_json_error(contract.errors.full_messages, status: 422)
end
on_exceptions(Hledger::UnavailableError) do |error|
log_failure(error)
render_json_error(I18n.t("hledger.errors.unavailable"), status: 503)
end
on_exceptions(Hledger::OutputLimitError) do |error|
log_failure(error)
render_json_error(I18n.t("hledger.errors.output_too_large"), status: 422)
end
on_exceptions(Hledger::Error) do |error|
log_failure(error)
render_json_error(I18n.t("hledger.errors.execution"), status: 422)
end
on_failure { render_json_error(I18n.t("hledger.errors.generic"), status: 422) }
end
end
private
def ensure_enabled
raise Discourse::NotFound if !SiteSetting.hledger_enabled
end
def find_topic
@topic = Topic.find_by(id: params[:topic_id])
raise Discourse::NotFound if @topic.blank?
raise Discourse::NotFound if !guardian.can_see?(@topic)
end
def enforce_rate_limit
RateLimiter.new(
current_user,
"hledger-reports-#{request.remote_ip}",
RATE_LIMIT_MAX,
RATE_LIMIT_SECONDS,
).performed!
end
def enforce_global_rate_limit
max = SiteSetting.hledger_global_rate_limit_per_minute.to_i
return if max <= 0
RateLimiter.new(nil, "hledger-reports-global", max, 60, global: true).performed!
end
def parse_date(value)
return nil if value.blank?
Date.iso8601(value.to_s)
rescue Date::Error
:invalid
end
def cache_key_for(runner, first_post, report_type, begin_date, end_date)
[
"hledger",
@topic.id,
first_post&.version,
report_type,
begin_date,
end_date,
SiteSetting.hledger_path,
runner.version,
].join(":")
end
def render_journal_error(error)
key = "hledger.errors.journal.#{error}"
key = "hledger.errors.journal.invalid" if !I18n.exists?(key)
render_json_error(I18n.t(key), status: error == :none ? 404 : 422)
end
def log_failure(error)
Rails.logger.warn(
"hledger report failed: report=#{params[:report_type]} topic=#{@topic&.id} " \
"user=#{current_user&.id} error=#{error.class} detail=#{error.message.to_s.truncate(500)}",
)
end
end
end