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
59 changes: 42 additions & 17 deletions lib/algora/bounties/bounties.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ defmodule Algora.Bounties do
alias Algora.Bounties.Jobs
alias Algora.Bounties.Tip
alias Algora.FeeTier
alias Algora.Github
alias Algora.MoneyUtils
alias Algora.Organizations.Member
alias Algora.Payments
Expand Down Expand Up @@ -50,7 +51,6 @@ defmodule Algora.Bounties do

case Repo.insert(changeset) do
{:ok, bounty} ->
broadcast()
{:ok, bounty}

{:error, %{errors: [ticket_id: {_, [constraint: :unique, constraint_name: _]}]}} ->
Expand All @@ -61,36 +61,61 @@ defmodule Algora.Bounties do
end
end

@spec create_bounty(%{
creator: User.t(),
owner: User.t(),
amount: Money.t(),
ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()}
}) ::
@spec create_bounty(
%{
creator: User.t(),
owner: User.t(),
amount: Money.t(),
ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()}
},
opts :: [installation_id: integer()]
) ::
{:ok, Bounty.t()} | {:error, atom()}
def create_bounty(%{
creator: creator,
owner: owner,
amount: amount,
ticket_ref: %{owner: repo_owner, repo: repo_name, number: number} = ticket_ref
}) do
def create_bounty(
%{
creator: creator,
owner: owner,
amount: amount,
ticket_ref: %{owner: repo_owner, repo: repo_name, number: number} = ticket_ref
},
opts \\ []
) do
installation_id = opts[:installation_id]

token_res =
if installation_id,
do: Github.get_installation_token(installation_id),
else: Accounts.get_access_token(creator)

Repo.transact(fn ->
with {:ok, token} <- Accounts.get_access_token(creator),
with {:ok, token} <- token_res,
{:ok, ticket} <- Workspace.ensure_ticket(token, repo_owner, repo_name, number),
{:ok, bounty} <- do_create_bounty(%{creator: creator, owner: owner, amount: amount, ticket: ticket}),
{:ok, _job} <- notify_bounty(%{owner: owner, bounty: bounty, ticket_ref: ticket_ref}) do
{:ok, _job} <-
notify_bounty(%{owner: owner, bounty: bounty, ticket_ref: ticket_ref}, installation_id: installation_id) do
broadcast()
{:ok, bounty}
else
{:error, _reason} = error -> error
end
end)
end

def notify_bounty(%{owner: owner, bounty: bounty, ticket_ref: ticket_ref}) do
@spec notify_bounty(
%{
owner: User.t(),
bounty: Bounty.t(),
ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()}
},
opts :: [installation_id: integer()]
) ::
{:ok, Oban.Job.t()} | {:error, atom()}
def notify_bounty(%{owner: owner, bounty: bounty, ticket_ref: ticket_ref}, opts \\ []) do
%{
owner_login: owner.provider_login,
amount: Money.to_string!(bounty.amount, no_fraction_if_integer: true),
ticket_ref: %{owner: ticket_ref.owner, repo: ticket_ref.repo, number: ticket_ref.number}
ticket_ref: %{owner: ticket_ref.owner, repo: ticket_ref.repo, number: ticket_ref.number},
installation_id: opts[:installation_id]
}
|> Jobs.NotifyBounty.new()
|> Oban.insert()
Expand Down
33 changes: 32 additions & 1 deletion lib/algora/bounties/jobs/notify_bounty.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ defmodule Algora.Bounties.Jobs.NotifyBounty do
@moduledoc false
use Oban.Worker, queue: :notify_bounty

alias Algora.Accounts
alias Algora.Accounts.User
alias Algora.Github
alias Algora.Workspace

require Logger

@impl Oban.Worker
def perform(%Oban.Job{args: %{"owner_login" => owner_login, "amount" => amount, "ticket_ref" => ticket_ref}}) do
def perform(%Oban.Job{
args: %{"owner_login" => owner_login, "amount" => amount, "ticket_ref" => ticket_ref, "installation_id" => nil}
}) do
body = """
💎 **#{owner_login}** is offering a **#{amount}** bounty for this issue

Expand All @@ -31,4 +36,30 @@ defmodule Algora.Bounties.Jobs.NotifyBounty do
""")
end
end

@impl Oban.Worker
def perform(%Oban.Job{args: %{"amount" => amount, "ticket_ref" => ticket_ref, "installation_id" => installation_id}}) do
with {:ok, token} <- Github.get_installation_token(installation_id),
{:ok, installation} <-
Workspace.fetch_installation_by(provider: "github", provider_id: to_string(installation_id)),
{:ok, owner} <- Accounts.fetch_user_by(id: installation.connected_user_id) do
body = """
## 💎 #{amount} bounty [• #{owner.name}](#{User.url(owner)})
### 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"]}!
"""

