feat: extract content providers from app/Plug/Content
Devto, Hashnode, Medium, Wordpress providers with Core\Plug\Content namespace alignment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
45db30d967
commit
97c1540add
19 changed files with 2236 additions and 0 deletions
17
composer.json
Normal file
17
composer.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "core/php-plug-content",
|
||||
"description": "Content platform integrations for the Plug framework",
|
||||
"type": "library",
|
||||
"license": "EUPL-1.2",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"core/php": "^1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Core\\Plug\\Content\\": "src/"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
84
src/Devto/Auth.php
Normal file
84
src/Devto/Auth.php
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Content\Devto;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Authenticable;
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* Dev.to API key authentication.
|
||||
*
|
||||
* Uses API key (no OAuth).
|
||||
*/
|
||||
class Auth implements Authenticable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use UsesHttp;
|
||||
|
||||
private const API_URL = 'https://dev.to/api';
|
||||
|
||||
private string $apiKey;
|
||||
|
||||
public function __construct(string $apiKey = '')
|
||||
{
|
||||
$this->apiKey = $apiKey;
|
||||
}
|
||||
|
||||
public static function identifier(): string
|
||||
{
|
||||
return 'devto';
|
||||
}
|
||||
|
||||
public static function name(): string
|
||||
{
|
||||
return 'DEV Community';
|
||||
}
|
||||
|
||||
/**
|
||||
* API key generation URL.
|
||||
*/
|
||||
public function getAuthUrl(): string
|
||||
{
|
||||
return 'https://dev.to/settings/extensions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate API key.
|
||||
*
|
||||
* @param array $params ['api_key' => string]
|
||||
*/
|
||||
public function requestAccessToken(array $params): array
|
||||
{
|
||||
$apiKey = $params['api_key'] ?? $this->apiKey;
|
||||
|
||||
if (! $apiKey) {
|
||||
return ['error' => 'API key is required'];
|
||||
}
|
||||
|
||||
// Validate by fetching user info
|
||||
$response = $this->http()
|
||||
->withHeaders(['api-key' => $apiKey])
|
||||
->get(self::API_URL.'/users/me');
|
||||
|
||||
if (! $response->successful()) {
|
||||
return ['error' => 'Invalid API key'];
|
||||
}
|
||||
|
||||
$user = $response->json();
|
||||
|
||||
return [
|
||||
'api_key' => $apiKey,
|
||||
'user_id' => $user['id'],
|
||||
'username' => $user['username'],
|
||||
];
|
||||
}
|
||||
|
||||
public function getAccount(): Response
|
||||
{
|
||||
return $this->error('Use Read::me() with API key');
|
||||
}
|
||||
}
|
||||
46
src/Devto/Delete.php
Normal file
46
src/Devto/Delete.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Content\Devto;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\ManagesTokens;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Deletable;
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* Dev.to article deletion (unpublishing).
|
||||
*
|
||||
* Note: Dev.to API doesn't support full deletion, only unpublishing.
|
||||
*/
|
||||
class Delete implements Deletable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use ManagesTokens;
|
||||
use UsesHttp;
|
||||
|
||||
private const API_URL = 'https://dev.to/api';
|
||||
|
||||
/**
|
||||
* Unpublish an article (Dev.to doesn't support full deletion via API).
|
||||
*/
|
||||
public function delete(string $id): Response
|
||||
{
|
||||
// Dev.to API doesn't have a DELETE endpoint
|
||||
// We unpublish instead
|
||||
$response = $this->http()
|
||||
->withHeaders(['api-key' => $this->accessToken()])
|
||||
->put(self::API_URL."/articles/{$id}", [
|
||||
'article' => [
|
||||
'published' => false,
|
||||
],
|
||||
]);
|
||||
|
||||
return $this->fromHttp($response, fn () => [
|
||||
'unpublished' => true,
|
||||
'note' => 'Article unpublished (Dev.to API does not support deletion)',
|
||||
]);
|
||||
}
|
||||
}
|
||||
112
src/Devto/Post.php
Normal file
112
src/Devto/Post.php
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Content\Devto;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\ManagesTokens;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Postable;
|
||||
use Core\Plug\Response;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Dev.to article publishing.
|
||||
*/
|
||||
class Post implements Postable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use ManagesTokens;
|
||||
use UsesHttp;
|
||||
|
||||
private const API_URL = 'https://dev.to/api';
|
||||
|
||||
/**
|
||||
* Publish an article.
|
||||
*
|
||||
* @param string $text Markdown content (body_markdown)
|
||||
* @param Collection $media Not used - Dev.to handles images via URLs
|
||||
* @param array $params title (required), tags, canonical_url, series, published, main_image, description
|
||||
*/
|
||||
public function publish(string $text, Collection $media, array $params = []): Response
|
||||
{
|
||||
$title = $params['title'] ?? null;
|
||||
|
||||
if (! $title) {
|
||||
return $this->error('Title is required');
|
||||
}
|
||||
|
||||
$articleData = [
|
||||
'article' => array_filter([
|
||||
'title' => $title,
|
||||
'body_markdown' => $text,
|
||||
'published' => $params['published'] ?? false,
|
||||
'tags' => isset($params['tags']) ? array_slice((array) $params['tags'], 0, 4) : null, // Max 4 tags
|
||||
'series' => $params['series'] ?? null,
|
||||
'canonical_url' => $params['canonical_url'] ?? null,
|
||||
'main_image' => $params['main_image'] ?? ($media->first()['url'] ?? null),
|
||||
'description' => $params['description'] ?? null,
|
||||
'organization_id' => $params['organization_id'] ?? null,
|
||||
]),
|
||||
];
|
||||
|
||||
$response = $this->http()
|
||||
->withHeaders(['api-key' => $this->accessToken()])
|
||||
->post(self::API_URL.'/articles', $articleData);
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'id' => $data['id'],
|
||||
'title' => $data['title'],
|
||||
'url' => $data['url'],
|
||||
'slug' => $data['slug'],
|
||||
'published' => $data['published'] ?? false,
|
||||
'published_at' => $data['published_at'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing article.
|
||||
*/
|
||||
public function update(int $articleId, array $params): Response
|
||||
{
|
||||
$articleData = [
|
||||
'article' => array_filter([
|
||||
'title' => $params['title'] ?? null,
|
||||
'body_markdown' => $params['body_markdown'] ?? $params['content'] ?? null,
|
||||
'published' => $params['published'] ?? null,
|
||||
'tags' => isset($params['tags']) ? array_slice((array) $params['tags'], 0, 4) : null,
|
||||
'series' => $params['series'] ?? null,
|
||||
'canonical_url' => $params['canonical_url'] ?? null,
|
||||
'main_image' => $params['main_image'] ?? null,
|
||||
]),
|
||||
];
|
||||
|
||||
$response = $this->http()
|
||||
->withHeaders(['api-key' => $this->accessToken()])
|
||||
->put(self::API_URL."/articles/{$articleId}", $articleData);
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'id' => $data['id'],
|
||||
'title' => $data['title'],
|
||||
'url' => $data['url'],
|
||||
'published' => $data['published'] ?? false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get external URL to an article.
|
||||
*/
|
||||
public static function externalPostUrl(string $username, string $slug): string
|
||||
{
|
||||
return "https://dev.to/{$username}/{$slug}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get external URL to a profile.
|
||||
*/
|
||||
public static function externalAccountUrl(string $username): string
|
||||
{
|
||||
return "https://dev.to/{$username}";
|
||||
}
|
||||
}
|
||||
163
src/Devto/Read.php
Normal file
163
src/Devto/Read.php
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Content\Devto;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\ManagesTokens;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Readable;
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* Dev.to article and profile reading.
|
||||
*/
|
||||
class Read implements Readable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use ManagesTokens;
|
||||
use UsesHttp;
|
||||
|
||||
private const API_URL = 'https://dev.to/api';
|
||||
|
||||
/**
|
||||
* Get an article by ID.
|
||||
*/
|
||||
public function get(string $id): Response
|
||||
{
|
||||
$response = $this->http()
|
||||
->withHeaders(['api-key' => $this->accessToken()])
|
||||
->get(self::API_URL."/articles/{$id}");
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'id' => $data['id'],
|
||||
'title' => $data['title'],
|
||||
'description' => $data['description'],
|
||||
'body_markdown' => $data['body_markdown'] ?? null,
|
||||
'body_html' => $data['body_html'] ?? null,
|
||||
'url' => $data['url'],
|
||||
'slug' => $data['slug'],
|
||||
'tags' => $data['tags'] ?? [],
|
||||
'published' => $data['published'] ?? false,
|
||||
'published_at' => $data['published_at'] ?? null,
|
||||
'reading_time' => $data['reading_time_minutes'] ?? null,
|
||||
'reactions_count' => $data['public_reactions_count'] ?? 0,
|
||||
'comments_count' => $data['comments_count'] ?? 0,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authenticated user's profile.
|
||||
*/
|
||||
public function me(): Response
|
||||
{
|
||||
$response = $this->http()
|
||||
->withHeaders(['api-key' => $this->accessToken()])
|
||||
->get(self::API_URL.'/users/me');
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'id' => $data['id'],
|
||||
'username' => $data['username'],
|
||||
'name' => $data['name'],
|
||||
'image' => $data['profile_image'],
|
||||
'bio' => $data['summary'] ?? null,
|
||||
'location' => $data['location'] ?? null,
|
||||
'website_url' => $data['website_url'] ?? null,
|
||||
'twitter_username' => $data['twitter_username'] ?? null,
|
||||
'github_username' => $data['github_username'] ?? null,
|
||||
'joined_at' => $data['joined_at'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* List user's articles.
|
||||
*/
|
||||
public function list(array $params = []): Response
|
||||
{
|
||||
$queryParams = [
|
||||
'page' => $params['page'] ?? 1,
|
||||
'per_page' => $params['per_page'] ?? 30,
|
||||
];
|
||||
|
||||
// Can filter by username, tag, state, etc.
|
||||
if (isset($params['username'])) {
|
||||
$queryParams['username'] = $params['username'];
|
||||
}
|
||||
|
||||
if (isset($params['tag'])) {
|
||||
$queryParams['tag'] = $params['tag'];
|
||||
}
|
||||
|
||||
if (isset($params['state'])) {
|
||||
$queryParams['state'] = $params['state']; // fresh, rising, all
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->withHeaders(['api-key' => $this->accessToken()])
|
||||
->get(self::API_URL.'/articles', $queryParams);
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'articles' => array_map(fn ($article) => [
|
||||
'id' => $article['id'],
|
||||
'title' => $article['title'],
|
||||
'description' => $article['description'],
|
||||
'url' => $article['url'],
|
||||
'tags' => $article['tag_list'] ?? [],
|
||||
'published_at' => $article['published_at'] ?? null,
|
||||
'reading_time' => $article['reading_time_minutes'] ?? null,
|
||||
'reactions_count' => $article['public_reactions_count'] ?? 0,
|
||||
'comments_count' => $article['comments_count'] ?? 0,
|
||||
], $data),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* List user's published articles.
|
||||
*/
|
||||
public function myArticles(array $params = []): Response
|
||||
{
|
||||
$queryParams = [
|
||||
'page' => $params['page'] ?? 1,
|
||||
'per_page' => $params['per_page'] ?? 30,
|
||||
];
|
||||
|
||||
$response = $this->http()
|
||||
->withHeaders(['api-key' => $this->accessToken()])
|
||||
->get(self::API_URL.'/articles/me', $queryParams);
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'articles' => array_map(fn ($article) => [
|
||||
'id' => $article['id'],
|
||||
'title' => $article['title'],
|
||||
'url' => $article['url'],
|
||||
'published' => $article['published'] ?? false,
|
||||
'published_at' => $article['published_at'] ?? null,
|
||||
'page_views_count' => $article['page_views_count'] ?? 0,
|
||||
], $data),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* List user's unpublished (draft) articles.
|
||||
*/
|
||||
public function myDrafts(array $params = []): Response
|
||||
{
|
||||
$queryParams = [
|
||||
'page' => $params['page'] ?? 1,
|
||||
'per_page' => $params['per_page'] ?? 30,
|
||||
];
|
||||
|
||||
$response = $this->http()
|
||||
->withHeaders(['api-key' => $this->accessToken()])
|
||||
->get(self::API_URL.'/articles/me/unpublished', $queryParams);
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'articles' => array_map(fn ($article) => [
|
||||
'id' => $article['id'],
|
||||
'title' => $article['title'],
|
||||
'body_markdown' => $article['body_markdown'] ?? null,
|
||||
], $data),
|
||||
]);
|
||||
}
|
||||
}
|
||||
91
src/Hashnode/Auth.php
Normal file
91
src/Hashnode/Auth.php
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Content\Hashnode;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Authenticable;
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* Hashnode Personal Access Token authentication.
|
||||
*
|
||||
* Uses PAT (no OAuth).
|
||||
*/
|
||||
class Auth implements Authenticable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use UsesHttp;
|
||||
|
||||
private const API_URL = 'https://gql.hashnode.com';
|
||||
|
||||
private string $accessToken;
|
||||
|
||||
public function __construct(string $accessToken = '')
|
||||
{
|
||||
$this->accessToken = $accessToken;
|
||||
}
|
||||
|
||||
public static function identifier(): string
|
||||
{
|
||||
return 'hashnode';
|
||||
}
|
||||
|
||||
public static function name(): string
|
||||
{
|
||||
return 'Hashnode';
|
||||
}
|
||||
|
||||
/**
|
||||
* PAT generation URL.
|
||||
*/
|
||||
public function getAuthUrl(): string
|
||||
{
|
||||
return 'https://hashnode.com/settings/developer';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate access token.
|
||||
*
|
||||
* @param array $params ['access_token' => string]
|
||||
*/
|
||||
public function requestAccessToken(array $params): array
|
||||
{
|
||||
$token = $params['access_token'] ?? $this->accessToken;
|
||||
|
||||
if (! $token) {
|
||||
return ['error' => 'Access token is required'];
|
||||
}
|
||||
|
||||
// Validate by fetching user info
|
||||
$response = $this->http()
|
||||
->withHeaders(['Authorization' => $token])
|
||||
->post(self::API_URL, [
|
||||
'query' => 'query { me { id username name } }',
|
||||
]);
|
||||
|
||||
if (! $response->successful()) {
|
||||
return ['error' => 'Invalid access token'];
|
||||
}
|
||||
|
||||
$data = $response->json('data.me');
|
||||
|
||||
if (! $data) {
|
||||
return ['error' => 'Invalid access token'];
|
||||
}
|
||||
|
||||
return [
|
||||
'access_token' => $token,
|
||||
'user_id' => $data['id'],
|
||||
'username' => $data['username'],
|
||||
'name' => $data['name'],
|
||||
];
|
||||
}
|
||||
|
||||
public function getAccount(): Response
|
||||
{
|
||||
return $this->error('Use Read::me() with access token');
|
||||
}
|
||||
}
|
||||
56
src/Hashnode/Delete.php
Normal file
56
src/Hashnode/Delete.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Content\Hashnode;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\ManagesTokens;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Deletable;
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* Hashnode post deletion.
|
||||
*/
|
||||
class Delete implements Deletable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use ManagesTokens;
|
||||
use UsesHttp;
|
||||
|
||||
private const API_URL = 'https://gql.hashnode.com';
|
||||
|
||||
/**
|
||||
* Delete a post.
|
||||
*/
|
||||
public function delete(string $id): Response
|
||||
{
|
||||
$query = <<<'GRAPHQL'
|
||||
mutation RemovePost($input: RemovePostInput!) {
|
||||
removePost(input: $input) {
|
||||
post {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
GRAPHQL;
|
||||
|
||||
$response = $this->http()
|
||||
->withHeaders(['Authorization' => $this->accessToken()])
|
||||
->post(self::API_URL, [
|
||||
'query' => $query,
|
||||
'variables' => [
|
||||
'input' => ['id' => $id],
|
||||
],
|
||||
]);
|
||||
|
||||
return $this->fromHttp($response, function ($data) {
|
||||
if (isset($data['errors'])) {
|
||||
return ['error' => $data['errors'][0]['message'] ?? 'Deletion failed'];
|
||||
}
|
||||
|
||||
return ['deleted' => true];
|
||||
});
|
||||
}
|
||||
}
|
||||
198
src/Hashnode/Post.php
Normal file
198
src/Hashnode/Post.php
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Content\Hashnode;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\ManagesTokens;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Postable;
|
||||
use Core\Plug\Response;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Hashnode article publishing via GraphQL API.
|
||||
*/
|
||||
class Post implements Postable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use ManagesTokens;
|
||||
use UsesHttp;
|
||||
|
||||
private const API_URL = 'https://gql.hashnode.com';
|
||||
|
||||
private ?string $publicationId = null;
|
||||
|
||||
/**
|
||||
* Set publication ID for posting.
|
||||
*/
|
||||
public function forPublication(string $publicationId): self
|
||||
{
|
||||
$this->publicationId = $publicationId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish an article.
|
||||
*
|
||||
* @param string $text Markdown content
|
||||
* @param Collection $media Cover image (first item used)
|
||||
* @param array $params title (required), subtitle, slug, tags, canonicalUrl, publishedAt
|
||||
*/
|
||||
public function publish(string $text, Collection $media, array $params = []): Response
|
||||
{
|
||||
$title = $params['title'] ?? null;
|
||||
$publicationId = $params['publication_id'] ?? $this->publicationId;
|
||||
|
||||
if (! $title) {
|
||||
return $this->error('Title is required');
|
||||
}
|
||||
|
||||
if (! $publicationId) {
|
||||
return $this->error('Publication ID is required');
|
||||
}
|
||||
|
||||
$input = [
|
||||
'title' => $title,
|
||||
'contentMarkdown' => $text,
|
||||
'publicationId' => $publicationId,
|
||||
];
|
||||
|
||||
// Optional fields
|
||||
if (isset($params['subtitle'])) {
|
||||
$input['subtitle'] = $params['subtitle'];
|
||||
}
|
||||
|
||||
if (isset($params['slug'])) {
|
||||
$input['slug'] = $params['slug'];
|
||||
}
|
||||
|
||||
if (isset($params['canonicalUrl'])) {
|
||||
$input['originalArticleURL'] = $params['canonicalUrl'];
|
||||
}
|
||||
|
||||
if ($media->isNotEmpty()) {
|
||||
$input['coverImageOptions'] = [
|
||||
'coverImageURL' => $media->first()['url'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
if (isset($params['tags'])) {
|
||||
$input['tags'] = array_map(fn ($tag) => [
|
||||
'slug' => is_array($tag) ? $tag['slug'] : $tag,
|
||||
'name' => is_array($tag) ? ($tag['name'] ?? $tag['slug']) : $tag,
|
||||
], (array) $params['tags']);
|
||||
}
|
||||
|
||||
if (isset($params['publishedAt'])) {
|
||||
$input['publishedAt'] = $params['publishedAt'];
|
||||
}
|
||||
|
||||
// Determine if draft or published
|
||||
$mutation = isset($params['draft']) && $params['draft']
|
||||
? 'createDraft'
|
||||
: 'publishPost';
|
||||
|
||||
$query = <<<'GRAPHQL'
|
||||
mutation PublishPost($input: PublishPostInput!) {
|
||||
publishPost(input: $input) {
|
||||
post {
|
||||
id
|
||||
title
|
||||
slug
|
||||
url
|
||||
publication {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
GRAPHQL;
|
||||
|
||||
$response = $this->http()
|
||||
->withHeaders(['Authorization' => $this->accessToken()])
|
||||
->post(self::API_URL, [
|
||||
'query' => $query,
|
||||
'variables' => ['input' => $input],
|
||||
]);
|
||||
|
||||
return $this->fromHttp($response, function ($data) {
|
||||
$post = $data['data']['publishPost']['post'] ?? null;
|
||||
|
||||
if (! $post) {
|
||||
$errors = $data['errors'] ?? [];
|
||||
|
||||
return ['error' => $errors[0]['message'] ?? 'Failed to publish'];
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $post['id'],
|
||||
'title' => $post['title'],
|
||||
'slug' => $post['slug'],
|
||||
'url' => $post['url'],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing post.
|
||||
*/
|
||||
public function update(string $postId, array $params): Response
|
||||
{
|
||||
$input = array_filter([
|
||||
'id' => $postId,
|
||||
'title' => $params['title'] ?? null,
|
||||
'contentMarkdown' => $params['content'] ?? $params['body'] ?? null,
|
||||
'subtitle' => $params['subtitle'] ?? null,
|
||||
'slug' => $params['slug'] ?? null,
|
||||
]);
|
||||
|
||||
$query = <<<'GRAPHQL'
|
||||
mutation UpdatePost($input: UpdatePostInput!) {
|
||||
updatePost(input: $input) {
|
||||
post {
|
||||
id
|
||||
title
|
||||
slug
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
GRAPHQL;
|
||||
|
||||
$response = $this->http()
|
||||
->withHeaders(['Authorization' => $this->accessToken()])
|
||||
->post(self::API_URL, [
|
||||
'query' => $query,
|
||||
'variables' => ['input' => $input],
|
||||
]);
|
||||
|
||||
return $this->fromHttp($response, function ($data) {
|
||||
$post = $data['data']['updatePost']['post'] ?? null;
|
||||
|
||||
return $post ? [
|
||||
'id' => $post['id'],
|
||||
'title' => $post['title'],
|
||||
'url' => $post['url'],
|
||||
] : ['error' => 'Failed to update'];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get external URL to an article.
|
||||
*/
|
||||
public static function externalPostUrl(string $publicationHost, string $slug): string
|
||||
{
|
||||
return "https://{$publicationHost}/{$slug}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get external URL to a profile.
|
||||
*/
|
||||
public static function externalAccountUrl(string $username): string
|
||||
{
|
||||
return "https://hashnode.com/@{$username}";
|
||||
}
|
||||
}
|
||||
128
src/Hashnode/Publications.php
Normal file
128
src/Hashnode/Publications.php
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Content\Hashnode;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\ManagesTokens;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Listable;
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* Hashnode publications listing.
|
||||
*/
|
||||
class Publications implements Listable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use ManagesTokens;
|
||||
use UsesHttp;
|
||||
|
||||
private const API_URL = 'https://gql.hashnode.com';
|
||||
|
||||
/**
|
||||
* List user's publications.
|
||||
*/
|
||||
public function listEntities(): Response
|
||||
{
|
||||
$query = <<<'GRAPHQL'
|
||||
query {
|
||||
me {
|
||||
publications(first: 50) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
displayTitle
|
||||
url
|
||||
about {
|
||||
markdown
|
||||
}
|
||||
favicon
|
||||
isTeam
|
||||
postsCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
GRAPHQL;
|
||||
|
||||
$response = $this->http()
|
||||
->withHeaders(['Authorization' => $this->accessToken()])
|
||||
->post(self::API_URL, ['query' => $query]);
|
||||
|
||||
return $this->fromHttp($response, function ($data) {
|
||||
$publications = $data['data']['me']['publications']['edges'] ?? [];
|
||||
|
||||
return [
|
||||
'publications' => array_map(fn ($edge) => [
|
||||
'id' => $edge['node']['id'],
|
||||
'title' => $edge['node']['title'],
|
||||
'display_title' => $edge['node']['displayTitle'] ?? $edge['node']['title'],
|
||||
'url' => $edge['node']['url'],
|
||||
'about' => $edge['node']['about']['markdown'] ?? null,
|
||||
'favicon' => $edge['node']['favicon'] ?? null,
|
||||
'is_team' => $edge['node']['isTeam'] ?? false,
|
||||
'posts_count' => $edge['node']['postsCount'] ?? 0,
|
||||
], $publications),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get publication by ID.
|
||||
*/
|
||||
public function get(string $publicationId): Response
|
||||
{
|
||||
$query = <<<'GRAPHQL'
|
||||
query GetPublication($id: ObjectId!) {
|
||||
publication(id: $id) {
|
||||
id
|
||||
title
|
||||
displayTitle
|
||||
url
|
||||
about {
|
||||
markdown
|
||||
}
|
||||
favicon
|
||||
isTeam
|
||||
postsCount
|
||||
author {
|
||||
id
|
||||
username
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
GRAPHQL;
|
||||
|
||||
$response = $this->http()
|
||||
->withHeaders(['Authorization' => $this->accessToken()])
|
||||
->post(self::API_URL, [
|
||||
'query' => $query,
|
||||
'variables' => ['id' => $publicationId],
|
||||
]);
|
||||
|
||||
return $this->fromHttp($response, function ($data) {
|
||||
$pub = $data['data']['publication'] ?? null;
|
||||
|
||||
if (! $pub) {
|
||||
return ['error' => 'Publication not found'];
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $pub['id'],
|
||||
'title' => $pub['title'],
|
||||
'display_title' => $pub['displayTitle'] ?? $pub['title'],
|
||||
'url' => $pub['url'],
|
||||
'about' => $pub['about']['markdown'] ?? null,
|
||||
'favicon' => $pub['favicon'] ?? null,
|
||||
'is_team' => $pub['isTeam'] ?? false,
|
||||
'posts_count' => $pub['postsCount'] ?? 0,
|
||||
'author' => $pub['author'] ?? null,
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
217
src/Hashnode/Read.php
Normal file
217
src/Hashnode/Read.php
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Content\Hashnode;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\ManagesTokens;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Readable;
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* Hashnode post and profile reading via GraphQL API.
|
||||
*/
|
||||
class Read implements Readable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use ManagesTokens;
|
||||
use UsesHttp;
|
||||
|
||||
private const API_URL = 'https://gql.hashnode.com';
|
||||
|
||||
/**
|
||||
* Get a post by ID.
|
||||
*/
|
||||
public function get(string $id): Response
|
||||
{
|
||||
$query = <<<'GRAPHQL'
|
||||
query GetPost($id: ID!) {
|
||||
post(id: $id) {
|
||||
id
|
||||
title
|
||||
slug
|
||||
url
|
||||
content {
|
||||
markdown
|
||||
html
|
||||
}
|
||||
brief
|
||||
coverImage {
|
||||
url
|
||||
}
|
||||
tags {
|
||||
name
|
||||
slug
|
||||
}
|
||||
publishedAt
|
||||
views
|
||||
reactionCount
|
||||
responseCount
|
||||
}
|
||||
}
|
||||
GRAPHQL;
|
||||
|
||||
$response = $this->http()
|
||||
->withHeaders(['Authorization' => $this->accessToken()])
|
||||
->post(self::API_URL, [
|
||||
'query' => $query,
|
||||
'variables' => ['id' => $id],
|
||||
]);
|
||||
|
||||
return $this->fromHttp($response, function ($data) {
|
||||
$post = $data['data']['post'] ?? null;
|
||||
|
||||
if (! $post) {
|
||||
return ['error' => 'Post not found'];
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $post['id'],
|
||||
'title' => $post['title'],
|
||||
'slug' => $post['slug'],
|
||||
'url' => $post['url'],
|
||||
'content_markdown' => $post['content']['markdown'] ?? null,
|
||||
'content_html' => $post['content']['html'] ?? null,
|
||||
'brief' => $post['brief'] ?? null,
|
||||
'cover_image' => $post['coverImage']['url'] ?? null,
|
||||
'tags' => $post['tags'] ?? [],
|
||||
'published_at' => $post['publishedAt'] ?? null,
|
||||
'views' => $post['views'] ?? 0,
|
||||
'reactions' => $post['reactionCount'] ?? 0,
|
||||
'responses' => $post['responseCount'] ?? 0,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authenticated user's profile.
|
||||
*/
|
||||
public function me(): Response
|
||||
{
|
||||
$query = <<<'GRAPHQL'
|
||||
query {
|
||||
me {
|
||||
id
|
||||
username
|
||||
name
|
||||
bio {
|
||||
markdown
|
||||
}
|
||||
profilePicture
|
||||
publications(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
followersCount
|
||||
followingsCount
|
||||
}
|
||||
}
|
||||
GRAPHQL;
|
||||
|
||||
$response = $this->http()
|
||||
->withHeaders(['Authorization' => $this->accessToken()])
|
||||
->post(self::API_URL, ['query' => $query]);
|
||||
|
||||
return $this->fromHttp($response, function ($data) {
|
||||
$user = $data['data']['me'] ?? null;
|
||||
|
||||
if (! $user) {
|
||||
return ['error' => 'Not authenticated'];
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $user['id'],
|
||||
'username' => $user['username'],
|
||||
'name' => $user['name'],
|
||||
'image' => $user['profilePicture'] ?? null,
|
||||
'bio' => $user['bio']['markdown'] ?? null,
|
||||
'followers_count' => $user['followersCount'] ?? 0,
|
||||
'following_count' => $user['followingsCount'] ?? 0,
|
||||
'publications' => array_map(fn ($edge) => [
|
||||
'id' => $edge['node']['id'],
|
||||
'title' => $edge['node']['title'],
|
||||
'url' => $edge['node']['url'],
|
||||
], $user['publications']['edges'] ?? []),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List posts from a publication.
|
||||
*/
|
||||
public function list(array $params = []): Response
|
||||
{
|
||||
$publicationId = $params['publication_id'] ?? null;
|
||||
|
||||
if (! $publicationId) {
|
||||
return $this->error('publication_id is required');
|
||||
}
|
||||
|
||||
$query = <<<'GRAPHQL'
|
||||
query GetPublicationPosts($publicationId: ObjectId!, $first: Int!, $after: String) {
|
||||
publication(id: $publicationId) {
|
||||
posts(first: $first, after: $after) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
slug
|
||||
url
|
||||
brief
|
||||
publishedAt
|
||||
views
|
||||
reactionCount
|
||||
}
|
||||
cursor
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
GRAPHQL;
|
||||
|
||||
$response = $this->http()
|
||||
->withHeaders(['Authorization' => $this->accessToken()])
|
||||
->post(self::API_URL, [
|
||||
'query' => $query,
|
||||
'variables' => [
|
||||
'publicationId' => $publicationId,
|
||||
'first' => $params['limit'] ?? 20,
|
||||
'after' => $params['cursor'] ?? null,
|
||||
],
|
||||
]);
|
||||
|
||||
return $this->fromHttp($response, function ($data) {
|
||||
$posts = $data['data']['publication']['posts'] ?? null;
|
||||
|
||||
if (! $posts) {
|
||||
return ['error' => 'Publication not found'];
|
||||
}
|
||||
|
||||
return [
|
||||
'posts' => array_map(fn ($edge) => [
|
||||
'id' => $edge['node']['id'],
|
||||
'title' => $edge['node']['title'],
|
||||
'slug' => $edge['node']['slug'],
|
||||
'url' => $edge['node']['url'],
|
||||
'brief' => $edge['node']['brief'],
|
||||
'published_at' => $edge['node']['publishedAt'],
|
||||
'views' => $edge['node']['views'] ?? 0,
|
||||
'reactions' => $edge['node']['reactionCount'] ?? 0,
|
||||
], $posts['edges'] ?? []),
|
||||
'has_next_page' => $posts['pageInfo']['hasNextPage'] ?? false,
|
||||
'cursor' => $posts['pageInfo']['endCursor'] ?? null,
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
123
src/Medium/Auth.php
Normal file
123
src/Medium/Auth.php
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Content\Medium;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Authenticable;
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* Medium OAuth 2.0 authentication.
|
||||
*/
|
||||
class Auth implements Authenticable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use UsesHttp;
|
||||
|
||||
private const AUTH_URL = 'https://medium.com/m/oauth/authorize';
|
||||
|
||||
private const TOKEN_URL = 'https://api.medium.com/v1/tokens';
|
||||
|
||||
private string $clientId;
|
||||
|
||||
private string $clientSecret;
|
||||
|
||||
private string $redirectUri;
|
||||
|
||||
public function __construct(
|
||||
string $clientId = '',
|
||||
string $clientSecret = '',
|
||||
string $redirectUri = ''
|
||||
) {
|
||||
$this->clientId = $clientId;
|
||||
$this->clientSecret = $clientSecret;
|
||||
$this->redirectUri = $redirectUri;
|
||||
}
|
||||
|
||||
public static function identifier(): string
|
||||
{
|
||||
return 'medium';
|
||||
}
|
||||
|
||||
public static function name(): string
|
||||
{
|
||||
return 'Medium';
|
||||
}
|
||||
|
||||
public function getAuthUrl(): string
|
||||
{
|
||||
$state = bin2hex(random_bytes(16));
|
||||
|
||||
$params = http_build_query([
|
||||
'client_id' => $this->clientId,
|
||||
'scope' => 'basicProfile,publishPost,listPublications',
|
||||
'state' => $state,
|
||||
'response_type' => 'code',
|
||||
'redirect_uri' => $this->redirectUri,
|
||||
]);
|
||||
|
||||
return self::AUTH_URL.'?'.$params;
|
||||
}
|
||||
|
||||
public function requestAccessToken(array $params): array
|
||||
{
|
||||
$code = $params['code'] ?? null;
|
||||
|
||||
if (! $code) {
|
||||
return ['error' => 'Authorization code is required'];
|
||||
}
|
||||
|
||||
$response = $this->http()->post(self::TOKEN_URL, [
|
||||
'code' => $code,
|
||||
'client_id' => $this->clientId,
|
||||
'client_secret' => $this->clientSecret,
|
||||
'grant_type' => 'authorization_code',
|
||||
'redirect_uri' => $this->redirectUri,
|
||||
]);
|
||||
|
||||
if (! $response->successful()) {
|
||||
return [
|
||||
'error' => $response->json('error') ?? 'Token exchange failed',
|
||||
];
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
return [
|
||||
'access_token' => $data['access_token'],
|
||||
'token_type' => $data['token_type'] ?? 'Bearer',
|
||||
'refresh_token' => $data['refresh_token'] ?? null,
|
||||
'scope' => $data['scope'] ?? [],
|
||||
'expires_at' => $data['expires_at'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh access token.
|
||||
*/
|
||||
public function refreshToken(string $refreshToken): array
|
||||
{
|
||||
$response = $this->http()->post(self::TOKEN_URL, [
|
||||
'refresh_token' => $refreshToken,
|
||||
'client_id' => $this->clientId,
|
||||
'client_secret' => $this->clientSecret,
|
||||
'grant_type' => 'refresh_token',
|
||||
]);
|
||||
|
||||
if (! $response->successful()) {
|
||||
return [
|
||||
'error' => $response->json('error') ?? 'Refresh failed',
|
||||
];
|
||||
}
|
||||
|
||||
return $response->json();
|
||||
}
|
||||
|
||||
public function getAccount(): Response
|
||||
{
|
||||
return $this->error('Use Read::me() with access token');
|
||||
}
|
||||
}
|
||||
126
src/Medium/Post.php
Normal file
126
src/Medium/Post.php
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Content\Medium;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\ManagesTokens;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Postable;
|
||||
use Core\Plug\Response;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Medium article publishing.
|
||||
*/
|
||||
class Post implements Postable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use ManagesTokens;
|
||||
use UsesHttp;
|
||||
|
||||
private const API_URL = 'https://api.medium.com/v1';
|
||||
|
||||
private ?string $authorId = null;
|
||||
|
||||
private ?string $publicationId = null;
|
||||
|
||||
/**
|
||||
* Set the author ID for posting.
|
||||
*/
|
||||
public function forAuthor(string $authorId): self
|
||||
{
|
||||
$this->authorId = $authorId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set publication ID for posting to a publication.
|
||||
*/
|
||||
public function forPublication(string $publicationId): self
|
||||
{
|
||||
$this->publicationId = $publicationId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish an article.
|
||||
*
|
||||
* @param string $text The HTML or Markdown content
|
||||
* @param Collection $media Not used - Medium handles media within content
|
||||
* @param array $params title (required), contentFormat (html/markdown), tags, canonicalUrl, publishStatus
|
||||
*/
|
||||
public function publish(string $text, Collection $media, array $params = []): Response
|
||||
{
|
||||
$title = $params['title'] ?? null;
|
||||
|
||||
if (! $title) {
|
||||
return $this->error('Title is required');
|
||||
}
|
||||
|
||||
$postData = [
|
||||
'title' => $title,
|
||||
'contentFormat' => $params['contentFormat'] ?? 'html', // html or markdown
|
||||
'content' => $text,
|
||||
'publishStatus' => $params['publishStatus'] ?? 'public', // public, draft, unlisted
|
||||
];
|
||||
|
||||
// Optional parameters
|
||||
if (isset($params['tags'])) {
|
||||
$postData['tags'] = array_slice((array) $params['tags'], 0, 5); // Max 5 tags
|
||||
}
|
||||
|
||||
if (isset($params['canonicalUrl'])) {
|
||||
$postData['canonicalUrl'] = $params['canonicalUrl'];
|
||||
}
|
||||
|
||||
if (isset($params['license'])) {
|
||||
$postData['license'] = $params['license']; // all-rights-reserved, cc-40-by, etc.
|
||||
}
|
||||
|
||||
if (isset($params['notifyFollowers'])) {
|
||||
$postData['notifyFollowers'] = $params['notifyFollowers'];
|
||||
}
|
||||
|
||||
// Determine endpoint
|
||||
$endpoint = $this->publicationId
|
||||
? self::API_URL."/publications/{$this->publicationId}/posts"
|
||||
: self::API_URL."/users/{$this->authorId}/posts";
|
||||
|
||||
if (! $this->publicationId && ! $this->authorId) {
|
||||
return $this->error('Author ID or Publication ID required');
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->withToken($this->accessToken())
|
||||
->post($endpoint, $postData);
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'id' => $data['data']['id'],
|
||||
'title' => $data['data']['title'],
|
||||
'url' => $data['data']['url'],
|
||||
'publishStatus' => $data['data']['publishStatus'],
|
||||
'publishedAt' => $data['data']['publishedAt'] ?? null,
|
||||
'authorId' => $data['data']['authorId'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get external URL to an article.
|
||||
*/
|
||||
public static function externalPostUrl(string $url): string
|
||||
{
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get external URL to a profile.
|
||||
*/
|
||||
public static function externalAccountUrl(string $username): string
|
||||
{
|
||||
return "https://medium.com/@{$username}";
|
||||
}
|
||||
}
|
||||
86
src/Medium/Publications.php
Normal file
86
src/Medium/Publications.php
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Content\Medium;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\ManagesTokens;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Listable;
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* Medium publications listing.
|
||||
*/
|
||||
class Publications implements Listable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use ManagesTokens;
|
||||
use UsesHttp;
|
||||
|
||||
private const API_URL = 'https://api.medium.com/v1';
|
||||
|
||||
private ?string $userId = null;
|
||||
|
||||
/**
|
||||
* Set user ID for listing publications.
|
||||
*/
|
||||
public function forUser(string $userId): self
|
||||
{
|
||||
$this->userId = $userId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* List publications the user can write to.
|
||||
*/
|
||||
public function listEntities(): Response
|
||||
{
|
||||
if (! $this->userId) {
|
||||
// Get user ID first
|
||||
$meResponse = $this->http()
|
||||
->withToken($this->accessToken())
|
||||
->get(self::API_URL.'/me');
|
||||
|
||||
if (! $meResponse->successful()) {
|
||||
return $this->fromHttp($meResponse);
|
||||
}
|
||||
|
||||
$this->userId = $meResponse->json('data.id');
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->withToken($this->accessToken())
|
||||
->get(self::API_URL."/users/{$this->userId}/publications");
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'publications' => array_map(fn ($pub) => [
|
||||
'id' => $pub['id'],
|
||||
'name' => $pub['name'],
|
||||
'description' => $pub['description'] ?? null,
|
||||
'url' => $pub['url'],
|
||||
'imageUrl' => $pub['imageUrl'] ?? null,
|
||||
], $data['data'] ?? []),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contributors to a publication.
|
||||
*/
|
||||
public function contributors(string $publicationId): Response
|
||||
{
|
||||
$response = $this->http()
|
||||
->withToken($this->accessToken())
|
||||
->get(self::API_URL."/publications/{$publicationId}/contributors");
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'contributors' => array_map(fn ($contributor) => [
|
||||
'userId' => $contributor['userId'],
|
||||
'publicationId' => $contributor['publicationId'],
|
||||
'role' => $contributor['role'],
|
||||
], $data['data'] ?? []),
|
||||
]);
|
||||
}
|
||||
}
|
||||
59
src/Medium/Read.php
Normal file
59
src/Medium/Read.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Content\Medium;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\ManagesTokens;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Readable;
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* Medium profile and publication reading.
|
||||
*/
|
||||
class Read implements Readable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use ManagesTokens;
|
||||
use UsesHttp;
|
||||
|
||||
private const API_URL = 'https://api.medium.com/v1';
|
||||
|
||||
/**
|
||||
* Get a post by ID.
|
||||
*
|
||||
* Note: Medium API doesn't provide a direct post retrieval endpoint.
|
||||
*/
|
||||
public function get(string $id): Response
|
||||
{
|
||||
return $this->error('Medium API does not support retrieving individual posts');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authenticated user's profile.
|
||||
*/
|
||||
public function me(): Response
|
||||
{
|
||||
$response = $this->http()
|
||||
->withToken($this->accessToken())
|
||||
->get(self::API_URL.'/me');
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'id' => $data['data']['id'],
|
||||
'username' => $data['data']['username'],
|
||||
'name' => $data['data']['name'],
|
||||
'image' => $data['data']['imageUrl'] ?? null,
|
||||
'url' => $data['data']['url'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* List not supported for Medium (no feed API).
|
||||
*/
|
||||
public function list(array $params = []): Response
|
||||
{
|
||||
return $this->error('Medium API does not support listing posts');
|
||||
}
|
||||
}
|
||||
126
src/Wordpress/Auth.php
Normal file
126
src/Wordpress/Auth.php
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Content\Wordpress;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Authenticable;
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* WordPress REST API authentication.
|
||||
*
|
||||
* Supports application passwords (self-hosted) and OAuth 2.0 (WordPress.com).
|
||||
*/
|
||||
class Auth implements Authenticable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use UsesHttp;
|
||||
|
||||
private string $siteUrl;
|
||||
|
||||
private ?string $username = null;
|
||||
|
||||
private ?string $applicationPassword = null;
|
||||
|
||||
public function __construct(string $siteUrl = '', ?string $username = null, ?string $applicationPassword = null)
|
||||
{
|
||||
$this->siteUrl = rtrim($siteUrl, '/');
|
||||
$this->username = $username;
|
||||
$this->applicationPassword = $applicationPassword;
|
||||
}
|
||||
|
||||
public static function identifier(): string
|
||||
{
|
||||
return 'wordpress';
|
||||
}
|
||||
|
||||
public static function name(): string
|
||||
{
|
||||
return 'WordPress';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set site URL.
|
||||
*/
|
||||
public function forSite(string $siteUrl): self
|
||||
{
|
||||
$this->siteUrl = rtrim($siteUrl, '/');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Application passwords setup URL.
|
||||
*/
|
||||
public function getAuthUrl(): string
|
||||
{
|
||||
return $this->siteUrl.'/wp-admin/profile.php#application-passwords';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate credentials.
|
||||
*
|
||||
* @param array $params ['site_url' => string, 'username' => string, 'application_password' => string]
|
||||
*/
|
||||
public function requestAccessToken(array $params): array
|
||||
{
|
||||
$siteUrl = rtrim($params['site_url'] ?? $this->siteUrl, '/');
|
||||
$username = $params['username'] ?? $this->username;
|
||||
$password = $params['application_password'] ?? $this->applicationPassword;
|
||||
|
||||
if (! $siteUrl) {
|
||||
return ['error' => 'Site URL is required'];
|
||||
}
|
||||
|
||||
if (! $username || ! $password) {
|
||||
return ['error' => 'Username and application password are required'];
|
||||
}
|
||||
|
||||
// Validate by fetching user info
|
||||
$response = $this->http()
|
||||
->withBasicAuth($username, $password)
|
||||
->get($siteUrl.'/wp-json/wp/v2/users/me');
|
||||
|
||||
if (! $response->successful()) {
|
||||
return ['error' => 'Invalid credentials or site configuration'];
|
||||
}
|
||||
|
||||
$user = $response->json();
|
||||
|
||||
return [
|
||||
'site_url' => $siteUrl,
|
||||
'username' => $username,
|
||||
'application_password' => $password,
|
||||
'user_id' => $user['id'],
|
||||
'user_slug' => $user['slug'],
|
||||
'user_name' => $user['name'],
|
||||
];
|
||||
}
|
||||
|
||||
public function getAccount(): Response
|
||||
{
|
||||
return $this->error('Use Read::me() with credentials');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if site has REST API enabled.
|
||||
*/
|
||||
public function checkSite(string $siteUrl): Response
|
||||
{
|
||||
$siteUrl = rtrim($siteUrl, '/');
|
||||
|
||||
$response = $this->http()->get($siteUrl.'/wp-json');
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'name' => $data['name'] ?? null,
|
||||
'description' => $data['description'] ?? null,
|
||||
'url' => $data['url'] ?? $siteUrl,
|
||||
'home' => $data['home'] ?? null,
|
||||
'namespaces' => $data['namespaces'] ?? [],
|
||||
'has_wp_v2' => in_array('wp/v2', $data['namespaces'] ?? []),
|
||||
]);
|
||||
}
|
||||
}
|
||||
94
src/Wordpress/Delete.php
Normal file
94
src/Wordpress/Delete.php
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Content\Wordpress;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\ManagesTokens;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Deletable;
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* WordPress post deletion.
|
||||
*/
|
||||
class Delete implements Deletable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use ManagesTokens;
|
||||
use UsesHttp;
|
||||
|
||||
private string $siteUrl = '';
|
||||
|
||||
private ?string $username = null;
|
||||
|
||||
private ?string $applicationPassword = null;
|
||||
|
||||
/**
|
||||
* Set site URL.
|
||||
*/
|
||||
public function forSite(string $siteUrl): self
|
||||
{
|
||||
$this->siteUrl = rtrim($siteUrl, '/');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set basic auth credentials.
|
||||
*/
|
||||
public function withCredentials(string $username, string $password): self
|
||||
{
|
||||
$this->username = $username;
|
||||
$this->applicationPassword = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a post.
|
||||
*
|
||||
* @param string $id Post ID
|
||||
*/
|
||||
public function delete(string $id): Response
|
||||
{
|
||||
if (! $this->siteUrl) {
|
||||
return $this->error('Site URL is required');
|
||||
}
|
||||
|
||||
// force=true permanently deletes, otherwise moves to trash
|
||||
$response = $this->http()
|
||||
->withBasicAuth($this->username, $this->applicationPassword)
|
||||
->delete($this->siteUrl."/wp-json/wp/v2/posts/{$id}", [
|
||||
'force' => false, // Move to trash
|
||||
]);
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'deleted' => true,
|
||||
'id' => $data['id'],
|
||||
'status' => $data['status'], // Should be 'trash'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently delete a post.
|
||||
*/
|
||||
public function forceDelete(int $postId): Response
|
||||
{
|
||||
if (! $this->siteUrl) {
|
||||
return $this->error('Site URL is required');
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->withBasicAuth($this->username, $this->applicationPassword)
|
||||
->delete($this->siteUrl."/wp-json/wp/v2/posts/{$postId}", [
|
||||
'force' => true,
|
||||
]);
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'deleted' => $data['deleted'] ?? true,
|
||||
'previous' => $data['previous'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
127
src/Wordpress/Media.php
Normal file
127
src/Wordpress/Media.php
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Content\Wordpress;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\ManagesTokens;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\MediaUploadable;
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* WordPress media upload.
|
||||
*/
|
||||
class Media implements MediaUploadable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use ManagesTokens;
|
||||
use UsesHttp;
|
||||
|
||||
private string $siteUrl = '';
|
||||
|
||||
private ?string $username = null;
|
||||
|
||||
private ?string $applicationPassword = null;
|
||||
|
||||
/**
|
||||
* Set site URL.
|
||||
*/
|
||||
public function forSite(string $siteUrl): self
|
||||
{
|
||||
$this->siteUrl = rtrim($siteUrl, '/');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set basic auth credentials.
|
||||
*/
|
||||
public function withCredentials(string $username, string $password): self
|
||||
{
|
||||
$this->username = $username;
|
||||
$this->applicationPassword = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function upload(array $item): Response
|
||||
{
|
||||
if (! $this->siteUrl) {
|
||||
return $this->error('Site URL is required');
|
||||
}
|
||||
|
||||
$path = $item['path'] ?? null;
|
||||
|
||||
if (! $path || ! file_exists($path)) {
|
||||
return $this->error('File not found');
|
||||
}
|
||||
|
||||
$filename = $item['name'] ?? basename($path);
|
||||
$mimeType = mime_content_type($path);
|
||||
|
||||
$response = $this->http()
|
||||
->withBasicAuth($this->username, $this->applicationPassword)
|
||||
->withHeaders([
|
||||
'Content-Disposition' => "attachment; filename=\"{$filename}\"",
|
||||
'Content-Type' => $mimeType,
|
||||
])
|
||||
->withBody(file_get_contents($path), $mimeType)
|
||||
->post($this->siteUrl.'/wp-json/wp/v2/media');
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'id' => $data['id'],
|
||||
'title' => $data['title']['rendered'] ?? $filename,
|
||||
'url' => $data['source_url'] ?? $data['guid']['rendered'],
|
||||
'link' => $data['link'],
|
||||
'mime_type' => $data['mime_type'],
|
||||
'media_details' => $data['media_details'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update media item metadata.
|
||||
*/
|
||||
public function update(int $mediaId, array $params): Response
|
||||
{
|
||||
if (! $this->siteUrl) {
|
||||
return $this->error('Site URL is required');
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->withBasicAuth($this->username, $this->applicationPassword)
|
||||
->post($this->siteUrl."/wp-json/wp/v2/media/{$mediaId}", array_filter([
|
||||
'title' => $params['title'] ?? null,
|
||||
'caption' => $params['caption'] ?? null,
|
||||
'alt_text' => $params['alt_text'] ?? $params['alt'] ?? null,
|
||||
'description' => $params['description'] ?? null,
|
||||
]));
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'id' => $data['id'],
|
||||
'title' => $data['title']['rendered'] ?? null,
|
||||
'url' => $data['source_url'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a media item.
|
||||
*/
|
||||
public function delete(int $mediaId, bool $force = false): Response
|
||||
{
|
||||
if (! $this->siteUrl) {
|
||||
return $this->error('Site URL is required');
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->withBasicAuth($this->username, $this->applicationPassword)
|
||||
->delete($this->siteUrl."/wp-json/wp/v2/media/{$mediaId}", [
|
||||
'force' => $force,
|
||||
]);
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'deleted' => $data['deleted'] ?? true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
174
src/Wordpress/Post.php
Normal file
174
src/Wordpress/Post.php
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Content\Wordpress;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\ManagesTokens;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Postable;
|
||||
use Core\Plug\Response;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* WordPress post publishing via REST API.
|
||||
*/
|
||||
class Post implements Postable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use ManagesTokens;
|
||||
use UsesHttp;
|
||||
|
||||
private string $siteUrl = '';
|
||||
|
||||
private ?string $username = null;
|
||||
|
||||
private ?string $applicationPassword = null;
|
||||
|
||||
/**
|
||||
* Set site URL.
|
||||
*/
|
||||
public function forSite(string $siteUrl): self
|
||||
{
|
||||
$this->siteUrl = rtrim($siteUrl, '/');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set basic auth credentials.
|
||||
*/
|
||||
public function withCredentials(string $username, string $password): self
|
||||
{
|
||||
$this->username = $username;
|
||||
$this->applicationPassword = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a post.
|
||||
*
|
||||
* @param string $text HTML content
|
||||
* @param Collection $media Featured image (first item used)
|
||||
* @param array $params title, status, excerpt, categories, tags, format, featured_media, slug
|
||||
*/
|
||||
public function publish(string $text, Collection $media, array $params = []): Response
|
||||
{
|
||||
if (! $this->siteUrl) {
|
||||
return $this->error('Site URL is required');
|
||||
}
|
||||
|
||||
$title = $params['title'] ?? null;
|
||||
|
||||
if (! $title) {
|
||||
return $this->error('Title is required');
|
||||
}
|
||||
|
||||
$postData = [
|
||||
'title' => $title,
|
||||
'content' => $text,
|
||||
'status' => $params['status'] ?? 'publish', // publish, draft, pending, private
|
||||
];
|
||||
|
||||
// Optional parameters
|
||||
if (isset($params['excerpt'])) {
|
||||
$postData['excerpt'] = $params['excerpt'];
|
||||
}
|
||||
|
||||
if (isset($params['slug'])) {
|
||||
$postData['slug'] = $params['slug'];
|
||||
}
|
||||
|
||||
if (isset($params['categories'])) {
|
||||
$postData['categories'] = (array) $params['categories'];
|
||||
}
|
||||
|
||||
if (isset($params['tags'])) {
|
||||
$postData['tags'] = (array) $params['tags'];
|
||||
}
|
||||
|
||||
if (isset($params['format'])) {
|
||||
$postData['format'] = $params['format']; // standard, aside, gallery, link, etc.
|
||||
}
|
||||
|
||||
if (isset($params['featured_media'])) {
|
||||
$postData['featured_media'] = $params['featured_media'];
|
||||
}
|
||||
|
||||
// Handle media upload for featured image
|
||||
if ($media->isNotEmpty() && ! isset($params['featured_media'])) {
|
||||
$mediaUploader = (new Media)
|
||||
->forSite($this->siteUrl)
|
||||
->withCredentials($this->username, $this->applicationPassword);
|
||||
|
||||
$uploadResult = $mediaUploader->upload($media->first());
|
||||
|
||||
if ($uploadResult->isOk()) {
|
||||
$postData['featured_media'] = $uploadResult->get('id');
|
||||
}
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->withBasicAuth($this->username, $this->applicationPassword)
|
||||
->post($this->siteUrl.'/wp-json/wp/v2/posts', $postData);
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'id' => $data['id'],
|
||||
'title' => $data['title']['rendered'] ?? $title,
|
||||
'slug' => $data['slug'],
|
||||
'url' => $data['link'],
|
||||
'status' => $data['status'],
|
||||
'guid' => $data['guid']['rendered'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing post.
|
||||
*/
|
||||
public function update(int $postId, array $params): Response
|
||||
{
|
||||
if (! $this->siteUrl) {
|
||||
return $this->error('Site URL is required');
|
||||
}
|
||||
|
||||
$postData = array_filter([
|
||||
'title' => $params['title'] ?? null,
|
||||
'content' => $params['content'] ?? null,
|
||||
'excerpt' => $params['excerpt'] ?? null,
|
||||
'status' => $params['status'] ?? null,
|
||||
'slug' => $params['slug'] ?? null,
|
||||
'categories' => $params['categories'] ?? null,
|
||||
'tags' => $params['tags'] ?? null,
|
||||
'featured_media' => $params['featured_media'] ?? null,
|
||||
]);
|
||||
|
||||
$response = $this->http()
|
||||
->withBasicAuth($this->username, $this->applicationPassword)
|
||||
->post($this->siteUrl."/wp-json/wp/v2/posts/{$postId}", $postData);
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'id' => $data['id'],
|
||||
'title' => $data['title']['rendered'] ?? null,
|
||||
'url' => $data['link'],
|
||||
'status' => $data['status'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get external URL to a post.
|
||||
*/
|
||||
public static function externalPostUrl(string $siteUrl, string $slug): string
|
||||
{
|
||||
return rtrim($siteUrl, '/').'/'.$slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get external URL to site.
|
||||
*/
|
||||
public static function externalAccountUrl(string $siteUrl): string
|
||||
{
|
||||
return $siteUrl;
|
||||
}
|
||||
}
|
||||
209
src/Wordpress/Read.php
Normal file
209
src/Wordpress/Read.php
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Content\Wordpress;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\ManagesTokens;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Readable;
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* WordPress post and profile reading.
|
||||
*/
|
||||
class Read implements Readable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use ManagesTokens;
|
||||
use UsesHttp;
|
||||
|
||||
private string $siteUrl = '';
|
||||
|
||||
private ?string $username = null;
|
||||
|
||||
private ?string $applicationPassword = null;
|
||||
|
||||
/**
|
||||
* Set site URL.
|
||||
*/
|
||||
public function forSite(string $siteUrl): self
|
||||
{
|
||||
$this->siteUrl = rtrim($siteUrl, '/');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set basic auth credentials.
|
||||
*/
|
||||
public function withCredentials(string $username, string $password): self
|
||||
{
|
||||
$this->username = $username;
|
||||
$this->applicationPassword = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a post by ID.
|
||||
*/
|
||||
public function get(string $id): Response
|
||||
{
|
||||
if (! $this->siteUrl) {
|
||||
return $this->error('Site URL is required');
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->withBasicAuth($this->username, $this->applicationPassword)
|
||||
->get($this->siteUrl."/wp-json/wp/v2/posts/{$id}");
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'id' => $data['id'],
|
||||
'title' => $data['title']['rendered'] ?? '',
|
||||
'content' => $data['content']['rendered'] ?? '',
|
||||
'excerpt' => $data['excerpt']['rendered'] ?? '',
|
||||
'slug' => $data['slug'],
|
||||
'url' => $data['link'],
|
||||
'status' => $data['status'],
|
||||
'date' => $data['date'],
|
||||
'modified' => $data['modified'],
|
||||
'author' => $data['author'],
|
||||
'categories' => $data['categories'] ?? [],
|
||||
'tags' => $data['tags'] ?? [],
|
||||
'featured_media' => $data['featured_media'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authenticated user's profile.
|
||||
*/
|
||||
public function me(): Response
|
||||
{
|
||||
if (! $this->siteUrl) {
|
||||
return $this->error('Site URL is required');
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->withBasicAuth($this->username, $this->applicationPassword)
|
||||
->get($this->siteUrl.'/wp-json/wp/v2/users/me');
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'id' => $data['id'],
|
||||
'username' => $data['slug'],
|
||||
'name' => $data['name'],
|
||||
'image' => $data['avatar_urls']['96'] ?? $data['avatar_urls']['48'] ?? null,
|
||||
'description' => $data['description'] ?? null,
|
||||
'url' => $data['url'] ?? null,
|
||||
'link' => $data['link'] ?? null,
|
||||
'roles' => $data['roles'] ?? [],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* List posts.
|
||||
*/
|
||||
public function list(array $params = []): Response
|
||||
{
|
||||
if (! $this->siteUrl) {
|
||||
return $this->error('Site URL is required');
|
||||
}
|
||||
|
||||
$queryParams = [
|
||||
'per_page' => $params['per_page'] ?? 10,
|
||||
'page' => $params['page'] ?? 1,
|
||||
'status' => $params['status'] ?? 'publish',
|
||||
'orderby' => $params['orderby'] ?? 'date',
|
||||
'order' => $params['order'] ?? 'desc',
|
||||
];
|
||||
|
||||
if (isset($params['author'])) {
|
||||
$queryParams['author'] = $params['author'];
|
||||
}
|
||||
|
||||
if (isset($params['categories'])) {
|
||||
$queryParams['categories'] = is_array($params['categories'])
|
||||
? implode(',', $params['categories'])
|
||||
: $params['categories'];
|
||||
}
|
||||
|
||||
if (isset($params['tags'])) {
|
||||
$queryParams['tags'] = is_array($params['tags'])
|
||||
? implode(',', $params['tags'])
|
||||
: $params['tags'];
|
||||
}
|
||||
|
||||
if (isset($params['search'])) {
|
||||
$queryParams['search'] = $params['search'];
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->withBasicAuth($this->username, $this->applicationPassword)
|
||||
->get($this->siteUrl.'/wp-json/wp/v2/posts', $queryParams);
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'posts' => array_map(fn ($post) => [
|
||||
'id' => $post['id'],
|
||||
'title' => $post['title']['rendered'] ?? '',
|
||||
'excerpt' => strip_tags($post['excerpt']['rendered'] ?? ''),
|
||||
'slug' => $post['slug'],
|
||||
'url' => $post['link'],
|
||||
'status' => $post['status'],
|
||||
'date' => $post['date'],
|
||||
], $data),
|
||||
'total' => (int) ($response->header('X-WP-Total') ?? count($data)),
|
||||
'total_pages' => (int) ($response->header('X-WP-TotalPages') ?? 1),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get categories.
|
||||
*/
|
||||
public function categories(array $params = []): Response
|
||||
{
|
||||
if (! $this->siteUrl) {
|
||||
return $this->error('Site URL is required');
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->withBasicAuth($this->username, $this->applicationPassword)
|
||||
->get($this->siteUrl.'/wp-json/wp/v2/categories', [
|
||||
'per_page' => $params['per_page'] ?? 100,
|
||||
]);
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'categories' => array_map(fn ($cat) => [
|
||||
'id' => $cat['id'],
|
||||
'name' => $cat['name'],
|
||||
'slug' => $cat['slug'],
|
||||
'count' => $cat['count'],
|
||||
], $data),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tags.
|
||||
*/
|
||||
public function tags(array $params = []): Response
|
||||
{
|
||||
if (! $this->siteUrl) {
|
||||
return $this->error('Site URL is required');
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->withBasicAuth($this->username, $this->applicationPassword)
|
||||
->get($this->siteUrl.'/wp-json/wp/v2/tags', [
|
||||
'per_page' => $params['per_page'] ?? 100,
|
||||
]);
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'tags' => array_map(fn ($tag) => [
|
||||
'id' => $tag['id'],
|
||||
'name' => $tag['name'],
|
||||
'slug' => $tag['slug'],
|
||||
'count' => $tag['count'],
|
||||
], $data),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue