Skip to content

Commit 7692668

Browse files
committed
add support for slack mentions from github handles
1 parent 892458d commit 7692668

File tree

5 files changed

+113
-39
lines changed

5 files changed

+113
-39
lines changed

lib/action.ml

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,46 @@ let action_error msg = raise (Action_error msg)
1010
let log = Log.from "action"
1111

1212
module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
13+
let canonical_regex = Re2.create_exn {|\.|\-|\+.*|@.*|}
14+
(* Match email domain, everything after '+', as well as dots and hyphens *)
15+
16+
let username_to_slack_id_tbl = Stringtbl.empty ()
17+
18+
let canonicalize_email_username email =
19+
email |> Re2.rewrite_exn ~template:"" canonical_regex |> String.lowercase_ascii
20+
21+
let refresh_username_to_slack_id_tbl ~ctx =
22+
log#info "updating github to slack username mapping";
23+
match%lwt Slack_api.list_users ~ctx () with
24+
| Error e ->
25+
log#warn "couldn't fetch list of Slack users: %s" e;
26+
Lwt.return_unit
27+
| Ok res ->
28+
List.iter
29+
(fun (user : Slack_t.user) ->
30+
match user.profile.email with
31+
| None -> ()
32+
| Some email ->
33+
let username = canonicalize_email_username email in
34+
Stringtbl.replace username_to_slack_id_tbl username user.id
35+
)
36+
res.members;
37+
Lwt.return_unit
38+
39+
let rec refresh_username_to_slack_id_tbl_background_lwt ~ctx : unit Lwt.t =
40+
let%lwt () = refresh_username_to_slack_id_tbl ~ctx in
41+
let%lwt () = Lwt_unix.sleep (Time.days 1) in
42+
(* Updates mapping every 24 hours *)
43+
refresh_username_to_slack_id_tbl_background_lwt ~ctx
44+
45+
let match_github_login_to_slack_id cfg_opt login =
46+
let login =
47+
match cfg_opt with
48+
| None -> login
49+
| Some cfg -> List.assoc_opt login cfg.user_mappings |> Option.default login
50+
in
51+
login |> canonicalize_email_username |> Stringtbl.find_opt username_to_slack_id_tbl
52+
1353
let partition_push (cfg : Config_t.config) n =
1454
let default = Stdlib.Option.to_list cfg.prefix_rules.default_channel in
1555
let rules = cfg.prefix_rules.rules in
@@ -191,21 +231,27 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
191231
let generate_notifications (ctx : Context.t) (req : Github.t) =
192232
let repo = Github.repo_of_notification req in
193233
let cfg = Context.find_repo_config_exn ctx repo.url in
234+
let slack_match_func = match_github_login_to_slack_id (Some cfg) in
194235
match ignore_notifications_from_user cfg req with
195236
| true -> Lwt.return []
196237
| false ->
197238
match req with
198239
| Github.Push n ->
199240
partition_push cfg n |> List.map (fun (channel, n) -> generate_push_notification n channel) |> Lwt.return
200-
| Pull_request n -> partition_pr cfg n |> List.map (generate_pull_request_notification n) |> Lwt.return
201-
| PR_review n -> partition_pr_review cfg n |> List.map (generate_pr_review_notification n) |> Lwt.return
241+
| Pull_request n ->
242+
partition_pr cfg n |> List.map (generate_pull_request_notification ~slack_match_func n) |> Lwt.return
243+
| PR_review n ->
244+
partition_pr_review cfg n |> List.map (generate_pr_review_notification ~slack_match_func n) |> Lwt.return
202245
| PR_review_comment n ->
203-
partition_pr_review_comment cfg n |> List.map (generate_pr_review_comment_notification n) |> Lwt.return
204-
| Issue n -> partition_issue cfg n |> List.map (generate_issue_notification n) |> Lwt.return
205-
| Issue_comment n -> partition_issue_comment cfg n |> List.map (generate_issue_comment_notification n) |> Lwt.return
246+
partition_pr_review_comment cfg n
247+
|> List.map (generate_pr_review_comment_notification ~slack_match_func n)
248+
|> Lwt.return
249+
| Issue n -> partition_issue cfg n |> List.map (generate_issue_notification ~slack_match_func n) |> Lwt.return
250+
| Issue_comment n ->
251+
partition_issue_comment cfg n |> List.map (generate_issue_comment_notification ~slack_match_func n) |> Lwt.return
206252
| Commit_comment n ->
207253
let%lwt channels, api_commit = partition_commit_comment ctx n in
208-
let notifs = List.map (generate_commit_comment_notification api_commit n) channels in
254+
let notifs = List.map (generate_commit_comment_notification ~slack_match_func api_commit n) channels in
209255
Lwt.return notifs
210256
| Status n ->
211257
let%lwt channels = partition_status ctx n in
@@ -220,28 +266,28 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
220266
in
221267
Lwt_list.iter_s notify notifications
222268

