En continuant à naviguer sur mon site, vous consentez à ce que j'utilise des cookies pour collecter les statistiques de visites. En savoir plus

#Informatique Développement du site Utopic Radio - Partie 5 : Créer un community-manager virtuel

Comme je suis tout seul à gérer Utopic Radio, et que je fais cela avant tout par passion, je ne peux malheureusement pas y consacrer 100% de mon temps. Donc, pour animer le compte Twitter sans m'en soucier, voici une petite astuce…

Certain diront que c'est de la triche, et ils auront tout à fait raison :) Mais dans mon cas, c'est tout à fait justifiable.

Nous allons donc commencer par mettre en place les données. Une fois n'est pas coutume, j'ai choisi le format YAML pour rassembler mes données :

bonap: 
  cron: 
    days: "dow:12345"
    start: "12:00"
    end: "13:30"
  messages: 
    - "Bon appétit à tous ! Vous mangez quoi ce midi ? :)"
    - "Régalez-vous avec #UtopicRadio. Bon appétit :)"
    - "Qu'est-ce qui est meilleur qu'un bon burger ? Un bon burger en écoutant #UtopicRadio. #BonAppetit :)"
    - "Passez une excellente journée avec #UtopicRadio et bon appétit ! #GardezLeSourire"
    - "Savourez votre repas avec #UtopicRadio :) #BonAppetit"
    - "Vous avez prévu un petit footing après avoir mangé ? Motivez-vous en écoutant #UtopicRadio !"

Si vous suivez le compte Twitter d'Utopic, vous aurez probablement déjà vu ces messages. Expliquons un peu comment fonctionne le format.

La première ligne ici représente la clef du groupe (parce qu'évidemment, il n'y a pas que des messages postés le midi). À chaque groupe, on doit déclarer à quel moment le message sera posé (cron), et quels messages doivent être postés (messages).

Ici, ça peut se comprendre assez facilement : le message doit être posté entre 12:00 et 13:30. Et pour "days", "dow" signifie "day of week", et les chiffres "12345" correspondent aux jours de la semaine (dimanche = 0, lundi = 1, mardi = 2, … , samedi = 6).

Pour le cron, j'ai également prévu d'autres possibilités afin de couvrir le plus possible de situations. Il est par exemple possible de spécifier une date de début (dayStart) et une date de fin (dayEnd ; pratique pour poster des messages spécifiques à l'approche de Noël par exemple), et pour le paramètre "days", plutôt que d'utiliser le préfixe "dow:", il est possible d'utiliser le préfixe "date:" pour indiquer une date spécifique dans le calendrier (sous le format mois/date), le préfixe "str:" pour indiquer une chaîne spécialement reconnue par la fonction strtotime de PHP (exemple : "last sunday of october"), enfin, il est possible de n'indiquer que "*" pour dire que cela doit se passer tous les jours.

Quant aux messages inscrits pour chaque groupe, ils sont choisis aléatoirement. Je n'en ai pas eu l'utilité dans mon script, mais j'avais songé à la possibilité d'ajouter des "jetons" dans les messages pour qu'ils soient personnalisés en contexte. Par exemple, imaginons un message :

Bonjour à tous ! Aujourd'hui, il {meteo} à Nantes. #UtopicRadio

Ici le jeton {meteo} serait remplacé par le temps qu'il fait après avoir interrogé une API appropriée.

Maintenant que vous avez fait votre superbe fichier et que vous avez bien pris le temps de faire 5-6 message par groupe, passons au code source :

$messages = loadData('messages', 'yml');
$planned = loadData('planned');

if (justChangedDay()) {
    resetFile($planned);
}
storeCurrentDay();

foreach ($planned as $key => $null) {
    if (canPost($planned, $key)) {
        postMessage($planned, $key);
    }
}

foreach ($messages as $key => $null) {
    if (canPlan($planned, $messages, $key)) {
        planify($planned, $messages, $key);
    }
}

Ça paraît simple comme ça, mais vous allez le voir, il y a quand même pas mal de code derrière les fonctions appelées.

Mais voyons d'abord la logique ici : après avoir chargé tous les messages et ceux qui sont planifiés (j'y reviens après), on regarde tout d'abord si, depuis le dernier appel de ce script, on a changé de jour. Cela permettra d'éviter de se souvenir qu'on a déjà posté un message de bon appétit le midi le jour-même, et de tout remettre à zéro pour le lendemain.

Si c'est le cas, on remet à zéro le fichier des planifications. Ensuite, storeCurrentDay() est là pour enregistrer le numéro "dow" (qu'on a vu tout à l'heure) dans un fichier. C'est grâce à ce fichier dont le contenu change chaque jour, que je peux justement savoir si le dernier appel date de la veille ou du jour-même.

Voici les fonctions en question :

function justChangedDay() {
    $prevDay = file_get_contents(__DIR__.'/data/communityManager.dow');
    $dow = date('w');
    return $dow != $prevDay;
}