Github.create_issue_comment(
token,
ticket_ref["owner"],
ticket_ref["repo"],
ticket_ref["number"],
body
)
end
end
end
6 changes: 3 additions & 3 deletions lib/algora/integrations/github/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,9 @@ defmodule Algora.Github.Client do
def get_installation_token(installation_id) do
path = "/app/installations/#{installation_id}/access_tokens"

case Crypto.generate_jwt() do
{:ok, jwt, _claims} -> fetch(jwt, path, "POST")
error -> error
with {:ok, jwt, _claims} <- Crypto.generate_jwt(),
{:ok, %{"token" => token}} <- fetch(jwt, path, "POST") do
{:ok, token}
end
end

Expand Down
6 changes: 6 additions & 0 deletions lib/algora/workspace/workspace.ex
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ defmodule Algora.Workspace do
end
end

@spec fetch_installation_by(clauses :: Keyword.t() | map()) ::
{:ok, Installation.t()} | {:error, :not_found}
def fetch_installation_by(clauses) do
Repo.fetch_by(Installation, clauses)
end

def get_installation_by(fields), do: Repo.get_by(Installation, fields)
def get_installation_by!(fields), do: Repo.get_by!(Installation, fields)

Expand Down
71 changes: 29 additions & 42 deletions lib/algora_web/controllers/webhooks/github_controller.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
defmodule AlgoraWeb.Webhooks.GithubController do
use AlgoraWeb, :controller

alias Algora.Accounts
alias Algora.Bounties
alias Algora.Github
alias Algora.Github.Webhook
alias Algora.Workspace

require Logger

Expand Down Expand Up @@ -32,8 +35,9 @@ defmodule AlgoraWeb.Webhooks.GithubController do
end

# TODO: cache installation tokens
# TODO: check org permissions on algora
defp get_permissions(author, %{"repository" => repository, "installation" => installation}) do
with {:ok, %{"token" => access_token}} <- Github.get_installation_token(installation["id"]),
with {:ok, access_token} <- Github.get_installation_token(installation["id"]),
{:ok, %{"permission" => permission}} <-
Github.get_repository_permissions(
access_token,
Expand All @@ -48,49 +52,32 @@ defmodule AlgoraWeb.Webhooks.GithubController do
defp get_permissions(_author, _params), do: {:error, :invalid_params}

defp execute_command({:bounty, args}, author, params) do
amount = Keyword.fetch!(args, :amount)

case get_permissions(author, params) do
{:ok, "admin"} ->
# Get repository and issue details from params
repo = params["repository"]
issue = params["issue"]

# Construct the bounty message
message = """
## 💎 $#{amount} bounty [• #{repo["owner"]["login"]}](https://console.algora.io/org/#{repo["owner"]["login"]})
### Steps to solve:
1. **Start working**: Comment `/attempt ##{issue["number"]}` with your implementation plan
2. **Submit work**: Create a pull request including `/claim ##{issue["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 #{repo["full_name"]}!

**[Add a bounty](https://console.algora.io/org/#{repo["owner"]["login"]}/bounties/community?fund=#{repo["full_name"]}%23#{issue["number"]})** • **[Share on socials](https://twitter.com/intent/tweet?text=%24#{amount}+bounty%21+%F0%9F%92%8E+#{issue["html_url"]}&related=algoraio)**

Attempt | Started (GMT+0) | Solution
--------|----------------|----------
"""

# Post comment to the issue
with {:ok, %{"token" => token}} <-
Github.get_installation_token(params["installation"]["id"]) do
Github.create_issue_comment(
token,
repo["owner"]["login"],
repo["name"],
issue["number"],
message
)
end

{:ok, amount}

amount = args[:amount]
repo = params["repository"]
issue = params["issue"]
installation_id = params["installation"]["id"]

with {:ok, "admin"} <- get_permissions(author, params),
{:ok, token} <- Github.get_installation_token(installation_id),
{:ok, installation} <-
Workspace.fetch_installation_by(provider: "github", provider_id: to_string(installation_id)),
{:ok, owner} <- Accounts.fetch_user_by(id: installation.connected_user_id),
{:ok, creator} <- Workspace.ensure_user(token, repo["owner"]["login"]) do
Bounties.create_bounty(
%{
creator: creator,
owner: owner,
amount: amount,
ticket_ref: %{owner: repo["owner"]["login"], repo: repo["name"], number: issue["number"]}
},
installation_id: installation_id
)
else
{:ok, _permission} ->
{:error, :unauthorized}

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

Expand All @@ -113,7 +100,7 @@ defmodule AlgoraWeb.Webhooks.GithubController do
do: Logger.info("Unhandled command: #{command} #{inspect(args)}")

def process_commands(body, author, params) when is_binary(body) do
case Algora.Github.Command.parse(body) do
case Github.Command.parse(body) do
{:ok, commands} -> Enum.map(commands, &execute_command(&1, author, params))
# TODO: handle errors
{:error, error} -> Logger.error("Error parsing commands: #{inspect(error)}")
Expand Down
Loading
Loading