Skip to content

Commit 637293f

Browse files
authored
feat: custom bot templates (#91)
1 parent ea65f19 commit 637293f

File tree

12 files changed

+414
-48
lines changed

12 files changed

+414
-48
lines changed

lib/algora/accounts/schemas/user.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ defmodule Algora.Accounts.User do
9797
has_many :client_contracts, Contract, foreign_key: :client_id
9898
has_many :activities, {"user_activities", Activity}, foreign_key: :assoc_id
9999

100+
has_one :bot_template, Algora.BotTemplates.BotTemplate, foreign_key: :user_id
100101
has_one :customer, Algora.Payments.Customer, foreign_key: :user_id
101102

102103
timestamps()

lib/algora/admin/migration/migration.ex

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ defmodule Algora.Admin.Migration do
2222
alias Algora.Accounts.Identity
2323
alias Algora.Accounts.User
2424
alias Algora.Admin
25+
alias Algora.BotTemplates.BotTemplate
2526
alias Algora.Bounties.Attempt
2627
alias Algora.Bounties.Bounty
2728
alias Algora.Bounties.Claim
@@ -54,6 +55,7 @@ defmodule Algora.Admin.Migration do
5455
{"BountyCharge", Transaction},
5556
{"BountyTransfer", Tip},
5657
{"BountyTransfer", Transaction},
58+
{"BotMessage", BotTemplate},
5759
{"OrgBalanceTransaction", Transaction},
5860
{"GithubInstallation", Installation},
5961
{"StripeAccount", Account},
@@ -627,6 +629,24 @@ defmodule Algora.Admin.Migration do
627629
end
628630
end
629631

632+
defp transform({"BotMessage", BotTemplate}, row, db) do
633+
user = find_by_index(db, "_MergedUser", "id", row["org_id"])
634+
635+
if !user do
636+
raise "User not found: #{inspect(row)}"
637+
end
638+
639+
%{
640+
"id" => row["id"],
641+
"inserted_at" => row["created_at"],
642+
"updated_at" => row["updated_at"],
643+
"active" => row["active"],
644+
"template" => row["template"],
645+
"type" => row["type"],
646+
"user_id" => user["id"]
647+
}
648+
end
649+
630650
defp transform({"OrgBalanceTransaction", Transaction}, row, db) do
631651
user = find_by_index(db, "_MergedUser", "id", row["org_id"])
632652

lib/algora/admin/migration/v1-progress.yaml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,13 @@
5757
- github_res_comment_id: -1
5858
- type: -1
5959
- "BotMessage":
60-
- id: -1
61-
- created_at: -1
62-
- updated_at: -1
63-
- active: -1
64-
- template: -1
65-
- type: -1
66-
- org_id: -1
60+
- id: 1
61+
- created_at: 1
62+
- updated_at: 1
63+
- active: 1
64+
- template: 1
65+
- type: 1
66+
- org_id: 1
6767
- "Bounty":
6868
- id: 1
6969
- created_at: 1
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
defmodule Algora.BotTemplates do
2+
@moduledoc false
3+
4+
alias Algora.BotTemplates.BotTemplate
5+
alias Algora.Repo
6+
7+
def get_default_template(:bounty_created) do
8+
"""
9+
${PRIZE_POOL}
10+
### Steps to solve:
11+
1. **Start working**: Comment `/attempt #${ISSUE_NUMBER}` with your implementation plan
12+
2. **Submit work**: Create a pull request including `/claim #${ISSUE_NUMBER}` in the PR body to claim the bounty
13+
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)
14+
15+
Thank you for contributing to ${REPO_FULL_NAME}!
16+
${ATTEMPTS}
17+
"""
18+
end
19+
20+
def get_default_template(_type), do: raise("Not implemented")
21+
22+
def placeholders(:bounty_created, user) do
23+
%{
24+
"PRIZE_POOL" => "## 💎 $1,000 bounty [• #{user.name}](#{AlgoraWeb.Endpoint.url()}/@/#{user.handle})",
25+
"ISSUE_NUMBER" => "100",
26+
"REPO_FULL_NAME" => "#{user.provider_login || user.handle}/repo",
27+
"ATTEMPTS" => """
28+
| Attempt | Started (UTC) | Solution | Actions |
29+
| --- | --- | --- | --- |
30+
| 🟢 [@jsmith](https://github.com/jsmith) | #{Calendar.strftime(DateTime.utc_now(), "%b %d, %Y, %I:%M:%S %p")} | [#101](https://github.com/#{user.provider_login || user.handle}/repo/pull/101) | [Reward](#{AlgoraWeb.Endpoint.url()}/claims/:id) |
31+
""",
32+
"FUND_URL" => AlgoraWeb.Endpoint.url(),
33+
"TWEET_URL" =>
34+
"https://twitter.com/intent/tweet?related=algoraio&text=%241%2C000+bounty%21+%F0%9F%92%8E+https%3A%2F%2Fgithub.com%2F#{user.provider_login || user.handle}%2Frepo%2Fissues%2F100",
35+
"ADDITIONAL_OPPORTUNITIES" => ""
36+
}
37+
end
38+
39+
def placeholders(_type, _user), do: raise("Not implemented")
40+
41+
def available_variables(:bounty_created) do
42+
[
43+
"PRIZE_POOL",
44+
"ISSUE_NUMBER",
45+
"REPO_FULL_NAME",
46+
"ATTEMPTS",
47+
"FUND_URL",
48+
"TWEET_URL"
49+
]
50+
end
51+
52+
def get_template(org_id, type) do
53+
Repo.get_by(BotTemplate, user_id: org_id, type: type, active: true)
54+
end
55+
56+
def save_template(org_id, type, template) do
57+
params = %{
58+
user_id: org_id,
59+
type: type,
60+
template: template,
61+
active: true
62+
}
63+
64+
%BotTemplate{}
65+
|> BotTemplate.changeset(params)
66+
|> Repo.insert(
67+
on_conflict: [set: [template: template, active: true]],
68+
conflict_target: [:user_id, :type]
69+
)
70+
end
71+
end
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
defmodule Algora.BotTemplates.BotTemplate do
2+
@moduledoc false
3+
use Algora.Schema
4+
5+
import Ecto.Changeset
6+
7+
@types [
8+
:multiple_attempts_detected,
9+
:attempt_rejected,
10+
:bounty_created,
11+
:claim_submitted,
12+
:bounty_awarded
13+
]
14+
15+
typed_schema "bot_templates" do
16+
field :template, :string, null: false
17+
field :type, Ecto.Enum, values: @types, null: false
18+
field :active, :boolean, null: false, default: true
19+
belongs_to :user, Algora.Accounts.User, null: false
20+
21+
timestamps()
22+
end
23+
24+
def changeset(bot_template, attrs) do
25+
bot_template
26+
|> cast(attrs, [:template, :type, :active, :user_id])
27+
|> validate_required([:template, :type, :user_id])
28+
|> generate_id()
29+
|> foreign_key_constraint(:user_id)
30+
|> unique_constraint([:user_id, :type])
31+
end
32+
end

lib/algora/bounties/bounties.ex

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ defmodule Algora.Bounties do
44
import Ecto.Query
55

66
alias Algora.Accounts.User
7+
alias Algora.BotTemplates
8+
alias Algora.BotTemplates.BotTemplate
79
alias Algora.Bounties.Attempt
810
alias Algora.Bounties.Bounty
911
alias Algora.Bounties.Claim
@@ -168,11 +170,62 @@ defmodule Algora.Bounties do
168170
claims :: list(Claim.t())
169171
) :: String.t()
170172
def get_response_body(bounties, ticket_ref, attempts, claims) do
171-
header =
172-
Enum.map_join(bounties, "\n", fn bounty ->
173-
"## 💎 #{bounty.amount} bounty [• #{bounty.owner.name}](#{User.url(bounty.owner)})"
174-
end)
173+
custom_template =
174+
Repo.one(
175+
from bt in BotTemplate,
176+
where: bt.type == :bounty_created,
177+
where: bt.active == true,
178+
join: u in assoc(bt, :user),
179+
join: r in assoc(u, :repositories),
180+
join: t in assoc(r, :tickets),
181+
where: t.id == ^List.first(bounties).ticket_id
182+
)
183+
184+
prize_pool = format_prize_pool(bounties)
185+
attempts_table = format_attempts_table(attempts, claims)
186+
187+
template =
188+
if custom_template do
189+
custom_template.template
190+
else
191+
BotTemplates.get_default_template(:bounty_created)
192+
end
175193

