PHP + Laravel でTwitterBot作成 #2『実装』

## 仕様

作成するBotの仕様は<a href = "https://qiita.com/H_Tsukamoto/items/48a5e13dba3c79911fa5" target="_blank">前記事</a>同様、以下の通りです。
特定ジャンルで多数RTされているツイートを自動でRTするようになっています。

:::note warn 
<strong>事前準備</strong>
- 事前に対象ジャンルのインフルエンサーをBotのアカウントで複数フォローしておく
:::

:::note 
<strong>以下を定期的に実行</strong>
- タイムライン上でRT数が一定以上のツイートをRT
- 特定のキーワード（対象ジャンルに関連の深いワード）で検索し、一定以上のRT数、かつ24時間以内に投稿されたツイートをRT
- トレンドのキーワードのうち、TLのツイートに含まれるものを対象ジャンルとみなし、該当キーワードで検索。24時間以内、かつ一定以上のRT数のツイートをRT
:::

この機能を満たすように実装していきたいと思います。

　
## ファイル構成

ファイル構成は以下のようにしたいと思います。
Constsフォルダのみ独自に作成。あとはLaravelで用意されたフォルダを使用しています。

:::note warn 
<strong>ファイル構成</strong>
app/ 
　　├ Consts/
　　　　├ <strong>Constants.php</strong> … 各Twitterアカウントで使い回せる定数定義用
　　　　└ <strong>appConfig.php</strong> … 各Twitterアカウント固有の設定定義用
　　└ Models/
    　　　　├ <strong>Tweet.php</strong> … ORM用Tweetクラス
　　　　└ <strong>TwitterAPIAccessor.php</strong> … TwitterAPI操作用ビジネスロジック
　　└ Http/
　　　　└ Controllers/
　　　　　　└ <strong>TweetController.php</strong> … Tweet関連のコントローラ
:::

　
## ソース

実際の実装内容はGitHubを参照ください。
肝の部分だけ書いておきます。

https://github.com/tsubame/FootRTBot-PHP

　
## TwitterAPI関連
　
TwitterAPIを使用するビジネスロジックをTwitterAPIAccessor.phpに記述していきます。
以下では各APIのパラメータの組み合わせを実際に確認できるため便利です。

https://developer.twitter.com/apitools/api?endpoint=%2F2%2Fusers%2F%7Bid%7D%2Ftimelines%2Freverse_chronological&method=get


### タイムライン

TwitterAPIでTLから一定以上のRTのツイートを取得する処理です。
今回はAPIv2のTimelines関数を使用し、そこから一定以上のRT数のものを取り出します。

各APIに渡すパラメータ、受け取る値は以下を参照ください。

https://developer.twitter.com/en/docs/twitter-api/tweets/timelines/quick-start/reverse-chron-quick-start

