# 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 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 cache_key = cache_key_for(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: Hledger::Runner.new, ) 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 parse_date(value) return nil if value.blank? Date.iso8601(value.to_s) rescue Date::Error :invalid end def cache_key_for(first_post, report_type, begin_date, end_date) [ "hledger", @topic.id, first_post&.version, report_type, begin_date, end_date, SiteSetting.hledger_path, ].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