Search code examples
javascriptgoogle-apps-scriptgoogle-sheetstwitter

Switching from Twitter API V1.1 to Twitter API V2 to send Tweets from a Profile to Google Sheets


The model I used to collect tweets from API V1.1 was like this:

function Twitter_get_tweets() 
{
  var string_Screen_name = "stakehighroller";
  var string_Consumer_key = "AAAAAAAAAAAAAAAAAAAAAAAAAA";
  var string_Consumer_secret = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";

  var spreadsheet_Tweets = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Tweets");
  spreadsheet_Tweets.getRange(3, 1, 2600, 20).clearContent(); 
  
  var tokenUrl = "https://api.twitter.com/oauth2/token";
  var tokenCredential = Utilities.base64EncodeWebSafe(string_Consumer_key + ":" + string_Consumer_secret);
  
  var tokenOptions = {
    headers : {
      Authorization: "Basic " + tokenCredential,
      "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
    },
    method: "post",
    payload: "grant_type=client_credentials"
  };
  
  var responseToken = UrlFetchApp.fetch(tokenUrl, tokenOptions);
  var parsedToken = JSON.parse(responseToken);
  var token = parsedToken.access_token;
  var apiUrl = "";
  var responseApi = "";
  
  var apiOptions = {
    headers : {
      Authorization: 'Bearer ' + token
    },
    "method" : "get"
  };
  
  var array_Column_a = []; 
  var array_Text = [];
  var array_Expanded_url = [];
  
  var string_Max_id = 0;
  var int_Line_counter = 1;
  var int_Break_loop = 0;
  
  do
  {
    if (int_Line_counter == 1)
    {
      apiUrl = 'https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name='+ string_Screen_name + '&count=200&include_rts=1';
    }
    else
    {
      apiUrl = 'https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name='+ string_Screen_name + '&count=200&include_rts=1&max_id=' + string_Max_id;
    }
    responseApi = UrlFetchApp.fetch(apiUrl, apiOptions);
    
    if (responseApi.getResponseCode() == 200) 
    {
      
      var obj_data = JSON.parse(responseApi.getContentText());
      
      for (var int_i = 0; int_i < obj_data.length; int_i++)
      { 
        array_Column_a.push([int_Line_counter]);
        array_Text.push([obj_data[int_i].text]);
        
        if (obj_data[int_i].entities.urls[0] != undefined && obj_data[int_i].entities != undefined)
        {
          array_Expanded_url.push([obj_data[int_i].entities.urls[0].expanded_url]);
        }
        else 
        {
          array_Expanded_url.push([""]);
        }
        
        int_Line_counter++;
        
      }
      
      if (obj_data[(obj_data.length-1)] != undefined && int_i < parseInt(obj_data[0].user.statuses_count))
      {
        string_Max_id = obj_data[(obj_data.length-1)].id;
      }
      else
      {
        int_Break_loop = 1;
      }
      
      
    }
    
  }while (int_Break_loop != 1 && int_Line_counter < 1000)
    
    if (array_Column_a.length > 0)
    {
      spreadsheet_Tweets.getRange("A3:A"+(array_Column_a.length + 2)).setValues(array_Column_a);
      spreadsheet_Tweets.getRange("C3:C"+(array_Text.length + 2)).setValues(array_Text);
      spreadsheet_Tweets.getRange("D3:D"+(array_Expanded_url.length + 2)).setValues(array_Expanded_url);
    }
  else
  {
    Browser.msgBox("0 Tweets found");
  }
}

It worked perfectly, but when I adjusted the requests model for API V2, the data is retrieved perfectly, because when I try to send var obj_data = JSON.parse(responseApi.getContentText()); to a cell, it tells me that the number of characters are above the cell limit.

The current model is as follows:

