コロナ禍で筋力低下が著しいkitoです。

気が付いたらもう夏だし、これからも外出しづらい日が続きそう。。。
新しい生活様式でも筋力を維持できるようにしていかねばと思ふ今日この頃。

さて、今回は kintoneをZendeskの記事エディターにした話 シリーズの最終章とも言うべき、
kintoneに添付されたファイルをZendesk Guide の記事にアップロードした話をご紹介します。

kintoneから外部サービスへファイルアップロードする

kintone JavaScript APIで外部サービスへファイルをアップロードするには、 kintone.proxy.upload を利用します。

ですが、 続:kintoneをZendeskの記事エディターにした話 でも触れた通り、multipart/form-data形式は非対応。。。

kintoneからZendesk Guide記事へのファイルアップロードができず、
手作業のコストが高い状況でした😔

Code By Zapierを使ってみる

そこで、今回は Zapierの Code By Zapier を使ってファイルアップロードを行ってみました。

実際に動作させると、Zendesk Guideの記事にファイルが添付されて
👇のようにkintoneで参照している画像のURLが
Zendeskアップロード後のURLに更新されます。

もちろん、Zendesk Guideも同内容で更新されます。

アップロード処理の一連の流れ

ざっくりですが、
Zapierを絡めたファイルアップロード処理の流れは👇の通りです。

  1. kintone プロセス管理のステータス更新時にZendesk Guide 記事を作成/更新する
  2. kintoneのWebhook設定で、Zapierにプロセス管理のイベントを送信する
  3. Zapierで、kintoneのWebhookを受信する
  4. Code By Zapier でkintoneの添付ファイルをダウンロードする
  5. Code By Zapier で Zendesk Guide の記事にダウンロードしたファイルをアップロードする
  6. Code By Zapier で記事内容に含まれる画像URLをZendesk Guide上の画像URLに置換する
  7. 置換後の記事内容で、kintone のレコードを更新する
  8. 置換後の記事内容で、Zendesk Guide の記事を更新する

ファイルをあれこれするZap設定

ステップ1~3は、後続する処理の下準備的な内容になってます。

ステップ4以降から、kintoneの添付ファイルダウンロードやZendesk Guideへのファイルアップロードを行っています。

ステップ1: Webhooks by Zapier – kintoneのWebhook受信

Catch Raw HookWebhooks by Zapier を配置します。
ステップ4で通知内容の文字列から処理を行うため、 raw hook のトリガーとします。

ステップ2: Code by Zapier(Python) – 受信内容の変換

Pythonの Code by Zapier でステップ1の文字列を変換します。
inputにステップ1の内容を「webhook_body」の名前で定義します。
このステップの変換内容は、ステップ3でフィルターの条件として利用します。

import json

return json.loads(input["webhook_body"])

ステップ3: Filter by Zapier – 受信内容のチェック

Filter by Zapier でステップ2の変換内容をフィルターし、
特定のkintoneステータスの場合のみ、次のステップへ進むように設定します。

ステップ4~7: Code by Zapier(Python) – ファイルの処理や更新など

ステップ4以降の Code by Zapier を使った主な処理内容は、👇の通りです。

  1. ステップ4 – kintoneの添付ファイル情報から、ファイルをダウンロード
  2. ステップ4 – ダウンロードしたファイルを、Zendesk Guide の記事に添付
  3. ステップ5 – kintoneの記事内容を添付したファイルのURLに置換
  4. ステップ6 – 置換した内容でkintoneを更新
  5. ステップ7 – 置換した内容でZendesk Guideを更新

Code by Zapierの制限として、10秒を超える処理はタイムアウトエラーになります。

このため、処理に時間のかかるkintoneのファイルダウンロードとZendesk Guideへのファイルアップロードを非同期で処理するようにしています。💪

ステップ4 – 非同期ファイル処理のサンプルコード
※初めてPython書いたので不安しかない……💦

from io import BytesIO
from functools import partial
import json
import requests
import urllib.parse
import re
import asyncio


