From 7ea6450bc65ac1d7a5548e73afd4626acce16f30 Mon Sep 17 00:00:00 2001 From: koonwen Date: Fri, 3 Jan 2025 07:30:26 +0000 Subject: [PATCH] deprecate file upload and implement new apis --- lib/api.ml | 8 ++++++ lib/api_local.ml | 33 ++++++++++++++++++++++++ lib/api_remote.ml | 64 +++++++++++++++++++++++++++++++++++++++++++++++ lib/slack.atd | 34 ++++++++++++++++++++++++- 4 files changed, 138 insertions(+), 1 deletion(-) diff --git a/lib/api.ml b/lib/api.ml index 5d50cc2..49c84b9 100644 --- a/lib/api.ml +++ b/lib/api.ml @@ -9,6 +9,14 @@ module type S = sig val send_message_webhook : ctx:Context.t -> url:string -> msg:post_message_req -> unit slack_response Lwt.t val update_message : ctx:Context.t -> msg:update_message_req -> update_message_res slack_response Lwt.t val upload_file : ctx:Context.t -> file:files_upload_req -> files_upload_res slack_response Lwt.t + [@@deprecated "Use get_upload_url_external with \ + complete_upload_external as per \ + https://api.slack.com/messaging/files#uploading_files. Otherwise, \ + there is upload_file_v2 convenience function"] + + val get_upload_url_external : ctx:Context.t -> req:get_upload_url_ext_req -> get_upload_url_ext_res slack_response Lwt.t + val complete_upload_external : ctx:Context.t -> req:complete_upload_ext_req -> complete_upload_ext_res slack_response Lwt.t + val upload_file_v2 : ctx:Context.t -> file:files_upload_req -> files_upload_res slack_response Lwt.t (* conversations *) val get_replies : diff --git a/lib/api_local.ml b/lib/api_local.ml index ce51d89..2785977 100644 --- a/lib/api_local.ml +++ b/lib/api_local.ml @@ -38,6 +38,9 @@ let default_files_res : Slack_t.file = let default_files_upload_res : Slack_t.files_upload_res = { ok = true; file = default_files_res } +let default_files_upload_res_v2 : Slack_t.complete_upload_ext_res = + { ok = true; files = [ { id = default_files_res.id; title = default_files_res.title } ] } + let send_message ~ctx:_ ~msg = let json = msg |> Slack_j.string_of_post_message_req |> Yojson.Basic.from_string |> Yojson.Basic.pretty_to_string in printf "will notify #%s\n" msg.channel; @@ -65,6 +68,36 @@ let upload_file ~ctx:_ ~file = Lwt.return_ok { default_files_upload_res with file = { default_files_res with channels = [ channels ] } } | None -> Lwt.return_error (`Other "invalid file upload") +let get_upload_url_external ~ctx:_ ~(req : Slack_t.get_upload_url_ext_req) = + let json = + req |> Slack_j.string_of_get_upload_url_ext_req |> Yojson.Basic.from_string |> Yojson.Basic.pretty_to_string + in + printf "getting upload url for %s\n" req.filename; + printf "%s\n" json; + Lwt.return_ok Slack_t.{ ok = true; upload_url = "http://fake-upload-url.com"; file_id = "file_id" } + +let complete_upload_external ~ctx:_ ~(req : Slack_t.complete_upload_ext_req) = + let json = + req |> Slack_j.string_of_complete_upload_ext_req |> Yojson.Basic.from_string |> Yojson.Basic.pretty_to_string + in + printf "complete upload for %s in channel:%s or ts:%s\n" (Slack_j.string_of_files_v2 req.files) + (Option.default "NULL" req.channel_id) (Option.default "NULL" req.thread_ts); + printf "%s\n" json; + Lwt.return_ok default_files_upload_res_v2 + +let upload_file_v2 ~ctx:_ ~(file : Slack_t.files_upload_req) = + let json = file |> Slack_j.string_of_files_upload_req |> Yojson.Basic.from_string |> Yojson.Basic.pretty_to_string in + match file.channels with + | Some channels -> + if List.length (String.split_on_char ',' channels) <> 1 then + Lwt.return_error (`Other "only allowed to specify 1 channel") + else ( + printf "will update #%s\n" channels; + printf "%s\n" json; + Lwt.return_ok { default_files_upload_res with file = { default_files_res with channels = [ channels ] }} + ) + | None -> Lwt.return_error (`Other "invalid file upload") + let join_conversation ~ctx:_ ~(channel : Slack_t.conversations_join_req) = printf "joining #%s...\n" channel.channel; let url = Filename.concat cache_dir (sprintf "%s_join" channel.channel) in diff --git a/lib/api_remote.ml b/lib/api_remote.ml index 825a6c8..cf1935e 100644 --- a/lib/api_remote.ml +++ b/lib/api_remote.ml @@ -105,6 +105,70 @@ let upload_file ~(ctx : Context.t) ~(file : Slack_t.files_upload_req) = ~name:(sprintf "file.upload (%s)" @@ Option.default "" file.channels) ~body `POST "files.upload" Slack_j.read_files_upload_res +let www_form_of_get_upload_url_ext (req : Slack_t.get_upload_url_ext_req) = + let fields = + [ + Some ("filename", req.filename); + Some ("length", Int.to_string req.length); + string_field_val req.alt_txt "alt_txt"; + string_field_val req.snippet_type "snippet_type"; + ] + in + list_filter_opt fields + +let get_upload_url_external ~(ctx : Context.t) ~(req : Slack_t.get_upload_url_ext_req) = + log#info "getting upload url for %s" req.filename; + let args = www_form_of_get_upload_url_ext req in + let data = Web.make_url_args args in + let body = `Raw ("application/x-www-form-urlencoded", data) in + log#info "data to upload req: %s" data; + request_token_auth ~ctx + ~name:(sprintf "files.getUploadURLExternal (%s)" req.filename) + ~body `POST "files.getUploadURLExternal" Slack_j.read_get_upload_url_ext_res + +let complete_upload_external ~(ctx : Context.t) ~(req : Slack_t.complete_upload_ext_req) = + log#info "completing upload url for %s" @@ Slack_j.string_of_files_v2 req.files; + let data = Slack_j.string_of_complete_upload_ext_req req in + let body = `Raw ("application/json", data) in + log#info "data to upload req: %s" data; + request_token_auth ~ctx + ~name:(sprintf "files.completeUploadExternal (%s)" @@ Slack_j.string_of_files_v2 req.files) + ~body `POST "files.completeUploadExternal" Slack_j.read_complete_upload_ext_res + +(** NOTE: this api only can specify one channel_id unlike in the deprecated version *) +let upload_file_v2 ~(ctx : Context.t) ~(file : Slack_t.files_upload_req) = + match file.filename, file.content with + | None, _ | _, None -> Exn.fail "need to supply both filename and content" + | Some filename, Some content -> + let length = String.length content in + let req = Slack_j.make_get_upload_url_ext_req ~filename ~length () in + ( match%lwt get_upload_url_external ~ctx ~req with + | Error e -> Lwt.return_error e + | Ok { upload_url; file_id; _ } -> + let body = `Form [ "file", filename ] in + ( match%lwt http_request ~ua:ctx.ua ~body `POST upload_url with + | Error e -> slack_lib_fail "upload file failed with: %s" e + | Ok _ -> + let files : Slack_t.file_v2 = { id = file_id; title = file.title } in + let req = + Slack_j.make_complete_upload_ext_req ~files:[ files ] ?channel_id:file.channels ?thread_ts:file.thread_ts () + in + ( match%lwt complete_upload_external ~ctx ~req with + | Error e -> Lwt.return_error e + | Ok { files; _ } -> + let f = + match files with + | [] -> slack_lib_fail "empty files on complete_upload_external response" + | [ f ] -> f + | f :: _ -> + log#warn "got more than 1 file in response, returning top"; + f + in + Lwt.return_ok Slack_t.{ ok = true; file = Slack_j.make_file ~id:f.id ?title:f.title () } + ) + ) + ) + (** [join_conversation ctx channel ] will join the token owner [ctx.secrets.slack_access_token] to the [channel]. *) let join_conversation ~(ctx : Context.t) ~(channel : Slack_t.conversations_join_req) = diff --git a/lib/slack.atd b/lib/slack.atd index f41a754..9ca887f 100644 --- a/lib/slack.atd +++ b/lib/slack.atd @@ -130,7 +130,7 @@ type files_upload_req = { ?title: string nullable; } -type file = { +type file = { id: string; ?name: string nullable; ?title: string nullable; @@ -151,6 +151,38 @@ type files_upload_res = { file: file; } +type file_v2 = { + id: string; + ?title : string nullable +} + +type files_v2 = file_v2 list + +type get_upload_url_ext_req = { + filename: string; + length: int; + ?alt_txt: string nullable; + ?snippet_type: string nullable +} + +type get_upload_url_ext_res = { + ok: bool; + upload_url: string; + file_id: string +} + +type complete_upload_ext_req = { + files: files_v2; + ?channel_id : string nullable; + ?initial_comment: string nullable; + ?thread_ts: string nullable +} + +type complete_upload_ext_res = { + ok: bool; + files: files_v2; +} + type conversations_info_req = { channel: string; ?include_locale: bool nullable;