```TwitterAPIAccessor.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
use Abraham\TwitterOAuth\TwitterOAuth;
use App\Consts\Constants;
use App\Models\Tweet;

/**
 * TwitterAPIアクセス用クラス
 * 
 * [索引]
 * 　□ 0. コンストラクタ
 * 　□ 1. タイムラインからツイートを取得
 * 　□ 2. 1件のツイートのRTを実行
 * 　□ 3. 対対象キーワードの検索結果のツイートデータを取得
 * 　□ 4. トレンドのキーワードのうち、ホームタイムラインでつぶやかれているキーワード一覧を取得
 */
class TwitterAPIAccessor
{
    // API connection
    private $connection;

    // 自分のTwitterID（数値文字列形式。APIから取得）
    private $myTwitterID = '';

    //======================================================
    //
    // 0. コンストラクタ
    //
    //======================================================

    /**
     * コンストラクタ
     * 　・TwitterAPI接続
     * 　・自分のTwitterIDをセット
     */
    function __construct() {
        try {
            // API接続
            $this->connection = new TwitterOAuth(Constants::API_KEY, Constants::API_SECRET, Constants::ACCESS_TOKEN_KEY, Constants::ACCESS_TOKEN_SECRET);
            // 自分のTwitterIDをセット
            $this->setMyTwitterID();
        } catch (\Exception $e) {
            Log::error($e);
        }
    }

    //======================================================
    // 自分のTwitterIDをセット
    //======================================================

    /**
     * 自分のTwitterIDをセット
     * 　・数値文字列形式のIDをセット
     * 
     * @return void
     */
    public function setMyTwitterID()
    {
        try {
            // API Ver2を使用
            $this->connection->setApiVersion("2");
            // 自分のTwitterIDをセット
            $res = $this->connection->get('users/me', ['expansions'=> ['pinned_tweet_id']]);
            $this->myTwitterID = $res->data->id;
        } catch (\Exception $e) {
            Log::error($e);
        }
    }

    //======================================================
    //
    // 1. タイムラインからツイートを取得
    //
    //======================================================

    /**
     * タイムラインからツイートを取得
     * 　・APIv2でタイムラインからデータを取得
     * 
     * @return array
     */
    public function getTweetsFromTimeLine()
    {
        $tweets = [];

        try {              
            // APIv2でタイムラインからデータを取得
            $this->connection->setApiVersion('2');            
            $res = $this->connection->get("users/{$this->myTwitterID}/timelines/reverse_chronological", 
                ['tweet.fields' => ['public_metrics,created_at,source,author_id'], 'user.fields' => ['name,username'], 'expansions'=> ['referenced_tweets.id,author_id']]);   

            // エラー時はロギング
            if (isset($res->errors)) {
                Log::error("Twitter API TimeLine Connect Error. {$res->errors[0]->message}");

                return $tweets;
            }

            // APIのツイートを走査
            foreach($res->data as $d) {
                // APIのユーザオブジェクトデータをセット
                $u = $this->getUserDataFromAPIResVal($res, $d->author_id);
                // APIのツイートオブジェクトデータからTweetクラスのデータをセット
                $tw = new Tweet;    
                $tw->setValsFromAPITwObj($d, $u);

                // 配列に追加
                array_push($tweets, $tw);
            }                              
        } catch (\Exception $e) {
            Log::error($e);
        }

        return $tweets;
    }

    //======================================================
    // APIのレスポンスデータから該当ユーザのオブジェクトデータを返す
    //======================================================

    /**
     * APIのレスポンスデータから該当ユーザのオブジェクトデータを返す
     * 　・APIのinclueds->usersフィールドから該当ユーザIDに該当する要素を返す
     * 
     * @param mixed $apiRes
     * @param string $uid
     * @return mixed
     */
    function getUserDataFromAPIResVal($apiRes, $uid)
    {
        try {
            // APIのユーザデータを連想配列にセット                
            foreach($apiRes->includes->users as $u) {
                if ($u->id == $uid) {
                    return $u;
                }
            }
        } catch (\Exception $e) {
           Log::error($e);
        }

        return null;
    }
```

APIv2では自分のTwitterIDを使用するものが多いため、コンストラクタではAPIへの接続、自分のTwitterID(数値文字列形式)のTwitterAPIからの取得処理を実装しています。


### リツイート

続けてリツイート処理を実装。
v2のretweet関数を使用します。

```TwitterAPIAccessor.php
    //======================================================
    //
    // 2. 1件のツイートのRTを実行
    //
    //======================================================

     /**
      * 1件のツイートのRTを実行
      * 　・RT実行
      * 　・ロギング、画面上に出力
      *
      * @param Tweet $tw 
      * @return void
      */
     public function retweetTargetTweet(Tweet $tw) {
        try {  
            // RT実行    
            $this->connection->setApiVersion('2');
            $res = $this->connection->post("users/{$this->myTwitterID}/retweets", ['tweet_id' => $tw->id_str_in_twitter], true);
            // エラー時はロギング
            if (isset($res->errors)) {
                Log::error("Twitter API Retweet Error. {$res->errors[0]->message}");

                return;
            }

            // 結果をロギング、画面上に出力
            $txt = '  [RT実施]' . $tw->user_name . ' (' . $tw->client_name . ')' . $tw->rt_count . 'RT ' . $tw->tweet_text;
            echo($txt);
            Log::debug($txt);            
        } catch (\Exception $e) {
            Log::error($e);
        }
    }
```


### 検索

続けて検索処理。APIv2使用。
こちらもAPI発行時にはTwitterIDが必要です。

対象キーワードで検索してキーワードを本文に含むツイートを取得します。

