module Api module V1 class BadgesController < ApplicationController skip_before_action :verify_authenticity_token # GET /api/v1/badge/:user_id/*project # # Generates a shields.io badge showing coding time for a project. # Supports lookup by slack_uid, username, or internal id. # Project can be a project name ("hackatime") or owner/repo ("hackclub/hackatime"). def show user = find_user(params[:user_id]) return render json: { error: "User not found" }, status: :not_found unless user unless user.allow_public_stats_lookup return render json: { error: "User has disabled public stats" }, status: :forbidden end project_name = resolve_project_name(user, params[:project]) return render json: { error: "Project not found" }, status: :not_found unless project_name seconds = user.heartbeats.where(project: project_name).duration_seconds return head :bad_request if seconds <= 0 label = params[:label] || "hackatime" color = params[:color] || "blue" time_text = format_duration(seconds) shields_url = "https://img.shields.io/badge/#{ERB::Util.url_encode(label)}-#{ERB::Util.url_encode(time_text)}-#{ERB::Util.url_encode(color)}" # Pass through any extra shields.io params (style, logo, etc.) extra = params.to_unsafe_h.except(:controller, :action, :user_id, :project, :label, :color, :aliases, :format) extra.each { |k, v| shields_url += "&#{ERB::Util.url_encode(k)}=#{ERB::Util.url_encode(v)}" } # Handle aliases (comma-separated project names to sum) if params[:aliases].present? alias_names = params[:aliases].split(",").map(&:strip) - [ project_name ] alias_seconds = user.heartbeats.where(project: alias_names).duration_seconds seconds += alias_seconds # Recalculate with alias time included time_text = format_duration(seconds) shields_url = "https://img.shields.io/badge/#{ERB::Util.url_encode(label)}-#{ERB::Util.url_encode(time_text)}-#{ERB::Util.url_encode(color)}" extra.each { |k, v| shields_url += "&#{ERB::Util.url_encode(k)}=#{ERB::Util.url_encode(v)}" } end redirect_to shields_url, allow_other_host: true, status: :temporary_redirect end private def find_user(identifier) return nil if identifier.blank? User.find_by(slack_uid: identifier) || User.find_by(username: identifier) || (identifier.match?(/^\d+$/) && User.find_by(id: identifier)) end # Resolve owner/repo format to a project name via ProjectRepoMapping def resolve_project_name(user, project_param) return nil if project_param.blank? # Direct match by project name first if user.heartbeats.where(project: project_param).exists? return project_param end # Try owner/repo → project_name lookup via repository if project_param.include?("/") mapping = user.project_repo_mappings .joins(:repository) .where(repositories: { owner: project_param.split("/", 2).first, name: project_param.split("/", 2).last }) .first return mapping.project_name if mapping end nil end def format_duration(seconds) hours = seconds / 3600 minutes = (seconds % 3600) / 60 hours > 0 ? "#{hours}h #{minutes}m" : "#{minutes}m" end end end end