function TwitterTest2() 
{
  var string_Screen_name = "1310800524619386880";
  var string_Consumer_key = "AAAAAAAAAAAAAAAAAAAAAAA";
  var string_Consumer_secret = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";

  var spreadsheet_Tweets = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Tweets");
  spreadsheet_Tweets.getRange(3, 1, 2600, 20).clearContent(); 

  var tokenUrl = "https://api.twitter.com/oauth2/token";
  var tokenCredential = Utilities.base64EncodeWebSafe(string_Consumer_key + ":" + string_Consumer_secret);

  var tokenOptions = {
    headers : {
      Authorization: "Basic " + tokenCredential,
      "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
    },
    method: "post",
    payload: "grant_type=client_credentials"
  };
  
  var responseToken = UrlFetchApp.fetch(tokenUrl, tokenOptions);
  var parsedToken = JSON.parse(responseToken);
  var token = parsedToken.access_token;
  var apiUrl = "";
  var responseApi = "";
  
  var apiOptions = {
    headers : {
      Authorization: 'Bearer ' + token
    },
    "method" : "get"
  };
  
  var array_Column_a = []; 
  var array_Text = [];
  var array_Expanded_url = [];
  
  var string_Max_id = 0;
  var int_Line_counter = 1;
  var int_Break_loop = 0;
  
  do
  {
    if (int_Line_counter == 1)
    {
      apiUrl = 'https://api.twitter.com/2/users/' + string_Screen_name + '/tweets?expansions=attachments.poll_ids,attachments.media_keys,author_id,entities.mentions.username,geo.place_id,in_reply_to_user_id,referenced_tweets.id,referenced_tweets.id.author_id&tweet.fields=attachments,author_id,context_annotations,conversation_id,created_at,entities,geo,id,in_reply_to_user_id,lang,possibly_sensitive,public_metrics,referenced_tweets,reply_settings,source,text,withheld&user.fields=created_at,description,entities,id,location,name,pinned_tweet_id,profile_image_url,protected,public_metrics,url,username,verified,withheld&place.fields=contained_within,country,country_code,full_name,geo,id,name,place_type&poll.fields=duration_minutes,end_datetime,id,options,voting_status&media.fields=duration_ms,height,media_key,preview_image_url,type,url,width,public_metrics,non_public_metrics,organic_metrics,promoted_metrics&max_results=100';
    }
    else
    {
      apiUrl = 'https://api.twitter.com/2/users/' + string_Screen_name + '/tweets?expansions=attachments.poll_ids,attachments.media_keys,author_id,entities.mentions.username,geo.place_id,in_reply_to_user_id,referenced_tweets.id,referenced_tweets.id.author_id&tweet.fields=attachments,author_id,context_annotations,conversation_id,created_at,entities,geo,id,in_reply_to_user_id,lang,possibly_sensitive,public_metrics,referenced_tweets,reply_settings,source,text,withheld&user.fields=created_at,description,entities,id,location,name,pinned_tweet_id,profile_image_url,protected,public_metrics,url,username,verified,withheld&place.fields=contained_within,country,country_code,full_name,geo,id,name,place_type&poll.fields=duration_minutes,end_datetime,id,options,voting_status&media.fields=duration_ms,height,media_key,preview_image_url,type,url,width,public_metrics,non_public_metrics,organic_metrics,promoted_metrics&max_results=100&until_id=' + string_Max_id;
    }
    responseApi = UrlFetchApp.fetch(apiUrl, apiOptions);
    
    if (responseApi.getResponseCode() == 200) 
    {
      
      var obj_data = JSON.parse(responseApi.getContentText());
      
      for (var int_i = 0; int_i < obj_data.length; int_i++)
      { 
        array_Column_a.push([int_Line_counter]);
        array_Text.push([obj_data.data[int_i].text]);
        
        if (obj_data.data[int_i].entities.urls[0] != undefined && obj_data.data[int_i].entities != undefined)
        {
          array_Expanded_url.push([obj_data.data[int_i].entities.urls[0].expanded_url]);
        }
        else 
        {
          array_Expanded_url.push([""]);
        }
        
        int_Line_counter++;
        
      }
      
      if (obj_data[(obj_data.length-1)] != undefined && int_i < parseInt(obj_data.includes.users[0].public_metrics.tweet_count))
      {
        string_Max_id = obj_data[(obj_data.length-1)].id;
      }
      else
      {
        int_Break_loop = 1;
      }
      
      
    }
    
  }while (int_Break_loop != 1 && int_Line_counter < 1000)
    
    if (array_Column_a.length > 0)
    {
      spreadsheet_Tweets.getRange("A3:A"+(array_Column_a.length + 2)).setValues(array_Column_a);
      spreadsheet_Tweets.getRange("C3:C"+(array_Text.length + 2)).setValues(array_Text);
      spreadsheet_Tweets.getRange("D3:D"+(array_Expanded_url.length + 2)).setValues(array_Expanded_url);
    }
  else
  {
    Browser.msgBox("0 Tweets found");
  }
}