function resetFile($planned) {
    $time = time();
    foreach ($planned as $key => $value) {
        if ($value['timestamp'] < $time) {
            unset($planned[$key]);
        }
    }
    saveData('planned', $planned);
}

function storeCurrentDay() {
    file_put_contents(__DIR__.'/data/communityManager.dow', date('w'));
}

Et je suis gentil, je vous donne les fonctions pour lire les données YAML/JSON et les enregistrer :)

function retrieveDataFile($f, $type = 'json') {
    return __DIR__.'/data/'.$f.'.'.$type;
}

function loadData($f, $type = 'json') {
    $dataFile = retrieveDataFile($f, $type);
    $contents = file_get_contents($dataFile);
    if ($type == 'yml') {
        return yaml_parse($contents);
    } else {
        return json_decode($contents, true);
    }
}

function saveData($f, $o) {
    $dataFile = retrieveDataFile($f);
    file_put_contents($dataFile, json_encode($o));
}

On continue avec la suite, et nous avons affaire à une première boucle :

foreach ($planned as $key => $null) {
    if (canPost($planned, $key)) {
        postMessage($planned, $key);
    }
}

Cette boucle fait le tour des messages planifiés, vérifie qu'elle peut les poster, et le fait en conséquence. On va d'abord entrer dans le détail de canPost().

function canPost($planned, $key) {
    return $planned[$key]['posted'] == false && time() > $planned[$key]['timestamp'];
}

Cette fonction permet de vérifier d'une part que le message n'a pas déjà été posté aujourd'hui, et si le timestamp courant est supérieur au timestamp qui a été planifié pour le message à poster.

Voici, rapidement, à quoi ressemble le fichier planned.json :

{
    "nuit": {
        "timestamp": 1442269906,
        "message": "On pense \u00e0 tous ceux qui bossent de nuit chez #UtopicRadio. Bon courage et #GardezLeSourire :)",
        "posted": true
    },
    "hello-tuewedthu": {
           "timestamp": 1442290195,
        "message": "Bonjour \u00e0 tous, d\u00e9marrez bien la journ\u00e9e avec #UtopicRadio :)",
        "posted": false
    }
}

Ensuite, la fonction postMessage() :

function postMessage($planned, $key) {
    $message = $planned[$key]['message'];
    $planned[$key]['posted'] = true;
    saveData('planned', $planned);
    postTweet($message);
    l('Message posté: ' . $message);
}

Pour poster un tweet, on indique d'abord que le message a été posté (et on enregistre le fichier de planification en conséquence, puis on poste le tweet, avec la fonction postTweet() que je détaille après. La fonction l() me permet juste de faire un peu de log.

global $twitter;
$twitter = new Twitter(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET); // À remplacer par les bonnes valeurs générées par Twitter

function postTweet($message) {
    global $twitter;
    $message = substr($message, 0, 140);
    $twitter->send($message);
}

Pour la gestion des appels de l'API Twitter, j'ai utilisé les sources de DG sur Github, et évidemment, je m'assure que mon message ne fait pas plus de 140 caractères pour éviter les problèmes.

Reprenons la suite de notre script initial, on arrive à la dernière boucle, qui elle, planifie les prochains messages à poster :

foreach ($messages as $key => $null) {
    if (canPlan($planned, $messages, $key)) {
        planify($planned, $messages, $key);
    }
}

Commençons par canPlan() :

function canPlan($planned, $messages, $key) {
    $cron = $messages[$key]['cron'];
    $days = $cron['days'];

    $isAlreadyPlanned = isset($planned[$key]);
    if ($isAlreadyPlanned) {
        return false;
    }

    if (isset($cron['dayStart'])) {
        $dayStart = strtotime($cron['dayStart']);
        if (time() < $dayStart) {
            return false;
        }
    }

    if (isset($cron['dayEnd'])) {
        $dayEnd = strtotime($cron['dayEnd']);
        if (time() > $dayEnd) {
            return false;
        }
    }

    $isToday = false;
    if (startsWith($days, 'dow:')) {
        $dow = str_split(removePrefix($days, 'dow:'));
        $dowToday = date('w');
        if (in_array($dowToday, $dow)) {
            $isToday = true;
        }
    } else if (startsWith($days, 'date:')) {
        $date = removePrefix($days, 'date:');
        $dateToday = date('m/d');
        if ($date == $dateToday) {
            $isToday = true;
        }
    } else if (startsWith($days, 'str:')) {
        $str = strtotime(removePrefix($days, 'str:'));
        $strToday = strtotime('today');
        if ($str == $strToday) {
            $isToday = true;
        }
    } else if ($days == '*') {
        $isToday = true;
    }

    if (!$isToday) {
        return false;
    }

    $canPlan = false;
    $start = $messages[$key]['cron']['start'];
    $end = $messages[$key]['cron']['end'];
    $s = intval(str_replace(':', '', $start));
    $e = intval(str_replace(':', '', $end));
    $start = strtotime('today ' . $start);
    $end = strtotime(($e > $s ? 'today ' : 'tomorrow ') . $end);
    $now = time();
    if ($now >= $start && $now <= $end) {
        $canPlan = true;
    }

    return $canPlan;
}

