feature #37830 [Notifier] Add LinkedIn provider (ismail1432)
This PR was merged into the 5.2-dev branch.
Discussion
----------
[Notifier] Add LinkedIn provider
| Q | A
| ------------- | ---
| Branch? | master for features 5.2
| Bug fix? | no
| New feature? | yes/no <!-- please update src/**/CHANGELOG.md files -->
| Deprecations? | yes/no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tickets | Fix #34563 <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead -->
| License | MIT
| Doc PR | symfony/symfony-docs#... <!-- required for new features -->
### Add the LinkedIn provider for the Notifier
Few months ago I created a bridge to send message to LinkedIn, following [the discussion here](https://github.com/symfony/symfony/issues/34563) I create the PR.
If you're curious [I wrote an article where I use the bridge](https://medium.com/@smaine.milianni/aws-lambda-and-symfony-6d3e9831c3cd)
[Edit] test are green ~missing improvement body construction and integration test with update changes + framework integration~
[Notification sent with this PR](https://www.linkedin.com/posts/smainemilianni_hello-linkedin-from-symfony-bridge-see-pull-activity-6700328970665177088-31tT)
<!--
Replace this notice by a short README for your feature/bugfix. This will help people
understand your PR and can be used as a start for the documentation.
Additionally (see https://symfony.com/releases):
- Always add tests and ensure they pass.
- Never break backward compatibility (see https://symfony.com/bc).
- Bug fixes must be submitted against the lowest maintained branch where they apply
(lowest branches are regularly merged to upper ones so they get the fixes too.)
- Features and deprecations must be submitted against branch master.
-->
Commits
-------
0064cae9a0
add Linkedin transport and option
This commit is contained in:
commit
14c9d05dc8
@ -15,6 +15,7 @@ use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\FreeMobile\FreeMobileTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\GoogleChat\GoogleChatTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Infobip\InfobipTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\LinkedIn\LinkedInTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Mobyt\MobytTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory;
|
||||
@ -38,6 +39,10 @@ return static function (ContainerConfigurator $container) {
|
||||
->parent('notifier.transport_factory.abstract')
|
||||
->tag('chatter.transport_factory')
|
||||
|
||||
->set('notifier.transport_factory.linkedin', LinkedInTransportFactory::class)
|
||||
->parent('notifier.transport_factory.abstract')
|
||||
->tag('chatter.transport_factory')
|
||||
|
||||
->set('notifier.transport_factory.telegram', TelegramTransportFactory::class)
|
||||
->parent('notifier.transport_factory.abstract')
|
||||
->tag('chatter.transport_factory')
|
||||
|
@ -0,0 +1,7 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
5.2.0
|
||||
-----
|
||||
|
||||
* Added the bridge
|
@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Notifier\Bridge\LinkedIn;
|
||||
|
||||
use Symfony\Component\Notifier\Bridge\LinkedIn\Share\AuthorShare;
|
||||
use Symfony\Component\Notifier\Bridge\LinkedIn\Share\LifecycleStateShare;
|
||||
use Symfony\Component\Notifier\Bridge\LinkedIn\Share\ShareContentShare;
|
||||
use Symfony\Component\Notifier\Bridge\LinkedIn\Share\VisibilityShare;
|
||||
use Symfony\Component\Notifier\Message\MessageOptionsInterface;
|
||||
use Symfony\Component\Notifier\Notification\Notification;
|
||||
|
||||
/**
|
||||
* @author Smaïne Milianni <smaine.milianni@gmail.com>
|
||||
*
|
||||
* @experimental in 5.2
|
||||
*/
|
||||
final class LinkedInOptions implements MessageOptionsInterface
|
||||
{
|
||||
private $options = [];
|
||||
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
public function getRecipientId(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function fromNotification(Notification $notification): self
|
||||
{
|
||||
$options = new self();
|
||||
$options->specificContent(new ShareContentShare($notification->getSubject()));
|
||||
|
||||
if ($notification->getContent()) {
|
||||
$options->specificContent(new ShareContentShare($notification->getContent()));
|
||||
}
|
||||
|
||||
$options->visibility(new VisibilityShare());
|
||||
$options->lifecycleState(new LifecycleStateShare());
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
public function contentCertificationRecord(string $contentCertificationRecord): self
|
||||
{
|
||||
$this->options['contentCertificationRecord'] = $contentCertificationRecord;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function firstPublishedAt(int $firstPublishedAt): self
|
||||
{
|
||||
$this->options['firstPublishedAt'] = $firstPublishedAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function lifecycleState(LifecycleStateShare $lifecycleStateOption): self
|
||||
{
|
||||
$this->options['lifecycleState'] = $lifecycleStateOption->lifecycleState();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function origin(string $origin): self
|
||||
{
|
||||
$this->options['origin'] = $origin;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function ugcOrigin(string $ugcOrigin): self
|
||||
{
|
||||
$this->options['ugcOrigin'] = $ugcOrigin;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function versionTag(string $versionTag): self
|
||||
{
|
||||
$this->options['versionTag'] = $versionTag;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function specificContent(ShareContentShare $specificContent): self
|
||||
{
|
||||
$this->options['specificContent']['com.linkedin.ugc.ShareContent'] = $specificContent->toArray();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function author(AuthorShare $authorOption): self
|
||||
{
|
||||
$this->options['author'] = $authorOption->author();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function visibility(VisibilityShare $visibilityOption): self
|
||||
{
|
||||
$this->options['visibility'] = $visibilityOption->toArray();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAuthor(): ?string
|
||||
{
|
||||
return $this->options['author'] ?? null;
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Notifier\Bridge\LinkedIn;
|
||||
|
||||
use Symfony\Component\Notifier\Bridge\LinkedIn\Share\AuthorShare;
|
||||
use Symfony\Component\Notifier\Exception\LogicException;
|
||||
use Symfony\Component\Notifier\Exception\TransportException;
|
||||
use Symfony\Component\Notifier\Message\ChatMessage;
|
||||
use Symfony\Component\Notifier\Message\MessageInterface;
|
||||
use Symfony\Component\Notifier\Message\SentMessage;
|
||||
use Symfony\Component\Notifier\Transport\AbstractTransport;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* @author Smaïne Milianni <smaine.milianni@gmail.com>
|
||||
*
|
||||
* @experimental in 5.2
|
||||
*
|
||||
* @see https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/ugc-post-api#sharecontent
|
||||
*/
|
||||
final class LinkedInTransport extends AbstractTransport
|
||||
{
|
||||
protected const PROTOCOL_VERSION = '2.0.0';
|
||||
protected const PROTOCOL_HEADER = 'X-Restli-Protocol-Version';
|
||||
protected const HOST = 'api.linkedin.com';
|
||||
|
||||
private $authToken;
|
||||
private $accountId;
|
||||
|
||||
public function __construct(string $authToken, string $accountId, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null)
|
||||
{
|
||||
$this->authToken = $authToken;
|
||||
$this->accountId = $accountId;
|
||||
|
||||
parent::__construct($client, $dispatcher);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf('linkedin://%s', $this->getEndpoint());
|
||||
}
|
||||
|
||||
public function supports(MessageInterface $message): bool
|
||||
{
|
||||
return $message instanceof ChatMessage && (null === $message->getOptions() || $message->getOptions() instanceof LinkedInOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/ugc-post-api
|
||||
*/
|
||||
protected function doSend(MessageInterface $message): SentMessage
|
||||
{
|
||||
if (!$message instanceof ChatMessage) {
|
||||
throw new LogicException(sprintf('The "%s" transport only supports instances of "%s" (instance of "%s" given).', __CLASS__, ChatMessage::class, \get_class($message)));
|
||||
}
|
||||
if ($message->getOptions() && !$message->getOptions() instanceof LinkedInOptions) {
|
||||
throw new LogicException(sprintf('The "%s" transport only supports instances of "%s" for options.', __CLASS__, LinkedInOptions::class));
|
||||
}
|
||||
|
||||
if (!($opts = $message->getOptions()) && $notification = $message->getNotification()) {
|
||||
$opts = LinkedInOptions::fromNotification($notification);
|
||||
$opts->author(new AuthorShare($this->accountId));
|
||||
}
|
||||
|
||||
$endpoint = sprintf('https://%s/v2/ugcPosts', $this->getEndpoint());
|
||||
|
||||
$response = $this->client->request('POST', $endpoint, [
|
||||
'auth_bearer' => $this->authToken,
|
||||
'headers' => [self::PROTOCOL_HEADER => self::PROTOCOL_VERSION],
|
||||
'json' => array_filter($opts ? $opts->toArray() : $this->bodyFromMessageWithNoOption($message)),
|
||||
]);
|
||||
|
||||
if (201 !== $response->getStatusCode()) {
|
||||
throw new TransportException(sprintf('Unable to post the Linkedin message: "%s".', $response->getContent(false)), $response);
|
||||
}
|
||||
|
||||
$result = $response->toArray(false);
|
||||
|
||||
if (!$result['id']) {
|
||||
throw new TransportException(sprintf('Unable to post the Linkedin message : "%s".', $result['error']), $response);
|
||||
}
|
||||
|
||||
return new SentMessage($message, (string) $this);
|
||||
}
|
||||
|
||||
private function bodyFromMessageWithNoOption(MessageInterface $message): array
|
||||
{
|
||||
return [
|
||||
'specificContent' => [
|
||||
'com.linkedin.ugc.ShareContent' => [
|
||||
'shareCommentary' => [
|
||||
'attributes' => [],
|
||||
'text' => $message->getSubject(),
|
||||
],
|
||||
'shareMediaCategory' => 'NONE',
|
||||
],
|
||||
],
|
||||
'visibility' => [
|
||||
'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC',
|
||||
],
|
||||
'lifecycleState' => 'PUBLISHED',
|
||||
'author' => sprintf('urn:li:person:%s', $this->accountId),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Notifier\Bridge\LinkedIn;
|
||||
|
||||
use Symfony\Component\Notifier\Exception\UnsupportedSchemeException;
|
||||
use Symfony\Component\Notifier\Transport\AbstractTransportFactory;
|
||||
use Symfony\Component\Notifier\Transport\Dsn;
|
||||
use Symfony\Component\Notifier\Transport\TransportInterface;
|
||||
|
||||
/**
|
||||
* @author Smaïne Milianni <smaine.milianni@gmail.com>
|
||||
*
|
||||
* @experimental in 5.2
|
||||
*/
|
||||
class LinkedInTransportFactory extends AbstractTransportFactory
|
||||
{
|
||||
public function create(Dsn $dsn): TransportInterface
|
||||
{
|
||||
$scheme = $dsn->getScheme();
|
||||
$authToken = $this->getUser($dsn);
|
||||
$accountId = $this->getPassword($dsn);
|
||||
$host = 'default' === $dsn->getHost() ? null : $dsn->getHost();
|
||||
$port = $dsn->getPort();
|
||||
|
||||
if ('linkedin' === $scheme) {
|
||||
return (new LinkedInTransport($authToken, $accountId, $this->client, $this->dispatcher))->setHost($host)->setPort($port);
|
||||
}
|
||||
|
||||
throw new UnsupportedSchemeException($dsn, 'linkedin', $this->getSupportedSchemes());
|
||||
}
|
||||
|
||||
protected function getSupportedSchemes(): array
|
||||
{
|
||||
return ['linkedin'];
|
||||
}
|
||||
}
|
20
src/Symfony/Component/Notifier/Bridge/LinkedIn/README.md
Normal file
20
src/Symfony/Component/Notifier/Bridge/LinkedIn/README.md
Normal file
@ -0,0 +1,20 @@
|
||||
LinkedIn Notifier
|
||||
=================
|
||||
|
||||
Provides LinkedIn integration for Symfony Notifier.
|
||||
|
||||
DSN example
|
||||
-----------
|
||||
|
||||
```
|
||||
// .env file
|
||||
LINKEDIN_DSN='linkedin://ACCESS_TOKEN:USER_ID@default'
|
||||
```
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Notifier\Bridge\LinkedIn\Share;
|
||||
|
||||
/**
|
||||
* @author Smaïne Milianni <smaine.milianni@gmail.com>
|
||||
*
|
||||
* @experimental in 5.2
|
||||
*/
|
||||
abstract class AbstractLinkedInShare
|
||||
{
|
||||
protected $options = [];
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Notifier\Bridge\LinkedIn\Share;
|
||||
|
||||
/**
|
||||
* @author Smaïne Milianni <smaine.milianni@gmail.com>
|
||||
*
|
||||
* @experimental in 5.2
|
||||
*/
|
||||
final class AuthorShare extends AbstractLinkedInShare
|
||||
{
|
||||
public const PERSON = 'person';
|
||||
|
||||
private $author;
|
||||
|
||||
public function __construct(string $value, string $organisation = self::PERSON)
|
||||
{
|
||||
$this->author = "urn:li:$organisation:$value";
|
||||
}
|
||||
|
||||
public function author(): string
|
||||
{
|
||||
return $this->author;
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Notifier\Bridge\LinkedIn\Share;
|
||||
|
||||
use Symfony\Component\Notifier\Exception\LogicException;
|
||||
|
||||
/**
|
||||
* @author Smaïne Milianni <smaine.milianni@gmail.com>
|
||||
*
|
||||
* @see https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/ugc-post-api#schema lifecycleState section
|
||||
*
|
||||
* @experimental in 5.2
|
||||
*/
|
||||
final class LifecycleStateShare extends AbstractLinkedInShare
|
||||
{
|
||||
public const DRAFT = 'DRAFT';
|
||||
public const PUBLISHED = 'PUBLISHED';
|
||||
public const PROCESSING = 'PROCESSING';
|
||||
public const PROCESSING_FAILED = 'PROCESSING_FAILED';
|
||||
public const DELETED = 'DELETED';
|
||||
public const PUBLISHED_EDITED = 'PUBLISHED_EDITED';
|
||||
|
||||
private const AVAILABLE_LIFECYCLE = [
|
||||
self::DRAFT,
|
||||
self::PUBLISHED,
|
||||
self::PROCESSING_FAILED,
|
||||
self::DELETED,
|
||||
self::PROCESSING_FAILED,
|
||||
self::PUBLISHED_EDITED,
|
||||
];
|
||||
|
||||
private $lifecycleState;
|
||||
|
||||
public function __construct(string $lifecycleState = self::PUBLISHED)
|
||||
{
|
||||
if (!\in_array($lifecycleState, self::AVAILABLE_LIFECYCLE)) {
|
||||
throw new LogicException(sprintf('"%s" is not a valid value, available lifecycle are "%s".', $lifecycleState, implode(', ', self::AVAILABLE_LIFECYCLE)));
|
||||
}
|
||||
|
||||
$this->lifecycleState = $lifecycleState;
|
||||
}
|
||||
|
||||
public function lifecycleState(): string
|
||||
{
|
||||
return $this->lifecycleState;
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Notifier\Bridge\LinkedIn\Share;
|
||||
|
||||
use Symfony\Component\Notifier\Exception\LogicException;
|
||||
|
||||
/**
|
||||
* @author Smaïne Milianni <smaine.milianni@gmail.com>
|
||||
*
|
||||
* @see https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/ugc-post-api#sharecontent
|
||||
*
|
||||
* @experimental in 5.2
|
||||
*/
|
||||
final class ShareContentShare extends AbstractLinkedInShare
|
||||
{
|
||||
public const ARTICLE = 'ARTICLE';
|
||||
public const IMAGE = 'IMAGE';
|
||||
public const NONE = 'NONE';
|
||||
public const RICH = 'RICH';
|
||||
public const VIDEO = 'VIDEO';
|
||||
public const LEARNING_COURSE = 'LEARNING_COURSE';
|
||||
public const JOB = 'JOB';
|
||||
public const QUESTION = 'QUESTION';
|
||||
public const ANSWER = 'ANSWER';
|
||||
public const CAROUSEL = 'CAROUSEL';
|
||||
public const TOPIC = 'TOPIC';
|
||||
public const NATIVE_DOCUMENT = 'NATIVE_DOCUMENT';
|
||||
public const URN_REFERENCE = 'URN_REFERENCE';
|
||||
public const LIVE_VIDEO = 'LIVE_VIDEO';
|
||||
|
||||
public const ALL = [
|
||||
self::ARTICLE,
|
||||
self::IMAGE,
|
||||
self::NONE,
|
||||
self::RICH,
|
||||
self::VIDEO,
|
||||
self::LEARNING_COURSE,
|
||||
self::JOB,
|
||||
self::QUESTION,
|
||||
self::ANSWER,
|
||||
self::CAROUSEL,
|
||||
self::TOPIC,
|
||||
self::NATIVE_DOCUMENT,
|
||||
self::URN_REFERENCE,
|
||||
self::LIVE_VIDEO,
|
||||
];
|
||||
|
||||
public function __construct(string $text, array $attributes = [], string $inferredLocale = null, ShareMediaShare $media = null, string $primaryLandingPageUrl = null, string $shareMediaCategory = self::NONE)
|
||||
{
|
||||
$this->options['shareCommentary'] = [
|
||||
'attributes' => $attributes,
|
||||
'text' => $text,
|
||||
];
|
||||
|
||||
if (null !== $inferredLocale) {
|
||||
$this->options['shareCommentary']['inferredLocale'] = $inferredLocale;
|
||||
}
|
||||
|
||||
if (null !== $media) {
|
||||
$this->options['media'] = $media->toArray();
|
||||
}
|
||||
|
||||
if (null !== $primaryLandingPageUrl) {
|
||||
$this->options['primaryLandingPageUrl'] = $primaryLandingPageUrl;
|
||||
}
|
||||
|
||||
if ($shareMediaCategory) {
|
||||
if (!\in_array($shareMediaCategory, self::ALL)) {
|
||||
throw new LogicException(sprintf('"%s" is not valid option, available options are "%s".', $shareMediaCategory, implode(', ', self::ALL)));
|
||||
}
|
||||
|
||||
$this->options['shareMediaCategory'] = $shareMediaCategory;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Notifier\Bridge\LinkedIn\Share;
|
||||
|
||||
use Symfony\Component\Notifier\Exception\LogicException;
|
||||
|
||||
/**
|
||||
* @author Smaïne Milianni <smaine.milianni@gmail.com>
|
||||
*
|
||||
* @see https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/ugc-post-api#sharemedia
|
||||
*
|
||||
* @experimental in 5.2
|
||||
*/
|
||||
class ShareMediaShare extends AbstractLinkedInShare
|
||||
{
|
||||
public const LEARN_MORE = 'LEARN_MORE';
|
||||
public const APPLY_NOW = 'APPLY_NOW ';
|
||||
public const DOWNLOAD = 'DOWNLOAD';
|
||||
public const GET_QUOTE = 'GET_QUOTE';
|
||||
public const SIGN_UP = 'SIGN_UP';
|
||||
public const SUBSCRIBE = 'SUBSCRIBE ';
|
||||
public const REGISTER = 'REGISTER';
|
||||
|
||||
public const ALL = [
|
||||
self::LEARN_MORE,
|
||||
self::APPLY_NOW,
|
||||
self::DOWNLOAD,
|
||||
self::GET_QUOTE,
|
||||
self::SIGN_UP,
|
||||
self::SUBSCRIBE,
|
||||
self::REGISTER,
|
||||
];
|
||||
|
||||
public function __construct(string $text, array $attributes = [], string $inferredLocale = null, bool $landingPage = false, string $landingPageTitle = null, string $landingPageUrl = null)
|
||||
{
|
||||
$this->options['description'] = [
|
||||
'text' => $text,
|
||||
'attributes' => $attributes,
|
||||
];
|
||||
|
||||
if ($inferredLocale) {
|
||||
$this->options['description']['inferredLocale'] = $inferredLocale;
|
||||
}
|
||||
|
||||
if ($landingPage || $landingPageUrl) {
|
||||
$this->options['landingPage']['landingPageUrl'] = $landingPageUrl;
|
||||
}
|
||||
|
||||
if (null !== $landingPageTitle) {
|
||||
if (!\in_array($landingPageTitle, self::ALL)) {
|
||||
throw new LogicException(sprintf('"%s" is not valid option, available options are "%s".', $landingPageTitle, implode(', ', self::ALL)));
|
||||
}
|
||||
|
||||
$this->options['landingPage']['landingPageTitle'] = $landingPageTitle;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Notifier\Bridge\LinkedIn\Share;
|
||||
|
||||
use Symfony\Component\Notifier\Exception\LogicException;
|
||||
|
||||
/**
|
||||
* @author Smaïne Milianni <smaine.milianni@gmail.com>
|
||||
*
|
||||
* @experimental in 5.2
|
||||
*/
|
||||
final class VisibilityShare extends AbstractLinkedInShare
|
||||
{
|
||||
public const MEMBER_NETWORK_VISIBILITY = 'MemberNetworkVisibility';
|
||||
public const SPONSORED_CONTENT_VISIBILITY = 'SponsoredContentVisibility';
|
||||
|
||||
public const CONNECTIONS = 'CONNECTIONS';
|
||||
public const PUBLIC = 'PUBLIC';
|
||||
public const LOGGED_IN = 'LOGGED_IN';
|
||||
public const DARK = 'DARK';
|
||||
|
||||
private const MEMBER_NETWORK = [
|
||||
self::CONNECTIONS,
|
||||
self::PUBLIC,
|
||||
self::LOGGED_IN,
|
||||
];
|
||||
|
||||
private const AVAILABLE_VISIBILITY = [
|
||||
self::MEMBER_NETWORK_VISIBILITY,
|
||||
self::SPONSORED_CONTENT_VISIBILITY,
|
||||
];
|
||||
|
||||
public function __construct(string $visibility = self::MEMBER_NETWORK_VISIBILITY, string $value = 'PUBLIC')
|
||||
{
|
||||
if (!\in_array($visibility, self::AVAILABLE_VISIBILITY)) {
|
||||
throw new LogicException(sprintf('"%s" is not a valid visibility, available visibility are "%s".', $visibility, implode(', ', self::AVAILABLE_VISIBILITY)));
|
||||
}
|
||||
|
||||
if (self::MEMBER_NETWORK_VISIBILITY === $visibility && !\in_array($value, self::MEMBER_NETWORK)) {
|
||||
throw new LogicException(sprintf('"%s" is not a valid value, available value for visibility "%s" are "%s".', $value, $visibility, implode(', ', self::MEMBER_NETWORK)));
|
||||
}
|
||||
|
||||
if (self::SPONSORED_CONTENT_VISIBILITY === $visibility && self::DARK !== $value) {
|
||||
throw new LogicException(sprintf('"%s" is not a valid value, available value for visibility "%s" is "%s".', $value, $visibility, self::DARK));
|
||||
}
|
||||
|
||||
$this->options['com.linkedin.ugc.'.$visibility] = $value;
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Notifier\Bridge\LinkedIn\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Notifier\Bridge\LinkedIn\LinkedInTransportFactory;
|
||||
use Symfony\Component\Notifier\Exception\IncompleteDsnException;
|
||||
use Symfony\Component\Notifier\Exception\UnsupportedSchemeException;
|
||||
use Symfony\Component\Notifier\Transport\Dsn;
|
||||
|
||||
final class LinkedInTransportFactoryTest extends TestCase
|
||||
{
|
||||
public function testCreateWithDsn(): void
|
||||
{
|
||||
$factory = new LinkedInTransportFactory();
|
||||
|
||||
$dsn = 'linkedin://login:pass@default';
|
||||
$transport = $factory->create(Dsn::fromString($dsn));
|
||||
$transport->setHost('testHost');
|
||||
|
||||
$this->assertSame('linkedin://testHost', (string) $transport);
|
||||
}
|
||||
|
||||
public function testSupportsLinkedinScheme(): void
|
||||
{
|
||||
$factory = new LinkedInTransportFactory();
|
||||
|
||||
$this->assertTrue($factory->supports(Dsn::fromString('linkedin://host/path')));
|
||||
$this->assertFalse($factory->supports(Dsn::fromString('somethingElse://host/path')));
|
||||
}
|
||||
|
||||
public function testNonLinkedinSchemeThrows(): void
|
||||
{
|
||||
$factory = new LinkedInTransportFactory();
|
||||
|
||||
$this->expectException(UnsupportedSchemeException::class);
|
||||
|
||||
$dsn = 'foo://login:pass@default';
|
||||
$factory->create(Dsn::fromString($dsn));
|
||||
}
|
||||
|
||||
public function testIncompleteDsnMissingUserThrows(): void
|
||||
{
|
||||
$factory = new LinkedInTransportFactory();
|
||||
|
||||
$this->expectException(IncompleteDsnException::class);
|
||||
|
||||
$factory->create(Dsn::fromString('somethingElse://host/path'));
|
||||
}
|
||||
}
|
@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Notifier\Bridge\LinkedIn\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\Notifier\Bridge\LinkedIn\LinkedInTransport;
|
||||
use Symfony\Component\Notifier\Exception\LogicException;
|
||||
use Symfony\Component\Notifier\Exception\TransportException;
|
||||
use Symfony\Component\Notifier\Message\ChatMessage;
|
||||
use Symfony\Component\Notifier\Message\MessageInterface;
|
||||
use Symfony\Component\Notifier\Message\MessageOptionsInterface;
|
||||
use Symfony\Component\Notifier\Notification\Notification;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
final class LinkedInTransportTest extends TestCase
|
||||
{
|
||||
public function testToString(): void
|
||||
{
|
||||
$this->assertSame(sprintf('linkedin://host.test'), (string) $this->getTransport());
|
||||
}
|
||||
|
||||
public function testSupportsChatMessage(): void
|
||||
{
|
||||
$transport = $this->getTransport();
|
||||
|
||||
$this->assertTrue($transport->supports(new ChatMessage('testChatMessage')));
|
||||
$this->assertFalse($transport->supports($this->createMock(MessageInterface::class)));
|
||||
}
|
||||
|
||||
public function testSendNonChatMessageThrows(): void
|
||||
{
|
||||
$this->expectException(LogicException::class);
|
||||
|
||||
$transport = $this->getTransport();
|
||||
|
||||
$transport->send($this->createMock(MessageInterface::class));
|
||||
}
|
||||
|
||||
public function testSendWithEmptyArrayResponseThrows(): void
|
||||
{
|
||||
$this->expectException(TransportException::class);
|
||||
|
||||
$response = $this->createMock(ResponseInterface::class);
|
||||
$response->expects($this->exactly(2))
|
||||
->method('getStatusCode')
|
||||
->willReturn(500);
|
||||
$response->expects($this->once())
|
||||
->method('getContent')
|
||||
->willReturn('[]');
|
||||
|
||||
$client = new MockHttpClient(static function () use ($response): ResponseInterface {
|
||||
return $response;
|
||||
});
|
||||
|
||||
$transport = $this->getTransport($client);
|
||||
|
||||
$transport->send(new ChatMessage('testMessage'));
|
||||
}
|
||||
|
||||
public function testSendWithErrorResponseThrows(): void
|
||||
{
|
||||
$this->expectException(TransportException::class);
|
||||
$this->expectExceptionMessage('testErrorCode');
|
||||
|
||||
$response = $this->createMock(ResponseInterface::class);
|
||||
$response->expects($this->exactly(2))
|
||||
->method('getStatusCode')
|
||||
->willReturn(400);
|
||||
|
||||
$response->expects($this->once())
|
||||
->method('getContent')
|
||||
->willReturn('testErrorCode');
|
||||
|
||||
$client = new MockHttpClient(static function () use ($response): ResponseInterface {
|
||||
return $response;
|
||||
});
|
||||
|
||||
$transport = $this->getTransport($client);
|
||||
|
||||
$transport->send(new ChatMessage('testMessage'));
|
||||
}
|
||||
|
||||
public function testSendWithOptions(): void
|
||||
{
|
||||
$message = 'testMessage';
|
||||
|
||||
$response = $this->createMock(ResponseInterface::class);
|
||||
|
||||
$response->expects($this->exactly(2))
|
||||
->method('getStatusCode')
|
||||
->willReturn(201);
|
||||
|
||||
$response->expects($this->once())
|
||||
->method('getContent')
|
||||
->willReturn(json_encode(['id' => '42']));
|
||||
|
||||
$expectedBody = json_encode([
|
||||
'specificContent' => [
|
||||
'com.linkedin.ugc.ShareContent' => [
|
||||
'shareCommentary' => [
|
||||
'attributes' => [],
|
||||
'text' => 'testMessage',
|
||||
],
|
||||
'shareMediaCategory' => 'NONE',
|
||||
],
|
||||
],
|
||||
'visibility' => [
|
||||
'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC',
|
||||
],
|
||||
'lifecycleState' => 'PUBLISHED',
|
||||
'author' => 'urn:li:person:MyLogin',
|
||||
]);
|
||||
|
||||
$client = new MockHttpClient(function (string $method, string $url, array $options = []) use (
|
||||
$response,
|
||||
$expectedBody
|
||||
): ResponseInterface {
|
||||
$this->assertSame($expectedBody, $options['body']);
|
||||
|
||||
return $response;
|
||||
});
|
||||
$transport = $this->getTransport($client);
|
||||
|
||||
$transport->send(new ChatMessage($message));
|
||||
}
|
||||
|
||||
public function testSendWithNotification(): void
|
||||
{
|
||||
$message = 'testMessage';
|
||||
|
||||
$response = $this->createMock(ResponseInterface::class);
|
||||
|
||||
$response->expects($this->exactly(2))
|
||||
->method('getStatusCode')
|
||||
->willReturn(201);
|
||||
|
||||
$response->expects($this->once())
|
||||
->method('getContent')
|
||||
->willReturn(json_encode(['id' => '42']));
|
||||
|
||||
$notification = new Notification($message);
|
||||
$chatMessage = ChatMessage::fromNotification($notification);
|
||||
|
||||
$expectedBody = json_encode([
|
||||
'specificContent' => [
|
||||
'com.linkedin.ugc.ShareContent' => [
|
||||
'shareCommentary' => [
|
||||
'attributes' => [],
|
||||
'text' => 'testMessage',
|
||||
],
|
||||
'shareMediaCategory' => 'NONE',
|
||||
],
|
||||
],
|
||||
'visibility' => [
|
||||
'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC',
|
||||
],
|
||||
'lifecycleState' => 'PUBLISHED',
|
||||
'author' => 'urn:li:person:MyLogin',
|
||||
]);
|
||||
|
||||
$client = new MockHttpClient(function (string $method, string $url, array $options = []) use (
|
||||
$response,
|
||||
$expectedBody
|
||||
): ResponseInterface {
|
||||
$this->assertSame($expectedBody, $options['body']);
|
||||
|
||||
return $response;
|
||||
});
|
||||
|
||||
$transport = $this->getTransport($client);
|
||||
|
||||
$transport->send($chatMessage);
|
||||
}
|
||||
|
||||
public function testSendWithInvalidOptions(): void
|
||||
{
|
||||
$this->expectException(LogicException::class);
|
||||
|
||||
$client = new MockHttpClient(function (string $method, string $url, array $options = []): ResponseInterface {
|
||||
return $this->createMock(ResponseInterface::class);
|
||||
});
|
||||
|
||||
$transport = $this->getTransport($client);
|
||||
|
||||
$transport->send(new ChatMessage('testMessage', $this->createMock(MessageOptionsInterface::class)));
|
||||
}
|
||||
|
||||
public function getTransport($client = null)
|
||||
{
|
||||
return (new LinkedInTransport(
|
||||
'MyToken',
|
||||
'MyLogin',
|
||||
$client ?? $this->createMock(HttpClientInterface::class)
|
||||
))->setHost('host.test');
|
||||
}
|
||||
}
|
35
src/Symfony/Component/Notifier/Bridge/LinkedIn/composer.json
Normal file
35
src/Symfony/Component/Notifier/Bridge/LinkedIn/composer.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "symfony/linkedin-notifier",
|
||||
"type": "symfony-bridge",
|
||||
"description": "Symfony LinkedIn Notifier Bridge",
|
||||
"keywords": ["linkedin", "notifier"],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Smaïne Milianni",
|
||||
"email": "smaine.milianni@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"symfony/http-client": "^4.3|^5.0",
|
||||
"symfony/notifier": "^5.2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\LinkedIn\\": "" },
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "5.2-dev"
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user