Créer un robot twitter sur un Raspberry Pi 3 avec R

Avec Marion Louveaux, nous avons décidé que nous devions construire un robot Twitter pour notre hashtag préféré. Nous avons exploré différentes possibilités mais la vérité est que je n’ai pas pu résister à l’envie de le construire en utilisant R et {rtweet}. Voici les étapes que j’ai utilisées pour installer un robot Twitter sur mon Raspberry Pi.

Créer un robot twitter

Nous allons créer un robot twitter qui retweets les tweets contenant #rspatial : https://twitter.com/talk_rspatial
Pour ça, nous avons besoin de :

  • Un script pour récupérer les tweets avec #rspatial.
  • Un script à retweeter tout en respectant l’utilisation de l’API Twitter
  • Un serveur qui exécutera régulièrement les scripts

J’utilise le package {rtweet} pour communiquer avec Twitter et mon Raspberry Pi personnel avec un CRON pour exécuter des scripts R. Les scripts créés sont disponibles sous forme de fonctions dans le package {tweetrbot}.

Procedure by [Marion Louveaux](https://marionlouveaux.fr)

Figure 1: Procedure by Marion Louveaux

Notez que la procédure est détaillée dans le paragraphe suivant, pour présenter le code, mais vous pouvez passer directement au paragraphe d’après “Procédure avec package {tweetrbot} et un Raspi” si vous voulez moins de détails.

Procédure détaillée et code

Créer ses tokens Twitter

  • Je recommande d’utiliser une adresse mail spécifique pour ce bot, au cas où Twitter aurait quelque chose à vous dire.
  • Vous devez créer un compte Twitter spécifique pour ce bot sur Twitter.
  • Lire la vignette de {rtweet} pour créer vos tokens : https://rtweet.info/articles/auth.html

Ensuite, vous exécuterez ce type de code pour sauvegarder correctement vos token.

## authenticate via access token
token <- rtweet::create_token(
  app = "my_twitter_research_app",
  consumer_key = "zzz",
  consumer_secret = "zzeee",
  access_token = "1234-zzzzz",
  access_secret = "zzzzaaaaa")

Les règles d’utilisation de l’API Twitter peuvent se trouver ici https://developer.twitter.com/en/docs/basics/rate-limiting et là https://developer.twitter.com/en/docs/basics/rate-limits.html . Information sélectionnée :

  • POST: The 300 per 3 hours is with the POST statuses/update and POST statuses/retweet/:id endpoints is a combined limit. You can only post 300 Tweets or Retweets during a 3 hour period. (e.g. La règle des 300 requêtes toutes les 3 heures se comprend avec les requêtes POST statuses/update et POST statuses/retweet/:id cumulées. Vous ne pouvez poster que 300 Tweets ou Retweets pendant une période de 3 heures.)
  • GET: All request windows are 15 minutes in length. Endpoint sGET earch/tweets: Resource family search: Requests / window (user auth) 180, Requests / window (app auth) 450. (e.g. Les limites de récupération des tweets par recherche sont de 180 (compte personnel) ou 450 (authentification avec une app) toutes les 15 minutes.)

Aussi, soyez sûrs de respecter les règles d’automatisation de Twitter: https://help.twitter.com/en/rules-and-policies/twitter-automation
Dans ce cas précis:

Retweets automatisés : tant que vous respectez toutes les règles, vous pouvez retweeter ou citer un Tweet de manière automatisée pour proposer du divertissement, des informations ou des nouveautés. Les Retweets automatisés engendrent souvent des expériences utilisateur négatives. Par ailleurs, les Retweets groupés, s’apparentant à du spam ou à caractère agressif constituent une infraction aux Règles de Twitter.

Récupérer les tweets et les stocker localement

La première fonction est utilisée pour récupérer les tweets de Twitter et les stocker sur le serveur. C’est le code de la fonction get_and_store() dans le package {tweetrbot}.

  • Pour chaque itération du CRON, nous téléchargeons les 20 derniers tweets avec #rspatial.
  • Nous créons deux bases de données :
    • Une petite pour garder les derniers tweets pour être sûr de ne pas retweeter les tweets déjà tweetés : to_tweet_rspatial.rds.
    • Une grande qui stockera tous les tweets récupérés depuis le début, pour de futures analyses : complete_tweets_rspatial.rds.
  • Nous stockons la sortie console du dernier CRON dans un fichier de log, juste au cas où.
# For logs
sink(file = "rtweet_console.log", append = FALSE)

# Number of tweets to retrieve
n_tweets <- 20

# Retrieve tweets for one hashtag
cat("Retrieve tweets\n") # for log
new_tweets <- rtweet::search_tweets(
  "#rspatial", n = n_tweets, include_rts = FALSE
) %>% 
  mutate(
    retweet_order = NA_real_,
    bot_retweet = FALSE)

# Add to the existing database
cat("Add tweets to to-tweet database\n") # for log
tweets_file <- "tweets_rspatial.rds"
if (file.exists(tweets_file)) {
  old_tweets <- readRDS(tweets_file)
  newold_tweets <- new_tweets %>% 
    bind_rows(old_tweets) %>% 
    arrange(desc(bot_retweet)) %>% # TRUE first 
    distinct(status_id, .keep_all = TRUE)
} else {
  newold_tweets <- new_tweets
}
saveRDS(newold_tweets, tweets_file)

# Add to the complete database
cat("Add tweets to complete database\n") # for log
complete_tweets_file <- "complete_tweets_rspatial.rds"
if (file.exists(complete_tweets_file)) {
  complete_old_tweets <- readRDS(complete_tweets_file)
  complete_newold_tweets <- new_tweets %>% 
    bind_rows(complete_old_tweets) %>% 
    distinct(status_id, .keep_all = TRUE)
} else {
  complete_newold_tweets <- new_tweets
}
saveRDS(complete_newold_tweets, complete_tweets_file)
## # A tibble: 20 x 92
##    user_id status_id created_at          screen_name text  source display_text_wi… reply_to_status… reply_to_user_id
##    <chr>   <chr>     <dttm>              <chr>       <chr> <chr>             <dbl> <chr>            <chr>           
##  1 948421… 12581094… 2020-05-06 19:00:41 statsandda… "📚Li… Twitt…              277 <NA>             <NA>            
##  2 948421… 12581065… 2020-05-06 18:49:04 statsandda… "📚#r… Twitt…              275 <NA>             <NA>            
##  3 507582… 12580813… 2020-05-06 17:08:56 chrisprener "Exc… Twitt…              277 <NA>             <NA>            
##  4 153192… 12580669… 2020-05-06 16:11:48 kaseyzap    "Jus… Twitt…              113 <NA>             <NA>            
##  5 473551… 12577737… 2020-05-05 20:46:38 SpatialPat… "Gi*… Twitt…              114 <NA>             <NA>            
##  6 973194… 12577220… 2020-05-05 17:21:30 allisongli… "@le… Twitt…              208 125770284998477… 2930387886      
##  7 712203… 12577052… 2020-05-05 16:14:41 TimSalabim3 "#rs… Twitt…               47 <NA>             <NA>            
##  8 986017… 12576860… 2020-05-05 14:58:08 v_valerioh  "@Ci… Twitt…               59 125767343544825… 742379544309567…
##  9 742379… 12576794… 2020-05-05 14:31:52 CivicAngela "@Sh… Tweet…              234 125767863578947… 742379544309567…
## 10 742379… 12576734… 2020-05-05 14:08:08 CivicAngela "Goo… Twitt…              196 <NA>             <NA>            
## 11 346069… 12576761… 2020-05-05 14:18:47 dikayodata  "I’v… Twitt…              132 <NA>             <NA>            
## 12 400694… 12576424… 2020-05-05 12:04:57 yabellini   "Mañ… Twitt…              111 <NA>             <NA>            
## 13 720884… 12576033… 2020-05-05 09:29:45 hanna123987 "Can… Twitt…              276 <NA>             <NA>            
## 14 103516… 12574612… 2020-05-05 00:04:52 mdsumner    "pol… Twitt…              102 <NA>             <NA>            
## 15 925963… 12573945… 2020-05-04 19:40:05 AlexaLFH    "I'm… Twitt…              255 <NA>             <NA>            
## 16 148518… 12573795… 2020-05-04 18:40:24 edzerpebes… "Hi … Twitt…              168 <NA>             <NA>            
## 17 148518… 12573776… 2020-05-04 18:32:50 edzerpebes… "@Md… Twitt…               73 125729207432981… 17159131        
## 18 148518… 12573763… 2020-05-04 18:27:40 edzerpebes… "sf … Twitt…              212 <NA>             <NA>            
## 19 281055… 12573099… 2020-05-04 14:03:47 jakub_nowo… "@ed… Twitt…              274 <NA>             148518970       
## 20 124906… 12572954… 2020-05-04 13:06:15 davsjob     "🗺️A… Twitt…              215 <NA>             <NA>            
## # … with 83 more variables: reply_to_screen_name <chr>, is_quote <lgl>, is_retweet <lgl>, favorite_count <int>,
## #   retweet_count <int>, quote_count <int>, reply_count <int>, hashtags <list>, symbols <list>, urls_url <list>,
## #   urls_t.co <list>, urls_expanded_url <list>, media_url <list>, media_t.co <list>, media_expanded_url <list>,
## #   media_type <list>, ext_media_url <list>, ext_media_t.co <list>, ext_media_expanded_url <list>,
## #   ext_media_type <chr>, mentions_user_id <list>, mentions_screen_name <list>, lang <chr>, quoted_status_id <chr>,
## #   quoted_text <chr>, quoted_created_at <dttm>, quoted_source <chr>, quoted_favorite_count <int>,
## #   quoted_retweet_count <int>, quoted_user_id <chr>, quoted_screen_name <chr>, quoted_name <chr>,
## #   quoted_followers_count <int>, quoted_friends_count <int>, quoted_statuses_count <int>, quoted_location <chr>,
## #   quoted_description <chr>, quoted_verified <lgl>, retweet_status_id <chr>, retweet_text <chr>,
## #   retweet_created_at <dttm>, retweet_source <chr>, retweet_favorite_count <int>, retweet_retweet_count <int>,
## #   retweet_user_id <chr>, retweet_screen_name <chr>, retweet_name <chr>, retweet_followers_count <int>,
## #   retweet_friends_count <int>, retweet_statuses_count <int>, retweet_location <chr>, retweet_description <chr>,
## #   retweet_verified <lgl>, place_url <chr>, place_name <chr>, place_full_name <chr>, place_type <chr>, country <chr>,
## #   country_code <chr>, geo_coords <list>, coords_coords <list>, bbox_coords <list>, status_url <chr>, name <chr>,
## #   location <chr>, description <chr>, url <chr>, protected <lgl>, followers_count <int>, friends_count <int>,
## #   listed_count <int>, statuses_count <int>, favourites_count <int>, account_created_at <dttm>, verified <lgl>,
## #   profile_url <chr>, profile_expanded_url <chr>, account_lang <lgl>, profile_banner_url <chr>,
## #   profile_background_url <chr>, profile_image_url <chr>, retweet_order <dbl>, bot_retweet <lgl>

Tweeter régulièrement et terminer le processus

La seconde fonction tweetera un par un s’il n’y a pas d’autre processus de tweeting. C’est le code de la fonction retweet_and_update() du package {tweetrbot}.

  • Vérifier s’il n’y a pas déjà un processus R qui exécute une boucle de tweets.
    • Remplir le PID de processus dans un fichier journal externe. Seulement s’il est vide, on peut exécuter la boucle.
  • Créer un script qui retweettera toutes les 10 minutes, si ce n’est déjà fait.
    • Définir l’ordre de tweeting : des tweets les plus anciens aux tweets les plus récents.
  • Mettre à jour l’information lorsqu’elle est retweetée avec bot_retweet=TRUE si ça a fonctionné et bot_retweet=NA si ça n’a pas fonctionné pour une investigation plus poussée si nécessaire. À l’origine, ce paramètre est défini sur bot_retweet=FALSE lorsqu’il est créé.
  • Mise à jour de la base de données à la fin de la boucle
    • Lire la dernière version de la base de données (au cas où un autre CRON serait arrivé pendant la boucle)
    • Mise à jour avec informations après retweet
    • Supprimez les tweets si leur taille est supérieure à 3 fois le nombre de tweets récupérés (20 ici, donc 60).
    • Sauvegarder la base de données mise à jour
# Get current PID
current_pid <- as.character(Sys.getpid())

# Read log PID to verify no running process
loop_pid_file <- "loop_pid.log"
if (!file.exists(loop_pid_file)) {file.create(loop_pid_file)}
loop_pid <- readLines(loop_pid_file)

# Run loop only if not already running
if (length(loop_pid) != 0)  {
  cat("Loop already running\n") # for log
  return(NULL)
}

cat("Start the loop\n") # for log
# Fill the log file to prevent other process
writeLines(current_pid, loop_pid_file)  

# Add a column to database to define retweeting order
tweets_file <- "tweets_rspatial.rds"
to_tweets <- readRDS(tweets_file) %>% 
  filter(bot_retweet == FALSE) %>% 
  arrange(desc(created_at)) %>% # older at the end
  mutate(retweet_order = rev(1:n())) %>% # older tweeted first
  select(retweet_order, bot_retweet, everything())

# Retweet
for (i in sort(to_tweets$retweet_order)) {
  cat("Loop: ", i, "/", max(to_tweets$retweet_order), "\n") # for log
  # which to retweet
  w.id <- which(to_tweets$retweet_order == i)
  print(paste(i, "- Retweet: N=", 
              to_tweets$retweet_order[w.id],
              "-",
              substr(to_tweets$text[w.id], 1, 180)))
  retweet_id <- to_tweets$status_id[w.id]
  r <- rtweet::post_tweet(retweet_id = retweet_id)
  # Change status
  if (r$status_code == 200) {
    # status OK
    to_tweets$bot_retweet[w.id] <- TRUE
  } else {
    # status not OK
    to_tweets$bot_retweet[w.id] <- NA
  }
  #   # Wait before the following retweet to avoid to be ban
  #   # Sys.sleep(60*10) # Sleep 10 minutes
  #   Sys.sleep(10)
  # }
  
  # Save failure in other database
  failed_tweets <- to_tweets %>% 
    filter(is.na(bot_retweet))
  
  # _Add failed to the existing database
  tweets_failed_file <- "tweets_failed_rspatial.rds"
  if (file.exists(tweets_failed_file)) {
    old_failed_tweets <- readRDS(tweets_failed_file)
    newold_failed_tweets <- failed_tweets %>% 
      bind_rows(old_failed_tweets) %>% 
      distinct(status_id, .keep_all = TRUE)
  } else {
    newold_failed_tweets <- failed_tweets
  }
  saveRDS(newold_failed_tweets, tweets_failed_file)
  
  # Read current dataset on disk again (in case there was an update)
  tweets_file <- "tweets_rspatial.rds"
  current_tweets <- readRDS(tweets_file)
  # Remove duplicates, keep retweet = TRUE (first in list)
  updated_tweets <- to_tweets %>% 
    bind_rows(current_tweets) %>% 
    arrange(desc(bot_retweet)) %>% # TRUE first 
    distinct(status_id, .keep_all = TRUE)
  # Remove data from the to-tweets database if number is bigger than 50 and already retweeted
  if (nrow(updated_tweets) > (n_tweets * 3)) {
    updated_tweets <- updated_tweets %>% 
      arrange(desc(created_at)) %>% 
      slice(1:(n_tweets * 3))
  }
  # Save updated list of tweets
  saveRDS(updated_tweets, tweets_file)
  
  # Wait before the following retweet to avoid to be ban
  # Sys.sleep(60*10) # Sleep 10 minutes
  Sys.sleep(10)
}

# remove pid when loop finished
file.remove(loop_pid_file)

# Stop sink
sink(file = NULL, append = FALSE)

Procedure avec le package {tweetrbot} et un Raspi

Installer R 3.6 sur le Raspberry Pi et les packages nécessaires

R doit être installé sur le serveur.
Sur le dépôt par défaut de Raspberry Pi, il y a la R 3.3, qui est, disons, assez ancienne… Pour obtenir la dernière version, vous devez compiler R à partir des sources. (Code adapté à partir de cette ressource intéressante : Configurez votre propre serveur Shiny / Rstudio sur un Raspberry Pi 3B+). Ça peut prendre beaucoup de temps!
Notez que pour l’installation de R, j’ai spécifié un répertoire personnalisé avec une option comme --prefix=$HOME/R, car le manque de place m’oblige à stocker R sur un disque externe. Ce n’est pas indispensable.

bash

sudo apt-get install -y gfortran libreadline6-dev libx11-dev libxt-dev \
       libpng-dev libjpeg-dev libcairo2-dev xvfb \
       libbz2-dev libzstd-dev liblzma-dev \
       libcurl4-openssl-dev libssl-dev \
       wget
cd /usr/local/src
sudo wget https://cran.rstudio.com/src/base/R-3/R-3.6.1.tar.gz
sudo su
tar zxvf R-3.6.1.tar.gz
cd R-3.6.1
./configure --enable-R-shlib --prefix=$HOME/R #--with-blas --with-lapack #optional
make
make install
cd ..
rm -rf R-3.6.1*
exit
cd

Maintenant, nous allons installer les packages R nécessaires, en particulier {tweetrbot}. Ici j’utilise sudo R pour ouvrir R dans le terminal, afin de pouvoir configurer {rtweet} avec le super-utilisateur. Ce même utilisateur qui exécutera le CRON. Si comme moi, vous avez installé R dans un répertoire spécifique, changez votre système PATH, ou appelez R en utilisant le chemin complet.

bash

sudo $HOME/R/bin/R

R

install.packages("remotes", repos = "https://cloud.r-project.org/")
remotes::install_github("statnmap/tweetrbot")

[EDIT 2019-09-07] Si vous avez des problèmes pour installer {httpuv} et/ou {later} sur votre Raspberry Pi, vous voudrez sûrement lire cette issue et compiler {later} vous-même : https://github.com/r-lib/later/issues/73
Récupérer le package :
bash

git clone https://github.com/r-lib/later.git
# sudo apt install libboost-atomic-dev #optional if you don't have libboost
sudo vi later/src/Makevars

Modifier Makevars :

PKG_LIBS = -pthread -latomic
# If that doesn't work, try:
PKG_LIBS = -pthread -lboost_atomic

Installer manuellement :
bash

sudo R CMD INSTALL later

Preparer le script R qui tournera régulièrement

Configurez vos token Twitter à l’aide de rtweet::create_token() en utilisant la session R appropriée, avant la création du script R. Il n’est pas nécessaire de laisser vos informations d’identification apparaître en clair dans ce script !
Ensuite, créez le script R qui sera exécuté par le CRON.

bash

mkdir ~/talk_rspatial
cd ~/talk_rspatial
vim rtweet_raspi.R
  • L’option complete_tweets_file permet de sauvegarder la liste complète des tweets retweetés depuis le début. Au cas où vous voudriez faire une analyse de tweets plus tard.
  • debug=FALSE dans la fonction retweet_and_update() tweetera réellement sur Twitter si le compte est correctement configuré. Pour les tests préliminaires, utilisez debug=TRUE.

R

library(tweetrbot)
# Where to store tweets and logs
my_dir <- "~/talk_rspatial"
## Retrieve tweets, store on the drive
get_and_store(
  query = "#rspatial", n_tweets = 20,
  dir = my_dir
)
## Tweet regularly and update the table stored on the drive
retweet_and_update(
  dir = my_dir,
  n_tweets = 20, n_limit = 3,
  sys_sleep = 600, debug = FALSE
)

Configurer le CRON

Un CRON est la version courte de crontab, une chrono table du système permettant de demander à votre système d’exécuter certaines tâches à un moment précis de l’année, du mois, du jour…
Nous éditons la crontab pour exécuter notre script R

bash

sudo crontab -e
# if you want to run for a specific user
crontab -u yourusername -e 

Ensuite, un fichier crontab s’ouvrira, dans lequel vous pourrez ajouter une commande avec la formule suivants :

Minute Hour Day-of-Month Month Day-Of-Week Command

Donc, pour exécuter le script R rtweet_raspi.R toutes les 2 heures pour chaque jour de l’année, nous devons ajouter au fichier crontab la ligne suivante. Si, comme moi, vous avez installé R dans un répertoire spécifique, changez votre système PATH, ou appelez R en utilisant le chemin complet.

0 */2 * * * sudo $HOME/R/bin/Rscript ~/talk_rspatial/rtweet_raspi.R

[EDIT: 2020-05-06] Par ailleurs, si votre raspi plante ou reboot au milieu d’une boucle en cours, les tweets reprendront quand même malgré la présence du fichier loop_pid.log. J’ai mis à jour les fonctions du package {tweetrbot} pour ça !

Aller plus loin

Maintenant que vous avez un script R pour récupérer et stocker les tweets d’une communauté spécifique, vous pouvez imaginer quelques analyses. De plus, comme vous savez comment configurer un CRON, vous pouvez imaginer quelques tweets programmés avec les analyses graphiques des tweets du mois passé par exemple.
Utilisez votre propre imagination, mais essayez de ne pas trop déranger trop de personnes (réelles) sur Twitter !

Maintenant que le bot est configuré, n’hésitez pas à utiliser #rspatial dans vos tweets et à suivre @talk_rspatial. Soyez patient pour le retweet car ils sont récupérés toutes les 2 heures seulement, puis retweetés toutes les 10 minutes. Aussi mon serveur est un Raspi, soyez indulgents !

Autres ressources



Citation :

Merci de citer ce travail avec :
Rochette Sébastien. (2019, août. 30). "Créer un robot twitter sur un Raspberry Pi 3 avec R". Retrieved from https://statnmap.com/fr/2019-08-30-creer-un-robot-twitter-sur-un-raspberry-pi-3-avec-r/.


Citation BibTex :
@misc{Roche2019Créer,
    author = {Rochette Sébastien},
    title = {Créer un robot twitter sur un Raspberry Pi 3 avec R},
    url = {https://statnmap.com/fr/2019-08-30-creer-un-robot-twitter-sur-un-raspberry-pi-3-avec-r/},
    year = {2019}
  }