<?php
// src/Service/LinkedInApiService.php
namespace App\Service;
use App\Repository\LinkedInTokenRepository;
use GuzzleHttp\Client;
use Symfony\Component\Security\Core\Security;
class LinkedInApiService
{
private Client $client;
private LinkedInTokenRepository $linkedInTokenRepository;
private Security $security;
private array $mimeTypes = [];
public function __construct(
LinkedInTokenRepository $linkedInTokenRepository,
Security $security
) {
$this->client = new Client();
$this->linkedInTokenRepository = $linkedInTokenRepository;
$this->security = $security;
}
private function getAccessToken(string $_site)
{
$user = $this->security->getUser();
if (!$user) {
throw new \RuntimeException('User not logged in.');
}
$linkedInToken = $this->linkedInTokenRepository->findValidTokenByUser($user,$_site);
if (!$linkedInToken) {
throw new \RuntimeException('No valid LinkedIn token found for the user.');
}
return $linkedInToken;
}
public function publishPost(string $text, string $type, array $mediaUrls = [], string $_site): array
{
$accessToken = $this->getAccessToken($_site);
$uploadedMedia = $this->upload($type, $mediaUrls,$_site);
// Construire les données du post
$postData = [
'author' => 'urn:li:organization:' . $accessToken->getLinkedInOrganizationId(),
'lifecycleState' => 'PUBLISHED',
'specificContent' => [
'com.linkedin.ugc.ShareContent' => [
'shareCommentary' => ['text' => $this->cleanHtmlForLinkedIn($text)],
'shareMediaCategory' => $type,
'media' => $uploadedMedia, // Utilisation des URNs retournés par upload
],
],
'visibility' => ['com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC'],
];
try {
$response = $this->client->post('https://api.linkedin.com/v2/ugcPosts', [
'headers' => [
'Authorization' => "Bearer {$accessToken->getToken()}",
'Content-Type' => 'application/json',
'X-Restli-Protocol-Version' => '2.0.0',
],
'json' => $postData,
]);
$linkedInId = json_decode($response->getBody()->getContents(), true);
return $linkedInId;
} catch (\GuzzleHttp\Exception\ClientException $e) {
return [
'error' => true,
'message' => $e->getResponse()->getBody()->getContents(),
];
}
}
public function updatePost(string $existingLinkedInId, string $text, string $type, array $mediaUrls = [], string $_site): array
{
$this->deletePost($existingLinkedInId,$_site);
return $this->publishPost($text, $type, $mediaUrls, $_site);
}
public function deletePost(string $linkedInId, string $_site): bool
{
$accessToken = $this->getAccessToken($_site);
$urn = rawurlencode($linkedInId);
$url = "https://api.linkedin.com/rest/posts/{$urn}";
$response = $this->client->delete($url, [
'headers' => [
'Authorization' => "Bearer {$accessToken->getToken()}",
'X-Restli-Protocol-Version' => '2.0.0',
'LinkedIn-Version' => '202306',
],
]);
return $response->getStatusCode() === 204;
}
public function upload(string $type, array $mediaUrls = [], string $_site): array
{
$accessToken = $this->getAccessToken($_site);
$uploadedMedia = [];
foreach ($mediaUrls as $mediaUrl) {
// Step 1: Check if the type is a document
// Handle other media types (images, videos) using the existing logic
$recipe = $this->getRecipeByMediaCategory($type);
$mimeType = mime_content_type($mediaUrl);
// Register upload for images/videos
$response = $this->client->request('POST', 'https://api.linkedin.com/v2/assets?action=registerUpload', [
'headers' => [
'Authorization' => "Bearer {$accessToken->getToken()}",
'Content-Type' => 'application/json',
'X-Restli-Protocol-Version' => '2.0.0',
],
'json' => [
'registerUploadRequest' => [
'owner' => 'urn:li:organization:' . $accessToken->getLinkedInOrganizationId(),
'recipes' => [$recipe],
'serviceRelationships' => [
[
'identifier' => 'urn:li:userGeneratedContent',
'relationshipType' => 'OWNER',
],
],
],
],
]);
$data = json_decode($response->getBody()->getContents(), true);
if (!isset($data['value']['uploadMechanism']['com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest']['uploadUrl'])) {
throw new \RuntimeException('uploadUrl is missing in LinkedIn API response: ' . json_encode($data));
}
$uploadUrl = $data['value']['uploadMechanism']['com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest']['uploadUrl'];
$asset = $data['value']['asset']; // URN of the media
// Upload the file
$this->client->request('PUT', $uploadUrl, [
'headers' => [
'Authorization' => "Bearer {$accessToken->getToken()}",
'Content-Type' => $mimeType,
],
'body' => file_get_contents($mediaUrl),
]);
$uploadedMedia[] = [
'status' => 'READY',
'media' => $asset, // Use the asset URN
];
}
return $uploadedMedia;
}
private function getRecipeByMediaCategory(string $categoryType): string
{
switch ($categoryType) {
case 'IMAGE':
return 'urn:li:digitalmediaRecipe:feedshare-image';
case 'VIDEO':
return 'urn:li:digitalmediaRecipe:feedshare-video';
default:
throw new \RuntimeException("Unsupported media type: $categoryType");
}
}
function cleanHtmlForLinkedIn(string $html): string
{
// 1. Remplacer les balises <p> par des doubles sauts de ligne
$html = preg_replace('#<p>(.*?)</p>#s', "$1\n\n", $html);
// 2. Remplacer les balises <br> par des sauts de ligne
$html = preg_replace('#<br\s*/?>#i', "\n", $html);
// 3. Remplacer les balises <ul> et <li> par des sauts de ligne avec tirets pour les listes
$html = preg_replace('#<b>(.*?)</b>#s', "$1", $html); // Supprimer <ul>
// 4. Remplacer les balises <ul> et <li> par des sauts de ligne avec tirets pour les listes
$html = preg_replace('#<i>(.*?)</i>#s', "$1", $html); // Supprimer <ul>
// 5. Remplacer les balises <ul> et <li> par des sauts de ligne avec tirets pour les listes
$html = preg_replace('#<ul>(.*?)</ul>#s', "$1", $html); // Supprimer <ul>
$html = preg_replace('#<li>(.*?)</li>#s', "- $1\n", $html); // Préfixer les <li> avec un tiret
// 6. Remplacer les balises <a> par leur texte avec l'URL entre parenthèses
$html = preg_replace_callback('#<a\s+href=["\'](.*?)["\'].*?>(.*?)</a>#s', function ($matches) {
return $matches[2] . ' (' . $matches[1] . ')';
}, $html);
// 7. Supprimer les balises spécifiques aux émojis <span class="ql-emojiblot">
$html = preg_replace('#<span\s+class=["\']ql-emojiblot["\'].*?>\s*<span[^>]*>(.*?)</span>\s*</span>#s', '$1', $html);
$html = preg_replace('#</span>#i', '', $html);
// 8. Supprimer les lignes vides multiples
$html = preg_replace("/\n{3,}/", "\n\n", trim($html));
return $html;
}
}