However, when trying to separate the collected values for each row of the spreadsheet with the respective values for each tweet, nothing is delivered to the spreadsheet. This region is not being analyzed:

      for (var int_i = 0; int_i < obj_data.length; int_i++)
      { 
        array_Column_a.push([int_Line_counter]);
        array_Text.push([obj_data.data[int_i].text]);
        
        if (obj_data.data[int_i].entities.urls[0] != undefined && obj_data.data[int_i].entities != undefined)
        {
          array_Expanded_url.push([obj_data.data[int_i].entities.urls[0].expanded_url]);
        }
        else 
        {
          array_Expanded_url.push([""]);
        }
        
        int_Line_counter++;
        
      }

it is jumping without even trying to analyse inside for (var int_i = 0; int_i < obj_data.length; int_i++) and delivering the warning about 0 Tweets found according to:

else
  {
    Browser.msgBox("0 Tweets found");
  }

I try to use

      for (var int_i = 0; int_i < obj_data.length; int_i++)
      { 
        array_Column_a.push([int_Line_counter]);
        array_Text.push([obj_data.data[int_i].text]);
  
        SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Tweets").getRange("A3").setValue("Test");
        return;


to know if inside for is being analyzed, but it's not because nothing value is placed in the cell, so I analyze that inside for the script is not being worked.

What do I need to do so that it doesn't happen and actually create the loop in for and be able to deliver the list of tweets? The ways to collect the data in theory are correct because by the tests they deliver the exact values.

Links:

API V1.1:
https://developer.twitter.com/en/docs/twitter-api/v1/tweets/timelines/api-reference/get-statuses-user_timeline

API V2:
https://developer.twitter.com/en/docs/twitter-api/tweets/timelines/api-reference/get-users-id-tweets

I reinforce that in API V1.1 the data was delivered perfectly.

V1.1 test result:

enter image description here


Solution

  • The structure of responses differ and you need to actually modify a little for your v2.

    Update your loop condition.

    Modifications:

    • obj_data in v2 contains additional meta, thus you need to get the length of obj_data.data instead of just obj_data
    // from: obj_data.length
    for (var int_i = 0; int_i < obj_data.length; int_i++) {
    
    // to: obj_data.data.length
    for (var int_i = 0; int_i < obj_data.data.length; int_i++) {
    
    • After this, you should be able to get the length which is 100.
    • Next problem is that there were entries that didn't have url under entities. Thus you need to check if entities.url exists first before accessing entities.url[0]. This will skip those rows url, BUT at least it won't error.
    // from: 
    if (obj_data.data[int_i].entities.urls[0] != undefined && obj_data.data[int_i].entities != undefined)
    
    // to: added obj_data.data[int_i].entities.urls to check if it exists
    if (obj_data.data[int_i].entities.urls && obj_data.data[int_i].entities.urls[0] != undefined && obj_data.data[int_i].entities != undefined)
    

    Resulting Snippet:

    if (responseApi.getResponseCode() == 200) {
      var obj_data = JSON.parse(responseApi.getContentText());
    
      for (var int_i = 0; int_i < obj_data.data.length; int_i++) {
        array_Column_a.push([int_Line_counter]);
        array_Text.push([obj_data.data[int_i].text]);
            
        if (obj_data.data[int_i].entities.urls && obj_data.data[int_i].entities.urls[0] != undefined && obj_data.data[int_i].entities != undefined)
          array_Expanded_url.push([obj_data.data[int_i].entities.urls[0].expanded_url]);
        else
          array_Expanded_url.push([""]);
    
        int_Line_counter++;
      }
          
      ...
    

    Output:

    output1 output2

    Entry 79, no entities.url

    output3