PHP
Twitter
laravel
bot

Laravelで簡単なTwitter botを作ってみた

はじめに

Laravelでなにか軽めのやつ作りたいなあ
と考えていたところ

〇〇「やれ」
✕✕「はい」

というジョークが目に入りました。

少し前に話題になった人 が話題をもみ消すために
最近話題になった人 に指示を出しているというやつ。

「これTwitter botにできるじゃん!」と思い早速つくってみました。

完成物

Twitter
ソースコード

仕組み

話題(トレンド)を取得するために Twitter のAPIを使用。
ざっくりこんなんです。
yare_hai仕組み.jpg

  1. Twitterからトレンドを取得、DBに保存
    1. TwitterAPIに認証
    2. トレンド取得のAPIを叩く
    3. レスポンスからトレンドを取り出しDBに保存   
  2. DB内のトレンドデータからツイート
    1. 24時間前と直近のトレンド情報をDBから取得する
    2. 24時間前にはあって直近にはないトレンドワード、またその逆のトレンドワードを絞り込む
    3. 絞り込んだトレンドワードを使ってツイート内容を作成
    4. ツイート

(24時間前のトレンドワード)「やれ」
(直近のトレンドワード)「はい」

と呟ければ完成です。

環境

OS Ubuntu 16.04.4
PHP 7.2.6
MySQL 5.7.22
Laravel Framework 5.6.23

PHPとMySQLはDocker上で動かしています。

migrationファイル、Trendモデルの作成

php artisan make:migration create_trends_table

のコマンドでmigrationファイルを作成します。

create_trends_table.php
    public function up()
    {
        Schema::create('trends', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('place_id');
            $table->timestamp('created_at');
        });
    }

テーブル構造を記述したらマイグレーションを実行します。

次に

php artisan make:model Trend

でTrendモデルを作成します。

Trend.php
class Trend extends Model
{
    protected $table = 'trends';
}

今回は作ったはいいものの特に何も設定しませんでした。

Twitter認証用クラス

Twitterに認証する箇所が複数あるので、app/Libs内に認証用クラスを作成します。
TwitterOAuthライブラリを使用しています。

TwitterConnection.php
class TwitterConnection
{
    public function connect()
    {
        return new TwitterOAuth(
            env('API_KEY'),
            env('API_SECRET'),
            env('ACCESS_TOKEN'),
            env('ACCESS_TOKEN_SECRET')
        );
    }
}

また、呼び出しやすくするためにFacadeを作成しました。

Twitterへの認証情報は..envファイルに記述しておきます。

.env
API_KEY=(Consumer Key (API Key))
API_SECRET=(Consumer Secret (API Secret))
ACCESS_TOKEN=(Access Token)
ACCESS_TOKEN_SECRET=(Access Token Secret)

トレンド取得、保存コマンド

php artisan make:command InsertTrends

でトレンドを取得し保存するコマンドクラスのスケルトンを作成します。
実際の処理はhandleメソッド内に記述します。
先程作成したTwitter認証用メソッドで認証し、トレンド取得APIを叩いた結果を保存します。

InsertTrends.php
<?php

namespace App\Console\Commands;

use App\Trend;
use Carbon\Carbon;
use Illuminate\Console\Command;

class InsertTrends extends Command
{

    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = '8081:instrd {place_id} {trend_count}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Get twitter trends and save them.';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        // TwitterAPIからトレンド情報を取得する
        $twitter = \TwitterConnection::connect();
        $apiResult = json_decode(
            $twitter->OAuthRequest(
                "https://api.twitter.com/1.1/trends/place.json",
                "GET",
                ["id" => $this->argument('place_id')]
            ),
            true
        );

        // 取得したトレンド情報をDBに挿入
        $now = Carbon::now();
        $trends = [];

        for ($idx = 0; $idx < $this->argument('trend_count'); $idx++) {
            array_push($trends, [
                'name' => $apiResult[0]['trends'][$idx]['name'],
                'place_id' => $this->argument('place_id'),
                'created_at' => $now,
            ]);
        }

        Trend::insert($trends);
    }
}

ツイートコマンド

DB内から24時間前と直近のトレンドワードを取得し、比較してツイートを作成します。
collectionクラスの配列操作は便利ですね。

TweetJoke.php
<?php

namespace App\Console\Commands;

use App\Trend;
use Carbon\Carbon;
use Illuminate\Console\Command;

class TweetJoke extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = '8081:twtjk {place_id} {trend_count}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Tweet a joke from query results.';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        // 直近と一日前のトレンドデータをDBから取得
        $createdFrom = (new Carbon(Carbon::now()->format('Y-m-d H:i') . ':00'))->subDay();

        $pastTrends = Trend::select('name')
            ->where('created_at', '>=', $createdFrom)
            ->where('place_id', $this->argument('place_id'))
            ->orderBy('id', 'asc')
            ->take($this->argument('trend_count'))
            ->get()
            ->pluck('name');

        $recentTrends = Trend::select('name')
            ->where('place_id', $this->argument('place_id'))
            ->orderBy('id', 'desc')
            ->take($this->argument('trend_count'))
            ->get()
            ->pluck('name');

        // 一日前/直近にあって直近/一日前にはないトレンドをランダムに1つ取得
        $sbjYare = $pastTrends->diff($recentTrends)->random();
        $sbjHai = $recentTrends->diff($pastTrends)->random();

        // ツイート
        $twitter = \TwitterConnection::connect();
        $twitter->post(
            "statuses/update",
            array("status" => \Lang::get(
                'tweet_template.yare_hai',
                ['sbjYare' => $sbjYare, 'sbjHai' => $sbjHai]
            ))
        );
    }
}

トレンドワードはランダムに選ぶようにしています。
resources/lang/ja/tweet_template.phpにジョークのテンプレートを記述し、選択されたトレンドワードを組み込んでツイートします。

tweet_template.php
<?php
    return [
        'yare_hai' => ':sbjYare「やれ」' . PHP_EOL
                    . PHP_EOL
                    . ':sbjHai「はい」',
    ];


削除コマンド

トレンドワードを保存し続けてサーバの容量を圧迫したくないので、トレンドデータの削除コマンドもついでに作成しました。

<?php

namespace App\Console\Commands;

use App\Trend;
use Illuminate\Console\Command;

class DeleteTrends extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = '8081:deltrd {place_id} {time}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Delete old trends in a database.';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        Trend::where('place_id', $this->argument('place_id'))
            ->where('created_at', '<=', $this->argument('time'))
            ->delete();
    }
}

スケジュールに登録

公式ドキュメントを参考にCronの設定をし、作成済のコマンドを定期実行する処理をscheduleメソッド内に記述します。今回の場合はトレンド保存コマンドとツイートコマンドは一時間おき、削除コマンドは一日おきに実行としました。

Kernel.php
    protected function schedule(Schedule $schedule)
    {
        // 東京のplace_id は 1118370. 使用するトレンドは1リクエストにつき10個とした。
        $schedule->command('8081:instrd 1118370 10')
            ->hourly();
        $schedule->command('8081:twtjk 1118370 10')
            ->hourly();
        $schedule->command('8081:deltrd 1118370 "' . Carbon::now()->addMinutes(3)->subWeek(1) . '"')
            ->daily();
    }

終わりに

時々ツイートを確認するのですが

あまり面白くないですね(泣)。

  1. そもそも発言者が名詞でない場合があり、チンプンカンプン
  2. 同じテンプレートのツイートしかしないので飽きる

というのが原因かなあと。

まあ、小さいものですが完成できたので満足です😋。