async def get_kintone_attachment(attachment):
    url = "https://{}.cybozu.com/k/v1/file.json?fileKey={}"
    download_api_url = url.format(input["KINTONE_SUB_DOMAIN"], attachment["fileKey"])
    headers = {"X-Cybozu-API-Token": input["KINTONE_API_TOKEN"]}

    loop = asyncio.get_event_loop()
    response = await loop.run_in_executor(
        None, partial(requests.get, download_api_url, headers=headers)
    )
    return BytesIO(response.content)


async def create_article_attachment(article_id, attachment, upload_file):
    create_attachment_api = (
        "https://{}.zendesk.com/api/v2/help_center/articles/{}/attachments.json"
    )
    attachment_api_url = create_attachment_api.format(
        input["ZENDESK_SUB_DOMAIN"], article_id
    )

    auth_token = input["ZENDESK_AUTH_TOKEN"]
    headers = {"Authorization": "Basic {}".format(auth_token)}

    # インライン表示する画像ファイルか
    is_inline_image = attachment["contentType"].find("image/") > -1
    payload = {"inline": is_inline_image}
    files = {"file": (attachment["name"], upload_file)}

    loop = asyncio.get_event_loop()
    response = await loop.run_in_executor(
        None,
        partial(
            requests.post,
            attachment_api_url,
            headers=headers,
            data=payload,
            files=files,
        ),
    )

    # インラインファイルでなければコンテンツURLは返さない
    if not is_inline_image:
        return ""

    create_result = json.loads(response.text)
    article_attachment = create_result["article_attachment"]
    return article_attachment["content_url"]


async def kintone_to_zendesk_attachement(file_name, attachment):
    # kintoneからファイルダウンロード
    upload_file = await get_kintone_attachment(attachment)

    # zendeskへファイルアップロード
    article_id = input["ARTICLE_ID"]
    content_url = await create_article_attachment(article_id, attachment, upload_file)
    return {"file_name": file_name, "zendesk_content_url": content_url}


# webhook通知内容を読み込む
kintone_webhook = json.loads(input["webhook_body"])

# app_id
app_id = kintone_webhook["app"]["id"]

# レコード番号
kintone_record = kintone_webhook["record"]
record_id = kintone_record["$id"]["value"]

# kintoneの添付ファイル
record_attachments = kintone_record["添付ファイル"]["value"]

# markdownのテキスト
markdown_text = kintone_record["markdown_text"]["value"]

# アップロード対象外の画像
exclude_attachments = []

# アップロード処理のtaskリスト
loop = asyncio.get_event_loop()
upload_tasks = []

# 添付ファイル分アップロード
for attachment in record_attachments:

    # イメージファイルか
    is_image_file = attachment["contentType"].find("image/") > -1

    # ファイル名
    file_name = urllib.parse.quote(attachment["name"])

    markdown_pattern = "\(.*/k/api/record/download.do/-/{}\?app={}.*&record={}.*?\)".format(
        file_name, app_id, record_id
    )
    # markdownでインラインイメージとして利用されていない画像を保持
    if is_image_file and re.search(markdown_pattern, markdown_text) is None:
        exclude_attachments.append({"fileKey": attachment["fileKey"]})
        continue

    # kintoneからZendeskへのアップロード処理をタスクとして保持
    upload_task = loop.create_task(
        kintone_to_zendesk_attachement(file_name, attachment)
    )
    upload_tasks.append(upload_task)


# 転送処理を非同期実行
gather = asyncio.gather(*upload_tasks)
upload_results = loop.run_until_complete(gather)
loop.close()

return {
    "exclude_attachments": json.dumps(exclude_attachments),
    "upload_results": json.dumps(upload_results),
}

まとめ

Code By Zapierの何でもできる感、イイっすね✨
添付ファイルの手作業がなくなって楽になりました🥳

が、しかし ❗❗❗❗

Zapierで処理をさせているので、kintone側で処理の完了を検知できません。。。

現状は、Zendesk記事の更新日時を監視して、ページの自動リロードで凌いでます。Zapierの処理がエラーになったら、結局手動になる場合も…😇😇😇

やはりkintoneのみで完結させたいので、このシリーズは終わらないですね(笑

それでは、また。