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
107 changes: 85 additions & 22 deletions lib/algora/bounties/bounties.ex
Original file line number Diff line number Diff line change
Expand Up @@ -127,42 +127,96 @@ defmodule Algora.Bounties do
end

@spec do_claim_bounty(%{
user: User.t(),
provider_login: String.t(),
token: String.t(),
target: Ticket.t(),
source: Ticket.t(),
group_id: String.t() | nil,
group_share: Decimal.t(),
status: :pending | :approved | :rejected | :paid,
type: :pull_request | :review | :video | :design | :article
}) ::
{:ok, Claim.t()} | {:error, atom()}
defp do_claim_bounty(%{user: user, target: target, source: source, status: status, type: type}) do
# TODO: ensure user is pull request author
changeset =
Claim.changeset(%Claim{}, %{
target_id: target.id,
source_id: source.id,
user_id: user.id,
type: type,
status: status,
url: source.url
})

activity_attrs = %{type: :claim_submitted, notify_users: [user.id]}

case Repo.insert_with_activity(changeset, activity_attrs) do
{:ok, claim} ->
{:ok, claim}

defp do_claim_bounty(%{
provider_login: provider_login,
token: token,
target: target,
source: source,
group_id: group_id,
group_share: group_share,
status: status,
type: type
}) do
with {:ok, user} <- Workspace.ensure_user(token, provider_login),
activity_attrs = %{type: :claim_submitted, notify_users: [user.id]},
{:ok, claim} <-
Repo.insert_with_activity(
Claim.changeset(%Claim{}, %{
target_id: target.id,
source_id: source.id,
user_id: user.id,
type: type,
status: status,
url: source.url,
group_id: group_id,
group_share: group_share
}),
activity_attrs
) do
{:ok, claim}
else
{:error, %{errors: [target_id: {_, [constraint: :unique, constraint_name: _]}]}} ->
{:error, :already_exists}

{:error, _changeset} = error ->
{:error, _reason} = error ->
error
end
end

@spec do_claim_bounties(%{
provider_logins: [String.t()],
token: String.t(),
target: Ticket.t(),
source: Ticket.t(),
status: :pending | :approved | :rejected | :paid,
type: :pull_request | :review | :video | :design | :article
}) ::
{:ok, [Claim.t()]} | {:error, atom()}
defp do_claim_bounties(%{
provider_logins: provider_logins,
token: token,
target: target,
source: source,
status: status,
type: type
}) do
Enum.reduce_while(provider_logins, {:ok, []}, fn provider_login, {:ok, acc} ->
group_id =
case List.last(acc) do
nil -> nil
primary_claim -> primary_claim.group_id
end

case do_claim_bounty(%{
provider_login: provider_login,
token: token,
target: target,
source: source,
status: status,
type: type,
group_id: group_id,
group_share: Decimal.div(1, length(provider_logins))
}) do
{:ok, claim} -> {:cont, {:ok, [claim | acc]}}
error -> {:halt, error}
end
end)
end