```TwitterAPIAccessor.php
    //======================================================
    //
    // 3. 対象キーワードの検索結果のツイートデータを取得
    //
    //======================================================

    /**
     * 対象キーワードの検索結果のツイートデータを取得
     * 　・検索用のパラメータをセット。RT、メンションは除外。日本語のみを検索。24時間以内のツイートのみを取得
     * 　・APIv2で検索実行
     * 　・本文にキーワードが含まれないツイートはスキップ（名前も検索に引っかかるため）
     * 　・NGワードを含むツイートはスキップ
     * 
     * @param string q 検索キーワード
     * @return array Tweetデータの配列
     */
    public function getTweetsBySearch(String $q) 
    {        
        $tweets = [];

        try {
            // 検索用のパラメータをセット
            $params = $this->getSearchParams($q);
            // APIv2で検索実行
            $this->connection->setApiVersion('2');
            $res = $this->connection->get('tweets/search/recent', $params);
            // エラー時はロギング
            if (isset($res->errors)) {
                Log::error("Twitter Search Retweet Error. {$res->errors[0]->message}");

                return $tweets;
            } 

            // APIの検索結果のツイートを走査
            foreach($res->data as $d) {
                // ユーザデータをセット
                $u = $this->getUserDataFromAPIResVal($res, $d->author_id);                              
                // Tweetデータをセット
                $tw = new Tweet;
                $tw->setValsFromAPITwObj($d, $u);

                // ツイート本文にキーワードを含まなければスキップ
                if (strpos($tw->tweet_text, $q) === false) {
                    continue;
                // NGワードを含めばスキップ
                } elseif ($this->checkTargetTweetContainsNGWords($tw)) {
                    continue;
                }

                array_push($tweets, $tw);                                
            }             
        } catch (\Exception $e) {
            Log::error($e);
        }

        return $tweets;
    }

    //======================================================
    // 検索用のパラメータを返す
    //======================================================

    /**
     * 検索用のパラメータを返す
     * 　・RT、メンションは除外。日本語のみを検索
     * 　・24時間以内のツイートのみを取得
     * 　・関連性の高いツイートを取得
     * 
     * @param string $q
     * @return array
     */
    function getSearchParams(string $q)
    {
        $params = [];

        try {
            // 検索キーワードをセット。キーワード + 日本語のみ、かつRT、メンション除外
            $qParamTxt = $q . Constants::SEARCH_KEYWORD_POSTFIX;
            // 24時間前をテキストでセット
            $sdtTxt = date('c', strtotime('now -' . Constants::SKIP_PAST_HOUR .' hours'));
            // パラメータをセット
            $params = [
                'query'        => $qParamTxt,
                'start_time'   => $sdtTxt,
                'tweet.fields' => 'public_metrics,referenced_tweets,created_at,source,author_id', 
                'expansions'   => 'author_id,referenced_tweets.id',
                'user.fields'  => 'name,username',
                'sort_order'   => Constants::SEARCH_TWEET_BY_RECENCY_OR_RELEVANT,
                'max_results'  => Constants::SEARCH_COUNT 
            ];
        } catch (\Exception $e) {
            Log::error($e);
        }

        return $params;
    }

    //======================================================
    // 該当ツイートがNGワードを含むかを返す
    //======================================================

    /**
     * 該当ツイートがNGワードを含むかを返す
     * 　・定数のNGワードを本文、ユーザ名、TwitterClientに含めばTrue
     * 
     * @param Tweet $tw
     * @return bool
     */
    private function checkTargetTweetContainsNGWords(Tweet $tw)
    {
        try {
            foreach (Constants::RT_NG_KEYWORDS as $q) {
                // 本文に含めばTrue
                if (strpos($tw->tweet_text, $q) !== false) {
                    return true;
                }
                // クライアント名に含めばTrue
                if (strpos($tw->client_name, $q) !== false) {
                    return true;
                }
                // 名前に含めばTrue
                if (strpos($tw->user_name, $q) !== false) {
                    return true;
                }
            }
        } catch (\Exception $e) {
            Log::error($e);
        }

        return false;
    }
```

### トレンド

トレンドから対象ジャンルのキーワードを取得する処理。APIv1.1を使用。
現在の日本のトレンドのワードのうち、自分のアカウントのTLでつぶやかれているものを対象ジャンルとみなしてセットします。

