Runtripというサービスをご存知でしょうか?
Runtripではランナー向けに様々なサービスを提供していますが、その一つにSNSがあります。
このSNSへの投稿を他のSNS(X/Twitter)にも投稿したいと思い、連携アプリを作ることにしました。
仕様
実装
1. アクセストークンの取得とAPI操作
連携したいX/Twitterアカウントで 開発者アカウント に登録し、開発者用のアクセストークンを取得します。
API操作には twitteroauth を利用します。
PHPでの実装例:
<?php function tweet(string $text, string $image, string $accessToken, string $accessTokenSecret): array { $connection = new Abraham\TwitterOAuth\TwitterOAuth( $_ENV['TWITTER_API_KEY'], $_ENV['TWITTER_API_KEY_SECRET'], $accessToken, $accessTokenSecret); $connection->setDecodeJsonAsArray(true); $parameters = ['text' => $text]; if ($image) { $connection->setApiVersion('1.1'); $media = $connection->upload('media/upload', ['media' => $image]); if (!isset($media['media_id_string'])) { return $media; } $parameters['media'] = ['media_ids' => [$media['media_id_string']]]; } $connection->setApiVersion('2'); return $connection->post('tweets', $parameters, ['jsonPayload' => true]); }
3. ジャーナルの取得と新規投稿の検出
ジャーナルの取得
ジャーナルはRuntripのWebサイトから取得します。
ユーザページ: https://runtrip.jp/users/{ユーザID} にアクセスすると、HTML内にJSONデータが埋め込まれているためこれを利用します。
<script id="__NEXT_DATA__" type="application/json"> JSONデータ </script>
JSONデータの中身は以下の通り。
{ "props": { "pageProps": { …省略… "swr": { "fallback": { "https://api.runtrip.jp/v1/users/me:$get": { …省略… }, "https://api.runtrip.jp/v1/users/462:$get": { …省略… }, "https://api.runtrip.jp/v1/users/462/courses?pageNumber=0&pageSize=9:$get": { …省略… }, "https://api.runtrip.jp/v1/users/462/favorite_courses?pageNumber=0&pageSize=9:$get": { …省略… }, "https://api.runtrip.jp/v1/users/462/visited_courses?pageNumber=0&pageSize=9:$get": { …省略… }, "https://api.runtrip.jp/v1/users/462/journals?pageNumber=0&pageSize=9:$get": { …省略… "journals": [ { "user": { …省略… }, "journal": { "id": 1111663, "date": "2023-09-21 00:00:00", "createdAt": "2023-09-21 19:04:49", "updatedAt": "2023-09-21 19:04:52", "description": "Runtripアプリから、PREMIUMサービスを近日リリース予定です。詳細は後日公開、お楽しみに。", "tags": [ "runtrippremium" ], "publicationScope": 0, "viewCount": 501, "likeCount": 223, "commentCount": 1, "status": 0, "imageUrls": [ "https://d3304ij6n73kfg.cloudfront.net/prod/thumb/journal_main/1111663/1000_500" ], "videoUrl": null, "videoThumbnailUrl": null, "distance": 0, "distanceUnit": 0, "time": 0 }, "isLiked": false } ] } } }, …省略… }, …省略… }, …省略… }
キー: "https://api.runtrip.jp/v1/users/{ユーザID}/journals?pageNumber=0&pageSize=9:$get" にジャーナルが列挙されていることが確認できます。
なお、キーがAPIのURIになっていますが、このAPIを直接呼び出しても残念ながら 403 Forbidden でエラーになります...
PHPでの実装例:
<?php function getJournals(int $userId): array { $html = file_get_contents('https://runtrip.jp/users/' . $userId); $json = substrBetween($html, '<script id="__NEXT_DATA__" type="application/json">', '</script>'); $array = json_decode($json, true); $uri = 'https://api.runtrip.jp/v1/users/' . $userId . '/journals?pageNumber=0&pageSize=9:$get'; return $array['props']['pageProps']['swr']['fallback'][$uri]['journals']; }
新規投稿の検出
新規投稿の検出方法として、現在の最新のジャーナルIDを保存しておき、
定期的にジャーナルを取得して、保存しておいたジャーナルIDより新しいものがあるかによって判定します。
PHPでの実装例:
<?php function getNewJournals(int $userId, int $journalId): Generator { $journals = getJournals($userId); $i = count($journals); $take = false; while ($i) { $journal = $journals[--$i]['journal']; if ($take) { yield $journal; } if ($journal['id'] === $journalId) { $take= true; } } }
4. 投稿内容の作成
ジャーナルのJSONデータから必要な情報を抽出し、X/Twitterに投稿するテキストを作成します。
ジャーナル本文:
"description": "Runtripアプリから、PREMIUMサービスを近日リリース予定です。詳細は後日公開、お楽しみに。",
"tags": [ "runtrippremium" ],
画像URL:
"imageUrls": [ "https://d3304ij6n73kfg.cloudfront.net/prod/thumb/journal_main/1111663/1000_500" ],
ハッシュタグやテキストには、X/Twitterに投稿できない文字等が含まれている可能性があるため、バリデーションを行います。
また、テキストがX/Twitterに投稿できる長さを超える可能性があるため、最大長までジャーナル本文の一部を省略します。
これらについては、別途、以下の記事に纏めました。
appl-rot13.hatenablog.jp
5. 実行
本アプリでは最新のジャーナルIDを保存しておく必要があります。そのため、
- サーバ起動時に実行する常駐アプリとし、ジャーナルIDはメモリ上に保持する
- cron等で定期的に実行するアプリとし、ジャーナルIDはファイル等に入出力する
のように工夫をする必要があります。
私の場合、ファイルアクセスを減らしたいため、1. の方法を採用しました。
PHPでの実装例:
<?php $userId = $_ENV['RUNTRIP_USER_ID']; $journalId = getLatestJournal($userId)['id']; while (true) { $journals = getNewJournals($userId, $journalId); foreach ($journals as $journal) { $journalId = $journal['id']; // 投稿処理 } sleep($_ENV['CHECK_INTERVAL']); }
成果物
GitHubに公開しています。ご参考まで。
github.com
余談
RuntripジャーナルをWeb上から自由に閲覧できることを問題視しているユーザがいらっしゃるため、
そのうちジャーナルの取得ができなくなるかもしれませんが、あしからずご承知おきください。
