Advertisement
  1. Code
  2. PHP

Using Social Media to Locate Eyewitnesses: The Twitter API

Scroll to top
Read Time: 11 min
This post is part of a series called Using Social Media to Locate Eyewitnesses.
Using Social Media to Locate Eyewitnesses to Important Events
Tweet about President Obama taking the stage at SelmaTweet about President Obama taking the stage at SelmaTweet about President Obama taking the stage at Selma

This is the second of a two-part series on using social media to locate eyewitnesses to important events. In part one, I showed you how to use the Instagram API to find eyewitnesses to a live video shoot of Macklemore's in Seattle. In this part, we'll use the Twitter API to find attendees of President Obama's speech in Selma at the Edmund Pettus Bridge.

You can download code for both episodes by using the GitHub repository link in the sidebar. You may also be interested in my Tuts+ series, Building With the Twitter API.

Twitter's geosearch capabilities are more limited and therefore require a bit more detailed code to use. Geotagged posts on Twitter can only be found from the last seven days. And they are only searchable by date (not time), so you have to filter the API results for precision. 

I do participate in the discussions below. If you have a question or topic suggestion, please post a comment below. You can also reach me on Twitter @reifman or email me directly. 

What We Covered in Part One

The phones we carry in our pockets record our every move, sharing it with cell providers and often third-party software companies whose motivations generally focus on profit. 

Many people don't realize that they've left geotagging on their social media apps, fully publicizing their location with every social media post. This included GOP Congressman Aaron Schock. The AP used his Instagram account's geotags to expose his use of taxpayer funds for extravagant private flights and other luxurious activities. 

Congressman Aaron Schock in South AmericaCongressman Aaron Schock in South AmericaCongressman Aaron Schock in South America

So, geotagging can be used for good. In this series, we're exploring how journalists or law enforcement might locate potential eyewitnesses to important events such as a crime or accident scene using social media.

However, geotagging can also be used abusively. Berkeley computer scientists and educators built the Ready or Not? app to showcase how geotagging in Twitter and Instagram record our every move. 

Here's Apple co-founder Steve Wozniak's Twitter account in the app:

Ready or Not App Steve Wozniak Twitter Geolocation HistoryReady or Not App Steve Wozniak Twitter Geolocation HistoryReady or Not App Steve Wozniak Twitter Geolocation History

The geotagging on Instagram and Twitter is accurate enough to allow someone to easily determine your residence, place of work and travel routine.

In this episode, I'll guide you through using the Twitter API. I've provided a GitHub repository (the link is in the sidebar) for you to download to try out the code. My "Eyewitness app" is written in the Yii Framework for PHP, which you can learn more about in my Programming With Yii2 series for Tuts+. 

If you don't wish to share your location for the public to see—or to leave a historical trail of your travels—the Ready or Not? app offers links and guides to turning these features off (look for the link on its home page). Frankly, I've turned mine off and I encourage you to do so too.

If you're a law enforcement agency or media entity that would like more information, please feel free to contact me directly. I would also be interested in any successful uses of this code (for good)—they'd make an interesting follow-up story.

What We Did With Instagram

Last episode, we used the Instagram API to find eyewitnesses to Mackelmore's live 2013 video shoot for White Cadillac. Quite easily, we managed to find Instagram member Joshua Lewis's photo of Macklemore stepping out of his vehicle (cool, huh?):

Macklemore Arrives Found with the Instagram APIMacklemore Arrives Found with the Instagram APIMacklemore Arrives Found with the Instagram API

Now, let's get started using the Twitter API.

Using the Twitter API

As with Instagram, you need to sign in to your Twitter account and register a developer application. You should register an app like this:

Twitter App RegistrationTwitter App RegistrationTwitter App Registration

Twitter will show you your application details:

Twitter App Eyewitness DetailsTwitter App Eyewitness DetailsTwitter App Eyewitness Details

Here's the settings page:

Twitter App DetailsTwitter App DetailsTwitter App Details

Here are the keys and access tokens for the application. Make note of these.

Twitter App Keys and Access TokensTwitter App Keys and Access TokensTwitter App Keys and Access Tokens

Then, scroll down and create access tokens for your account. Make note of these too.

Twitter App Account Access TokensTwitter App Account Access TokensTwitter App Account Access Tokens

Add all four of these configuration keys and secrets to your /var/secure/eyew.ini file:

