【第2回】Dify×GAS×X APIでポストを自動取得する方法

Dify

前回は、DifyとGASを使って、指定したXアカウントの投稿を自動収集し、スプレッドシートに保存する仕組みの全体像を整理しました。

【第1回】Xポストを自動取得!Dify×GASでスプレッドシートに保存する方法
Dify×GAS×X APIで任意のアカウントのポストを自動取得してスプレッドシートに保存するツールを作成しました。ここでは、全体像とどういった目的のツールなのかについて紹介しています。ツール全体の設計については、この記事シリーズの2でお話しています。

今回はその続きとして、Dify側でどのようにワークフローを組み、X投稿の取得と整形を行ったのかをまとめます。

【第3回】Xポスト自動取得|スプレッドシート保存のGAS側の解説
X APIで取得したポストをスプレッドシートに保存して、重複したものを除いて処理するGASについてご紹介しています。

特に今回は、単純にHTTP Requestをつなぐだけでは進まなかった部分も含めて、Difyで詰まりやすいポイントと対処方法もあわせて紹介します。

この記事でわかること

  • GASから監視対象アカウント一覧を取得する部分
  • X APIで各アカウントの投稿を取得する部分
  • 投稿データを保存しやすい形に整える部分
  • Dify特有の詰まりやすい仕様と対処方法

この記事ではDify側の構成と処理の流れについて紹介します。
スプレッドシート保存や重複除外を受け持つGAS側は次の記事で紹介させていただきます。

Difyワークフロー全体像

今回のDifyワークフローは、次の流れで組んでいます。

  1. Webhookでワークフローを開始する
  2. 実行時に使う値をCodeノードで整える
  3. GASから監視対象アカウント一覧を取得する
  4. 各アカウントをIterationで順番に処理する
  5. X APIから投稿を取得する
  6. 必要な項目だけを取り出して整形する
  7. 全アカウント分をまとめる
  8. 保存用のJSONを組み立ててGASへ送る

ワークフロー構成は、概ね次のようになります。

Webhook
→ コード_build_runtime
→ HTTP リクエスト_get_accounts_from_gas
→ コード_parse_accounts_response
→ Iteration_for_each_account
   → コード_extract_current_account
   → HTTP リクエスト_get_x_posts
   → Document Extractor
   → コード_parse_x_posts_response
   → コード_filter_quote_and_map
→ コード_flatten_save_new_posts_to_gas
→ コード_build_save_new_posts_payload
→ HTTP リクエスト_save_new_posts_to_gas
→ コード_parse_save_new_posts_response
→ Output

役割ごとのノード構成

役割 ノード 内容
初期設定 Webhook / build_runtime 実行開始と共通変数の整形
対象取得 get_accounts_from_gas / parse_accounts_response 監視対象アカウント一覧を取得
投稿取得 Iteration / get_x_posts アカウントごとにX APIから投稿取得
投稿整形 Document Extractor / parse_x_posts_response / filter_quote_and_map 必要な項目だけに整形し、引用投稿を除外
保存準備 flatten_save_new_posts_to_gas / build_save_new_posts_payload 全件をまとめて保存用JSONを作成
保存実行 save_new_posts_to_gas / parse_save_new_posts_response GASへ渡して保存結果を返す

初期設定とアカウント一覧の取得

Webhookで開始する理由

Webhook Triggerを起点にしてDify上でのテスト実行だけでなく、外部からの起動もできる形にしています。

たとえば、将来的に次のような広げ方がしやすくなります。

  • 定期実行の入口にする
  • 別ツールや外部処理から起動する
  • テスト用と本番用の入口を分ける

build_runtime で共通変数を整える

最初にCodeノードで、取得件数などの共通値を整えています。

たとえば、最大取得件数のような値を後続で使いやすい形にしています。

function main() {
  return {
    max_posts_total_sanitized: 30
  };
}

最初に値を整えておくと、後続のノードで同じ値を何度も組み直さずに済みます。

GASから監視対象アカウントを取得する

監視対象アカウントはDify側に直接書かず、スプレッドシートで管理しています。
そのため、DifyからはまずGASを呼び出して一覧を受け取ります。

この形にしておくと、対象アカウントの追加や削除があってもスプレッドシートの更新するだけで対応できます。

アカウントごとの投稿取得

Iterationで複数アカウントを順番に処理する

アカウント一覧を受け取ったら、Iterationノードで1件ずつ処理します。

