Creating a Laravel Twitter bot

In this post, I will explain how to create a Twitter bot in Laravel. This app will send random tweets (selected from a database) periodically (each hour) and automatically, through the Laravel task scheduling. This project doesn’t have a graphical user interface.

You can download or clone the code from this GitHub repository.

Why have I developed this app?

I have just created a project (https://descargarmaquinasvirtuales.com/) where the user can download virtual machines from the main Linux distributions, in OVA format (compatible with VirtualBox and VMware) and in Spanish. This project has 19 different virtual machines, so I can create at least 19 different tweets to promote the virtual machines.

I have developed this project using Homestead, a Vagrant box designed to develop with Laravel. I have used the Laravel version 5.5.

Creating the project

The first thing I do is create a Laravel project called “laravel-twitter-bot

$ composer create-project --prefer-dist laravel/laravel laravel-twitter-bot

I will use an SQLite database so I don’t need a database server. I create an empty database file (database/laravel-twitter-bot.sqlite file)

$ touch database/laravel-twitter-bot.sqlite

Then I change the database reference in the .env file

DB_CONNECTION=sqlite
DB_DATABASE=database/laravel-twitter-bot.sqlite

I create the model (app/Models/Tweet.php file)

$ php artisan make:model Models/Tweet

and the migration (database/migrations/2017_10_31_143327_create_tweets_table.php file)

$ php artisan  make:migration create_tweets_table

Then I add the required fields in the migration  (database/migrations/2017_10_31_143327_create_tweets_table.php file)

 public function up()
 {
     Schema::create('tweets', function (Blueprint $table) {
         $table->increments('id');
         $table->string('prefix')->nullable();
         $table->string('body')->nullable();
         $table->softDeletes();
         $table->timestamps();
     });
 }

Each tweet will have two fields: prefix and body, so I can combine and create more tweets. If I have 4 prefix strings and 19 body strings (the 19 different virtual machines that I want to tweet), the application can generate 4*19= 76 different random tweets.

Once I have the database and the required table, I will generate the seeder (database/seeders/TweetsTableSeeder.php file), so I can create the tweets in the database automatically.

$ php artisan make:seeder TweetsTableSeeder

Then I create all the seeds in the database/seeders/TweetsTableSeeder.php file

public function run()
{
    DB::table('tweets')->insert([
        'prefix' => 'Puedes descargar la máquina virtual',
        'body' => 'Antergos Escritorio (Gnome) 17.10 en https://descargarmaquinasvirtuales.com/distribuciones/antergos/',
        'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
        'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
    ]);
    DB::table('tweets')->insert([
        'prefix' => 'Disponible una máquina virtual',
        'body' => 'CentOS Escritorio (Gnome) 7 en https://descargarmaquinasvirtuales.com/distribuciones/centos/',
        'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
        'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
    ]);
    DB::table('tweets')->insert([
        'prefix' => 'Descarga una máquina virtual gratuita de',
        'body' => 'CentOS Servidor 7 en https://descargarmaquinasvirtuales.com/distribuciones/centos/',
        'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
        'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
    ]);
    DB::table('tweets')->insert([
        'prefix' => 'Máquina virtual en formato OVA',
        'body' => 'Debian Escritorio (Gnome) 9.2.1 en https://descargarmaquinasvirtuales.com/distribuciones/debian/',
        'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
        'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
    ]);
    DB::table('tweets')->insert([
        'prefix' => 'Máquina virtual en formato OVA',
        'body' => 'Debian Servidor 9.2.1 en https://descargarmaquinasvirtuales.com/distribuciones/debian/',
        'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
        'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
    ]);
    DB::table('tweets')->insert([
        'prefix' => 'Máquina virtual para VirtualBox',
        'body' => 'deepin Escritorio (DDE) 15.4.1 en https://descargarmaquinasvirtuales.com/distribuciones/deepin/',
        'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
        'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
    ]);
    DB::table('tweets')->insert([
        'prefix' => 'Máquina virtual para VMware',
        'body' => 'elementary OS Escritorio (Pantheon) 0.4.1 en https://descargarmaquinasvirtuales.com/distribuciones/elementary-os/',
        'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
        'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
    ]);
    DB::table('tweets')->insert([
        'prefix' => 'Máquina virtual para VirtualBox o VMware',
        'body' => 'Fedora Escritorio (Gnome) 26-1.5 en https://descargarmaquinasvirtuales.com/distribuciones/fedora/',
        'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
        'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
    ]);
    DB::table('tweets')->insert([
        'body' => 'Kali Escritorio (Gnome) 2017.2 en https://descargarmaquinasvirtuales.com/distribuciones/kali-linux/',
        'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
        'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
    ]);
    DB::table('tweets')->insert([
        'body' => 'Lite Escritorio (Xfce) 3.6 en https://descargarmaquinasvirtuales.com/distribuciones/linux-lite/',
        'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
        'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
    ]);
    DB::table('tweets')->insert([
        'body' => 'Manjaro Escritorio (Xfce) 17.0.4 en https://descargarmaquinasvirtuales.com/distribuciones/manjaro/',
        'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
        'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
    ]);
    DB::table('tweets')->insert([
        'body' => 'Mint Escritorio (Cinnamon) 18.2 en https://descargarmaquinasvirtuales.com/distribuciones/linux-mint/',
        'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
        'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
    ]);
    DB::table('tweets')->insert([
        'body' => 'openSUSE Escritorio (Kde) 42.3 en https://descargarmaquinasvirtuales.com/distribuciones/opensuse/',
        'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
        'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
    ]);
    DB::table('tweets')->insert([
        'body' => 'PCLinuxOS Escritorio (Mate) 2017.07 en https://descargarmaquinasvirtuales.com/distribuciones/pclinuxos/',
        'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
        'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
    ]);
    DB::table('tweets')->insert([
        'body' => 'ReactOS Escritorio 0.4.6 en https://descargarmaquinasvirtuales.com/distribuciones/reactos/',
        'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
        'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
    ]);
    DB::table('tweets')->insert([
        'body' => 'Solus Escritorio (Budgie) 3 en https://descargarmaquinasvirtuales.com/distribuciones/solus/',
        'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
        'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
    ]);
    DB::table('tweets')->insert([
        'body' => 'Ubuntu Escritorio (Gnome) 17.10 en https://descargarmaquinasvirtuales.com/distribuciones/ubuntu/',
        'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
        'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
    ]);
    DB::table('tweets')->insert([
        'body' => 'Ubuntu Servidor 17.10 en https://descargarmaquinasvirtuales.com/distribuciones/ubuntu/',
        'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
        'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
    ]);
    DB::table('tweets')->insert([
        'body' => 'Zorin OS Escritorio (Gnome) 12.2 en https://descargarmaquinasvirtuales.com/distribuciones/zorin-os/',
        'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
        'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
    ]);
}

and add the reference to this seed in the database/seeders/DatabaseSeeder.php file

public function run()
{
    $this->call(TweetsTableSeeder::class);
}

Now I can run the migration with the seeder

$ php artisan migrate:refresh --seed

If I examine the database file I can see the columns and the rows created in the tweets table.

Creating the command

The next thing I have to do is to create the command to send the notifications

$ php artisan make:command SendTweets

I change the signature and the description in the app/Console/Commands/SendTweets.php file

protected $signature = 'ltb:sendRandomTweet';
protected $description = 'Send a random tweet';

Then I generate a tweet with the prefix from a random tweet and the body from another random tweet. Then I print this combination to see the output from this command.

public function handle()
{
    $prefix = Tweet::inRandomOrder()->whereNotNull('prefix')->first()->prefix;
    $tweet = Tweet::inRandomOrder()->whereNotNull('body')->first();
    $tweet->prefix = $prefix;
    $this->info($tweet->prefix . ' ' . $tweet->body . PHP_EOL);
}

Then I add the reference to the command in the app/Console/Kernel.php

protected $commands = [
    Commands\SendTweets::class,
];

Now I can check the random tweet generation from the console, executing the ltb:sendRandomTweet command 3 times:

$ php artisan ltb:sendRandomTweet
Descarga una máquina virtual gratuita de Debian Escritorio (Gnome) 9.2.1 en https://descargarmaquinasvirtuales.com/distribuciones/debian/
$ php artisan ltb:sendRandomTweet
Puedes descargar la máquina virtual Zorin OS Escritorio (Gnome) 12.2 en https://descargarmaquinasvirtuales.com/distribuciones/zorin-os/
$ php artisan ltb:sendRandomTweet
Máquina virtual en formato OVA Debian Escritorio (Gnome) 9.2.1 en https://descargarmaquinasvirtuales.com/distribuciones/debian/

Creating the notification

Now I have to send this random tweets to the Twitter account I want to use. I will use the Laravel Notifications. I follow the instructions from an excellent tutorial I found in Laravel News.

Before I can send tweets to an account I have to create a Twitter app and then I have to get 4 keys. I go to https://apps.twitter.com/, create a new app and get the 4 keys:

I will use a package to send notifications to Twitter. I will use a package from the Laravel Notification Channels project: Twitter notification channel for Laravel 5.3+. You can see the package documentation in the GitHub project.

I add the package to the project

$ composer require laravel-notification-channels/twitter

Then I add the provider to the config/app.php file

'providers' => 
    NotificationChannels\Twitter\TwitterServiceProvider::class,
],

