仕様
作成するBotの仕様は前記事同様、以下の通りです。
特定ジャンルで多数RTされているツイートを自動でRTするようになっています。
事前準備
- 事前に対象ジャンルのインフルエンサーをBotのアカウントで複数フォローしておく
以下を定期的に実行
- タイムライン上でRT数が一定以上のツイートをRT
- 特定のキーワード(対象ジャンルに関連の深いワード)で検索し、一定以上のRT数、かつ24時間以内に投稿されたツイートをRT
- トレンドのキーワードのうち、TLのツイートに含まれるものを対象ジャンルとみなし、該当キーワードで検索。24時間以内、かつ一定以上のRT数のツイートをRT
この機能を満たすように実装していきたいと思います。
ファイル構成
ファイル構成は以下のようにしたいと思います。
Constsフォルダのみ独自に作成。あとはLaravelで用意されたフォルダを使用しています。
ファイル構成
app/
├ Consts/
├ Constants.php … 各Twitterアカウントで使い回せる定数定義用
└ appConfig.php … 各Twitterアカウント固有の設定定義用
└ Models/
├ Tweet.php … ORM用Tweetクラス
└ TwitterAPIAccessor.php … TwitterAPI操作用ビジネスロジック
└ Http/
└ Controllers/
└ TweetController.php … Tweet関連のコントローラ
ソース
実際の実装内容はGitHubを参照ください。
肝の部分だけ書いておきます。
TwitterAPI関連
TwitterAPIを使用するビジネスロジックをTwitterAPIAccessor.phpに記述していきます。
以下では各APIのパラメータの組み合わせを実際に確認できるため便利です。
タイムライン
TwitterAPIでTLから一定以上のRTのツイートを取得する処理です。
今回はAPIv2のTimelines関数を使用し、そこから一定以上のRT数のものを取り出します。
各APIに渡すパラメータ、受け取る値は以下を参照ください。
<?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関数を使用します。
//======================================================
//
// 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が必要です。
対象キーワードで検索してキーワードを本文に含むツイートを取得します。
//======================================================
//
// 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でつぶやかれているものを対象ジャンルとみなしてセットします。
//======================================================
//
// 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オブジェクトからデータをセットする関数を用意しました。
<?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);
}
}
}
コントローラ
上記を呼び出すコントローラを実装します。
<?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に記述しています。
<?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つの関数をルーティングに追加。
<?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で定期的にアクセスすれば各処理が実行されます。