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
21 changes: 21 additions & 0 deletions lib/algora/bounties/bounties.ex
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,27 @@ defmodule Algora.Bounties do
end)
end

@spec get_response_body(
bounties :: list(Bounty.t()),
ticket_ref :: %{owner: String.t(), repo: String.t(), number: integer()}
) :: String.t()
def get_response_body(bounties, ticket_ref) do
header =
Enum.map_join(bounties, "\n", fn bounty ->
"## 💎 #{bounty.amount} bounty [• #{bounty.owner.name}](#{User.url(bounty.owner)})"
end)

"""
#{header}
### Steps to solve:
1. **Start working**: Comment `/attempt ##{ticket_ref["number"]}` with your implementation plan
2. **Submit work**: Create a pull request including `/claim ##{ticket_ref["number"]}` in the PR body to claim the bounty
3. **Receive payment**: 100% of the bounty is received 2-5 days post-reward. [Make sure you are eligible for payouts](https://docs.algora.io/bounties/payments#supported-countries-regions)

Thank you for contributing to #{ticket_ref["owner"]}/#{ticket_ref["repo"]}!
"""
end

@spec notify_bounty(
%{
owner: User.t(),
Expand Down
100 changes: 16 additions & 84 deletions lib/algora/bounties/jobs/notify_bounty.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,9 @@ defmodule Algora.Bounties.Jobs.NotifyBounty do
queue: :notify_bounty,
max_attempts: 1

alias Algora.Accounts.User
alias Algora.Bounties
alias Algora.Github
alias Algora.Repo
alias Algora.Util
alias Algora.Workspace
alias Algora.Workspace.CommandResponse

require Logger

Expand All @@ -32,7 +28,7 @@ defmodule Algora.Bounties.Jobs.NotifyBounty do
"""

if Github.pat_enabled() do
with {:ok, response} <-
with {:ok, comment} <-
Github.create_issue_comment(
Github.pat(),
ticket_ref["owner"],
Expand All @@ -43,7 +39,12 @@ defmodule Algora.Bounties.Jobs.NotifyBounty do
{:ok, ticket} <-
Workspace.ensure_ticket(Github.pat(), ticket_ref["owner"], ticket_ref["repo"], ticket_ref["number"]) do
# TODO: update existing command response if it exists
create_command_response(response, command_source, command_id, ticket.id)
Workspace.create_command_response(%{
comment: comment,
command_source: command_source,
command_id: command_id,
ticket_id: ticket.id
})
end
else
Logger.info("""
Expand All @@ -69,84 +70,15 @@ defmodule Algora.Bounties.Jobs.NotifyBounty do
{:ok, ticket} <- Workspace.ensure_ticket(token, ticket_ref["owner"], ticket_ref["repo"], ticket_ref["number"]),
bounties when bounties != [] <- Bounties.list_bounties(ticket_id: ticket.id),
{:ok, _} <- Github.add_labels(token, ticket_ref["owner"], ticket_ref["repo"], ticket_ref["number"], ["💎 Bounty"]) do
header =
Enum.map_join(bounties, "\n", fn bounty ->
"## 💎 #{bounty.amount} bounty [• #{bounty.owner.name}](#{User.url(bounty.owner)})"
end)

body = """
#{header}
### Steps to solve:
1. **Start working**: Comment `/attempt ##{ticket_ref["number"]}` with your implementation plan
2. **Submit work**: Create a pull request including `/claim ##{ticket_ref["number"]}` in the PR body to claim the bounty
3. **Receive payment**: 100% of the bounty is received 2-5 days post-reward. [Make sure you are eligible for payouts](https://docs.algora.io/bounties/payments#supported-countries-regions)

Thank you for contributing to #{ticket_ref["owner"]}/#{ticket_ref["repo"]}!
"""

ensure_command_response(token, ticket_ref, command_id, command_source, ticket, body)
end
end

defp ensure_command_response(token, ticket_ref, command_id, command_source, ticket, body) do
case Workspace.fetch_command_response(ticket.id, :bounty) do
{:ok, response} ->
case Github.update_issue_comment(
token,
ticket_ref["owner"],
ticket_ref["repo"],
response.provider_response_id,
body
) do
{:ok, comment} ->
try_update_command_response(response, comment)

{:error, "404 Not Found"} ->
with {:ok, _} <- Workspace.delete_command_response(response.id) do
post_response(token, ticket_ref, command_id, command_source, ticket, body)
end

{:error, reason} ->
Logger.error("Failed to update command response #{response.id}: #{inspect(reason)}")
{:error, reason}
end

{:error, _reason} ->
post_response(token, ticket_ref, command_id, command_source, ticket, body)
end
end

defp post_response(token, ticket_ref, command_id, command_source, ticket, body) do
with {:ok, comment} <-
Github.create_issue_comment(token, ticket_ref["owner"], ticket_ref["repo"], ticket_ref["number"], body) do
create_command_response(comment, command_source, command_id, ticket.id)
end
end

defp create_command_response(comment, command_source, command_id, ticket_id) do
%CommandResponse{}
|> CommandResponse.changeset(%{
provider: "github",
provider_meta: Util.normalize_struct(comment),
provider_command_id: to_string(command_id),
provider_response_id: to_string(comment["id"]),
command_source: command_source,
command_type: :bounty,
ticket_id: ticket_id
})
|> Repo.insert()
end

defp try_update_command_response(command_response, body) do
case command_response
|> CommandResponse.changeset(%{provider_meta: Util.normalize_struct(body)})
|> Repo.update() do
{:ok, command_response} ->
{:ok, command_response}

{:error, reason} ->
Logger.error("Failed to update command response #{command_response.id}: #{inspect(reason)}")
{:ok, command_response}
Workspace.ensure_command_response(%{
token: token,
ticket_ref: ticket_ref,
command_id: command_id,
command_type: :bounty,
command_source: command_source,
ticket: ticket,
body: Bounties.get_response_body(bounties, ticket_ref)
})
end
end
end
133 changes: 133 additions & 0 deletions lib/algora/workspace/workspace.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule Algora.Workspace do
alias Algora.Accounts.User
alias Algora.Github
alias Algora.Repo
alias Algora.Util
alias Algora.Workspace.CommandResponse
alias Algora.Workspace.Installation
alias Algora.Workspace.Jobs
Expand Down Expand Up @@ -164,4 +165,136 @@ defmodule Algora.Workspace do
end

def delete_command_response(id), do: Repo.delete(Repo.get(CommandResponse, id))

@spec ensure_command_response(%{
token: String.t(),
ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()},
command_id: integer(),
command_type: :bounty | :attempt | :claim,
command_source: :ticket | :comment,
ticket: Ticket.t(),
body: String.t()
}) :: {:ok, CommandResponse.t()} | {:error, any()}
def ensure_command_response(%{
token: token,
ticket_ref: ticket_ref,
command_id: command_id,
command_type: command_type,
command_source: command_source,
ticket: ticket,
body: body
}) do
case refresh_command_response(%{
token: token,
ticket_ref: ticket_ref,
ticket: ticket,
body: body,
command_type: command_type
}) do
{:ok, response} ->
{:ok, response}

{:error, :command_response_not_found} ->
post_response(token, ticket_ref, command_id, command_source, ticket, body)

{:error, {:comment_not_found, response_id}} ->
with {:ok, _} <- delete_command_response(response_id) do
post_response(token, ticket_ref, command_id, command_source, ticket, body)
end

{:error, reason} ->
{:error, reason}
end
end

@spec refresh_command_response(%{
token: String.t(),
command_type: :bounty | :attempt | :claim,
ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()},
ticket: Ticket.t(),
body: String.t()
}) :: {:ok, CommandResponse.t()} | {:error, any()}
defp refresh_command_response(%{
token: token,
command_type: command_type,
ticket_ref: ticket_ref,
ticket: ticket,
body: body
}) do
case fetch_command_response(ticket.id, command_type) do
{:ok, response} ->
case Github.update_issue_comment(
token,
ticket_ref["owner"],
ticket_ref["repo"],
response.provider_response_id,
body
) do
{:ok, comment} ->
try_update_command_response(response, comment)

