Je blog delen met LinkedIn

Ingediend door Dirk Hornstra op 24-mar-2018 21:46

The API of linkedin has been updated. To read how to implement the code, go to this post: https://techblog.dirkhornstra.nl/node/106

Als IT-er loop je altijd achter de laatste ontwikkelingen aan. Dus je moet zorgen dat je bij blijft. Daar heb ik dit blog voor opgezet, je kunt je tijd vullen door het lezen van boeken, theater en concerten te bezoeken, je sociale contacten bijhouden via Facebook, je mailbox bij te houden. Dat doe ik in ruime mate. Maar dat is niet direct gerelateerd aan het up-to-date houden van je vaardigheden. Angular is nu een algemeen gebruikte techniek (waar ik te weinig mee doe), ik wil mijn certificering voor Microsoft op de rit krijgen. Als ik zie dat het "alweer" 7 dagen geleden is dat ik hier wat gepost heb, dan is het tijd om actie te ondernemen. Omdat dit allemaal werk-gerelateerd is wil ik het ook meteen delen op LinkedIn. Want bij mijn huidige werkgever bevalt het goed, maar dat zou over een jaar anders kunnen zijn. Of er komt iemand met zo'n goed aanbod dat ik me toch achter de oren moet gaan krabben. 

Via iftt.com kun je jouw applicaties aan elkaar knopen. Tweet je een foto? Dan kun je die ook automatisch op Facebook laten plaatsen. Zo was er ook een app om via een rss-feed ( die zit standaard in Drupal, https://techblog.dirkhornstra.nl/rss.xml ) het bericht op LinkedIn te laten plaatsen. Omdat ik zelf nog niets "kant en klaar" op de plank had liggen heb ik die gebruikt. 

Mijn oud collega Gosse Wijnsma merkte op dat de link echter niet klopte. Die verwees niet naar het artikel, maar naar de XML-feed. Omdat je bij iftt.com een applicatie van iemand anders gebruikt en rechten geeft op je LinkedIn-account, wat ik toch al niet zo fijn vond, het besluit genomen om dan zelf hier wat voor te maken. 

Eerst deze applicatie van iftt geen rechten meer geven op mijn LinkedIn-account. Dat kun je doen door op www.linkedin.com in te loggen, vervolgens op het pijltje naar me onder je profielfoto te klikken en dan naar Settings & Privacy te gaan. Onder Partners en Services heb je Permitted Services. Door op Remove te klikken verwijder je de applicaties die op jouw account acties mogen uitvoeren. Ik had hier een redelijk lijstje, meteen alles weggehaald. Als ik een applicatie gebruik die nu geen rechten meer heeft, dan krijg ik daar wel de melding te zien.

Wat moet er gebeuren om mijn berichten te delen op LinkedIn? Je moet hiervoor een eigen applicatie maken. LinkedIn werkt hier met OAuth2 authenticatie, wat je onder andere ook bij Twitter-applicaties gebruikt. Het komt erop neer dat je een URL aanroept met de gegevens van die applicatie. Je krijgt de vraag of je deze applicatie toegang wilt geven. Je kiest hier voor ja (mocht je nog niet ingelogd zijn, dan moest je hiervoor eerst inloggen), hierna worden een aantal gegevens teruggestuurd naar een bepaalde pagina. Die pagina doet met die gegevens weer een aanroep naar een pagina van LinkedIn die aan die pagina dan 2 dingen teruggeeft, een access-token waarmee je zonder in te loggen de berichten op LinkedIn kunt zetten en een tijdsindicator hoe lang dat token geldig is (meestal 60 dagen). Hierna moet je dezelfde flow dus doorlopen.

Wat heb ik gedaan? Ik ben eerst naar https://developer.linkedin.com/ gegaan. Bovenin klik je op My Apps. Hier heb ik een nieuwe app aangemaakt. r_basicprofile, r_emailaddress en w_share aangevinkt. Bij de oAuth2 redirect URL een URL ingevuld (in mijn geval https://techblog.dirkhornstra.nl/linkedin/callback ). De Client ID en Client Secret die je hier ziet heb je straks weer nodig.

Dan moeten we eerst de code maken zodat je een access-token hebt en artikelen kunt delen. Hierboven had ik al schematisch beschreven hoe dat werkt, op deze pagina zijn de stappen beschreven: https://developer.linkedin.com/docs/oauth2

Omdat dit een Drupal-site is, zijn mijn stappen dus gericht op Drupal. Maar als je een beetje ontwikkelaar bent kun je het ook zo omzetten naar Wordpress of een ander type CMS :) In de database heb ik een tabel aangemaakt, laten we zeggen met de naam linkedin. Omdat mijn drupal-prefix dpl is, heet de tabel dpl_linkedin. Deze tabel bestaat uit 2 velden: name (varchar(250)) en value (varchar(512)). Hier gaan we straks onze gegevens in opslaan (omdat het access-token meestal 350 karakters lang is, heb ik er een varchar(512) veld van gemaakt. Ik maak meteen 4 records aan, met de volgende namen (veld value blijft leeg): access_token, clientid, clientsecret, expires_in, last_post. N.B. Dit was niet helemaal waar, je moet natuurlijk wel bij clientid en clientsecret in het veld value de waardes invoeren die bij je app getoond worden. De variabelen zijn zo beschikbaar en daardoor hoeven we niet te checken op "INSERT" of "UPDATE", het is altijd een UPDATE.

Hierna heb ik in mijn submap het bestand settings.php geplaatst. Deze heeft de volgende inhoud:


 

<?php
// settings.php
if (isset($VARIABELEDOORANDERBESTAND)) {
chdir("..");
require("sites/default/settings.php");
$host = $databases["default"]["default"]["host"];
$user = $databases["default"]["default"]["username"];
$pass = $databases["default"]["default"]["password"];
$prefix = $databases["default"]["default"]["prefix"];
$clientID = "";
$clientSecret = "";
$accessToken = "";
$expiresIn = "";

$dbconn = mysql_connect($host, $user, $pass);
mysql_select_db($databases["default"]["default"]["database"], $dbconn);
$result = mysql_query("SELECT * FROM ".$prefix."linkedin");

while ($data = mysql_fetch_assoc($result))
{
  switch($data["name"])
  {
    case "access_token": $accessToken = $data["value"]; break;
    case "clientid": $clientID = $data["value"]; break;
    case "clientsecret": $clientSecret = $data["value"]; break;
    case "expires_in": $expiresIn = $data["value"]; break;
  }
}
mysql_close($dbconn);
}
else die("Ongeldige aanroep!");
?>

Dit bestand moet door een eigen bestand ingeladen worden. Om dat af te dwingen zit de controle op de waarde die ingesteld moet zijn ($VARIABELEDOORANDERBESTAND). 

In deze map staat ook het bestand index.php. Deze voert de eerste aanroep uit naar LinkedIn om je koppeling in te stellen:


 

<?php
// index.php

$VARIABELEDOORANDERBESTAND= true;
if (isset($_GET["test"]) && ($_GET["test"] == "test"))
{
  require("settings.php");
  $redirectUri = "https://techblog.dirkhornstra.nl/linkedin/callback";
  $scope = "r_basicprofile%20r_emailaddress%20w_share";
  $location = "https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=".$clientID."&redirect_uri=".$redirectUri."&state=".$settings["hash_salt"]."&scope=".$scope;
  header("Location: ".$location);
}
else die("Invalid call!");

?>

Om te voorkomen dat iedereen die pagina aan kan roepen, zit er een extra controle in. In het voorbeeld moet je de pagina dus aanroepen met /linkedin/index.php?test=test
Je ziet ook dat er een $settings["hash_salt"] meegegeven wordt. Voor de controle is het handig dat je een waarde meegeeft die je later kunt controleren. Drupal heeft standaard deze unieke waarde opgeslagen in de eigen settings.php, die heb je beschikbaar doordat je die inlaadt in je eigen settings.php.

Dan is er voor dit stappenplan nog 1 bestand van belang, in /linkedin/callback/index.php. Dat is het bestand wat de gegevens terug ontvangt als je zegt "ok, ik geef deze applicatie toegang". Ook moet deze dan nog een keer een aanroep uitvoeren om de definitieve access-key op te vragen.

 

<?php
// callback/index.php

$VARIABELEDOORANDERBESTAND= true;
chdir("..");
require("settings.php");
if ((isset($_GET["state"]))&&(isset($_GET["code"]))) {
  $code = $_GET["code"];
  $state = $_GET["state"];
  if ($state != $settings["hash_salt"]) die("foutieve aanroep!");
  $redirectUri = "https://techblog.dirkhornstra.nl/linkedin/callback";
  $url = "https://www.linkedin.com/oauth/v2/accessToken";
  $postdata = "grant_type=authorization_code&code=".$code."&redirect_uri=".$redirectUri."&client_id=".$clientID."&client_secret=".$clientSecret;
  $ch = curl_init($url);
  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_POSTFIELDS, $postdata);
  $data = curl_exec($ch);
  curl_close($ch);
  $linkedInData = json_decode($data);
  if (isset($linkedInData->access_token)&&isset($linkedInData->expires_in))
  {
    $timespan = time() + intval($linkedInData->expires_in);
    $dbconn = mysql_connect($host, $user, $pass);
    mysql_select_db($databases["default"]["default"]["database"], $dbconn);
    mysql_query("UPDATE ".$prefix."linkedin SET value='".addslashes($linkedInData->access_token)."' WHERE name='access_token'");
    mysql_query("UPDATE ".$prefix."linkedin SET value='".addslashes($timespan)."' WHERE name='expires_in'");
    mysql_close($dbconn);
    echo "DONE!";
  }
}

