Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/algora/bounties/bounties.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,9 @@ defmodule Algora.Bounties do
{:owner_id, owner_id}, query ->
from([b, r: r] in query, where: b.owner_id == ^owner_id or r.user_id == ^owner_id)

{:owner_handle, owner_handle}, query ->
from([b, o: o] in query, where: o.handle == ^owner_handle)

{:status, status}, query ->
query = where(query, [b], b.status == ^status)

Expand Down Expand Up @@ -1114,8 +1117,10 @@ defmodule Algora.Bounties do
status: b.status,
owner: %{
id: o.id,
inserted_at: o.inserted_at,
name: o.name,
handle: o.handle,
provider_login: o.provider_login,
avatar_url: o.avatar_url,
tech_stack: o.tech_stack
},
Expand Down
65 changes: 65 additions & 0 deletions lib/algora_web/controllers/api/bounty_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
defmodule AlgoraWeb.API.BountyController do
use AlgoraWeb, :controller

alias Algora.Bounties
alias AlgoraWeb.API.FallbackController

action_fallback FallbackController

@doc """
Get a list of bounties with optional filtering parameters.

Query Parameters:
- status: string (optional) - Filter by status (open, paid)
- org: string (optional) - Filter by organization handle
- limit: integer (optional) - Limit the number of bounties returned
"""
def index(conn, %{"batch" => _batch, "input" => input} = _params) do
with {:ok, decoded} <- Jason.decode(input),
%{"0" => %{"json" => json}} <- decoded do
criteria = to_criteria(json)
bounties = Bounties.list_bounties(criteria)
render(conn, :index, bounties: bounties)
end
end

def index(conn, params) do
criteria = to_criteria(params)
bounties = Bounties.list_bounties(criteria)
render(conn, :index, bounties: bounties)
end

# Convert JSON/map parameters to keyword list criteria
defp to_criteria(params) when is_map(params) do
params
|> Enum.map(&parse_params/1)
|> Enum.reject(&is_nil/1)
|> Keyword.new()
end

defp to_criteria(_), do: []

defp parse_params({"status", status}) do
{:status, parse_status(status)}
end

defp parse_params({"org", org_handle}) do
{:owner_handle, org_handle}
end

defp parse_params({"limit", limit}) do
{:limit, limit}
end

defp parse_params(_), do: nil

# Parse status string to corresponding enum atom
defp parse_status(status) when is_binary(status) do
case String.downcase(status) do
"paid" -> :paid
_ -> :open
end
end

defp parse_status(_), do: :open
end
113 changes: 113 additions & 0 deletions lib/algora_web/controllers/api/bounty_json.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
defmodule AlgoraWeb.API.BountyJSON do
alias Algora.Bounties.Bounty

def index(%{bounties: bounties}) do
[
%{
result: %{
data: %{
json: %{
next_cursor: nil,
items: for(bounty <- bounties, do: data(bounty))
}
}
}
}
]
end

defp data(%{} = bounty) do
%{
id: bounty.id,
point_reward: nil,
reward: %{
amount: Algora.MoneyUtils.to_minor_units(bounty.amount),
currency: bounty.amount.currency
},
reward_formatted: Money.to_string!(bounty.amount, no_fraction_if_integer: true),
reward_tiers: [],
tech: bounty.owner.tech_stack,
status: bounty.status,
is_external: false,
org: org_data(bounty.owner),
task: task_data(bounty),
type: "standard",
kind: "dev",
reward_type: "cash",
visibility: "public",
bids: [],
autopay_disabled: false,
timeouts_disabled: false,
manual_assignments: false,
created_at: bounty.inserted_at,
updated_at: bounty.inserted_at
}
end

defp task_data(bounty) do
%{
id: bounty.ticket.id,
forge: "github",
repo_owner: bounty.repository.owner.provider_login,
repo_name: bounty.repository.name,
number: bounty.ticket.number,
source: %{
type: "github",
data: %{
id: bounty.ticket.id,
html_url: bounty.ticket.url,
title: bounty.ticket.title,
body: "",
user: %{
id: 0,
login: "",
avatar_url: "",
html_url: "",
name: "",
company: "",
location: "",
twitter_username: ""
}
}
},
status: "open",
title: bounty.ticket.title,
url: bounty.ticket.url,
body: "",
type: "issue",
hash: Bounty.path(bounty),
tech: []
}
end

defp org_data(nil), do: nil

defp org_data(org) do
%{
id: org.id,
created_at: org.inserted_at,
handle: org.handle,
name: org.name,
display_name: org.name,
description: "",
avatar_url: org.avatar_url,
website_url: "",
twitter_url: "",
youtube_url: "",
discord_url: "",
slack_url: "",
stargazers_count: 0,
tech: org.tech_stack,
accepts_sponsorships: false,
members: members_data(org),
enabled_expert_recs: false,
enabled_private_bounties: false,
days_until_timeout: nil,
github_handle: org.provider_login
}
end

defp members_data(_org) do
[]
end
end
24 changes: 24 additions & 0 deletions lib/algora_web/controllers/api/error_json.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule AlgoraWeb.API.ErrorJSON do
def error(%{changeset: changeset}) do
%{
errors: Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
}
end

def error(%{message: message}) do
%{error: message}
end

defp translate_error({msg, opts}) do
Enum.reduce(opts, msg, fn
{key, value}, acc when is_binary(value) ->
String.replace(acc, "%{#{key}}", value)

{key, value}, acc when is_integer(value) ->
String.replace(acc, "%{#{key}}", Integer.to_string(value))

{key, value}, acc ->
String.replace(acc, "%{#{key}}", inspect(value))
end)
end
end
34 changes: 34 additions & 0 deletions lib/algora_web/controllers/api/fallback_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
defmodule AlgoraWeb.API.FallbackController do
use AlgoraWeb, :controller

alias AlgoraWeb.API.ErrorJSON

def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
conn
|> put_status(:unprocessable_entity)
|> put_view(json: ErrorJSON)
|> render(:error, changeset: changeset)
end

def call(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> put_view(json: ErrorJSON)
|> render(:error, message: "Not found")
end

def call(conn, {:error, :unauthorized}) do
conn
|> put_status(:unauthorized)
|> put_view(json: ErrorJSON)
|> render(:error, message: "Unauthorized")
end

# Catch-all error handler
def call(conn, {:error, error}) do
conn
|> put_status(:bad_request)
|> put_view(json: ErrorJSON)
|> render(:error, message: to_string(error))
end
end
8 changes: 8 additions & 0 deletions lib/algora_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,14 @@ defmodule AlgoraWeb.Router do
end
end

scope "/api", AlgoraWeb.API do
pipe_through :api

# Legacy tRPC endpoints
get "/trpc/bounty.list", BountyController, :index
post "/trpc/bounty.list", BountyController, :index
end

# Other scopes may use custom stacks.
# scope "/api", AlgoraWeb do
# pipe_through :api
Expand Down