@spec claim_bounty(
%{
user: User.t(),
coauthor_provider_logins: [String.t()],
target_ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()},
source_ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()},
status: :pending | :approved | :rejected | :paid,
Expand All @@ -174,6 +228,7 @@ defmodule Algora.Bounties do
def claim_bounty(
%{
user: user,
coauthor_provider_logins: coauthor_provider_logins,
target_ticket_ref: %{owner: target_repo_owner, repo: target_repo_name, number: target_number},
source_ticket_ref: %{owner: source_repo_owner, repo: source_repo_name, number: source_number},
status: status,
Expand All @@ -192,7 +247,15 @@ defmodule Algora.Bounties do
with {:ok, token} <- token_res,
{:ok, target} <- Workspace.ensure_ticket(token, target_repo_owner, target_repo_name, target_number),
{:ok, source} <- Workspace.ensure_ticket(token, source_repo_owner, source_repo_name, source_number),
{:ok, claim} <- do_claim_bounty(%{user: user, target: target, source: source, status: status, type: type}),
{:ok, [claim | _]} <-
do_claim_bounties(%{
provider_logins: [user.provider_login | coauthor_provider_logins],
token: token,
target: target,
source: source,
status: status,
type: type
}),
{:ok, _job} <- notify_claim(%{claim: claim}, installation_id: installation_id) do
broadcast()
{:ok, claim}
Expand All @@ -208,7 +271,7 @@ defmodule Algora.Bounties do
) ::
{:ok, Oban.Job.t()} | {:error, atom()}
def notify_claim(%{claim: claim}, opts \\ []) do
%{claim_id: claim.id, installation_id: opts[:installation_id]}
%{claim_group_id: claim.group_id, installation_id: opts[:installation_id]}
|> Jobs.NotifyClaim.new()
|> Oban.insert()
end
Expand Down
69 changes: 49 additions & 20 deletions lib/algora/bounties/jobs/notify_claim.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,76 @@ defmodule Algora.Bounties.Jobs.NotifyClaim do
@moduledoc false
use Oban.Worker, queue: :notify_claim

import Ecto.Query

alias Algora.Bounties.Claim
alias Algora.Github
alias Algora.Repo

require Logger

@impl Oban.Worker
def perform(%Oban.Job{args: %{"claim_id" => _claim_id, "installation_id" => nil}}) do
def perform(%Oban.Job{args: %{"claim_group_id" => _claim_group_id, "installation_id" => nil}}) do
:ok
end

@impl Oban.Worker
def perform(%Oban.Job{args: %{"claim_id" => claim_id, "installation_id" => installation_id}}) do
def perform(%Oban.Job{args: %{"claim_group_id" => claim_group_id, "installation_id" => installation_id}}) do
with {:ok, token} <- Github.get_installation_token(installation_id),
{:ok, claim} <- Repo.fetch(Claim, claim_id),
claim = Repo.preload(claim, source: [repository: [:user]], target: [repository: [:user]], user: []),
{:ok, _} <- maybe_add_labels(token, claim),
{:ok, _} <- add_comment(token, claim) do
claims =
from(c in Claim,
where: c.group_id == ^claim_group_id,
order_by: [asc: c.inserted_at]
)
|> Repo.all()
|> Repo.preload([:user, source: [repository: [:user]], target: [repository: [:user]]]),
{:ok, _} <- maybe_add_labels(token, claims),
{:ok, _} <- add_comment(token, claims) do
:ok
end
end

defp add_comment(token, claim) do
defp add_comment(token, claims) do
primary_claim = List.first(claims)

Github.create_issue_comment(
token,
claim.target.repository.user.provider_login,
claim.target.repository.name,
claim.target.number,
"💡 @#{claim.user.provider_login} submitted [#{Claim.type_label(claim.type)}](#{claim.url}) that claims the bounty. You can visit [Algora](#{Claim.reward_url(claim)}) to reward."
primary_claim.target.repository.user.provider_login,
primary_claim.target.repository.name,
primary_claim.target.number,
"💡 #{names(claims)} submitted [#{Claim.type_label(primary_claim.type)}](#{primary_claim.url}) that claims the bounty. You can visit [Algora](#{Claim.reward_url(primary_claim)}) to reward."
)
end

defp maybe_add_labels(token, %Claim{source: source} = claim) when not is_nil(source) do
Github.add_labels(
token,
claim.source.repository.user.provider_login,
claim.source.repository.name,
claim.source.number,
["🙋 Bounty claim"]
)
defp maybe_add_labels(token, claims) do
primary_claim = List.first(claims)

if primary_claim.source do
Github.add_labels(
token,
primary_claim.source.repository.user.provider_login,
primary_claim.source.repository.name,
primary_claim.source.number,
["🙋 Bounty claim"]
)
else
{:ok, nil}
end
end

defp maybe_add_labels(_token, _claim), do: {:ok, nil}
defp names([claim]) do
"@#{claim.user.provider_login}"
end

defp names([c1, c2]) do
"@#{c1.user.provider_login} and @#{c2.user.provider_login}"
end

defp names([c1, c2, c3]) do
"@#{c1.user.provider_login}, @#{c2.user.provider_login} and @#{c3.user.provider_login}"
end

defp names([c1, c2 | claims]) do
"@#{c1.user.provider_login}, @#{c2.user.provider_login} and #{length(claims)} others"
end
end
4 changes: 2 additions & 2 deletions lib/algora/bounties/schemas/claim.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ defmodule Algora.Bounties.Claim do

def changeset(claim, attrs) do
claim
|> cast(attrs, [:source_id, :target_id, :user_id, :status, :type, :url, :group_id])
|> cast(attrs, [:source_id, :target_id, :user_id, :status, :type, :url, :group_id, :group_share])
|> validate_required([:target_id, :user_id, :status, :type, :url])
|> generate_id()
|> put_group_id()
Expand Down Expand Up @@ -54,7 +54,7 @@ defmodule Algora.Bounties.Claim do
def type_label(:article), do: "an article"
def type_label(nil), do: "a URL"

def reward_url(claim), do: "#{AlgoraWeb.Endpoint.url()}/claims/#{claim.id}"
def reward_url(claim), do: "#{AlgoraWeb.Endpoint.url()}/claims/#{claim.group_id}"

def rewarded(query \\ Claim) do
from c in query,
Expand Down
16 changes: 14 additions & 2 deletions lib/algora/integrations/github/command.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ defmodule Algora.Github.Command do
@usage %{
bounty: "/bounty <amount>",
tip: "/tip <amount> @username or /tip @username <amount>",
claim: "/claim <issue-ref> (e.g. #123, repo#123, owner/repo#123, or full GitHub URL)"
claim: "/claim <issue-ref> (e.g. #123, repo#123, owner/repo#123, or full GitHub URL)",
split: "/split @username"
}

def commands do
Expand All @@ -21,7 +22,8 @@ defmodule Algora.Github.Command do
choice([
bounty_command(),
tip_command(),
claim_command()
claim_command(),
split_command()
]),

# Unknown command
Expand Down Expand Up @@ -58,6 +60,16 @@ defmodule Algora.Github.Command do
|> label(@usage.tip)
end

def split_command do
"/split"
|> string()
|> ignore()
|> concat(ignore(whitespace()))
|> concat(recipient())
|> tag(:split)
|> label(@usage.split)
end

def claim_command do
"/claim"
|> string()
Expand Down
4 changes: 2 additions & 2 deletions lib/algora/shared/parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ defmodule Algora.Parser do
@moduledoc false
def whitespace, do: ascii_string([?\s, ?\t], min: 1)
def digits, do: ascii_string([?0..?9], min: 1)
def word_chars, do: ascii_string([not: ?\s, not: ?\t], min: 1)
def non_separator_chars, do: ascii_string([not: ?#, not: ?/, not: ?\s, not: ?\t], min: 1)
def word_chars, do: ascii_string([not: ?\s, not: ?\t, not: ?\n], min: 1)
def non_separator_chars, do: ascii_string([not: ?#, not: ?/, not: ?\s, not: ?\t, not: ?\n], min: 1)
def integer, do: reduce(digits(), {__MODULE__, :to_integer, []})

def amount do
Expand Down
26 changes: 25 additions & 1 deletion lib/algora_web/controllers/webhooks/github_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ defmodule AlgoraWeb.Webhooks.GithubController do
Bounties.claim_bounty(
%{
user: user,
coauthor_provider_logins: (args[:splits] || []) |> Enum.map(& &1[:recipient]) |> Enum.uniq(),
target_ticket_ref: target_ticket_ref,
source_ticket_ref: source_ticket_ref,
status: if(pull_request["merged_at"], do: :approved, else: :pending),
Expand All @@ -152,13 +153,36 @@ defmodule AlgoraWeb.Webhooks.GithubController do
{:ok, nil}
end

def build_command({:claim, args}, commands) do
splits = Keyword.get_values(commands, :split)
{:claim, Keyword.put(args, :splits, splits)}
end

def build_command({:split, _args}, _commands), do: nil

def build_command(command, _commands), do: command

def build_commands(body) do
case Github.Command.parse(body) do
{:ok, commands} ->
{:ok,
commands
|> Enum.map(&build_command(&1, commands))
|> Enum.reject(&is_nil/1)}

{:error, reason} = error ->
Logger.error("Error parsing commands: #{inspect(reason)}")
error
end
end

def process_commands(%Webhook{event: event, hook_id: hook_id}, params) do
author = get_author(event, params)
body = get_body(event, params)

event_action = event <> "." <> params["action"]

case Github.Command.parse(body) do
case build_commands(body) do
{:ok, commands} ->
Enum.reduce_while(commands, {:ok, []}, fn command, {:ok, results} ->
case execute_command(event_action, command, author, params) do
Expand Down
4 changes: 0 additions & 4 deletions lib/algora_web/live/claim_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -397,10 +397,6 @@ defmodule AlgoraWeb.ClaimLive do
<.card_title>
Authors
</.card_title>
<!-- TODO: hide if user is not an admin -->
<.button variant="secondary" phx-click="split_bounty">
Split bounty
</.button>
</div>
</.card_header>
<.card_content>
Expand Down
1 change: 1 addition & 0 deletions test/algora/bounties_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ defmodule Algora.BountiesTest do
Algora.Bounties.claim_bounty(
%{
user: recipient,
coauthor_provider_logins: [],
target_ticket_ref: ticket_ref,
source_ticket_ref: ticket_ref,
status: :approved,
Expand Down
3 changes: 2 additions & 1 deletion test/support/factory.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ defmodule Algora.Factory do
twitter_url: "https://twitter.com/erich",
github_url: "https://github.com/erich",
linkedin_url: "https://linkedin.com/in/erich",
provider: "github"
provider: "github",
provider_login: sequence(:provider_login, &"erlich#{&1}")
}
end

Expand Down
Loading