269+
let fetch_config ~ctx ~repo =
270+
match%lwt Github_api.get_config ~ctx ~repo with
271+
| Ok config ->
272+
Context.set_repo_config ctx repo.url config;
273+
Context.print_config ctx repo.url;
274+
Lwt.return @@ Ok ()
275+
| Error e -> action_error e
276+
223277
(** [refresh_repo_config ctx n] fetches the latest repo config if it's
224278
uninitialized, or if the incoming request [n] is a push
225279
notification containing commits that touched the config file. *)
226280
let refresh_repo_config (ctx : Context.t) notification =
227281
let repo = Github.repo_of_notification notification in
228-
let fetch_config () =
229-
match%lwt Github_api.get_config ~ctx ~repo with
230-
| Ok config ->
231-
Context.set_repo_config ctx repo.url config;
232-
Context.print_config ctx repo.url;
233-
Lwt.return @@ Ok ()
234-
| Error e -> action_error e
235-
in
236282
match Context.find_repo_config ctx repo.url with
237-
| None -> fetch_config ()
283+
| None -> fetch_config ~ctx ~repo
238284
| Some _ ->
239285
match notification with
240286
| Github.Push commit_pushed_notification ->
241287
let commits = commit_pushed_notification.commits in
242288
let modified_files = List.concat_map Github.modified_files_of_commit commits in
243289
let config_was_modified = List.exists (String.equal ctx.config_filename) modified_files in
244-
if config_was_modified then fetch_config () else Lwt.return @@ Ok ()
290+
if config_was_modified then fetch_config ~ctx ~repo else Lwt.return @@ Ok ()
245291
| _ -> Lwt.return @@ Ok ()
246292

247293
let do_github_tasks ctx (repo : repository) (req : Github.t) =
@@ -265,15 +311,15 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
265311
end
266312
| _ -> Lwt.return_unit
267313

314+
let repo_is_supported secrets (repo : Github_t.repository) =
315+
List.exists (fun (r : repo_config) -> String.equal r.url repo.url) secrets.repos
316+
268317
let process_github_notification (ctx : Context.t) headers body =
269318
let validate_signature secrets payload =
270319
let repo = Github.repo_of_notification payload in
271320
let signing_key = Context.gh_hook_secret_token_of_secrets secrets repo.url in
272321
Github.validate_signature ?signing_key ~headers body
273322
in
274-
let repo_is_supported secrets (repo : Github_t.repository) =
275-
List.exists (fun (r : repo_config) -> String.equal r.url repo.url) secrets.repos
276-
in
277323
try%lwt
278324
let secrets = Context.get_secrets_exn ctx in
279325
match Github.parse_exn headers body with
@@ -337,9 +383,7 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
337383
Lwt.return_none
338384
in
339385
let process link =
340-
let with_gh_result_populate_slack (type a) ~(api_result : (a, string) Result.t)
341-
~(populate : repository -> a -> Slack_t.message_attachment) ~repo
342-
=
386+
let with_gh_result_populate_slack (type a) ~(api_result : (a, string) Result.t) ~populate ~repo =
343387
match api_result with
344388
| Error _ -> Lwt.return_none
345389
| Ok item -> Lwt.return_some @@ (link, populate repo item)

lib/slack.ml

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,24 @@ let markdown_text_attachment ~footer markdown_body =
4646
let make_message ?username ?text ?attachments ?blocks ~channel () =
4747
{ channel; text; attachments; blocks; username; unfurl_links = Some false; unfurl_media = None }
4848

