11defmodule AlgoraWeb.Webhooks.GithubController do
22 use AlgoraWeb , :controller
33
4+ import Ecto.Query
5+
46 alias Algora.Accounts
57 alias Algora.Bounties
8+ alias Algora.Bounties.Bounty
9+ alias Algora.Bounties.Claim
610 alias Algora.Github
711 alias Algora.Github.Webhook
812 alias Algora.Repo
913 alias Algora.Workspace
1014 alias Algora.Workspace.CommandResponse
15+ alias Algora.Workspace.Installation
1116
1217 require Logger
1318
1419 # TODO: persist & alert about failed deliveries
1520 # TODO: auto-retry failed deliveries with exponential backoff
1621
1722 def new ( conn , params ) do
18- with { :ok , webhook } <- Webhook . new ( conn ) ,
23+ with { :ok , % { event: event } = webhook } <- Webhook . new ( conn ) ,
1924 :ok <- ensure_human_author ( webhook , params ) ,
20- { :ok , _ } <- process_commands ( webhook , params ) do
25+ author = get_author ( event , params ) ,
26+ body = get_body ( event , params ) ,
27+ event_action = join_event_action ( event , params ) ,
28+ { :ok , _ } <- process_commands ( webhook , event_action , author , body , params ) ,
29+ :ok <- process_event ( event_action , params ) do
2130 conn |> put_status ( :accepted ) |> json ( % { status: "ok" } )
2231 else
2332 { :error , :bot_event } ->
@@ -32,10 +41,14 @@ defmodule AlgoraWeb.Webhooks.GithubController do
3241 { :error , reason } ->
3342 Logger . error ( "Error processing webhook: #{ inspect ( reason ) } " )
3443 conn |> put_status ( :internal_server_error ) |> json ( % { error: "Internal server error" } )
44+
45+ error ->
46+ Logger . error ( "Error processing webhook: #{ inspect ( error ) } " )
47+ conn |> put_status ( :internal_server_error ) |> json ( % { error: "Internal server error" } )
3548 end
3649 rescue
3750 e ->
38- Logger . error ( "Unexpected error: #{ inspect ( e ) } " )
51+ Logger . error ( Exception . format ( :error , e , __STACKTRACE__ ) )
3952 conn |> put_status ( :internal_server_error ) |> json ( % { error: "Internal server error" } )
4053 end
4154
@@ -63,6 +76,161 @@ defmodule AlgoraWeb.Webhooks.GithubController do
6376
6477 defp get_permissions ( _author , _params ) , do: { :error , :invalid_params }
6578
79+ def process_event ( event_action , % { "pull_request" => % { "merged_at" => nil } } )
80+ when event_action in [ "pull_request.closed" ] do
81+ :ok
82+ end
83+
84+ def process_event ( event_action , % {
85+ "repository" => repository ,
86+ "pull_request" => pull_request ,
87+ "installation" => installation
88+ } )
89+ when event_action in [ "pull_request.closed" ] do
90+ with { :ok , token } <- Github . get_installation_token ( installation [ "id" ] ) ,
91+ { :ok , source } <-
92+ Workspace . ensure_ticket ( token , repository [ "owner" ] [ "login" ] , repository [ "name" ] , pull_request [ "number" ] ) do
93+ claims =
94+ case Repo . one (
95+ from c in Claim ,
96+ join: s in assoc ( c , :source ) ,
97+ join: u in assoc ( c , :user ) ,
98+ where: s . id == ^ source . id ,
99+ where: u . provider == "github" ,
100+ where: u . provider_id == ^ to_string ( pull_request [ "user" ] [ "id" ] ) ,
101+ order_by: [ asc: c . inserted_at ] ,
102+ limit: 1
103+ ) do
104+ nil ->
105+ [ ]
106+
107+ % Claim { group_id: group_id } ->
108+ Repo . update_all (
109+ from ( c in Claim , where: c . group_id == ^ group_id ) ,
110+ set: [ status: :approved ]
111+ )
112+
113+ Repo . all (
114+ from c in Claim ,
115+ join: t in assoc ( c , :target ) ,
116+ join: tr in assoc ( t , :repository ) ,
117+ join: tru in assoc ( tr , :user ) ,
118+ join: u in assoc ( c , :user ) ,
119+ where: c . group_id == ^ group_id ,
120+ order_by: [ desc: c . group_share , asc: c . inserted_at ] ,
121+ select_merge: % {
122+ target: % { t | repository: % { tr | user: tru } } ,
123+ user: u
124+ }
125+ )
126+ end
127+
128+ if claims == [ ] do
129+ :ok
130+ else
131+ primary_claim = List . first ( claims )
132+
133+ installation =
134+ Repo . one (
135+ from i in Installation ,
136+ where: i . provider == "github" ,
137+ where: i . provider_id == ^ to_string ( installation [ "id" ] )
138+ )
139+
140+ bounties =
141+ Repo . all (
142+ from ( b in Bounty ,
143+ join: t in assoc ( b , :ticket ) ,
144+ join: o in assoc ( b , :owner ) ,
145+ left_join: u in assoc ( b , :creator ) ,
146+ left_join: c in assoc ( o , :customer ) ,
147+ left_join: p in assoc ( c , :default_payment_method ) ,
148+ where: t . id == ^ primary_claim . target_id ,
149+ select_merge: % { owner: % { o | customer: % { default_payment_method: p } } , creator: u }
150+ )
151+ )
152+
153+ autopayable_bounty =
154+ Enum . find (
155+ bounties ,
156+ & ( not is_nil ( installation ) and
157+ & 1 . owner . id == installation . connected_user_id and
158+ not is_nil ( & 1 . owner . customer ) and
159+ not is_nil ( & 1 . owner . customer . default_payment_method ) )
160+ )
161+
162+ autopay_result =
163+ if autopayable_bounty do
164+ with { :ok , invoice } <-
165+ Bounties . create_invoice (
166+ % {
167+ owner: autopayable_bounty . owner ,
168+ amount: autopayable_bounty . amount
169+ } ,
170+ ticket_ref: % {
171+ owner: repository [ "owner" ] [ "login" ] ,
172+ repo: repository [ "name" ] ,
173+ number: pull_request [ "number" ]
174+ } ,
175+ bounty_id: autopayable_bounty . id ,
176+ claims: claims
177+ ) ,
178+ { :ok , _invoice } <-
179+ Algora.Stripe . pay_invoice ( invoice , % {
180+ payment_method: autopayable_bounty . owner . customer . default_payment_method . provider_id ,
181+ off_session: true
182+ } ) do
183+ Logger . info ( "Autopay successful (#{ autopayable_bounty . owner . name } - #{ autopayable_bounty . amount } )." )
184+ :ok
185+ else
186+ { :error , reason } ->
187+ Logger . error (
188+ "Autopay failed (#{ autopayable_bounty . owner . name } - #{ autopayable_bounty . amount } ): #{ inspect ( reason ) } "
189+ )
190+
191+ :error
192+ end
193+ end
194+
195+ unpaid_bounties =
196+ Enum . filter (
197+ bounties ,
198+ & case autopay_result do
199+ :ok -> & 1 . id != autopayable_bounty . id
200+ _ -> true
201+ end
202+ )
203+
204+ sponsors_to_notify =
205+ unpaid_bounties
206+ |> Enum . map ( & ( & 1 . creator || & 1 . owner ) )
207+ |> Enum . map_join ( ", " , & "@#{ & 1 . provider_login } " )
208+
209+ if unpaid_bounties != [ ] do
210+ names =
211+ claims
212+ |> Enum . map ( fn c -> "@#{ c . user . provider_login } " end )
213+ |> Algora.Util . format_name_list ( )
214+
215+ Github . create_issue_comment (
216+ token ,
217+ primary_claim . target . repository . user . provider_login ,
218+ primary_claim . target . repository . name ,
219+ primary_claim . target . number ,
220+ "🎉 The pull request of #{ names } has been merged. You can visit [Algora](#{ Claim . reward_url ( primary_claim ) } ) to award the bounty." <>
221+ if ( sponsors_to_notify == "" , do: "" , else: "\n \n cc #{ sponsors_to_notify } " )
222+ )
223+ end
224+
225+ :ok
226+ end
227+ end
228+ end
229+
230+ def process_event ( _event_action , _params ) do
231+ :ok
232+ end
233+
66234 defp execute_command ( event_action , { :bounty , args } , author , params )
67235 when event_action in [ "issues.opened" , "issues.edited" , "issue_comment.created" , "issue_comment.edited" ] do
68236 [ event , _action ] = String . split ( event_action , "." )
@@ -242,12 +410,7 @@ defmodule AlgoraWeb.Webhooks.GithubController do
242410 end
243411 end
244412
245- def process_commands ( % Webhook { event: event , hook_id: hook_id } , params ) do
246- author = get_author ( event , params )
247- body = get_body ( event , params )
248-
249- event_action = event <> "." <> params [ "action" ]
250-
413+ def process_commands ( % Webhook { hook_id: hook_id } , event_action , author , body , params ) do
251414 case build_commands ( body ) do
252415 { :ok , commands } ->
253416 Enum . reduce_while ( commands , { :ok , [ ] } , fn command , { :ok , results } ->
@@ -270,17 +433,20 @@ defmodule AlgoraWeb.Webhooks.GithubController do
270433 end
271434 end
272435
273- defp get_author ( "issues" , params ) , do: params [ "issue" ] [ "user" ]
274- defp get_author ( "issue_comment" , params ) , do: params [ "comment" ] [ "user" ]
275- defp get_author ( "pull_request" , params ) , do: params [ "pull_request" ] [ "user" ]
276- defp get_author ( "pull_request_review" , params ) , do: params [ "review" ] [ "user" ]
277- defp get_author ( "pull_request_review_comment" , params ) , do: params [ "comment" ] [ "user" ]
278- defp get_author ( _event , _params ) , do: nil
279-
280- defp get_body ( "issues" , params ) , do: params [ "issue" ] [ "body" ]
281- defp get_body ( "issue_comment" , params ) , do: params [ "comment" ] [ "body" ]
282- defp get_body ( "pull_request" , params ) , do: params [ "pull_request" ] [ "body" ]
283- defp get_body ( "pull_request_review" , params ) , do: params [ "review" ] [ "body" ]
284- defp get_body ( "pull_request_review_comment" , params ) , do: params [ "comment" ] [ "body" ]
285- defp get_body ( _event , _params ) , do: nil
436+ def join_event_action ( event , params ) , do: event <> "." <> params [ "action" ]
437+
438+ def split_event_action ( event_action ) do
439+ [ event , action ] = String . split ( event_action , "." )
440+ { event , action }
441+ end
442+
443+ def get_entity_key ( "issues" ) , do: "issue"
444+ def get_entity_key ( "issue_comment" ) , do: "comment"
445+ def get_entity_key ( "pull_request" ) , do: "pull_request"
446+ def get_entity_key ( "pull_request_review" ) , do: "review"
447+ def get_entity_key ( "pull_request_review_comment" ) , do: "comment"
448+ def get_entity_key ( _event ) , do: nil
449+
450+ def get_author ( event , params ) , do: get_in ( params , [ "#{ get_entity_key ( event ) } " , "user" ] )
451+ def get_body ( event , params ) , do: get_in ( params , [ "#{ get_entity_key ( event ) } " , "body" ] )
286452end
0 commit comments