# TODO: don't rely on string matching
{:error, "404 Not Found"} ->
Logger.error("Command response #{response.id} not found")
{:error, {:comment_not_found, response.id}}

{:error, reason} ->
Logger.error("Failed to update command response #{response.id}: #{inspect(reason)}")
{:error, reason}
end

{:error, _reason} ->
{:error, :command_response_not_found}
end
end

defp post_response(token, ticket_ref, command_id, command_source, ticket, body) do
with {:ok, comment} <-
Github.create_issue_comment(token, ticket_ref["owner"], ticket_ref["repo"], ticket_ref["number"], body) do
create_command_response(%{
comment: comment,
command_source: command_source,
command_id: command_id,
ticket_id: ticket.id
})
end
end

@spec create_command_response(%{
comment: map(),
command_source: :ticket | :comment,
command_id: integer(),
ticket_id: integer()
}) :: {:ok, CommandResponse.t()} | {:error, any()}
def create_command_response(%{
comment: comment,
command_source: command_source,
command_id: command_id,
ticket_id: ticket_id
}) do
%CommandResponse{}
|> CommandResponse.changeset(%{
provider: "github",
provider_meta: Util.normalize_struct(comment),
provider_command_id: to_string(command_id),
provider_response_id: to_string(comment["id"]),
command_source: command_source,
command_type: :bounty,
ticket_id: ticket_id
})
|> Repo.insert()
end

defp try_update_command_response(command_response, body) do
case command_response
|> CommandResponse.changeset(%{provider_meta: Util.normalize_struct(body)})
|> Repo.update() do
{:ok, command_response} ->
{:ok, command_response}

{:error, reason} ->
Logger.error("Failed to update command response #{command_response.id}: #{inspect(reason)}")
{:ok, command_response}
end
end
end
Loading