49-
let generate_pull_request_notification notification channel =
49+
let format_slack_mention = Option.map_default (sprintf " (<@%s>)") ""
50+
let github_handle_regex = Re2.create_exn {|(?:^|\s)@([\w][\w-]{1,})|} (* Match GH handles in messages *)
51+
52+
let add_slack_mentions_to_body slack_match_func body =
53+
let replace_match m =
54+
let gh_handle = Re2.Match.get_exn ~sub:(`Index 0) m in
55+
let gh_handle_without_at = Re2.Match.get_exn ~sub:(`Index 1) m in
56+
sprintf "%s%s" gh_handle (format_slack_mention (slack_match_func gh_handle_without_at))
57+
in
58+
Re2.replace_exn github_handle_regex body ~f:replace_match
59+
60+
let format_attachments ~slack_match_func ~footer ~body =
61+
let format_mention_in_markdown (md : unfurl) =
62+
{ md with text = Option.map (add_slack_mentions_to_body slack_match_func) md.text }
63+
in
64+
Option.map (fun t -> markdown_text_attachment ~footer t |> List.map format_mention_in_markdown) body
65+
66+
let generate_pull_request_notification ~slack_match_func notification channel =
5067
let { action; number; sender; pull_request; repository } = notification in
5168
let ({ body; title; html_url; labels; merged; _ } : pull_request) = pull_request in
5269
let action, body =
@@ -65,9 +82,9 @@ let generate_pull_request_notification notification channel =
6582
sprintf "<%s|[%s]> Pull request #%d %s %s by *%s*" repository.url repository.full_name number
6683
(pp_link ~url:html_url title) action sender.login
6784
in
68-
make_message ~text:summary ?attachments:(Option.map (markdown_text_attachment ~footer:None) body) ~channel ()
85+
make_message ~text:summary ?attachments:(format_attachments ~slack_match_func ~footer:None ~body) ~channel ()
6986

70-
let generate_pr_review_notification notification channel =
87+
let generate_pr_review_notification ~slack_match_func notification channel =
7188
let { action; sender; pull_request; review; repository } = notification in
7289
let ({ number; title; html_url; _ } : pull_request) = pull_request in
7390
let action_str =
@@ -89,9 +106,11 @@ let generate_pr_review_notification notification channel =
89106
sprintf "<%s|[%s]> *%s* <%s|%s> #%d %s" repository.url repository.full_name sender.login review.html_url action_str
90107
number (pp_link ~url:html_url title)
91108
in
92-
make_message ~text:summary ?attachments:(Option.map (markdown_text_attachment ~footer:None) review.body) ~channel ()
109+
make_message ~text:summary
110+
?attachments:(format_attachments ~slack_match_func ~footer:None ~body:review.body)
111+
~channel ()
93112

94-
let generate_pr_review_comment_notification notification channel =
113+
let generate_pr_review_comment_notification ~slack_match_func notification channel =
95114
let { action; pull_request; sender; comment; repository } = notification in
96115
let ({ number; title; html_url; _ } : pull_request) = pull_request in
97116
let action_str =
@@ -112,9 +131,11 @@ let generate_pr_review_comment_notification notification channel =
112131
| None -> None
113132
| Some a -> Some (sprintf "New comment by %s in <%s|%s>" sender.login comment.html_url a)
114133
in
115-
make_message ~text:summary ~attachments:(markdown_text_attachment ~footer:file comment.body) ~channel ()
134+
make_message ~text:summary
135+
?attachments:(format_attachments ~slack_match_func ~footer:file ~body:(Some comment.body))
136+
~channel ()
116137

117-
let generate_issue_notification notification channel =
138+
let generate_issue_notification ~slack_match_func notification channel =
118139
let ({ action; sender; issue; repository } : issue_notification) = notification in
119140
let { number; body; title; html_url; labels; _ } = issue in
120141
let action, body =
@@ -133,9 +154,10 @@ let generate_issue_notification notification channel =
133154
sprintf "<%s|[%s]> Issue #%d %s %s by *%s*" repository.url repository.full_name number (pp_link ~url:html_url title)
134155
action sender.login
135156
in
136-
make_message ~text:summary ?attachments:(Option.map (markdown_text_attachment ~footer:None) body) ~channel ()
137157

138-
let generate_issue_comment_notification notification channel =
158+
make_message ~text:summary ?attachments:(format_attachments ~slack_match_func ~footer:None ~body) ~channel ()
159+
160+
let generate_issue_comment_notification ~slack_match_func notification channel =
139161
let { action; issue; sender; comment; repository } = notification in
140162
let { number; title; _ } = issue in
141163
let action_str =
@@ -152,7 +174,9 @@ let generate_issue_comment_notification notification channel =
152174
sprintf "<%s|[%s]> *%s* <%s|%s> on #%d %s" repository.url repository.full_name sender.login comment.html_url
153175
action_str number (pp_link ~url:issue.html_url title)
154176
in
155-
make_message ~text:summary ~attachments:(markdown_text_attachment ~footer:None comment.body) ~channel ()
177+
make_message ~text:summary
178+
?attachments:(format_attachments ~slack_match_func ~footer:None ~body:(Some comment.body))
179+
~channel ()
156180