and the Twitter array in the config/services.php file

'twitter' => [
    'consumer_key' => getenv('TWITTER_CONSUMER_KEY'),
    'consumer_secret' => getenv('TWITTER_CONSUMER_SECRET'),
    'access_token' => getenv('TWITTER_ACCESS_TOKEN'),
    'access_secret' => getenv('TWITTER_ACCESS_SECRET')
]

Then I add this 4 variables in the .env file with the values I get from the Twitter app

TWITTER_CONSUMER_KEY=xxxxxxxxxxxxxxxxxxxxxxxxx
TWITTER_CONSUMER_SECRET=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
TWITTER_ACCESS_TOKEN=zzzzzzzzzzzzzzzzzzz-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
TWITTER_ACCESS_SECRET=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Then I add the Notifiable trait in the app/Models/Tweet.php model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;

class Tweet extends Model
{
    use Notifiable;
}

Next, I have to create the notification class:

$ php artisan make:notification SendTweet

In this file app/Notifications/SendTweet.php I add the references to the package

use NotificationChannels\Twitter\TwitterChannel;
use NotificationChannels\Twitter\TwitterStatusUpdate;

and the method to send the tweet

/**
 * @param $tweet
 * @return TwitterStatusUpdate
 */
public function toTwitter($tweet) {
    return new TwitterStatusUpdate($tweet->prefix . ' ' . $tweet->body);
}