?>

Hiermee is het opzetten van de connectie afgerond. Nu willen we natuurlijk de berichten gaan delen.
Hoe je dat doet, dat staat beschreven op https://developer.linkedin.com/docs/share-on-linkedin

Ik maak dus een pagina die controleert of ik een artikel in Drupal gepubliceerd heb en dit niet de laatste post is die met LinkedIn gedeeld is. Eerst nog even een tabel aanmaken om de gegevens van het delen in op te slaan. Zo kun je controleren of je het artikel al gedeeld had en als je een bericht deelt, dan krijg je nog een updateKey en updateUrl van LinkedIn terug, handig om die op te slaan.
De tabel noem ik dpl_share en bevat de velden: nid (int): primary key, updateKey (varchar(250)), updateUrl (varchar(250)) en sharedate (timestamp). Dan gaan we door met het bestand wat de publicatie moet uitvoeren. Laat ik dit bestand /linkedin/share.php noemen:


<?php
// share.php

VARIABELEDOORANDERBESTAND = true;
if (isset($_GET["test"]) && ($_GET["test"] == "test"))
{
  require("settings.php");
  $dbconn = mysql_connect($host, $user, $pass);
  mysql_select_db($databases["default"]["default"]["database"], $dbconn);
  $result = mysql_query("SELECT nid FROM ".$prefix."node_field_data WHERE status=1 ORDER BY changed DESC LIMIT 0,1");
  $field = intval(mysql_fetch_assoc($result)["nid"]);
  if ($field > 0)
  {
    $result = mysql_query("SELECT COUNT(1) as aantal FROM ".$prefix."share WHERE nid=".$field);
    $addcount = intval(mysql_fetch_assoc($result)["aantal"]);
    if ($addcount == 0)
    {
      $valid = true;  
$postdata = "{
\"comment\": \"{COMMENT}\",
\"content\": {
\"title\": \"{TITLE}\",
\"description\": \"{DESCRIPTION}\",
\"submitted-url\": \"{URL}\",
\"submitted-image-url\": \"{IMAGE}\"
},
\"visibility\": {
\"code\": \"anyone\"
}
}";

      $result = mysql_query("SELECT body_summary FROM ".$prefix."node__body WHERE entity_id=".$field);
      $description = addslashes(mysql_fetch_assoc($result)["body_summary"]);
      if ((strlen($description) == 0)||(strlen($description) > 256))
      {
        mysql_close($dbconn);
        die("niet geldige samenvatting, groter dan 0, minder dan 256! lengte is " . strlen($description));
      }  
      $postdata = str_replace("{DESCRIPTION}", $description, $postdata);
      $result = mysql_query("SELECT type, title FROM ".$prefix."node_field_data WHERE nid=".$field);
      $row = mysql_fetch_row($result);
      $title = addslashes($row[0] . ": " . $row[1]);
      $postdata = str_replace("{TITLE}", $title, $postdata);
      $postdata = str_replace("{IMAGE}", "https://techblog.dirkhornstra.nl/img/profile-image.png", $postdata);
      $postdata = str_replace("{URL}", "https://techblog.dirkhornstra.nl/node/".$field, $postdata);  
      $postdata = str_replace("{COMMENT}", $description." https://techblog.dirkhornstra.nl/node/".$field, $postdata);    
      $url = "https://api.linkedin.com/v1/people/~/shares?format=json";
      $header = array();
      $header[] = "x-li-format: json";
      $header[] = "Content-type: application/json";
      $header[] = "Authorization: Bearer ".$accessToken;
      $ch = curl_init($url);
      curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
      curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
      curl_setopt($ch, CURLOPT_POSTFIELDS, $postdata);
      $data = curl_exec($ch);
      curl_close($ch);
      $linkedInData = json_decode($data);
      if (isset($linkedInData->updateKey)&&isset($linkedInData->updateUrl))  
      {
        mysql_query("INSERT INTO ".$prefix."share (nid, updateKey, updateURL) SELECT ".$field.",'".addslashes($linkedInData->updateKey)."','".addslashes($linkedInData->updateUrl)."'");
      }
    }
  }
  mysql_close($dbconn);
}
else die("Invalid call!");

