2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

概要

X(旧Twitter)にてAPIを通じて投稿を行う方法をまとめる。

前提

X側の認可サーバーにアクセスする必要があるcURLのみでサクッと動作確認する事ができない。そのため今回は慣れているlaravelを用いて動作確認を実施する。
動作確認に使用するlaravelはsailを用いて環境構築を行った。

方法

  1. https://developer.x.com/にアクセスし、ログイン

  2. 「Find the right access for you」の「Free」の「Get started」をクリック

    CleanShot 2025-07-04 at 21.58.02@2x.jpg

  3. 「Sign up for Free Account」をクリック

    CleanShot 2025-07-04 at 22.00.52@2x.jpg

  4. 「Describe all of your use cases of Twitter’s data and API」の欄になぜAPIを使う必要があるのかの説明を英文で記述、その他のチェックボックスの内容に同意できるならチェックをつけ「Submit」をクリック

    CleanShot 2025-07-04 at 22.03.50@2x.jpg

  5. 下記の様な画面に遷移

    CleanShot 2025-07-04 at 22.11.24@2x.jpg

  6. サイドバーの「Projects & Apps」をクリック

    CleanShot 2025-07-04 at 22.14.55@2x.jpg

  7. アプリ名をクリック(URLにsettingと入っている方)

    CleanShot 2025-07-04 at 22.17.54@2x.jpg

  8. Setupをクリック

    CleanShot 2025-07-04 at 22.22.59@2x.jpg

  9. 下記のように設定

    • 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を指定

    CleanShot 2025-07-04 at 22.33.45@2x.jpg
    CleanShot 2025-07-04 at 22.34.59@2x.jpg

  10. YESをクリック

    CleanShot 2025-07-04 at 22.36.13@2x.jpg

  11. クライアントIDとクライアントシークレットが表示されるので必ずメモ

    CleanShot 2025-07-04 at 22.36.34@2x.jpg

  12. Consumer KeysのRegenerateをクリック

    CleanShot 2025-07-05 at 00.49.52@2x.jpg

  13. Yes, regenerateをクリック

    CleanShot 2025-07-05 at 00.50.51@2x.jpg

  14. API KeyとAPI Key Secretが表示されるので必ずメモ

    CleanShot 2025-07-05 at 00.51.17@2x.jpg

  15. Authentication TokensのAccess Token and SecretのGenerateをクリック

    CleanShot 2025-07-05 at 00.54.07@2x.jpg

  16. Access TokenとAccess Token Secretが表示されるので必ずメモ

    CleanShot 2025-07-05 at 00.54.57@2x.jpg

  17. クライアントPC側のlaravelのプロジェクトルートにて下記コマンドを実行し必要なライブラリをインストール。

    ./vendor/bin/sail composer require abraham/twitteroauth
    
  18. .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=先にコピーしたアクセストークンシークレット
    
  19. .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'),
    ],
    
  20. 下記コマンドを実行してconfigのキャッシュをクリア

     ./vendor/bin/sail artisan config:clear
    
  21. 下記のビューを新規追加

    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>
    
  22. 下記のコントローラーを新規追加

    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');
        }
    }
    
  23. 下記のルーティングを追加

    routes/web.php
    // Twitter テスト投稿用ルート
    Route::get('/twitter/test', [TwitterController::class, 'showTestForm'])->name('twitter.test');
    Route::post('/twitter/test', [TwitterController::class, 'testPost'])->name('twitter.post');
    
  24. 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'";
    
  25. http://localhost/twitter/testにアクセスし任意の内容を入力して「ツイートを送信」をクリックして紐づくアカウントでポストされれば完了

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?