```TwitterAPIAccessor.php
    //======================================================
    //
    // 4. トレンドのキーワードのうち、ホームタイムラインでつぶやかれているキーワード一覧を取得
    //
    //======================================================

    /**
     * トレンドのキーワードのうち、ホームタイムラインでつぶやかれているキーワード一覧を取得
     *　 ・日本のトレンドのキーワードを取得
     * 　・タイムラインのツイートを取得
     * 　・タイムラインに含まれているツイートのみを配列にセット
     * 
     * @return array キーワードの配列 
     */
    public function getTrendKeywordsInHomeTimeLine()
    {
        $relatedTrWords = [];

        try { 
            // 日本のトレンドのキーワードを取得
            $allTrWords = $this->getJPTrendWords();
            // タイムラインのツイートを取得
            $htTweets = $this->getTweetsFromTimeLine();
            // トレンドのキーワードを走査
            foreach ($allTrWords as $q) {
                // タイムラインに含まれなければスキップ
                if (!$this->checkTimeLineTweetsContainsTargetWord($htTweets, $q)) {
                    continue;
                }

                // デバッグ用ロギング
                Log::debug("[トレンドから取得した検索ワード] {$q}");
                // 配列に追加
                array_push($relatedTrWords, $q);
            }
        } catch (\Exception $e) {
            Log::error($e);
        }

        return $relatedTrWords;
    }

    //======================================================
    // 日本のトレンドのキーワードを配列で取得
    //======================================================

    /**
     * 日本のトレンドのキーワードを配列で取得
     * 
     * @return array
     */
    function getJPTrendWords()
    {
        $trWords = [];

        try { 
            // 日本のトレンドを取得
            $this->connection->setApiVersion('1.1');                   
            $res = $this->connection->get('trends/place', ['id' => Constants::JAPAN_WOEID]);
            // エラー時はロギング
            if (isset($res->errors)) {
                Log::error("Twitter Trend Retweet Error. {$res->errors[0]->message}");

                return $trWords;
            } 

            // キーワードを配列に追加
            foreach ($res[0]->trends as $tr) {
                array_push($trWords, $tr->name);
            }
        } catch (\Exception $e) {
            Log::error($e);
        }

        return $trWords;
    }

    //======================================================
    // タイムラインのツイートに該当キーワードが含まれているかを返す
    //======================================================

    /**
     * タイムラインのツイートに該当キーワードが含まれているかを返す
     * 　・RTされたツイートはスキップ（フォローしているユーザ以外のツイートは該当ジャンル以外を含む可能性があるため）
     * 
     * @param array $tweets タイムラインのツイートデータ
     * @param string $q キーワード
     * @return bool
     */
    function checkTimeLineTweetsContainsTargetWord(array $htTweets, string $q)
    {
        try {
            // ツイートを走査
            foreach ($htTweets as $tw) {
                // RTされたツイートはスキップ
                if (strpos($tw->tweet_text, 'RT @') !== false) {
                    continue;
                }

                // キーワードを含めばtrue
                if (strpos($tw->tweet_text, $q) !== false) {
                    return true;
                }
            }
        } catch (\Exception $e) {
            Log::error($e);
        }

        return false;
    }

```
 
　
## モデル

続けてTweetテーブルのモデル。
APIv2のResponseオブジェクトからデータをセットする関数を用意しました。

```Tweet.php
<?php

namespace App\Models;

use PHPUnit\TextUI\XmlConfiguration\Constant;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;

/**
 * TweetテーブルのORM用クラス
 * 
 * [索引]
 * 　□ 1. TwitterAPIレスポンスからデータをセット
 */
class Tweet extends Model
{
    use HasFactory;

    //======================================================
    //
    // 1. TwitterAPIレスポンスからデータをセット
    //
    //======================================================

    /**
     * TwitterAPIレスポンスからデータをセット
     * 　・ユーザ名、ユーザの@形式のアカウント名はAPIレスポンスのユーザのオブジェクトデータからセット
     * 　・上記以外はAPIレスポンスのツイートのオブジェクトデータからセット
     * 　・本文は改行を除去してセット
     * 
     * @param mixed $td APIレスポンスのdata内の1件のツイートオブジェクト
     * @param mixed $ud APIレスポンスのinclues->users内の該当ユーザのオブジェクト
     * @return void
     */
    public function setValsFromAPITwObj($td, $ud)
    {        
        try {
            // ツイートID
            $this->id_str_in_twitter = $td->id;
            // ユーザ名
            $this->user_name = $ud->name;
            // ユーザの@形式のアカウント名
            $this->user_screen_name = $ud->username;
            // 本文。改行を除去してセット
            $this->tweet_text = str_replace(array("\r\n", "\r", "\n"), '', $td->text);
            // RT数
            $this->rt_count = (int)$td->public_metrics->retweet_count;
            // クライアント名
            $this->client_name = $td->source;
            // 投稿日時
            $this->posted_date = date($td->created_at);
        } catch (\Exception $e) {
            Log::error($e);
        }            
    }
}

```
　

