Skip to content

Commit f1b0b93

Browse files
committed
send slack notifications with web api if token is configured
Adds a way to send notifications without per-channel webhooks. The sending mechanism will authenticate with a bearer token that is valid for any channel that the bot has been added to. The channel is specified in a new `channel` field in the notification payload. A note on error handling: for web API requests, Slack always returns a 200 response and communicates the error message in the response body, so checking for status alone isn't enough.
1 parent f10278d commit f1b0b93

File tree

5 files changed

+53
-18
lines changed

5 files changed

+53
-18
lines changed

lib/api.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ module type Github = sig
99
end
1010

1111
module type Slack = sig
12-
val send_notification : chan:string -> msg:post_message_req -> url:string -> (unit, string) Result.t Lwt.t
12+
val send_notification : ctx:Context.t -> msg:post_message_req -> (unit, string) Result.t Lwt.t
1313
end

lib/api_local.ml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,18 @@ module Github : Api.Github = struct
2121
end
2222

2323
module Slack : Api.Slack = struct
24-
let send_notification ~chan ~msg ~url:_ =
24+
let send_notification ~ctx:_ ~msg =
2525
let json = msg |> Slack_j.string_of_post_message_req |> Yojson.Basic.from_string |> Yojson.Basic.pretty_to_string in
26-
Stdio.printf "will notify #%s\n" chan;
26+
Stdio.printf "will notify #%s\n" msg.channel;
2727
Stdio.printf "%s\n" json;
2828
Lwt.return @@ Ok ()
2929
end
3030

3131
module Slack_simple : Api.Slack = struct
3232
let log = Log.from "slack"
3333

34-
let send_notification ~chan ~msg ~url:_ =
35-
log#info "will notify %s%s" chan
34+
let send_notification ~ctx:_ ~(msg : Slack_t.post_message_req) =
35+
log#info "will notify %s%s" msg.channel
3636
( match msg.Slack_t.text with
3737
| None -> ""
3838
| Some s -> Printf.sprintf " with %S" s
@@ -43,9 +43,9 @@ end
4343
module Slack_json : Api.Slack = struct
4444
let log = Log.from "slack"
4545

46-
let send_notification ~chan ~msg ~url:_ =
46+
let send_notification ~ctx:_ ~(msg : Slack_t.post_message_req) =
47+
log#info "will notify %s" msg.channel;
4748
let json = Slack_j.string_of_post_message_req msg in
48-
log#info "will notify %s" chan;
4949
let url = Uri.of_string "https://api.slack.com/docs/messages/builder" in
5050
let url = Uri.add_query_param url ("msg", [ json ]) in
5151
log#info "%s" (Uri.to_string url);

lib/api_remote.ml

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,35 @@ end
5050
module Slack : Api.Slack = struct
5151
let log = Log.from "slack"
5252

53-
let send_notification ~chan ~msg ~url =
54-
let data = Slack_j.string_of_post_message_req msg in
55-
let body = `Raw ("application/json", data) in
56-
log#info "sending to %s : %s" chan data;
57-
match%lwt http_request ~body `POST url with
58-
| Ok _ -> Lwt.return @@ Ok ()
59-
| Error e ->
60-
Lwt.return @@ fmt_error "error while querying remote: %s\nfailed to send Slack notification to %s" e url
53+
let bearer_token_header access_token = sprintf "Authorization: Bearer %s" (Uri.pct_decode access_token)
54+
55+
(** `send_notification ctx msg` notifies `msg.channel` with the payload `msg`;
56+
uses web API with access token if available, or with webhook otherwise *)
57+
let send_notification ~(ctx : Context.t) ~(msg : Slack_t.post_message_req) =
58+
log#info "sending to %s" msg.channel;
59+
let build_error e = fmt_error "%s\nfailed to send Slack notification" e in
60+
let build_query_error url e = build_error @@ sprintf "error while querying %s: %s" url e in
61+
let secrets = Context.get_secrets_exn ctx in
62+
let headers, url, webhook_mode =
63+
match secrets.slack_access_token with
64+
| Some access_token -> [ bearer_token_header access_token ], Some "https://slack.com/api/chat.postMessage", false
65+
| None -> [], Context.hook_of_channel ctx msg.channel, true
66+
in
67+
match url with
68+
| None -> Lwt.return @@ build_error @@ sprintf "no token or webhook configured to notify channel %s" msg.channel
69+
| Some url ->
70+
let data = Slack_j.string_of_post_message_req msg in
71+
let body = `Raw ("application/json", data) in
72+
log#info "data: %s" data;
73+
( match%lwt http_request ~body ~headers `POST url with
74+
(* error detection in response: slack uses status codes for webhooks versus a 200 code w/ `error` field for web api *)
75+
| Ok s ->
76+
if webhook_mode then Lwt.return @@ Ok ()
77+
else (
78+
let res = Slack_j.post_message_res_of_string s in
79+
if res.ok then Lwt.return @@ Ok ()
80+
else Lwt.return @@ build_query_error url (Option.value ~default:"an unknown error occurred" res.error)
81+
)
82+
| Error e -> Lwt.return @@ build_query_error url e
83+
)
6184
end

lib/config.atd

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ type webhook = {
3939
(* This is the structure of the secrets file which stores sensitive information, and
4040
shouldn't be checked into version control. *)
4141
type secrets = {
42-
slack_hooks : webhook list;
43-
?gh_token : string option; (* GitHub personal access token, if repo access requires it *)
44-
?gh_hook_token : string option; (* GitHub webhook token to secure the webhook *)
42+
(* GitHub personal access token, if repo access requires it *)
43+
?gh_token : string nullable;
44+
(* GitHub webhook token to secure the webhook *)
45+
?gh_hook_token : string nullable;
46+
(* list of Slack webhook & channel name pairs *)
47+
~slack_hooks <ocaml default="[]"> : webhook list;
48+
(* Slack bot token obtained via OAuth, enabling message posting to the workspace *)
49+
?slack_access_token : string nullable;
4550
}

lib/slack.atd

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,14 @@ type message_block = [
5454
] <json adapter.ocaml="Atdgen_runtime.Json_adapter.Type_field">
5555

5656
type post_message_req = {
57+
channel: string;
5758
?text: string nullable;
5859
?attachments: message_attachment list nullable;
5960
?blocks: message_block list nullable;
6061
}
62+
63+
type post_message_res = {
64+
ok: bool;
65+
?channel: string nullable;
66+
?error: string nullable;
67+
}

0 commit comments

Comments
 (0)