Skip to content

Commit 3ce4212

Browse files
committed
feat: bounty splits
1 parent 9d9e55f commit 3ce4212

File tree

6 files changed

+173
-49
lines changed

6 files changed

+173
-49
lines changed

lib/algora/bounties/bounties.ex

Lines changed: 83 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -122,40 +122,94 @@ defmodule Algora.Bounties do
122122
end
123123

124124
@spec do_claim_bounty(%{
125-
user: User.t(),
125+
provider_login: String.t(),
126+
token: String.t(),
126127
target: Ticket.t(),
127128
source: Ticket.t(),
129+
group_id: String.t() | nil,
130+
group_share: Decimal.t(),
128131
status: :pending | :approved | :rejected | :paid,
129132
type: :pull_request | :review | :video | :design | :article
130133
}) ::
131134
{:ok, Claim.t()} | {:error, atom()}
132-
defp do_claim_bounty(%{user: user, target: target, source: source, status: status, type: type}) do
133-
# TODO: ensure user is pull request author
134-
changeset =
135-
Claim.changeset(%Claim{}, %{
136-
target_id: target.id,
137-
source_id: source.id,
138-
user_id: user.id,
139-
type: type,
140-
status: status,
141-
url: source.url
142-
})
143-
144-
case Repo.insert(changeset) do
145-
{:ok, claim} ->
146-
{:ok, claim}
147-
135+
defp do_claim_bounty(%{
136+
provider_login: provider_login,
137+
token: token,
138+
target: target,
139+
source: source,
140+
group_id: group_id,
141+
group_share: group_share,
142+
status: status,
143+
type: type
144+
}) do
145+
with {:ok, user} <- Workspace.ensure_user(token, provider_login),
146+
{:ok, claim} <-
147+
Repo.insert(
148+
Claim.changeset(%Claim{}, %{
149+
target_id: target.id,
150+
source_id: source.id,
151+
user_id: user.id,
152+
type: type,
153+
status: status,
154+
url: source.url,
155+
group_id: group_id,
156+
group_share: group_share
157+
})
158+
) do
159+
{:ok, claim}
160+
else
148161
{:error, %{errors: [target_id: {_, [constraint: :unique, constraint_name: _]}]}} ->
149162
{:error, :already_exists}
150163

151-
{:error, _changeset} = error ->
164+
{:error, _reason} = error ->
152165
error
153166
end
154167
end
155168