処理の流れはシンプルで、各アカウントごとに次を行います。

  • 現在処理中のアカウント情報を取り出す
  • X APIにリクエストを送る
  • レスポンスを整形する
  • 保存しやすい形式へ変換する

X APIの取得条件

今回は各アカウントの投稿を取得するために、ユーザーIDベースのタイムライン取得を使いました。

主な条件は次のとおりです。

  • 対象は指定アカウントの投稿
  • 返信とリポストは除外
  • 作成日時と referenced_tweets を取得

引用投稿については、取得後に後段で除外しています。

投稿データの整形

HTTPレスポンスをそのまま読めないケース

今回、Difyで詰まりやすかったポイントの1つがここです。

X APIのレスポンスはJSONですが、Dify上では単純なテキスト本文として扱われず、filesとして認識されるケースがありました。

この場合、HTTP Requestノードの出力は次のような状態になります。

  • body は空
  • files にJSONファイル情報が入る
この状態でそのままCodeノードで body を読もうとしても投稿データは取り出せません。

Document Extractor でbodyの空を解決

HTTP Requestの直後に Document Extractor を入れ、files を読み出して文字列化することで解決できます。
流れとしては次のようになります。

HTTP リクエスト_get_x_posts
→ Document Extractor
→ コード_parse_x_posts_response

この構成にすると、Document Extractor の text にJSON文字列が入り、Codeノードで安全にパースしやすくなります。

必要な項目だけを取り出す

投稿取得後は、保存に必要な情報だけを残すようにしています。
今回使っている主な項目は次のとおりです。

  • id
  • text
  • created_at
  • referenced_tweets

整形用のCodeノードは次のようになります。

function main({ text }) {
  const emptyResult = {
    posts: [],
    posts_count: 0
  };

  const rawText = Array.isArray(text)
    ? String(text[0] || "").trim()
    : String(text || "").trim();

  if (!rawText) {
    return emptyResult;
  }

  let parsed;
  try {
    parsed = JSON.parse(rawText);
  } catch (error) {
    return emptyResult;
  }

  const rawPosts = Array.isArray(parsed.data) ? parsed.data : [];

  const posts = rawPosts
    .map(item => ({
      id: String((item || {}).id || "").trim(),
      text: String((item || {}).text || "").trim(),
      created_at: String((item || {}).created_at || "").trim(),
      referenced_tweets: Array.isArray((item || {}).referenced_tweets)
        ? item.referenced_tweets
        : []
    }))
    .filter(item => item.id !== "");

  return {
    posts,
    posts_count: posts.length
  };
}

引用投稿を除外して保存用の形にする

取得した投稿をそのまま保存するのではなく、今回は引用投稿を除外し保存用に必要なURLなどを作っています。

function main({ x_posts, username, user_id }) {
  const posts = Array.isArray(x_posts) ? x_posts : [];
  const safeUsername = String(username || "").trim();
  const safeUserId = String(user_id || "").trim();

  const candidate_posts = [];
  let quote_skipped_count = 0;

  for (const post of posts) {
    const refs = Array.isArray(post.referenced_tweets) ? post.referenced_tweets : [];
    const isQuoted = refs.some(ref => String((ref || {}).type || "").trim() === "quoted");

    if (isQuoted) {
      quote_skipped_count += 1;
      continue;
    }

    const postId = String((post || {}).id || "").trim();
    if (!postId) {
      continue;
    }

    candidate_posts.push({
      post_id: postId,
      username: safeUsername,
      user_id: safeUserId,
      post_url: safeUsername ? `https://x.com/${safeUsername}/status/${postId}` : "",
      created_at: String((post || {}).created_at || "").trim()
    });
  }

  return {
    candidate_posts,
    quote_skipped_count
  };
}

全アカウント分の集約と保存用データの作成

Iteration後の出力をそのまま使わない理由

Iterationの後は、期待していた形の配列にならないことがあります。
そのため実際の出力JSONを確認しながら、後続で扱いやすい形に整える必要がありました。

最終的に保存対象の投稿一覧を1つにまとめるため、専用のCodeノードで flatten しています。