Now I have to call the notification method from the command in the app/Console/Commands/SendTweets.php file

I add this reference in this package

use App\Notifications\SendTweet;

and the line to send the notification

public function handle()
{
    $prefix = Tweet::inRandomOrder()->whereNotNull('prefix')->first()->prefix;
    $tweet = Tweet::inRandomOrder()->whereNotNull('body')->first();
    $tweet->prefix = $prefix;
    $this->info($tweet->prefix . ' ' . $tweet->body . PHP_EOL);
    $tweet->notify(new SendTweet());
}

If I execute this command 3 times I get 3 tweets in the account

$ php artisan ltb:sendRandomTweet
Máquina virtual en formato OVA CentOS Servidor 7 en https://descargarmaquinasvirtuales.com/distribuciones/centos/

$ php artisan ltb:sendRandomTweet
Máquina virtual en formato OVA Ubuntu Escritorio (Gnome) 17.10 en https://descargarmaquinasvirtuales.com/distribuciones/ubuntu/

$ php artisan ltb:sendRandomTweet
Máquina virtual en formato OVA Manjaro Escritorio (Xfce) 17.0.4 en https://descargarmaquinasvirtuales.com/distribuciones/manjaro/

 

Automatizing the sending

The final step is automatizing the sending of the tweet, so an automatic task sends this tweets periodically. I will use the Task Scheduling for this purpose.

The first thing is to start the scheduler in the cron. I edit the cron

$ crontab -e

and add this line

* * * * * php /home/vagrant/Code/web/laravel-twitter-bot/artisan schedule:run >> /dev/null 2>&1

Then I have to schedule the task in the App\Console\Kernel.php file. I update the schedule method with this code, so I send one tweet per hour between the 8:00 and 20:00, using the “Europe/Madrid” timezone.

 /**
 * Define the application's command schedule.
 *
 * @param \Illuminate\Console\Scheduling\Schedule $schedule
 * @return void
 */
 protected function schedule(Schedule $schedule)
 {
     $schedule->command('ltb:sendRandomTweet')
         ->hourly()
         ->between('8:00', '20:00')
         ->timezone('Europe/Madrid');
 }

This is the result from the Twitter bot.

Posible errors

Duplicate

If you have few tweet combinations and/or you have a very aggressive post policy (you send a lot of tweets in a small amount of time) you could get the duplicate error.

[NotificationChannels\Twitter\Exceptions\CouldNotSendNotification]
Couldn't post Notification. Response: Status is a duplicate.

Bad authentication

If you haven’t the correct Twitter app keys in the .env file, you will get this error

[NotificationChannels\Twitter\Exceptions\CouldNotSendNotification]
Couldn't post Notification. Response: Bad Authentication data.

:wq

Leave a Reply

Your email address will not be published. Required fields are marked *