## コントローラ

上記を呼び出すコントローラを実装します。

```TweetController.php 
<?php

namespace App\Http\Controllers;

use App\Models\TwitterAPIAccessor;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
use App\Consts\Constants;
use App\Models\Tweet;

/**
 * ツイート関連のコントローラクラス
 * 
 * [索引]
 *  □ 1. タイムラインからRTを実施
 *  □ 2. 検索キーワードからRTを実施
 *  □ 3. トレンドのキーワードからRTを実施
 */
class TweetController extends Controller
{

    //======================================================
    //
    // 1. タイムラインからRTを実施
    //
    //======================================================

    /**
     * タイムラインからRTを実施
     * 　・タイムラインからツイートを取得
     * 　・リツイート対象外（一定のRT数未満、かつDB保存済）のツイートはスキップ
     * 　・該当ツイートをRTしDBに保存
     */
    public function rtFromTimeLine(Request $request, Response $response) {
        try {
            // タイムラインからツイートを取得
            $twApiAccessor = new TwitterAPIAccessor();
            $tweets = $twApiAccessor->getTweetsFromTimeLine();
            // ツイートを走査
            foreach ($tweets as $tw) {
                // リツイート対象外ならスキップ
                if (!$this->isRTTarget($tw)) {
                    continue;
                }

                // DB保存
                $tw->save();
                // RT実行
                $twApiAccessor->retweetTargetTweet($tw);   
            }
        } catch (\Exception $e) {
            Log::error($e);
        }

        return $response;
    }

    //======================================================
    // RT対象かを返す
    //======================================================

    /**
     * RT対象かを返す
     * 　・DB保存済ならfalse
     * 　・RT数が一定未満ならfalse
     * 　・該当ツイートが他のツイートをRTしたものであり、該当ツイートをスキップする設定の場合はfalse
     * 
     * @param Tweet $tw
     * @return bool
     */
    function isRTTarget(Tweet $tw)
    {
        try {
            // DB保存済ならfalse
            $res = Tweet::where('id_str_in_twitter', $tw->id_str_in_twitter)->exists();
            if ($res) {
                return false;
            }
            // RT数が一定未満ならfalse
            if ($tw->rt_count < Constants::RETWEET_LEAST_RT) {
                return false;
            }
            // 該当ツイートが他のツイートをRTしたものであり、スキップする設定ならfalse
            if (strpos($tw->tweet_text, Constants::RT_TWEET_KEYWORD)) {
                if (Constants::SKIP_TIMELINE_RT_TWEET) {
                    return false;
                }
            }
        } catch (\Exception $e) {
            Log::error($e);
        }

        return true;
    }

    //======================================================
    //
    // 2. 検索キーワードからRTを実施
    //
    //======================================================

    /**
     * 検索キーワードからRTを実施
     * 　・定数の検索キーワードで検索してツイートを取得
     * 　・一定のRT数未満、かつDB保存済（RT済）のツイートはスキップ
     * 　・該当ツイートをRTしDBに保存
     */
    public function rtFromSearch(Request $request, Response $response) {
        try {
            $twApiAccessor = new TwitterAPIAccessor();

            // 検索キーワードを走査
            foreach(Constants::SEARCH_TARGET_KEYWORDS as $q) {                
                // 検索キーワードからツイートを取得
                $tweets = $twApiAccessor->getTweetsBySearch($q);
                // ツイートを走査
                foreach ($tweets as $tw) {
                    // リツイート対象外ならスキップ
                    if (!$this->isRTTarget($tw)) {
                        continue;
                    }

                    // RT実施
                    $twApiAccessor->retweetTargetTweet($tw);
                    // DB保存
                    $tw->save();
                }
            }
        } catch (\Exception $e) {
            Log::error($e);
        }

        return $response;
    }

    //======================================================
    //
    // 3. トレンドのキーワードからRTを実施
    //
    //======================================================

    /**
     * トレンドのキーワードからRTを実施
     */
    public function rtFromTrend(Request $request, Response $response) {
        try {
            // トレンドのキーワードのうち、タイムラインに含まれるのものを取得
            $twApiAccessor = new TwitterAPIAccessor();
            $trWords = $twApiAccessor->getTrendKeywordsInHomeTimeLine();

            // キーワードを走査
            foreach ($trWords as $q) {
                // 検索キーワードからツイートを取得
                $tweets = $twApiAccessor->getTweetsBySearch($q);
                foreach ($tweets as $tw) {
                    // リツイート対象外ならスキップ
                    if (!$this->isRTTarget($tw)) {
                        continue;
                    }

                    // RT実施
                    $twApiAccessor->retweetTargetTweet($tw);
                    // DB保存
                    $tw->save();
                }                
            }
        } catch (\Exception $e) {
            Log::error($e);
        }

        return $response;
    }
}
```