169+
@spec do_claim_bounties(%{
170+
provider_logins: [String.t()],
171+
token: String.t(),
172+
target: Ticket.t(),
173+
source: Ticket.t(),
174+
status: :pending | :approved | :rejected | :paid,
175+
type: :pull_request | :review | :video | :design | :article
176+
}) ::
177+
{:ok, Claim.t()} | {:error, atom()}
178+
defp do_claim_bounties(%{
179+
provider_logins: provider_logins,
180+
token: token,
181+
target: target,
182+
source: source,
183+
status: status,
184+
type: type
185+
}) do
186+
Enum.reduce_while(provider_logins, {:ok, []}, fn provider_login, {:ok, acc} ->
187+
group_id =
188+
case List.last(acc) do
189+
nil -> nil
190+
primary_claim -> primary_claim.group_id
191+
end
192+
193+
case do_claim_bounty(%{
194+
provider_login: provider_login,
195+
token: token,
196+
target: target,
197+
source: source,
198+
status: status,
199+
type: type,
200+
group_id: group_id,
201+
group_share: Decimal.div(1, length(provider_logins))
202+
}) do
203+
{:ok, claim} -> {:cont, {:ok, [claim | acc]}}
204+
error -> {:halt, error}
205+
end
206+
end)
207+
end
208+
156209
@spec claim_bounty(
157210
%{
158211
user: User.t(),
212+
coauthor_provider_logins: [String.t()],
159213
target_ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()},
160214
source_ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()},
161215
status: :pending | :approved | :rejected | :paid,
@@ -167,6 +221,7 @@ defmodule Algora.Bounties do
167221
def claim_bounty(
168222
%{
169223
user: user,
224+
coauthor_provider_logins: coauthor_provider_logins,
170225
target_ticket_ref: %{owner: target_repo_owner, repo: target_repo_name, number: target_number},
171226
source_ticket_ref: %{owner: source_repo_owner, repo: source_repo_name, number: source_number},
172227
status: status,
@@ -185,7 +240,15 @@ defmodule Algora.Bounties do
185240
with {:ok, token} <- token_res,
186241
{:ok, target} <- Workspace.ensure_ticket(token, target_repo_owner, target_repo_name, target_number),
187242
{:ok, source} <- Workspace.ensure_ticket(token, source_repo_owner, source_repo_name, source_number),
188-
{:ok, claim} <- do_claim_bounty(%{user: user, target: target, source: source, status: status, type: type}),
243+
{:ok, [claim | _]} <-
244+
do_claim_bounties(%{
245+
provider_logins: [user.provider_login | coauthor_provider_logins],
246+
token: token,
247+
target: target,
248+
source: source,
249+
status: status,
250+
type: type
251+
}),
189252
{:ok, _job} <- notify_claim(%{claim: claim}, installation_id: installation_id) do
190253
broadcast()
191254
{:ok, claim}
@@ -201,7 +264,7 @@ defmodule Algora.Bounties do
201264
) ::
202265
{:ok, Oban.Job.t()} | {:error, atom()}
203266
def notify_claim(%{claim: claim}, opts \\ []) do
204-
%{claim_id: claim.id, installation_id: opts[:installation_id]}
267+
%{claim_group_id: claim.group_id, installation_id: opts[:installation_id]}
205268
|> Jobs.NotifyClaim.new()
206269
|> Oban.insert()
207270
end

lib/algora/bounties/jobs/notify_claim.ex

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,76 @@ defmodule Algora.Bounties.Jobs.NotifyClaim do
22
@moduledoc false
33
use Oban.Worker, queue: :notify_claim
44

5+
import Ecto.Query
6+
57
alias Algora.Bounties.Claim
68
alias Algora.Github
79
alias Algora.Repo
810

911
require Logger
1012

1113
@impl Oban.Worker
12-
def perform(%Oban.Job{args: %{"claim_id" => _claim_id, "installation_id" => nil}}) do
14+
def perform(%Oban.Job{args: %{"claim_group_id" => _claim_group_id, "installation_id" => nil}}) do
1315
:ok
1416
end
1517

1618
@impl Oban.Worker
17-
def perform(%Oban.Job{args: %{"claim_id" => claim_id, "installation_id" => installation_id}}) do
19+
def perform(%Oban.Job{args: %{"claim_group_id" => claim_group_id, "installation_id" => installation_id}}) do
1820
with {:ok, token} <- Github.get_installation_token(installation_id),
19-
{:ok, claim} <- Repo.fetch(Claim, claim_id),
20-
claim = Repo.preload(claim, source: [repository: [:user]], target: [repository: [:user]], user: []),
21-
{:ok, _} <- maybe_add_labels(token, claim),
22-
{:ok, _} <- add_comment(token, claim) do
21+
claims =
22+
from(c in Claim,
23+
where: c.group_id == ^claim_group_id,
24+
order_by: [asc: c.created_at]
25+
)
26+
|> Repo.all()
27+
|> Repo.preload([:user, source: [repository: [:user]], target: [repository: [:user]]]),
28+
{:ok, _} <- maybe_add_labels(token, claims),
29+
{:ok, _} <- add_comment(token, claims) do
2330
:ok
2431
end
2532
end
2633

27-
defp add_comment(token, claim) do
34+
defp add_comment(token, claims) do
35+
primary_claim = List.first(claims)
36+
2837
Github.create_issue_comment(
2938
token,
30-
claim.target.repository.user.provider_login,
31-
claim.target.repository.name,
32-
claim.target.number,
33-
"💡 @#{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."
39+
primary_claim.target.repository.user.provider_login,
40+
primary_claim.target.repository.name,
41+
primary_claim.target.number,
42+
"💡 #{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."
3443
)
3544
end
3645

37-
defp maybe_add_labels(token, %Claim{source: source} = claim) when not is_nil(source) do
38-
Github.add_labels(
39-
token,
40-
claim.source.repository.user.provider_login,
41-
claim.source.repository.name,
42-
claim.source.number,
43-
["🙋 Bounty claim"]
44-
)
46+
defp maybe_add_labels(token, claims) do
47+
primary_claim = List.first(claims)
48+
49+
if primary_claim.source do
50+
Github.add_labels(
51+
token,
52+
primary_claim.source.repository.user.provider_login,
53+
primary_claim.source.repository.name,
54+
primary_claim.source.number,
55+
["🙋 Bounty claim"]
56+
)
57+
else
58+
{:ok, nil}
59+
end
4560
end
4661

47-
defp maybe_add_labels(_token, _claim), do: {:ok, nil}
62+
defp names([claim]) do
63+
claim.user.provider_login
64+
end
65+
66+
defp names([c1, c2]) do
67+
"#{c1.user.provider_login} and #{c2.user.provider_login}"
68+
end
69+
70+
defp names([c1, c2, c3]) do
71+
"#{c1.user.provider_login}, #{c2.user.provider_login} and #{c3.user.provider_login}"
72+
end
73+
74+
defp names([c1, c2 | claims]) do
75+
"#{c1.user.provider_login}, #{c2.user.provider_login} and #{length(claims)} others"
76+
end
4877
end

lib/algora/bounties/schemas/claim.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ defmodule Algora.Bounties.Claim do
2222

2323
def changeset(claim, attrs) do
2424
claim
25-
|> cast(attrs, [:source_id, :target_id, :user_id, :status, :type, :url, :group_id])
25+
|> cast(attrs, [:source_id, :target_id, :user_id, :status, :type, :url, :group_id, :group_share])
2626
|> validate_required([:target_id, :user_id, :status, :type, :url])
2727
|> generate_id()
2828
|> put_group_id()
@@ -46,7 +46,7 @@ defmodule Algora.Bounties.Claim do
4646
def type_label(:article), do: "an article"
4747
def type_label(nil), do: "a URL"
4848

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

5151
def rewarded(query \\ Claim) do
5252
from c in query,

lib/algora/integrations/github/command.ex

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ defmodule Algora.Github.Command do
88
@usage %{
99
bounty: "/bounty <amount>",
1010
tip: "/tip <amount> @username or /tip @username <amount>",
11-
claim: "/claim <issue-ref> (e.g. #123, repo#123, owner/repo#123, or full GitHub URL)"
11+
claim: "/claim <issue-ref> (e.g. #123, repo#123, owner/repo#123, or full GitHub URL)",
12+
split: "/split @username"
1213
}
1314

1415
def commands do
@@ -21,7 +22,8 @@ defmodule Algora.Github.Command do
2122
choice([
2223
bounty_command(),
2324
tip_command(),
24-
claim_command()
25+
claim_command(),
26+
split_command()
2527
]),
2628

2729
# Unknown command
@@ -58,6 +60,16 @@ defmodule Algora.Github.Command do
5860
|> label(@usage.tip)
5961
end
6062

63+
def split_command do
64+
"/split"
65+
|> string()
66+
|> ignore()
67+
|> concat(ignore(whitespace()))
68+
|> concat(recipient())
69+
|> tag(:split)
70+
|> label(@usage.split)
71+
end
72+
6173
def claim_command do
6274
"/claim"
6375
|> string()

lib/algora_web/controllers/webhooks/github_controller.ex

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ defmodule AlgoraWeb.Webhooks.GithubController do
138138
Bounties.claim_bounty(
139139
%{
140140
user: user,
141+
coauthor_provider_logins: (args[:splits] || []) |> Enum.map(& &1[:recipient]) |> Enum.uniq(),
141142
target_ticket_ref: target_ticket_ref,
142143
source_ticket_ref: source_ticket_ref,
143144
status: if(pull_request["merged_at"], do: :approved, else: :pending),
@@ -152,13 +153,36 @@ defmodule AlgoraWeb.Webhooks.GithubController do
152153
{:ok, nil}
153154
end
154155

156+
def build_command({:claim, args}, commands) do
157+
splits = Keyword.get_values(commands, :split)
158+
{:claim, Keyword.put(args, :splits, splits)}
159+
end
160+
161+
def build_command({:split, _args}, _commands), do: nil
162+
163+
def build_command(command, _commands), do: command
164+
165+
def build_commands(body) do
166+
case Github.Command.parse(body) do
167+
{:ok, commands} ->
168+
{:ok,
169+
commands
170+
|> Enum.map(&build_command(&1, commands))
171+
|> Enum.reject(&is_nil/1)}
172+
173+
{:error, reason} = error ->
174+
Logger.error("Error parsing commands: #{inspect(reason)}")
175+
error
176+
end
177+
end
178+
155179
def process_commands(%Webhook{event: event, hook_id: hook_id}, params) do
156180
author = get_author(event, params)
157181
body = get_body(event, params)
158182

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

161-
case Github.Command.parse(body) do
185+
case build_commands(body) do
162186
{:ok, commands} ->
163187
Enum.reduce_while(commands, {:ok, []}, fn command, {:ok, results} ->
164188
case execute_command(event_action, command, author, params) do

lib/algora_web/live/claim_live.ex

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -397,10 +397,6 @@ defmodule AlgoraWeb.ClaimLive do
397397
<.card_title>
398398
Authors
399399
</.card_title>
400-
<!-- TODO: hide if user is not an admin -->
401-
<.button variant="secondary" phx-click="split_bounty">
402-
Split bounty
403-
</.button>
404400
</div>
405401
</.card_header>
406402
<.card_content>

0 commit comments

Comments
 (0)