1
mysql_host="localhost"
2
mysql_db="eyew"
3
mysql_un="xxxxxxxxx"
4
mysql_pwd="xxxxxxxxxxxx"
5
instagram_client_id = "4xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx7"
6
instagram_client_secret = "1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx4"
7
twitter_key = "zxxxxxxxxxxxxxxxxxxxx2"
8
twitter_secret ="4xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxp"
9
twitter_oauth_token="1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxs"
10
twitter_oauth_secret="exxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxV"

Then, we'll create an Active Record migration to create our Twitter model. This will store the tweets we receive from the API calls.

1
<?php
2
3
use yii\db\Schema;
4
use yii\db\Migration;
5
6
class m150309_174014_create_twitter_table extends Migration
7
{
8
    public function up()
9
    {
10
          $tableOptions = null;
11
          if ($this->db->driverName === 'mysql') {
12
              $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB';
13
          }
14
15
          $this->createTable('{{%twitter}}', [
16
              'id' => Schema::TYPE_PK,
17
              'moment_id' => Schema::TYPE_INTEGER . ' NOT NULL',
18
              'tweet_id' => Schema::TYPE_BIGINT . ' NOT NULL',
19
              'twitter_id' => Schema::TYPE_BIGINT . ' NOT NULL',
20
              'screen_name' => Schema::TYPE_STRING . ' NOT NULL DEFAULT 0',
21
              'text' => Schema::TYPE_TEXT . ' NOT NULL ',
22
              'tweeted_at' => Schema::TYPE_INTEGER . ' NOT NULL',
23
              'created_at' => Schema::TYPE_INTEGER . ' NOT NULL',
24
              'updated_at' => Schema::TYPE_INTEGER . ' NOT NULL',
25
          ], $tableOptions);          
26
      $this->addForeignKey('fk_twitter_moment', '{{%twitter}}', 'moment_id', '{{%moment}}', 'id', 'CASCADE', 'CASCADE');     
27
    }
28
    
29
    
30
    public function down()
31
    {
32
      $this->dropForeignKey('fk_twitter_moment','{{%twitter}}');
33
      $this->dropTable('{{%twitter}}');
34
    }
35
}

Just as we did in part one, you need to run the migration:

1
./yii migrate/up
2
Yii Migration Tool (based on Yii v2.0.3)
3
4
Total 1 new migration to be applied:
5
    m150309_174014_create_twitter_table
6
7
Apply the above migration? (yes|no) [no]:yes
8
*** applying m150309_174014_create_twitter_table
9
    > create table {{%twitter}} ... done (time: 0.008s)
10
    > add foreign key fk_twitter_moment: {{%twitter}} (moment_id) references {{%moment}} (id) ... done (time: 0.007s)
11
*** applied m150309_174014_create_twitter_table (time: 0.019s)
12
13
Migrated up successfully.

Then, I used Yii2's code generator, Gii, to create the model and CRUD controllers for the Twitter model. If you get the latest GitHub repository code using the sidebar link on this tutorial, you'll have the code as well.

Create a New Moment

Because Twitter limits geolocation searches to the past week, I eventually chose President Obama's Selma 50th Anniversary speech at the Edmund Pettus Bridge.

I used Google Maps again to get the GPS coordinates for the bridge:

Edmund Pettus Bridge Selma Alabama GeolocationEdmund Pettus Bridge Selma Alabama GeolocationEdmund Pettus Bridge Selma Alabama Geolocation

