背景と目的
皆さんは、スマホにX(旧Twitter)の公式アプリを入れていますか?
実は私は、スマホにXの公式アプリを入れていません。
「青い鳥」のアイコンがなくなってしまったのが寂しく、Xを見るのはPCやスマホの「ブラウザ版」のみです。
しかし、ブラウザ版でXを利用する場合、どうしても「見られる時間が限られる」という問題が発生します。
タイムラインを常にチェックしているわけではないため、推しの「○○時から雑談配信」といったポストをタイムリーに追うことができず、後から気づくことが何度もありました。
- 「青い鳥のいない公式アプリに違和感を感じる」
- 「ブラウザを四六時中チェックするのも厳しい」
- 「投稿はある程度リアルタイムに近い形で知りたい」
上記の課題がある事実を受けとめ、解決法を模索しました。
そこで考えたのが、「普段利用するコミュニケーションツールに新着ポストを自動で通知させる」という方法でした。
課題と要件
「定期的にRSSを取得してDiscord Webhookに投げれば完成でしょ!」と最初は軽く考えていました。
しかし、いざ作ろうとすると以下の課題が立ち塞がりました。
-
X (Twitter) APIの有料化
かつての無料枠のAPIを叩いてツイートを取得するような従来のBOT開発手法は、現在ほぼ使えません。サードパーティ製のRSS(Nitter等)に頼らざるを得ない状況です。 -
セルフホストマシンを24時間常駐稼働させたくない
スクリプトを書いて「自宅のPCやラズパイ、VPSで常時実行させればいいのでは?」と思うかもしれません。一方で、「自宅ネットワーク切断・PC再起動でスクリプトが止まるリスク」「VPSの維持費を払ってまで本格運用は考えていない」という理由からセルフホストでの常駐稼働は避けたかった。
つまり、「完全無料で、インフラ管理不要で完結させる」ことが条件でした。
全体構成
事前検討の段階で単純なスクリプトから直接アクセスを試みたところ、Bot 判定に阻まれ、安定して RSS を取得できないケースがありました。
これらの課題を解決するため、「Cloudflare Workers によるプロキシ層でのアクセス最適化」 と 「GAS(Google Apps Script)によるサーバーレスな処理」 を組み合わせたアーキテクチャを採用しました。
[ X (旧Twitter) / Nitter RSS ]
│
▼ (データ正規化)
[ Cloudflare Workers (プロキシ層) ]
│
▼ (Fetch & HTML検知・例外保護)
[ Google Apps Script (定時トリガー) ]
│
▼ (送信)
[ Discord Webhook (通知チャンネル) ]
役割分担のメリット
-
Cloudflare Workers(プロキシ)
一般的なブラウザからのアクセス手順に合わせたヘッダー調整、後続の処理層へ「整えたデータ」を渡す前処理の役割を果たします。 -
Google Apps Script(実行)
プロキシを経由して届いたXMLデータをパースし、ScriptProperties を用いた既読管理(重複通知防止)と Discord Webhook へのポスティングを実行します。Google のサービスで定期実行トリガーを組めるのが利点です。
構築作業
Step 1: Cloudflare アカウントの作成とログイン
- Cloudflare 公式サイト にアクセスし、アカウントを作成します。
- 登録完了後、ダッシュボードにログインします。
Step 2: Workers サービスの新規作成
- 左側サイドメニューの [Build] > [Compute] をクリックします。
- [Workers & Pages] をクリックします([Ctrl + K] のショートカットで検索窓を開いて移動してもOKです)。
- [Create Application] ボタンをクリックします。
- 最初は何でもよいです。[Start with Hello World!]をクリックして、Hello World をデプロイします。
- Worker の名前(例: twitter-proxy)を入力し、[Deploy] をクリックします(私はデプロイ済みのため既にtwitter-proxyが表示されています) 。
※デプロイ直後は、デフォルトで以下のようなサンプルコード(Hello World を返すだけのコード)が配置されていますが、次のステップで書き換えるのでそのままで問題ありません。
/**
* Welcome to Cloudflare Workers! This is your first worker.
*
* - Run "npm run dev" in your terminal to start a development server
* - Open a browser tab at http://localhost:8787/ to see your worker in action
* - Run "npm run deploy" to publish your worker
*
* Learn more at https://developers.cloudflare.com/workers/
*/
export default {
async fetch(request, env, ctx) {
// You can view your logs in the Observability dashboard
console.info({ message: 'Hello World Worker received a request!' });
return new Response('Hello World!');
}
};
Step 3: プロキシコードの実装
- リクエストから
?url=パラメータを検知・デコード - 機械的なアクセス(デフォルトの
fetch)はサードパーティサーバーから即ブロックされるリスクがあります。User-Agentをセットし、振る舞いを決めています。 - Unicode BOM や不要な文字列を除去し、GAS 側でクラッシュする問題を防止します。
※コード内の具体的な文字列は意図的に不完全な記述としています。実装の際は、ご自身の利用環境に合わせた適切な文字列を設定してください。
export default {
async fetch(request) {
const url = new URL(request.url);
let paramUrl = url.searchParams.get("url");
if (!paramUrl) {
return new Response("Missing 'url' parameter", { status: 400 });
}
// URLエンコードされている場合はデコードを実行
let targetUrl = paramUrl;
try {
targetUrl = decodeURIComponent(paramUrl);
} catch (e) {
console.log("Decode error:", e);
}
// ※ 自身の環境・用途に合わせた User-Agent や Accept ヘッダーを定義
const headers = {
"User-Agent": "", // ここに適切な文字列を設定
"Accept": "", // ここに適切な文字列を設定
"Accept-Language": "" // ここに適切な文字列を設定
};
try {
// 渡されたURLへリクエスト
const response = await fetch(targetUrl, { method: "GET", headers, redirect: "follow" });
if (response.ok) {
let text = await response.text();
// 1. Unicode BOM 除去
text = text.replace(/^\uFEFF/, '').trim();
// 2. '<' より前の文字切捨て
const firstTag = text.indexOf('<');
if (firstTag > 0) {
text = text.substring(firstTag);
}
// 3. 正常なXMLレスポンスか検知
if (text.startsWith("<") && !text.includes("Attention Required!") && !text.startsWith("<!DOCTYPE html")) {
return new Response(text, {
status: 200,
headers: {
"Content-Type": "application/xml; charset=utf-8",
"Access-Control-Allow-Origin": "*"
}
});
}
}
} catch (e) {
console.error(`Fetch error: ${e.message}`);
}
return new Response("Failed to retrieve valid RSS feed.", { status: 502 });
}
};
Step 4: 動作確認
コードのデプロイ完了後、Workers のドメインに対してクエリパラメータ形式(?url=)で対象のRSSフィードURLを渡し、ブラウザから動作検証を行います。
以下の URL をブラウザに貼り付けてください(<> 内は任意の値で読み替えてください)。
https://twitter-proxy.<ユーザー名>.workers.dev/?url=https://twiiit.com/<X(旧Twitter)ID>/rss
今回クエリで指定した RSS の取得元の twiiit.com は、稼働中の Nitter インスタンスへ自動振り分けを行うサービスです。
単一の Nitter サーバーへ連続してアクセスし続けると、相手側サーバに対する負荷増加やアクセス制限を受けるリスクがあります。twiiit.com を経由してリクエスト先を分散させることで、安定したデータ取得が可能になります。
以下のようにブラウザ上で XML 形式の RSS フィードが表示されれば、プロキシの構築は完了です!

所感
ここまで、Cloudflare Workers を利用したプロキシを構築したことで、最適化された「XML データ」を受け取れる土台を作成しました。
一方で、必ず XML が所得できるとは言えず、失敗してしまう場合も見られますが、極めてリアルタイム性を求めるようなアプリケーションではないです。
サーバーレスかつ無料でここまで堅牢なフロントエンドが作れるのは、Cloudflare Workers ならではの大きな強みだと実感しました。
続編である「X新着ポストのDiscord通知Bot -GAS編-」 はいずれ投稿しようと思います。