また、DB関連の操作はLaravelだと簡単なので、CRUD操作もコントローラ内で実装しています。
　
## 定数

定数の定義です。
アカウント固有の設定やAPIのキーは別ファイルAppConfig.phpに記述しています。

```Constants.php
<?php

namespace App\Consts;
use App\Consts\AppConfig;

/**
 * 定数定義用クラス
 * 　・複数アカウントで使い回せる値を定義
 * 　・アカウント固有の値はAppConfigに記載
 */
class Constants 
{
    //======================================================
    // リツイート設定
    //======================================================

    // この時間(h)より以前のツイートは無視
    const SKIP_PAST_HOUR = 24;

    // TLから1度に取得するツイートの数
    const TWEET_GET_COUNT_FROM_TL = 200;

    // 検索時、最新のツイートを検索するか　recency = 最新のツイートを検索, relevancy = 関連性の高いツイートを検索
    const SEARCH_TWEET_BY_RECENCY_OR_RELEVANT = 'relevancy';

    // 検索時、何件のツイートを検索するか　MAX:100
    const SEARCH_COUNT = 100;

    // 他の人のツイートをRTしているツイートに含まれるキーワード
    const RT_TWEET_KEYWORD = 'RT @';

    // この数以上のRT数でリツイート
    const RETWEET_LEAST_RT = AppConfig::RETWEET_LEAST_RT;

    // タイムライン上でRTされたツイートをRT対象外にするか
    const SKIP_TIMELINE_RT_TWEET = AppConfig::SKIP_TIMELINE_RT_TWEET;


    //======================================================
    // 検索キーワード
    //======================================================

    // 検索キーワード　このキーワードで検索し、RT数の多いツイートをRT
    const SEARCH_TARGET_KEYWORDS = AppConfig::SEARCH_TARGET_KEYWORDS;

    // RT時のNGワード　このキーワードを含むツイートはRTしない
    const RT_NG_KEYWORDS = AppConfig::RT_NG_KEYWORDS;

    // 検索時にキーワードの末尾につける文字列。日本語、RT以外、メンション以外をセット
    const SEARCH_KEYWORD_POSTFIX = ' lang:ja -is:retweet -has:mentions';


    //======================================================
    // トレンド
    //======================================================

    // 日本のWOEID　トレンドで使用
    const JAPAN_WOEID = 23424856;


    //======================================================
    // Twitterアカウント
    //======================================================

    // API_KEY
    const API_KEY = AppConfig::API_KEY;

    // API Secret
    const API_SECRET = AppConfig::API_SECRET;

    // AccessToken Key
    const ACCESS_TOKEN_KEY = AppConfig::ACCESS_TOKEN_KEY;
    // AccessToken Secret
    const ACCESS_TOKEN_SECRET = AppConfig::ACCESS_TOKEN_SECRET;
}

```

　
## ルーティング

web.phpにはコントローラの3つの関数をルーティングに追加。

```web.php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\TweetController;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

// ルーティング　タイムラインからRTを実施
Route::get('/timeline', [TweetController::class, 'rtFromTimeLine']);

// ルーティング 検索キーワードからRTを実施
Route::get('/search', [TweetController::class, 'rtFromSearch']);

// ルーティング トレンドの関連キーワードからRTを実施
Route::get('/trend', [TweetController::class, 'rtFromTrend']);
```

　
以上で実装完了です。
あとは本番環境に置いてcurlで定期的にアクセスすれば各処理が実行されます。