📝

WorkatoでSlack Connectorを自作してみた

に公開

注意事項

  • 今回共有したコネクタはご自由にご利用ください
  • 共有したコネクタは「サンプル」としての提供です
    • 品質について保証されません。ご自身の責任のもとご使用ください。
    • 今後のメンテナンスについて保証するものではありません
    • バグの修正や改善などはご自身のWorkspaceにてお願いします

はじめに

  • Workatoは基本的に便利なツールです
  • 様々なコネクタが用意されていてコネクタの開発をせずとも使用可能です
    • コミュニティにもコネクタが公開されていて、目的にあったものがあればインストールして使用することもできます
  • しかし「提供されているコネクタが目的を満たす」とは限らず、今回そういったときの一つの対応方法としての「コネクタ作成」をやったものです

抱えていた不満(課題)

Workatoで提供されているSlackコネクタでは不満がありました

  • Slack AppのBot Tokenを使用した認証が提供されていない
    • 任意のSlack Appとの連携に難儀
  • 提供機能では満足できないユースケースがあった
    • Custom Actionで対応もできるが、都度定義する必要があり再利用がしにくい
  • Slackへファイルをアップロードする方法が変わり、3ステップの実行になった
    • Uploading files
    • 毎回レシピで実装するのが手間
      • Recipe Functionでも実装できるが呼び出しにもタスクが必要
    • それぞれのStepをレシピで実装すると必ず3タスクを消費することになった
      • 消費タスクの純増

解決方法

使い方

インストール方法

  1. インストールしたいWorkspaceにログインします
  2. [Sample] Slack Connector(Authentication with Slack App Bot Token) にアクセスします
  3. Installを押下(赤矢印のところです)

認証方法

  1. 接続したいSlack AppのBuildの画面を開きBot Tokenをコピーします
  2. Workatoのコネクション作成画面で、1でコピーしたBot TokenをBot Tokenの欄にペーストしConnectを押下

SlackへFileのアップロードする

  • Upload file(v2)を使用します
  • Fileにはアップロードしたいファイルの「バイナリ」を期待しています
    • 例としてWorkatoの「File tooks by Workato」を使用して取得できた「File contents」を設定しています

実際のコード

長いのでmethodsとUpload file (v2)のexecuteのみ抜粋

知見

  • Methodsに書いた処理はExecuteするときに呼び出し可能
    • コネクタ内で再利用な可能な処理を定義して呼び出せる
  • Executeの中では複数回のHTTPリクエストの実行が可能
    • HTTPリクエスト以外でもRubyで記述できる処理は実装できる
    • 単純なHTTPの実行だけではなく、後処理で使いやすいように変換しておくなども記述可能
  • putsで出力すると実行ログの console から確認可能

methods

  methods: {
    # Helper method for Step 1: Get a temporary upload URL from Slack
    _get_upload_url: lambda do |connection, filename, length, alt_txt|
      get_url_params = {
        filename: filename,
        length: length,
        alt_txt: alt_txt
      }.compact

      get('files.getUploadURLExternal')
        .params(get_url_params)
        .headers(Authorization: "Bearer #{connection['app_token']}")
        .after_response do |_code, body, _headers|
          body
        end
        .after_error_response(/.*/) do |code, body, _header, message|
          error("Failed to get upload URL: [#{code}] #{message} - #{body}")
        end
    end,

    # Helper method for Step 2: POST the file content to the provided upload URL
    _upload_file_content: lambda do |upload_url, binary_content|
      post(upload_url)
        .headers('Content-Type': 'application/octet-stream')
        .request_body(binary_content)
        .response_format_raw # The response is just the string "ok"
        .after_response do |_code, body, _headers|
          body
        end
        .after_error_response(/.*/) do |code, body, _header, message|
          error("Failed to upload file content: HTTP #{code} - #{message} - #{body}")
        end
    end,

    # Helper method for Step 3: Finalize the file upload with Slack
    _complete_file_upload: lambda do |connection, files, channel_ids, initial_comment, thread_ts|
      # Construct the final parameters, removing any optional fields that are nil.
      completion_params = {
        files: files,
        channel_id: channel_ids,
        initial_comment: initial_comment,
        thread_ts: thread_ts
      }.compact

      post('files.completeUploadExternal')
        .headers(
          Authorization: "Bearer #{connection['app_token']}",
          'Content-Type': 'application/json; charset=utf-8'
        )
        .payload(completion_params)
        .after_response do |_code, body, _headers|
          body
        end
        .after_error_response(/.*/) do |code, body, _header, message|
          error("Failed to complete file upload: [#{code}] #{message} - #{body}")
        end
    end
  },

Execute

      execute: lambda do |connection, input, _extended_input_schema, _extended_output_schema, _continue|
        # Step 0: Validate that file content exists
        error('File content is missing. Please provide a file to upload.') unless input['file'].present?
        error('Filename is missing.') unless input['filename'].present?

        # Step 1: Get a temporary upload URL from Slack
        get_url_response = call(
          :_get_upload_url,
          connection,
          input['filename'],
          input['file'].size,
          input['alt_txt']
        )
        puts(get_url_response)
        # Ensure Step 1 was successful before proceeding
        error("Could not get upload URL from Slack. Response: #{get_url_response}") unless get_url_response&.[]('ok')
 
        upload_url = get_url_response['upload_url']
        file_id = get_url_response['file_id']
 
        # Step 2: POST the file content to the provided upload URL
        upload_response = call(:_upload_file_content, upload_url, input['file'])
        puts(upload_response)
        # Check if the raw upload was successful
        unless upload_response&.strip&.downcase&.start_with?('ok')
          error("File content upload failed. Response: #{upload_response}")
        end
 
        # Step 3: Finalize the file upload with Slack
        files = [{
          id: file_id,
          title: input['title'] || input['filename']
        }]
        complete_file_upload_response = call(
          :_complete_file_upload,
          connection,
          files,
          input['channel_ids'],
          input['initial_comment'],
          input['thread_ts']
        )
        puts(complete_file_upload_response)
      end,

まとめ

メリット

  • 接続先のコネクタが提供されていない場合でも社内向けに自作することで全体の効率を上げられる
    • 汎用のHTTPコネクタで各々自作すると車輪の再発明であり品質にバラつきがでてしまう
  • 自身のユースケースにあったアクションが作成できるためレシピの作成がシンプルになる
  • 1つのActionに複数の処理を盛り込めるのでTaskを節約できる

デメリット

  • Rubyで実装するためのスキルが必要
    • AIのCopilotを使用してもエラー対応やデバッグなどでRubyを読み書きするスキルは必要になる
  • 接続先のAPIに変更が発生するとメンテナンスのコストが発生する
    • Workato社提供のコネクタだと自身でメンテナンスするコストは発生しない

所感

  • Wokratoを使い倒すのであれば経験としてやってみるのはアリだと思います
  • Connector SDKのお作法に慣れるまでは大変
    • Connector SDKのエディタの描画領域が狭いのでそれだけでやろうとすると大変
    • Copilotも動作するが過度な期待はできない

(改めて)注意事項

  • 今回共有したコネクタはご自由にご利用ください
  • 共有したコネクタは「サンプル」としての提供です
    • 品質について保証されません。ご自身の責任のもとご使用ください。
    • 今後のメンテナンスについて保証するものではありません
    • バグの修正や改善などはご自身のWorkspaceにてお願いします

Discussion