Récupérer des tweets et naviguer dans une timeline

C’EST REPARTI

Aujourd’hui, on va continuer notre petit « tutoriel » de l’utilisation de base de l’API Twitter. Ceci est la suite de l’article Première approche avec Twitter.

La dernière fois, on s’est contenté d’envoyer un tweet, mais ce n’est pas vraiment incroyable. Le principe de Twitter est d’avant tout de lire ce que le monde nous propose ! On suppose que le compte que vous utilisez pour vous connecter à l’API a des abonnements, et donc a une « home timeline », l’accueil Twitter dans la langue d’Astérix.

On va donc récupérer cette timeline, et disséquer un poil l’anatomie d’un tweet.

Récupération d’une timeline

On va aller un peu plus vite avec la formation d’une requête aujourd’hui, sachez que la documentation de l’endpoint que nous allons utiliser est ici et que l’adresse de l’endpoint est https://api.twitter.com/1.1/statuses/home_timeline.json.

Cet endpoint renvoie un tableau de tweets (formaté en JSON) avec au maximum :count éléments (:count étant la valeur passée en paramètre à Twitter lors de l’appel).

Avant de récupérer notre première timeline, il faut comprendre deux trois petites choses; La pagination n’existe pas dans les timelines : en effet, imaginez que vous récupérez une première fois la timeline, 5 tweets. Vous décidez de récupérer les suivants, mais voilà : deux tweets ont été postés entre temps. En précisant un « décale de 5 » à Twitter, vous obtiendrez des doublons par rapport à la requête précédente. Explications en image !

Source: Documentation Twitter

Pour éviter ce problème, Twitter travaille avec des « curseurs » : En gros, l’ID des tweets servira de curseur pour vous déplacer. Vous préciserez à Twitter « récupère les tweets seulement à partir de cet ID » ou « récupère les tweets plus anciens que cet ID ».

Ces paramètres correspondent au « since_id » (depuis cet ID, non compris) et au « max_id » (avant cet ID, ID compris).

Une fois que vous avez bien compris ça, la navigation dans les timelines sera très simple.

Codons un peu

Pour débuter, on va écrire une méthode getHomeTimeline() pour notre classe Twitter. Mais ça ne sera pas tout ! Pour pouvoir « retenir » la position dans la timeline entre deux utilisations du script, il faudra se « souvenir » du précédent ID maximum de tweet. Comme ça, entre deux utilisations, on récupérera uniquement les tweets les plus récents, sans doublons !

Vous pouvez utiliser une base SQL (gros moyens quand même) ou un simple fichier texte qui ne contiendra que l’ID, ou encore un JSON. Comme vous voulez, l’important est d’avoir un moyen de stockage. J’utiliserai un JSON.

Mettons en place ce système, donc.

Notre belle classe Twitter va désormais ressembler à ça :

class Twitter {
    protected $oauth;

    protected $json;
    protected const JSON_PATH = 'bot.json';

    public function __construct(string $oauth_consumer, string $oauth_consumer_secret) {
        $this->oauth = new OAuth($oauth_consumer, $oauth_consumer_secret);
        $this->json = json_decode(file_get_contents(self::JSON_PATH), true);
    }

    public function __destruct() {
        $this->json = file_put_contents(self::JSON_PATH, json_encode($this->json));
    }

    public function getSinceID() : string {
        return ($this->json['next_since_id'] ?? '1');
    }

    public function setSinceID(string $since_id) : void {
        $this->json['next_since_id'] = $since_id;
    }

    public function setToken(string $access_token, string $access_token_secret) : void { ... }

    public function sendTweet(string $status) : ?array { ... }
}

 Twitter.php

Et voilà ! J’utilise le destructeur et le constructeur pour « synchroniser » la RAM et le fichier JSON au début et à la fin du script, plutôt que d’écrire sur le fichier à chaque modification.

Attaquons nous enfin à notre timeline.

Notez que je choisis souvent d’interpréter le JSON comme un tableau (deuxième paramètre de json_decode) mais vous pouvez très bien renvoyer en mode objet (omettre le paramètre), l’utilisation est la même.

 

public function getHomeTimeline() : ?array {
    $since_id = $this->getSinceID();

    try {
        $this->oauth->fetch('https://api.twitter.com/1.1/statuses/home_timeline.json',     
                            ['count' => 200, 'since_id' => $since_id, 'include_entities' => true, 'tweet_mode' => 'extended'], 
                            OAUTH_HTTP_METHOD_GET);
    } catch (OAuthException $e) {
        echo "Impossible d'obtenir la timeline : {$e->lastResponse}";
        return null;
    }

    $tweets = json_decode($this->oauth->getLastResponse());

    if($tweets) {
        if(count($tweets) > 0)
            $this->setSinceID($tweets[0]->id_str);

        return $tweets;
    }
    else {
        return null;
    }
}

Notez les paramètres utilisés : count pour le nombre maximum de tweets renvoyés, since_id pour la pagination entre deux appels, tweet_mode pour les tweets en 280 caractères, et include_entities pour… vous verrez plus tard, mais c’est pour avoir des métadonnées complètes sur les tweets.