Désolé par avance, mais je ne détaillerai pas cette fonction un peu complexe. Retenez une seule chose : elle analyse les valeurs de "days", "dayStart" et "dayEnd" (on en a parlé tout à l'heure), et la logique se déroule petit à petit, en fonction de la valeur renseignée : on regarde d'abord si la date d'aujourd'hui correspond bien à ce qui est demandé (jour de la semaine, date précise, créneau de date, ou chaîne de type « dernier dimanche du mois d'octobre »). Ensuite, si la date d'aujourd'hui correspond à la règle décrite, on vérifie que l'on se trouve dans le créneau horaire décrit. Si c'est le cas, alors, on peut planifier le message.

J'ajoute que la fonction startsWith() (pas incluse dans PHP) permet de savoir si une chaîne commence par les caractères décrits en deuxième paramètre, et la fonction removePrefix() permet de retirer le début d'une chaîne. Voici leurs sources :

function startsWith($haystack, $needle) {
    return $needle === "" || strrpos($haystack, $needle, -strlen($haystack)) !== FALSE;
}

function endsWith($haystack, $needle) {
    return $needle === "" || (($temp = strlen($haystack) - strlen($needle)) >= 0 && strpos($haystack, $needle, $temp) !== FALSE);
}

function removePrefix($haystack, $needle) {
    return substr($haystack, strlen($needle));
}

En bonus, vous avez même le endsWith() :) .

Comment se passe la planification, donc ?

function planify($planned, $messages, $key) {
    $timestamp = retrieveNextTimestamp($messages, $key);
    $message = retrieveNewMessage($messages, $key);
    $planned[$key] = [
        'timestamp' => $timestamp,
        'message' => $message,
        'posted' => false
    ];
    saveData('planned', $planned);
    l('Message planifié: ' . $message . ' (' . date('Y-m-d H:i:s', $timestamp) . ')');
}

On commence par récupérer le timestamp auquel on postera le message. C'est bien, mais on a à notre disposition qu'un créneau horaire… On va donc miser sur le hasard ! En plus, ça ajoute un côté naturel.

function retrieveNextTimestamp($messages, $key) {
    $start = $messages[$key]['cron']['start'];
    $end = $messages[$key]['cron']['end'];
    $s = intval(str_replace(':', '', $start));
    $e = intval(str_replace(':', '', $end));
    $start = max(time(), strtotime('today ' . $start));
    $end = strtotime(($e > $s ? 'today ' : 'tomorrow ') . $end);
    $timestamp = rand($start, $end);
    return $timestamp;
}

Cette fonction, au même titre que canPlan(), tient également compte des créneaux horaires à cheval sur deux jours (exemple, entre 23:00 et 2:00).

Ensuite, on récupère un message au hasard dans la liste :

function retrieveNewMessage($messages, $key) {
    $messages = $messages[$key]['messages'];
    $message = $messages[rand(0, count($messages) - 1)];
    return $message;
}

C'est d'ailleurs au sein de cette fonction qu'on pourrait ajouter un contrôle de jetons dont on parlait tout à l'heure.

Une fois que ces deux données sont récupérées, on les ajoute au fichier de planification, et le tour est joué :) !


Il nous reste cependant une dernière chose à voir. Car jusqu'ici, on a vu comment poster des messages sur Twitter, mais vous avez certainement déjà vu des messages du style "En ce moment, vous écoutez Untel avec Truc sur #UtopicRadio".

Là c'est un deuxième script que je vous laisserai concevoir vous-même à partir de tout ce que nous avons déjà vu depuis le début de ce dossier. Notez juste deux choses : pour ce script, pensez d'abord à ne pas poster de tweet, si vous êtes en présence d'un élément d'habillage (ça ne serait que peu pertinent), et ensuite vous n'avez pas besoin de planifier les tweets ou de savoir si vous l'avez déjà posté.

Pourquoi ? Parce que, qu'il s'agisse du script qui poste la chanson en cours de lecture, ou qu'il s'agisse du script qui anime votre Twitter, ils seront tous les deux appelés par le cron de votre serveur.

Aussi, je vous recommande de faire appel au script que nous avons détaillé dans cet article toutes les minutes, afin de bien gérer d'avoir la meilleure granularité vis-à-vis des timestamp. Quant à l'autre script qui poste la chanson en cours de lecture, pour ne pas spammer vos abonnés, je vous recommande d'appeler votre script toutes les 3h, c'est, à mon goût, ni trop, ni trop peu. Et histoire de paraître un poil plus naturel, songez à appelez ce script toutes les 3h, mais pas à l'heure pile :) .

Rédigé le .

Commentaires

comments powered by Disqus