157181
let git_short_sha_hash hash = String.sub hash 0 8
158182

@@ -312,7 +336,7 @@ let generate_status_notification (cfg : Config_t.config) (notification : status_
312336
in
313337
make_message ~text:summary ~attachments:[ attachment ] ~channel ()
314338

315-
let generate_commit_comment_notification api_commit notification channel =
339+
let generate_commit_comment_notification ~slack_match_func api_commit notification channel =
316340
let { commit; _ } = api_commit in
317341
let { sender; comment; repository; _ } = notification in
318342
let commit_id =
@@ -330,7 +354,9 @@ let generate_commit_comment_notification api_commit notification channel =
330354
| None -> None
331355
| Some p -> Some (sprintf "New comment by %s in <%s|%s>" sender.login comment.html_url p)
332356
in
333-
make_message ~text:summary ~attachments:(markdown_text_attachment ~footer:path comment.body) ~channel ()
357+
make_message ~text:summary
358+
?attachments:(format_attachments ~slack_match_func ~footer:path ~body:(Some comment.body))
359+
~channel ()
334360

335361
let validate_signature ?(version = "v0") ?signing_key ~headers body =
336362
match signing_key with

lib/slack_message.ml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ let base_attachment repository = { empty_attachment with footer = Some (simple_f
4040
let pp_label (label : label) = label.name
4141
let pp_github_user (user : github_user) = gh_name_of_string user.login
4242
let pp_github_team (team : github_team) = gh_name_of_string team.slug
43+
let pretext_slack_mention = Option.map (sprintf "<@%s>")
4344

4445
let populate_pull_request repository (pull_request : pull_request) =
4546
let ({
@@ -182,10 +183,6 @@ let populate_commit ?(include_changes = true) repository (api_commit : api_commi
182183
{
183184
(base_attachment repository) with
184185
footer = Some (simple_footer repository ^ " " ^ commit.committer.date);
185-
(*
186-
author_name = Some author.login;
187-
author_link = Some author.html_url;
188-
*)
189186
author_icon =
190187
( match author with
191188
| Some author -> Some author.avatar_url

src/monorobot.ml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ let check_gh_action file json config secrets state =
4747
Lwt_main.run
4848
( if json then
4949
let module Action = Action.Action (Api_remote.Github) (Api_local.Slack_json) in
50+
let%lwt () = Action.refresh_username_to_slack_id_tbl ~ctx in
5051
Action.process_github_notification ctx headers body
5152
else
5253
let module Action = Action.Action (Api_remote.Github) (Api_local.Slack_simple) in
54+
let%lwt () = Action.refresh_username_to_slack_id_tbl ~ctx in
5355
Action.process_github_notification ctx headers body
5456
)
5557
)

src/request_handler.ml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ let run ~ctx ~addr ~port =
1111
let ip = Unix.inet_addr_of_string addr in
1212
let signature = sprintf "listen %s:%d" (Unix.string_of_inet_addr ip) port in
1313
let connection = Unix.ADDR_INET (ip, port) in
14-
let%lwt () =
14+
let request_handler_lwt =
1515
Httpev.setup_lwt { default with name = "monorobot"; connection; access_log_enabled = false } (fun _http request ->
1616
let module Arg = Args (struct let req = request end) in
1717
let body r = Lwt.return (`Body r) in
@@ -56,4 +56,9 @@ let run ~ctx ~addr ~port =
5656
)
5757
)
5858
in
59+
let refresh_username_to_slack_id_tbl_background_lwt =
60+
try%lwt Daemon.unless_exit @@ Action.refresh_username_to_slack_id_tbl_background_lwt ~ctx
61+
with Daemon.ShouldExit -> Lwt.return_unit
62+
in
63+
let%lwt () = Lwt.join [ refresh_username_to_slack_id_tbl_background_lwt; request_handler_lwt ] in
5964
Lwt.return_unit

0 commit comments

Comments
 (0)