Enfin, le setSinceID est fait sur le premier tweet renvoyé (si il existe) (le premier, vu que c’est toujours renvoyé dans l’ordre chronologique, le plus récent en premier), auquel on accède à son ID par l’attribut id_str.

Anatomie d’un tweet

Notre fonction renvoie donc un tableau de tweets. Voyons comment se forme un tweet dans les grandes lignes [en mode étendu, c’est à dire avec tweet_mode: extended] :

  • full_text : Contient le texte réel du tweet, mentions de tête comprises
  • created_at : La date de postage du tweet, dans un format plutôt étrange (ddd mmm DD HH:ii:ss offset YYYY)
  • id_str : Son ID sous forme de chaîne de caractères (utile quand le script est exécuté en mode 32 bits, ou sur JS par exemple)
  • in_reply_to_status_id_str : Si le tweet est une réponse, si tel est le cas, il y aura un ID de tweet ici, sinon null
  • user : L’objet utilisateur du posteur
  • retweeted_status : Si le tweet est un retweet, ce champ apparaîtra et contiendra le tweet object du tweet retweeté
  • favorite_count : Le nombre de favoris (likes) laissés sur le tweet
  • entities : Les métadonnées (mentions, URLs, hashtags, médias liés, sondages…)
  • display_text_range : Indique les « bornes » de lecture du tweet (sans les mentions de tête interprétées par Twitter); En gros un substr(full_text, display_text_range[0], display_text_range[1]) retournera le tweet tel affiché sur Twitter Web / Twitter Android, sans mentions de tête

Exploiter les tweets et répondre

Imaginons que nous voudrions faire un bot qui répond automatiquement à un tweet au hasard de sa TL à chaque fois qu’il est lancé; Et que cette réponse serait un mot aléatoire du tweet choisi, suivi de quelques infos du genre l’@ de la personne, son TN, et combien il a eu de retweets. Ce qui nous donne :

$tweets = $twitter->getHomeTimeline();

if($tweets) {
    // Choisir un tweet aléatoire
    $choosen_one = rand(0, count($tweets) - 1);

    $tweet = &$tweets[$choosen_one];

    // On élimine les mentions de tête : elles ne font pas partie du tweet !
    $tweet_text = substr($tweet->full_text, $tweet->display_text_range[0], $tweet->display_text_range[1]);
    // On décompose les mots du tweet
    $words = explode(' ', $tweet_text);

    $choosen_word = rand(0, count($words) - 1);

    $word = $words[$choosen_word];

    // On écrit la future réponse
    $final_tweet = "Coucou {$tweet->user->name} (@{$tweet->user->screen_name}), ça va ? $word ! Ton tweet a eu {$tweet->retweet_count} retweets.";
}

main.php

Il nous reste encore à coder la fonction pour répondre.

C’est très apparenté à la fonction pour envoyer un tweet, mais avec un paramètre en plus. On va modifier pour ça notre fonction sendTweet pour lui autoriser tous les paramètres que l’on veut, et faire une macro dans la méthode replyTo().

public function sendTweet(string $status, array $parameters = []) : ?array {
    $parameters['status'] = $status;
    $parameters['tweet_mode'] = 'extended';

    try {
        // L'URL d'envoi a été déplacée comme constante de classe
        $this->oauth->fetch(self::SEND_TWEET_URL,     
                            $parameters, 
                            OAUTH_HTTP_METHOD_POST);
    } catch (OAuthException $e) {
        echo "Impossible d'uploader un tweet : {$e->lastResponse}";
        return null;
    }

    $tweet = json_decode($this->oauth->getLastResponse(), true);

    if($tweet) {
        return $tweet;
    }
    else {
        return null;
    }
}

public function replyTo(string $id_str, string $status, array $parameters = []) : ?array {
    $parameters['in_reply_to_status_id'] = $id_str;
    return $this->sendTweet($status, $parameters);
}

Twitter.php

Il ne nous reste plus qu’à envoyer notre réponse au tweet.

$twitter->replyTo($tweet->id_str, $final_tweet, ['auto_populate_reply_metadata' => true]);

main.php

Superbe. Et le since_id dans tout ça ?

Eh bien justement. Imaginons que 5 minutes après, vous relancez votre script.
Si tout va bien, il va récupérer la timeline, choisir un tweet au hasard… Mais jamais le même !

Eh oui, grâce au since_id enregistré que nous avons mis en place, il se rappelle du moment où il en était resté la fois précédente ! Ainsi, il ne relira jamais les mêmes tweets, vous êtes certains de traiter des données uniques à chaque utilisation.

Si vous n’avez pas encore tout compris, ne vous inquiétez pas, on reviendra sur le principe de navigation dans une timeline plus tard.

Mais ici, aujourd’hui, cet article s’arrête là, merci de l’avoir lu !

Et pour le reste, ce sera pour une prochaine fois…