194+
template
195+
|> String.replace("${PRIZE_POOL}", prize_pool)
196+
|> String.replace("${ISSUE_NUMBER}", to_string(ticket_ref[:number]))
197+
|> String.replace("${REPO_FULL_NAME}", "#{ticket_ref[:owner]}/#{ticket_ref[:repo]}")
198+
|> String.replace("${ATTEMPTS}", attempts_table)
199+
|> String.replace("${FUND_URL}", AlgoraWeb.Endpoint.url())
200+
|> String.replace("${TWEET_URL}", generate_tweet_url(bounties, ticket_ref))
201+
|> String.replace("${ADDITIONAL_OPPORTUNITIES}", "")
202+
|> String.trim()
203+
end
204+
205+
defp generate_tweet_url(bounties, ticket_ref) do
206+
total_amount = Enum.reduce(bounties, Money.new(0, :USD), &Money.add!(&2, &1.amount))
207+
208+
text =
209+
"#{Money.to_string!(total_amount, no_fraction_if_integer: true)} bounty! 💎 https://github.com/#{ticket_ref[:owner]}/#{ticket_ref[:repo]}/issues/#{ticket_ref[:number]}"
210+
211+
uri = URI.parse("https://twitter.com/intent/tweet")
212+
213+
query =
214+
URI.encode_query(%{
215+
"text" => text,
216+
"related" => "algoraio"
217+
})
218+
219+
URI.to_string(%{uri | query: query})
220+
end
221+
222+
defp format_prize_pool(bounties) do
223+
Enum.map_join(bounties, "\n", fn bounty ->
224+
"## 💎 #{Money.to_string!(bounty.amount, no_fraction_if_integer: true)} bounty [• #{bounty.owner.name}](#{User.url(bounty.owner)})"
225+
end)
226+
end
227+
228+
defp format_attempts_table(attempts, claims) do
176229
solutions =
177230
[]
178231
|> Enum.concat(Enum.map(claims, &claim_to_solution/1))
@@ -205,28 +258,16 @@ defmodule Algora.Bounties do
205258
"| #{primary_solution.indicator} #{users} | #{timestamp} | #{primary_solution.solution} | #{actions} |"
206259
end)
207260