?>

Zoals duidelijk mag zijn kan er in deze code nog wat verbeterd worden. De afbeelding bij het type post. De notificatie naar mij als een samenvatting leeg is of te lang is. De notificatie naar mij als de access-key verlopen is en we deze opnieuw moeten opvragen. Maar goed, dat is voor later!

Update 5 september 2018

Ik loop nu dus inderdaad zo nu en dan tegen het punt aan dat het token verlopen is. Volgens de documentatie kun je opnieuw door de autorisatieflow lopen zolang het token nog geldig is, dan hoef je geen user-input uit te voeren en kun je dus "automatisch verlengen". Daarom heb ik in mijn share.php als laatste de controle toegevoegd of het token over 2 dagen verlopen is. Zo ja, dan het dan nu vernieuwen:

 


// vervolgens checken of token ververst moet worden
$refeshToken = false;
$result = mysql_query("SELECT value FROM ".$prefix."linkedin WHERE name='expires_in'");
$expireDate = intval(mysql_fetch_assoc($result)["value"]);
$twoDaysMargin = time() + (2 * 24 * 60 * 60);
if ($expireDate < $twoDaysMargin) {
     $refeshToken = true;
}
// ... andere acties
if ($refeshToken) {
    $location = "index.php";
    header("Location: ".$location);
}