{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "b3ebac65-12a0-4329-b543-bbb5c0d5c23b",
      "name": "Other Error",
      "type": "n8n-nodes-base.set",
      "position": [
        -800,
        80
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "status-other-error",
              "name": "status",
              "type": "string",
              "value": "\u274c \u305d\u306e\u4ed6\u306e\u30a8\u30e9\u30fc"
            },
            {
              "id": "error-type-other",
              "name": "error_type",
              "type": "string",
              "value": "={{ $json.errorType }}"
            },
            {
              "id": "error-message-other",
              "name": "error_message",
              "type": "string",
              "value": "={{ $json.errorMessage || $json.message }}"
            },
            {
              "id": "timestamp-error",
              "name": "checked_at",
              "type": "string",
              "value": "={{ $now.toISO() }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "a3dfa5f8-abff-4a5b-a06e-e600e94e221d",
      "name": "No Tweet Found",
      "type": "n8n-nodes-base.set",
      "position": [
        -800,
        224
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "status-no-tweet",
              "name": "status",
              "type": "string",
              "value": "\ud83d\udced \u30c4\u30a4\u30fc\u30c8\u306a\u3057"
            },
            {
              "id": "reason-no-tweet",
              "name": "reason",
              "type": "string",
              "value": "\u76e3\u8996\u5bfe\u8c61\u30e6\u30fc\u30b6\u30fc\u304b\u3089\u306e\u30c4\u30a4\u30fc\u30c8\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f"
            },
            {
              "id": "timestamp-no-tweet",
              "name": "checked_at",
              "type": "string",
              "value": "={{ $now.toISO() }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "91e51d65-cbac-4fc0-95b2-ec77f6db281a",
      "name": "Is No Tweets?",
      "type": "n8n-nodes-base.if",
      "position": [
        -1024,
        80
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.status }}",
              "value2": "no_tweets",
              "operation": "equals"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "1fde1a49-89b3-463e-94cf-9cbd54a506b9",
      "name": "Rate Limit Error",
      "type": "n8n-nodes-base.set",
      "position": [
        -1024,
        224
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "status-rate-limit",
              "name": "status",
              "type": "string",
              "value": "\u26a0\ufe0f API\u5236\u9650\u30a8\u30e9\u30fc"
            },
            {
              "id": "reason-rate-limit",
              "name": "reason",
              "type": "string",
              "value": "Twitter API\u306e\u30ec\u30fc\u30c8\u5236\u9650\u306b\u9054\u3057\u307e\u3057\u305f\u300215\u5206\u5f8c\u306b\u81ea\u52d5\u7684\u306b\u56de\u5fa9\u3057\u307e\u3059\u3002"
            },
            {
              "id": "error-detail",
              "name": "error_message",
              "type": "string",
              "value": "={{ $json.errorMessage }}"
            },
            {
              "id": "timestamp-rate",
              "name": "checked_at",
              "type": "string",
              "value": "={{ $now.toISO() }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "6e668adb-3f14-4c2f-80b2-4f8299998171",
      "name": "Is Rate Limit?",
      "type": "n8n-nodes-base.if",
      "position": [
        -1248,
        80
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.errorType }}",
              "value2": "rate_limit",
              "operation": "equals"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "80fd702a-9366-4a8c-8682-160632bd63ec",
      "name": "Reply Error",
      "type": "n8n-nodes-base.set",
      "position": [
        -576,
        -128
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "error-status",
              "name": "status",
              "type": "string",
              "value": "\u274c \u30ea\u30d7\u30e9\u30a4\u9001\u4fe1\u30a8\u30e9\u30fc"
            },
            {
              "id": "error-message",
              "name": "error_message",
              "type": "string",
              "value": "={{ $json.error }}"
            },
            {
              "id": "tweet-id",
              "name": "tweet_id",
              "type": "string",
              "value": "={{ $('Prepare Reply').item.json.replyToTweetId }}"
            },
            {
              "id": "checked-time",
              "name": "checked_at",
              "type": "string",
              "value": "={{ $now.toISO() }}"
            },
            {
              "id": "api-response",
              "name": "api_response",
              "type": "object",
              "value": "={{ $json }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "a24a0504-049d-4054-9082-a7195e4e44ee",
      "name": "Already Replied (Skip)",
      "type": "n8n-nodes-base.set",
      "position": [
        -576,
        32
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "duplicate-status",
              "name": "status",
              "type": "string",
              "value": "\u2705 \u65e2\u306b\u30ea\u30d7\u30e9\u30a4\u6e08\u307f\uff08\u30b9\u30ad\u30c3\u30d7\uff09"
            },
            {
              "id": "duplicate-reason",
              "name": "reason",
              "type": "string",
              "value": "\u3053\u306e\u30c4\u30a4\u30fc\u30c8\u306b\u306f\u65e2\u306b\u30ea\u30d7\u30e9\u30a4\u3092\u9001\u4fe1\u6e08\u307f\u3067\u3059\u3002\u91cd\u8907\u30ea\u30d7\u30e9\u30a4\u306f\u9001\u4fe1\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f\u3002"
            },
            {
              "id": "tweet-id",
              "name": "tweet_id",
              "type": "string",
              "value": "={{ $('Prepare Reply').item.json.replyToTweetId }}"
            },
            {
              "id": "username",
              "name": "target_username",
              "type": "string",
              "value": "={{ $('Prepare Reply').item.json.extractedUsername }}"
            },
            {
              "id": "checked-time",
              "name": "checked_at",
              "type": "string",
              "value": "={{ $now.toISO() }}"
            },
            {
              "id": "original-tweet",
              "name": "original_tweet",
              "type": "string",
              "value": "={{ $('Prepare Reply').item.json.originalTweet }}"
            },
            {
              "id": "error-detail",
              "name": "api_message",
              "type": "string",
              "value": "={{ $json.error }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "534ae83f-b785-4605-b5e0-20e997525f54",
      "name": "Reply Success",
      "type": "n8n-nodes-base.set",
      "position": [
        -576,
        -288
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "success-status",
              "name": "status",
              "type": "string",
              "value": "\u2705 \u30ea\u30d7\u30e9\u30a4\u9001\u4fe1\u6210\u529f"
            },
            {
              "id": "reply-to",
              "name": "replied_to_tweet_id",
              "type": "string",
              "value": "={{ $('Prepare Reply').item.json.replyToTweetId }}"
            },
            {
              "id": "reply-time",
              "name": "replied_at",
              "type": "string",
              "value": "={{ $now.toISO() }}"
            },
            {
              "id": "reply-text",
              "name": "reply_text",
              "type": "string",
              "value": "={{ $('Prepare Reply').item.json.replyText }}"
            },
            {
              "id": "original-tweet",
              "name": "original_tweet",
              "type": "string",
              "value": "={{ $('Prepare Reply').item.json.originalTweet }}"
            },
            {
              "id": "username",
              "name": "target_username",
              "type": "string",
              "value": "={{ $('Prepare Reply').item.json.extractedUsername }}"
            },
            {
              "id": "api-response",
              "name": "api_response",
              "type": "object",
              "value": "={{ $json }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "36704cb4-d6f0-49b0-b461-8be94d685324",
      "name": "Is Duplicate?",
      "type": "n8n-nodes-base.if",
      "position": [
        -800,
        -144
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.error }}",
              "value2": "duplicate content",
              "operation": "contains"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "8a16457f-d3b7-4725-bc95-971c9bfc3ec7",
      "name": "Send Reply (HTTP)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1024,
        -144
      ],
      "parameters": {
        "url": "https://api.twitter.com/2/tweets",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ { \"text\": $json.replyText, \"reply\": { \"in_reply_to_tweet_id\": $json.replyToTweetId } } }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "twitterOAuth2Api"
      },
      "credentials": {},
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "3085cff0-b825-4bcc-b011-411dcc069199",
      "name": "Prepare Reply",
      "type": "n8n-nodes-base.code",
      "position": [
        -1248,
        -144
      ],
      "parameters": {
        "jsCode": "// === \u4fee\u6b63\u7248 Prepare Reply ===\n\n// \u5404item\u3054\u3068\u306b\u30ea\u30d7\u30e9\u30a4\u30c7\u30fc\u30bf\u3092\u751f\u6210\nreturn items.map(item => {\n  const tweet = item.json.text;\n  const tweetId = item.json.id;\n  const authorId = item.json.author_id;\n  const includes = item.json.includes || {};\n  const users = includes.users || [];\n\n  // \u30e6\u30fc\u30b6\u30fc\u540d\u3092\u53d6\u5f97\n  let username = 'unknown';\n  const user = users.find(u => u.id === authorId);\n  if (user && user.username) {\n    username = user.username;\n  }\n\n  // \u56fa\u5b9a\u30e1\u30c3\u30bb\u30fc\u30b8\uff08\u5fc5\u8981\u306a\u3089\u3053\u3053\u3092\u500b\u5225\u5bfe\u5fdc\u306b\u5909\u66f4\u53ef\uff09\n  const replyMessage = `\u5f0a\u793e\u306e\u65b0\u30b5\u30fc\u30d3\u30b9\u3092\u3054\u89a7\u304f\u3060\u3055\u3044\uff01 https://x.com/linepi_pi/status/1988478476761133063`;\n\n  return {\n    json: {\n      ...item.json,\n      replyText: replyMessage,\n      replyToTweetId: tweetId,\n      extractedUsername: username,\n      originalTweet: tweet,\n    },\n  };\n});\n"
      },
      "typeVersion": 2
    },
    {
      "id": "e88fb60b-4a53-4880-9060-b32cae3ee7f1",
      "name": "Is Success?",
      "type": "n8n-nodes-base.if",
      "position": [
        -1472,
        -32
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.status }}",
              "value2": "success",
              "operation": "equals"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "767a017c-52a9-4927-80b0-9075c233a5f9",
      "name": "Get Latest Tweet Per Account",
      "type": "n8n-nodes-base.code",
      "position": [
        -1696,
        -32
      ],
      "parameters": {
        "jsCode": "// === \u4fee\u6b63\u7248 ===\n// Twitter API\u306e\u30ec\u30b9\u30dd\u30f3\u30b9\u3092\u3059\u3079\u3066\u306eitems\u304b\u3089\u7d50\u5408\nconst allResponses = items.map(item => item.json);\n\n// \u30c7\u30fc\u30bf\u3092\u7d71\u5408\nconst mergedData = {\n  data: [],\n  includes: { users: [] },\n};\n\nfor (const res of allResponses) {\n  if (res.data) mergedData.data.push(...res.data);\n  if (res.includes?.users) {\n    const existingUserIds = new Set(mergedData.includes.users.map(u => u.id));\n    for (const user of res.includes.users) {\n      if (!existingUserIds.has(user.id)) {\n        mergedData.includes.users.push(user);\n      }\n    }\n  }\n}\n\n// \u30a8\u30e9\u30fc\u30c1\u30a7\u30c3\u30af\nif (mergedData.error || mergedData.errors) {\n  const errorMessage =\n    mergedData.error?.message ||\n    mergedData.errors?.[0]?.message ||\n    JSON.stringify(mergedData.error || mergedData.errors);\n\n  let errorType = 'unknown_error';\n  if (errorMessage.includes('Too Many Requests') || errorMessage.includes('Rate limit')) {\n    errorType = 'rate_limit';\n  } else if (errorMessage.includes('Unauthorized') || errorMessage.includes('authentication')) {\n    errorType = 'auth_error';\n  } else if (errorMessage.includes('Bad request')) {\n    errorType = 'bad_request';\n  }\n\n  return [\n    {\n      json: {\n        status: 'error',\n        errorType,\n        errorMessage,\n      },\n    },\n  ];\n}\n\nconst tweets = mergedData.data || [];\nconst users = mergedData.includes.users || [];\n\nif (tweets.length === 0) {\n  return [\n    {\n      json: {\n        status: 'no_tweets',\n        message: '\u76e3\u8996\u5bfe\u8c61\u30e6\u30fc\u30b6\u30fc\u304b\u3089\u306e\u30c4\u30a4\u30fc\u30c8\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f',\n      },\n    },\n  ];\n}\n\n// author_id\u3054\u3068\u306b\u6700\u65b0\u30c4\u30a4\u30fc\u30c8\u306e\u307f\u53d6\u5f97\nconst latestTweetsByAuthor = {};\nfor (const tweet of tweets) {\n  const authorId = tweet.author_id;\n  if (\n    !latestTweetsByAuthor[authorId] ||\n    new Date(tweet.created_at) > new Date(latestTweetsByAuthor[authorId].created_at)\n  ) {\n    latestTweetsByAuthor[authorId] = tweet;\n  }\n}\n\n// \u7d50\u679c\u914d\u5217\nconst latestTweets = Object.values(latestTweetsByAuthor);\n\n// includes\u3092\u518d\u4ed8\u4e0e\u3057\u3066\u8fd4\u3059\nreturn latestTweets.map(tweet => ({\n  json: {\n    ...tweet,\n    includes: { users },\n    status: 'success',\n    totalAccountsFound: latestTweets.length,\n    totalTweetsScanned: tweets.length,\n  },\n}));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "7568127e-1144-41b1-b863-f56c9b1b044d",
      "name": "Search Tweets (HTTP)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1904,
        -32
      ],
      "parameters": {
        "url": "https://api.twitter.com/2/tweets/search/recent",
        "options": {},
        "sendQuery": true,
        "authentication": "predefinedCredentialType",
        "queryParameters": {
          "parameters": [
            {
              "name": "query",
              "value": "from:badassceo OR from:bozu_108 OR from:hirox246 OR from:takaichi_sanae -is:retweet"
            },
            {
              "name": "tweet.fields",
              "value": "created_at,author_id"
            },
            {
              "name": "expansions",
              "value": "author_id"
            },
            {
              "name": "user.fields",
              "value": "username"
            },
            {
              "name": "max_results",
              "value": "50"
            }
          ]
        },
        "nodeCredentialType": "twitterOAuth2Api"
      },
      "credentials": {},
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "e20df0a0-fe20-4830-856d-4d4504c75947",
      "name": "Schedule Trigger (15\u5206\u3054\u3068)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -2112,
        -32
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 30
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "cd49a9e5-36a7-4327-91ef-156ab7221e1e",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2528,
        -256
      ],
      "parameters": {
        "width": 304,
        "height": 608,
        "content": "This workflow automates your X (Twitter) engagement. It runs on a schedule, searches for new tweets based on a query, and automatically sends a reply.\n\n### How it works\n1.  **Trigger:** Runs the workflow automatically at your chosen interval (e.read, every 15 minutes).\n2.  **Search:** Uses the X (Twitter) API v2 to find recent tweets matching your query.\n3.  **Reply:** Posts a pre-defined reply to the tweet.\n4.  **Handle Errors:** Includes logic to manage API rate limits and avoid sending duplicate replies.\n\n### Setup steps\n1.  **Add Credentials:** You must have an X (Twitter) Developer Account (v2). Add your credentials to n8n.\n2.  **Set Query:** In the \"Search Tweets\" node (Section 1), you MUST define your search query (e.g., `#n8n`).\n3.  **Customize Reply:** In the \"Prepare Reply\" node (Section 2), you MUST write the reply text you want to send."
      },
      "typeVersion": 1
    },
    {
      "id": "985fc4bc-80f5-4395-89a6-2a72a86373ec",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2160,
        -128
      ],
      "parameters": {
        "color": 7,
        "width": 832,
        "height": 256,
        "content": "## 1. Search for Tweets\nThis section runs on a schedule and fetches new tweets based on your query."
      },
      "typeVersion": 1
    },
    {
      "id": "9d5b2a74-46ba-4f41-ac28-16875202cd9f",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1280,
        -256
      ],
      "parameters": {
        "color": 7,
        "width": 656,
        "height": 272,
        "content": "## 2. Process & Send Reply\nThis section prepares and sends your defined reply to the new tweets."
      },
      "typeVersion": 1
    },
    {
      "id": "0053ada0-8be0-4a8c-8340-0dd0a0bf76d5",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1280,
        64
      ],
      "parameters": {
        "color": 7,
        "width": 656,
        "height": 352,
        "content": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n## 3. Error Handling\nThis section catches common errors like API rate limits or empty search results."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Is Success?": {
      "main": [
        [
          {
            "node": "Prepare Reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Is Rate Limit?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Duplicate?": {
      "main": [
        [
          {
            "node": "Already Replied (Skip)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reply Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is No Tweets?": {
      "main": [
        [
          {
            "node": "No Tweet Found",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Other Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Reply": {
      "main": [
        [
          {
            "node": "Send Reply (HTTP)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Rate Limit?": {
      "main": [
        [
          {
            "node": "Rate Limit Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Is No Tweets?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Reply (HTTP)": {
      "main": [
        [
          {
            "node": "Is Duplicate?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Tweets (HTTP)": {
      "main": [
        [
          {
            "node": "Get Latest Tweet Per Account",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Latest Tweet Per Account": {
      "main": [
        [
          {
            "node": "Is Success?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger (15\u5206\u3054\u3068)": {
      "main": [
        [
          {
            "node": "Search Tweets (HTTP)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}