208-
solutions_table =
209-
if solutions == [] do
210-
""
211-
else
212-
"""
213-
214-
| Attempt | Started (UTC) | Solution | Actions |
215-
| --- | --- | --- | --- |
216-
#{Enum.join(solutions, "\n")}
217-
"""
218-
end
261+
if solutions == [] do
262+
""
263+
else
264+
"""
219265
220-
String.trim("""
221-
#{header}
222-
### Steps to solve:
223-
1. **Start working**: Comment `/attempt ##{ticket_ref[:number]}` with your implementation plan
224-
2. **Submit work**: Create a pull request including `/claim ##{ticket_ref[:number]}` in the PR body to claim the bounty
225-
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)
226-
227-
Thank you for contributing to #{ticket_ref[:owner]}/#{ticket_ref[:repo]}!
228-
#{solutions_table}
229-
""")
266+
| Attempt | Started (UTC) | Solution | Actions |
267+
| --- | --- | --- | --- |
268+
#{Enum.join(solutions, "\n")}
269+
"""
270+
end
230271
end
231272

232273
def refresh_bounty_response(token, ticket_ref, ticket) do
@@ -1009,6 +1050,7 @@ defmodule Algora.Bounties do
10091050
avatar_url: o.avatar_url,
10101051
tech_stack: o.tech_stack
10111052
},
1053+
ticket_id: t.id,
10121054
ticket: %{
10131055
id: t.id,
10141056
title: t.title,

lib/algora_web/components/core_components.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -756,7 +756,7 @@ defmodule AlgoraWeb.CoreComponents do
756756
id={@id || @name}
757757
name={@name}
758758
class={[
759-
"min-h-[6rem] py-[7px] px-[11px] mt-2 block w-full rounded-lg border-input bg-background",
759+
"min-h-[6rem] py-[7px] px-[11px] block w-full rounded-lg border-input bg-background",
760760
"text-foreground focus:border-ring focus:outline-none focus:ring-4 focus:ring-ring/5 sm:text-sm sm:leading-6",
761761
"border-input focus:border-ring focus:ring-ring/5",
762762
@errors != [] && "border-destructive focus:border-destructive focus:ring-destructive/10",

0 commit comments

Comments
 (0)