Then, I created a Moment for the speech to search. I updated it a few times to tweak the geographic radius of the search (it's a bridge) and the time range:

Edmund Pettus Bridge Selma Alabama Geolocation MomentEdmund Pettus Bridge Selma Alabama Geolocation MomentEdmund Pettus Bridge Selma Alabama Geolocation Moment

Search Using the Twitter API

The limitations of the Twitter API are that it only allows you to search by date, e.g. 2015-03-07, whereas Instagram is indexed by precise Unix timestamps. Therefore, we have to begin our Twitter search a full day ahead and search backwards.

Since we're likely to obtain a lot of tweets outside our desired time range, we have to make repeated calls to the Twitter API. Twitter returns up to 100 tweets per API request, and allows 180 requests per 15-minute window.

I'm using James Mallison's Twitter API Library for PHP. Here's how we set up the library to make calls:

1
<?php
2
3
namespace app\models;
4
5
use Yii;
6
use yii\db\ActiveRecord;
7
use app\models\Gram;
8
use Instagram;
9
use TwitterAPIExchange;
10
11
...
12
13
public function searchTwitter() {
14
   date_default_timezone_set('America/Los_Angeles');       
15
   Yii::trace('start searchTwitter '.date('y-m-d h:m '));
16
   // Load your Twitter application keys

17
   $settings = array(
18
       'oauth_access_token' => \Yii::$app->params['twitter']['oauth_token'],
19
       'oauth_access_token_secret' => \Yii::$app->params['twitter']['oauth_secret'],
20
       'consumer_key' => \Yii::$app->params['twitter']['key'],
21
       'consumer_secret' => \Yii::$app->params['twitter']['secret'],
22
   );
23
   // Connect to Twitter 

24
   $twitter = new TwitterAPIExchange($settings);

Initially, we request 100 results from Twitter at our GPS coordinates up to a specific date. 

1
public function searchTwitter() {
2
   date_default_timezone_set('America/Los_Angeles');       
3
   Yii::trace('start searchTwitter '.date('y-m-d h:m '));
4
   // Load your Twitter application keys

5
   $settings = array(
6
       'oauth_access_token' => \Yii::$app->params['twitter']['oauth_token'],
7
       'oauth_access_token_secret' => \Yii::$app->params['twitter']['oauth_secret'],
8
       'consumer_key' => \Yii::$app->params['twitter']['key'],
9
       'consumer_secret' => \Yii::$app->params['twitter']['secret'],
10
   );
11
   // Connect to Twitter 

12
   $twitter = new TwitterAPIExchange($settings);
13
   // Query settings for search

14
   $url = 'https://api.twitter.com/1.1/search/tweets.json';
15
   $requestMethod = 'GET';
16
   // rate limit of 180 queries

17
   $limit = 180;
18
   $query_count=1;
19
   $count = 100;
20
   $result_type = 'recent';
21
   // calculate valid timestamp range

22
   $valid_start = $this->start_at;
23
   // $until_date and $valid_end = // start time + duration

24
   $valid_end = $this->start_at + ($this->duration*60);
25
   Yii::trace( 'Valid Range: '.$valid_start.' -> '.$valid_end);
26
   $until_date = date('Y-m-d',$valid_end+(24*3600)); // add one day 

27
   $distance_km = $this->distance/1000; // distance in km

28
   // Unused: &since=$since_date

29
   // $since_date = '2015-03-05'; 

30
   // Perform first query with until_date

31
   $getfield ="?result_type=$result_type&geocode=".$this->latitude.",".$this->longitude.",".$distance_km."mi&include_entities=false&until=$until_date&count=$count";

We only record tweets within our precise time range, ignoring the other results. As we process these, we make note of the lowest tweet ID received.

1
       $tweets = json_decode($twitter->setGetfield($getfield)
2
                    ->buildOauth($url, $requestMethod)
3
                    ->performRequest());
4
        if (isset($tweets->errors)) {
5
          Yii::$app->session->setFlash('error', 'Twitter Rate Limit Reached.');
6
          Yii::error($tweets->errors[0]->message);
7
          return;
8
        }
9
       $max_id = 0;
10
       Yii::trace( 'Count Statuses: '.count($tweets->statuses));
11
       Yii::trace( 'Max Tweet Id: '.$max_id);
12
       foreach ($tweets->statuses as $t) {
13
         // check if tweet in valid time range

14
         $unix_created_at = strtotime($t->created_at);         
15
         Yii::trace('Tweet @ '.$t->created_at.' '.$unix_created_at.':'.$t->user->screen_name.' '.(isset($t->text)?$t->text:''));
16
         if ($unix_created_at >= $valid_start && $unix_created_at <= $valid_end)
17
         {
18
           // print_r($t);

19
            $i = new Twitter();           $i->add($this->id,$t->id_str,$t->user->id_str,$t->user->screen_name,$unix_created_at,(isset($t->text)?$t->text:''));
20
         }        
21
         if ($max_id ==0) {
22
           $max_id = intval($t->id_str);
23
         } else {
24
           $max_id = min($max_id, intval($t->id_str));
25
         }
26
       }

Then we loop, making repeated requests to Twitter (up to 179 more times), requesting additional records that are earlier than the previous batch's lowest tweet ID. In other words, on subsequent requests, instead of querying up to a specific date, we query to the max_id of the lowest tweet ID that we've received.

We stop when less than 100 records are returned or when returned tweets are earlier than our actual range. 

If you need access to more than 18,000 tweets, you'll need to implement a background task to call the Twitter API, as we've done in our other Twitter API series.

As we process API results, we need to filter tweets, only recording those that fall within our actual start time and end time.

Note: The Twitter API has a lot of frustrating quirks which make paging more difficult than it should be. Quite frequently Twitter returns no results without an error code. Other times, I found it returning a small number of results, but that didn't mean that another request would not return more. There are no very clear ways to know when Twitter is done returning results to you. It's inconsistent. Thus, you may notice my code has a few interesting workarounds in it, e.g. examine $count_max_repeats.

1
       $count_repeat_max =0;
2
       // Perform all subsequent queries with addition of updated maximum_tweet_id

3
       while ($query_count<=$limit) {
4
         $prior_max_id = $max_id;
5
         $query_count+=1;
6
         Yii::trace( 'Request #: '.$query_count);
7
         
8
         // Perform subsequent query with max_id

9
         $getfield ="?result_type=$result_type&geocode=".$this->latitude.",".$this->longitude.",".$distance_km."mi&include_entities=false&max_id=$max_id&count=$count";
10
         
11
         $tweets = json_decode($twitter->setGetfield($getfield)
12
                      ->buildOauth($url, $requestMethod)
13
                      ->performRequest());
14
15
          if (isset($tweets->errors)) {
16
            Yii::$app->session->setFlash('error', 'Twitter Rate Limit Reached.');
17
            Yii::error($tweets->errors[0]->message);
18
            return;
19
          }
20
          // sometimes twitter api fails

21
          if (!isset($tweets->statuses)) continue;
22
          
23
          Yii::trace( 'Count Statuses: '.count($tweets->statuses));
24
          Yii::trace( 'Max Tweet Id: '.$max_id);
25
         foreach ($tweets->statuses as $t) {           
26
           // check if tweet in valid time range

27
           $unix_created_at = strtotime($t->created_at);
28
           if ($unix_created_at >= $valid_start && $unix_created_at <= $valid_end)
29
           {
30
              $i = new Twitter();           $i->add($this->id,$t->id_str,$t->user->id_str,$t->user->screen_name,$unix_created_at,(isset($t->text)?$t->text:''));
31
           } else if ($unix_created_at < $valid_start) {
32
             // stop querying when earlier than valid_start

33
             return;
34
           }
35
           $max_id = min($max_id,intval($t->id_str))-1;
36
         }       
37
         if ($prior_max_id - $max_id <=1 OR count($tweets->statuses)<1) {
38
           $count_repeat_max+=1;
39
         }           
40
         if ($count_repeat_max>5) {           
41
           // when the api isn't returning more results

42
           break;
43
         }
44
       } // end while 

One of the first results returned included the tweet below by Fred Davenport showing President Obama on stage:

The First Results for Selma 50th via the Twitter Search APIThe First Results for Selma 50th via the Twitter Search APIThe First Results for Selma 50th via the Twitter Search API

Here's it is on Twitter:

President Obama in Selma via the Twitter API President Obama in Selma via the Twitter API President Obama in Selma via the Twitter API

Then, as you browse the results further, you can find many more people present tweeting about Obama—including the media:

More Selma Twitter API ResultsMore Selma Twitter API ResultsMore Selma Twitter API Results

Now, let's do a more local search.

A Second, More Local Search

Key Arena is Seattle's large concert and sports arena. This past weekend they held the Pac-12 Women's Basketball Tournament:

Key Arena Calendar of EventsKey Arena Calendar of EventsKey Arena Calendar of Events

Let's get our GPS coordinates for Key Arena from Google Maps:

Key Arena Geolocation on Google MapsKey Arena Geolocation on Google MapsKey Arena Geolocation on Google Maps

Then, I created and tweaked a moment to find a longer time range for the weekend of tweets:

Create a moment to search Key Arena Basketball TournamentCreate a moment to search Key Arena Basketball TournamentCreate a moment to search Key Arena Basketball Tournament

And, here are some of the results. My favorite is:

"I wanna leave this basketball game. I hate basketball."
Twitter Search Results for Key Arena TournamentTwitter Search Results for Key Arena TournamentTwitter Search Results for Key Arena Tournament

For the most part, it seems to me that Instagram's API is far more powerful than Twitter's and yields generally more intriguing results. However, it depends on the kind of person that you're looking for. If you just want to identify people who were there, either API works well.

What We've Learned

I hope you've enjoyed this series. I found it fascinating and was impressed by the results. And it highlights the concerns we should all have about our level of privacy in this interconnected digital age.

The APIs for Instagram and Twitter are both incredibly powerful services for finding social media users who were nearby certain places at certain times. This information can be used for good and it can be abused. You should probably consider turning off your geolocation posting—follow the links at the Ready or Not? app.

You may also want to check out my Building With the Twitter API series, also on Tuts+.

Please feel free to post your questions and comments below. You can also reach me on Twitter @reifman or email me directly. I'd especially appreciate hearing from journalists and law enforcement that make use of these examples.

You can also browse my Tuts+ instructor page to see other tutorials I've written. 

Related Links

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.