function main({ iteration_posts, max_posts_total_sanitized }) {
  const input = Array.isArray(iteration_posts) ? iteration_posts : [];
  const flat = [];

  for (const item of input) {
    if (!item) continue;

    if (Array.isArray(item)) {
      flat.push(...item);
      continue;
    }

    if (typeof item === "object") {
      flat.push(item);
    }
  }

  const uniqueMap = new Map();

  for (const item of flat) {
    const postId = String((item || {}).post_id || "").trim();
    if (!postId) continue;
    if (!uniqueMap.has(postId)) {
      uniqueMap.set(postId, item);
    }
  }

  const all = Array.from(uniqueMap.values()).sort((a, b) => {
    const at = new Date((a || {}).created_at || 0).getTime();
    const bt = new Date((b || {}).created_at || 0).getTime();
    return bt - at;
  });

  const limit = Number.isFinite(Number(max_posts_total_sanitized))
    ? Math.max(1, Math.min(30, Math.floor(Number(max_posts_total_sanitized))))
    : 30;

  return {
    all_candidates: all.slice(0, limit),
    quote_skipped_count_total: 0
  };
}

保存用のJSONをCodeノードで作る理由

もう1つ詰まりやすかったのが、配列やオブジェクトをHTTP Requestへそのまま渡した時の崩れ方です。

Difyでは配列やオブジェクトをHTTP RequestのJSON bodyに直接差し込むと、送信先で想定どおりに受け取れないことがあります。

そのため今回はHTTP Requestの前にCodeノードを挟み、request_body_json を組み立てています。

function main({ all_candidates, max_posts_total_sanitized, gas_shared_secret }) {
  const candidates = Array.isArray(all_candidates) ? all_candidates : [];

  return {
    request_body_json: JSON.stringify({
      secret: String(gas_shared_secret || "").trim(),
      action: "save_new_posts",
      max_posts_total: Number(max_posts_total_sanitized || 30),
      candidates: candidates
    })
  };
}

そしてHTTP Request側では raw body としてそのまま送信しています。

この形にしておくと配列やオブジェクトの形が崩れにくくGAS側でも扱いやすくなります。

Difyで詰まりやすかった点と整理しておきたいこと

レスポンスの見た目と実データが一致しないことがある

DifyではUI上で見えているイメージと、実際の入力・出力データの形が一致しないことがあります。
JSONレスポンスのつもりで扱っていたものが files になっていたりなどです。

そのため、詰まったときはまず次を確認する方が整理しやすいです。

  • 現在のノードの入力JSON
  • 1つ前のノードの出力JSON
  • 値が単体なのか配列なのか
  • ノード全体を渡しているのか、出力値を渡しているのか

Codeノードの引数の受け方に注意が必要だった

Codeノードでは、複数入力があるときに function main(a, b) のように受けるより、function main({ a, b }) の形で受けた方が合う場面がありました。

この違いで値は入っているのに空扱いになることもありました。
空の出力が出たときは、入力JSONと関数の受け方を見直す必要があります。

配列やオブジェクトは HTTP 送信前に明示的に整える方が安定しやすい

複数投稿をまとめて保存する場合、配列データをそのままHTTP Requestへ渡すより、Codeノードで一度 JSON.stringify(...) してから送る方が安定しやすいです。

このあたりは「どの方法が絶対に正しい」というより、送信先で空になる、型がズレる、崩れるといった症状が出たときに見直したいポイントです。

この記事でできるようになったこと

記事の内容の作業をここまでやると、下記のことができるようになっています。

  • 監視対象アカウント一覧の取得
  • 各アカウントのX投稿取得
  • 引用投稿の除外
  • 保存用データの整形
  • GASへ渡すためのJSON組み立て

つまり、Dify側では「集める」「整える」「保存に渡せる形にする」ところまで進んでいます。

まとめ

この記事では、Dify側でX投稿取得ツールをどう組んだのかを紹介してきました。

X APIの呼び出しそのものよりも、Dify上でデータをどう受け取り、どう整え、どう次のノードへ渡すかが特に重要だと感じました。

files扱いになるレスポンス、Iteration後の出力、配列を含むHTTP送信は、事前に知っておくと組みやすくなる部分でした。

次の記事では、GAS側で受け取ったデータをどう保存し、どう重複除外しスプレッドシート上でどう管理するのかを紹介しています。

【第3回】Xポスト自動取得|スプレッドシート保存のGAS側の解説
X APIで取得したポストをスプレッドシートに保存して、重複したものを除いて処理するGASについてご紹介しています。
  • 競合アカウントのポストを保存したい
  • SNS運用の業務を効率化したい
  • 自社の運用に合う形で補助の仕組みを考えたい

という方がいれば、無料相談・お問い合わせからお気軽にご連絡ください。
今の運用に合わせて、どこまで自動化するのがちょうど良いか、一緒に整理できればと思います。

問い合わせフォームはこちら

コメント

タイトルとURLをコピーしました