概要
X(旧Twitter)にてAPIを通じて投稿を行う方法をまとめる。
前提
X側の認可サーバーにアクセスする必要があるcURLのみでサクッと動作確認する事ができない。そのため今回は慣れているlaravelを用いて動作確認を実施する。
動作確認に使用するlaravelはsailを用いて環境構築を行った。
方法
-
https://developer.x.com/にアクセスし、ログイン
-
「Find the right access for you」の「Free」の「Get started」をクリック
-
「Sign up for Free Account」をクリック
-
「Describe all of your use cases of Twitter’s data and API」の欄になぜAPIを使う必要があるのかの説明を英文で記述、その他のチェックボックスの内容に同意できるならチェックをつけ「Submit」をクリック
-
下記の様な画面に遷移
-
サイドバーの「Projects & Apps」をクリック
-
アプリ名をクリック(URLにsettingと入っている方)
-
Setupをクリック
-
下記のように設定
- App permissions:
Read and writeを選択 - Type of App:
Web App, Automated App or Botを選択 - App info
- Callback URI / Redirect URL:
http://localhost/x/callback - Website URL:
実際に存在するドメインにてURLを指定※筆者はGithubのユーザー画面のURLを指定
- Callback URI / Redirect URL:
- App permissions:
-
YESをクリック
-
クライアントIDとクライアントシークレットが表示されるので必ずメモ
-
Consumer KeysのRegenerateをクリック
-
Yes, regenerateをクリック
-
API KeyとAPI Key Secretが表示されるので必ずメモ
-
Authentication TokensのAccess Token and SecretのGenerateをクリック
-
Access TokenとAccess Token Secretが表示されるので必ずメモ
-
クライアントPC側のlaravelのプロジェクトルートにて下記コマンドを実行し必要なライブラリをインストール。
./vendor/bin/sail composer require abraham/twitteroauth -
.envに下記のキーバリューを追加
.env# Twitter API Settings TWITTER_CLIENT_ID=先にコピーしたクライアントID TWITTER_CLIENT_SECRET=先にコピーしたクライアントシークレット TWITTER_API_KEY=先にコピーしたAPIキー TWITTER_API_SECRET=先にコピーしたAPIシークレット TWITTER_ACCESS_TOKEN=先にコピーしたアクセストークン TWITTER_ACCESS_TOKEN_SECRET=先にコピーしたアクセストークンシークレット -
.envで定義したキーバリューをlaravel内部で使えるように下記を追記
config/services.php'twitter' => [ 'client_id' => env('TWITTER_CLIENT_ID'), 'client_secret' => env('TWITTER_CLIENT_SECRET'), 'api_key' => env('TWITTER_API_KEY'), 'api_secret' => env('TWITTER_API_SECRET'), 'access_token' => env('TWITTER_ACCESS_TOKEN'), 'access_token_secret' => env('TWITTER_ACCESS_TOKEN_SECRET'), ], -
下記コマンドを実行してconfigのキャッシュをクリア
./vendor/bin/sail artisan config:clear -
下記のビューを新規追加
resources/views/twitter/test.blade.php<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Twitter テスト投稿</title> <meta name="csrf-token" content="{{ csrf_token() }}"> <style> body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; background-color: #f5f5f5; } .container { background-color: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } h1 { color: #1da1f2; text-align: center; margin-bottom: 30px; } .form-group { margin-bottom: 20px; } label { display: block; margin-bottom: 5px; font-weight: bold; color: #333; } textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; resize: vertical; font-family: inherit; } .char-count { text-align: right; font-size: 12px; color: #666; margin-top: 5px; } .btn { background-color: #1da1f2; color: white; padding: 12px 30px; border: none; border-radius: 25px; cursor: pointer; font-size: 16px; font-weight: bold; width: 100%; transition: background-color 0.3s; } .btn:hover { background-color: #1991db; } .btn:disabled { background-color: #ccc; cursor: not-allowed; } .alert { padding: 15px; margin-bottom: 20px; border-radius: 5px; } .alert-success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; } .alert-error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } .loading { display: none; text-align: center; margin: 20px 0; } </style> </head> <body> <div class="container"> <h1>Twitter テスト投稿</h1> <div id="alert-container"></div> <form id="tweetForm"> <div class="form-group"> <label for="message">投稿内容</label> <textarea id="message" name="message" rows="4" placeholder="ツイート内容を入力してください..." maxlength="280"></textarea> <div class="char-count"> <span id="charCount">0</span>/280 </div> </div> <div class="loading" id="loading"> 投稿中... </div> <button type="submit" class="btn" id="submitBtn"> ツイートを投稿 </button> </form> </div> <script> document.addEventListener('DOMContentLoaded', function() { const form = document.getElementById('tweetForm'); const messageInput = document.getElementById('message'); const charCount = document.getElementById('charCount'); const submitBtn = document.getElementById('submitBtn'); const loading = document.getElementById('loading'); const alertContainer = document.getElementById('alert-container'); // 文字数カウント messageInput.addEventListener('input', function() { const length = this.value.length; charCount.textContent = length; if (length > 280) { charCount.style.color = '#e0245e'; submitBtn.disabled = true; } else { charCount.style.color = '#666'; submitBtn.disabled = false; } }); // フォーム送信 form.addEventListener('submit', function(e) { e.preventDefault(); const message = messageInput.value.trim(); if (!message) { showAlert('メッセージを入力してください', 'error'); return; } submitBtn.disabled = true; loading.style.display = 'block'; fetch('/twitter/test', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content') }, body: JSON.stringify({ message: message }) }) .then(response => { console.log('Response status:', response.status); console.log('Response headers:', response.headers); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { console.log('Response data:', data); if (data.success) { showAlert('ツイートが正常に投稿されました!', 'success'); if (data.tweet_url) { showAlert(`<a href="${data.tweet_url}" target="_blank">投稿を確認する</a>`, 'success'); } messageInput.value = ''; charCount.textContent = '0'; } else { showAlert('投稿に失敗しました: ' + (data.message || '不明なエラー'), 'error'); if (data.error) { console.error('Server error:', data.error); } } }) .catch(error => { console.error('Fetch error:', error); showAlert('エラーが発生しました: ' + error.message, 'error'); }) .finally(() => { submitBtn.disabled = false; loading.style.display = 'none'; }); }); function showAlert(message, type) { const alert = document.createElement('div'); alert.className = `alert alert-${type}`; alert.innerHTML = message; alertContainer.appendChild(alert); setTimeout(() => { alert.remove(); }, 5000); } }); </script> </body> </html> -
下記のコントローラーを新規追加
app/Http/Controllers/TwitterController.php<?php namespace App\Http\Controllers; use Abraham\TwitterOAuth\TwitterOAuth; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; class TwitterController extends Controller { private $twitter; public function __construct() { $this->twitter = new TwitterOAuth( config('services.twitter.api_key'), config('services.twitter.api_secret'), config('services.twitter.access_token'), config('services.twitter.access_token_secret') ); // API バージョンを2に設定 $this->twitter->setApiVersion('2'); } public function testPost(Request $request) { Log::info('Twitter test post started', [ 'request' => $request->all(), 'method' => $request->method(), 'url' => $request->url(), 'headers' => $request->headers->all() ]); try { $request->validate([ 'message' => 'required|string|max:280', ]); } catch (\Illuminate\Validation\ValidationException $e) { Log::error('Validation failed', ['errors' => $e->errors()]); return response()->json([ 'success' => false, 'message' => 'バリデーションエラー', 'errors' => $e->errors() ], 422); } try { // 認証情報をログに出力(セキュリティを考慮して一部マスク) Log::info('Twitter credentials check', [ 'api_key' => config('services.twitter.api_key') ? 'SET' : 'NOT SET', 'api_secret' => config('services.twitter.api_secret') ? 'SET' : 'NOT SET', 'access_token' => config('services.twitter.access_token') ? 'SET' : 'NOT SET', 'access_token_secret' => config('services.twitter.access_token_secret') ? 'SET' : 'NOT SET' ]); // TwitterOAuthオブジェクトの確認 if (!$this->twitter) { Log::error('Twitter connection is null'); return response()->json([ 'success' => false, 'message' => 'Twitter接続オブジェクトが初期化されていません' ], 500); } Log::info('Attempting to post tweet', ['message' => $request->message]); // Twitter API v2を使用 $tweet = $this->twitter->post('tweets', [ 'text' => $request->message ]); $httpCode = $this->twitter->getLastHttpCode(); Log::info('Twitter API response', [ 'http_code' => $httpCode, 'response' => $tweet ]); if ($httpCode == 201) { $tweetId = $tweet->data->id ?? null; Log::info('Tweet posted successfully', ['tweet_id' => $tweetId]); return response()->json([ 'success' => true, 'message' => 'ツイートが正常に投稿されました', 'tweet_id' => $tweetId, 'tweet_url' => $tweetId ? "https://twitter.com/user/status/{$tweetId}" : null ]); } else { Log::error('Failed to post tweet', [ 'http_code' => $httpCode, 'response' => $tweet ]); return response()->json([ 'success' => false, 'message' => 'ツイートの投稿に失敗しました', 'error' => $tweet, 'http_code' => $httpCode ], 400); } } catch (\Exception $e) { Log::error('Twitter API error', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), 'file' => $e->getFile(), 'line' => $e->getLine() ]); return response()->json([ 'success' => false, 'message' => 'API接続エラーが発生しました', 'error' => $e->getMessage(), 'debug' => [ 'file' => $e->getFile(), 'line' => $e->getLine() ] ], 500); } } public function showTestForm() { return view('twitter.test'); } } -
下記のルーティングを追加
routes/web.php// Twitter テスト投稿用ルート Route::get('/twitter/test', [TwitterController::class, 'showTestForm'])->name('twitter.test'); Route::post('/twitter/test', [TwitterController::class, 'testPost'])->name('twitter.post'); -
bladeにscriptタグで記載したjsの実行を許容するよう(Dev toolsのconsoleでエラーが出ないよう)に指定 ※セキュリティリスクがあるのは承知の上で動作確認を優先するための実装、本番環境では絶対にNG、悪意のあるjsの処理が実行される可能性あり
app/Http/Middleware/SecurityHeaders.php// Content Security Policy $csp = "default-src 'self'; " . "style-src 'self' 'unsafe-inline'; " . "script-src 'self' 'unsafe-inline'; " . // 本行の'unsafe-inline'を追記 "img-src 'self' data:; " . "font-src 'self'; " . "connect-src 'self'; " . "frame-ancestors 'none'"; -
http://localhost/twitter/testにアクセスし任意の内容を入力して「ツイートを送信」をクリックして紐づくアカウントでポストされれば完了















