trypost/app/Actions/Post/CreatePost.php

107 lines
3.3 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
namespace App\Actions\Post;
use App\Enums\Post\Status as PostStatus;
use App\Events\PostCreated;
use App\Models\Post;
use App\Models\User;
use App\Models\Workspace;
use Carbon\Carbon;
feat: complete create + publish post flow via MCP and REST API Lets ChatGPT (MCP) and external clients (REST API) drive the full lifecycle of a post — create with platform selection, attach media from URLs, schedule or publish immediately, and fetch engagement metrics — without touching the web UI. MCP tools added: UpdatePostTool, PublishPostTool, AttachMediaFromUrlTool, ListContentTypesTool, GetPostMetricsTool, PreviewPostTool. CreatePostTool now accepts platforms[] + scheduled_at + label_ids; ListPostsTool gains status/search/limit filters. REST endpoints added: POST /api/posts/{post}/media, GET /api/posts/{post}/metrics, GET /api/posts/{post}/preview, GET /api/content-types. Also fixes a silent CreatePost::execute bug — the action validated platforms[] but ignored it, so REST callers never saw their selection persisted. Adds cross validation rules (ContentTypeMatchesPlatform / ContentTypeMatchesPostPlatform) so a LinkedIn account can't be saddled with x_post, and rejects inactive social accounts during validation instead of failing silently downstream. Shared services (PostMetricsFetcher, PostPreviewer, MediaAttacher) back both MCP tools and REST controllers so behaviour stays aligned. New Resources (PlatformContentTypesResource, PostMetricsResource, PostPreviewResource, PostMediaAttachResource) keep controllers free of inline model mapping. Suite: 1.332 passing, 0 failing — covers web (PostControllerTest), REST (PostApiTest, PlatformApiTest, PostMediaApiTest), MCP (66 tool tests), and the publish job (PublishToSocialPlatformTest). Removes /docs from git tracking and TIKTOK_REVIEW_VIDEO_SCRIPT.md.
2026-05-04 11:12:28 +00:00
use Illuminate\Support\Facades\DB;
class CreatePost
{
feat: complete create + publish post flow via MCP and REST API Lets ChatGPT (MCP) and external clients (REST API) drive the full lifecycle of a post — create with platform selection, attach media from URLs, schedule or publish immediately, and fetch engagement metrics — without touching the web UI. MCP tools added: UpdatePostTool, PublishPostTool, AttachMediaFromUrlTool, ListContentTypesTool, GetPostMetricsTool, PreviewPostTool. CreatePostTool now accepts platforms[] + scheduled_at + label_ids; ListPostsTool gains status/search/limit filters. REST endpoints added: POST /api/posts/{post}/media, GET /api/posts/{post}/metrics, GET /api/posts/{post}/preview, GET /api/content-types. Also fixes a silent CreatePost::execute bug — the action validated platforms[] but ignored it, so REST callers never saw their selection persisted. Adds cross validation rules (ContentTypeMatchesPlatform / ContentTypeMatchesPostPlatform) so a LinkedIn account can't be saddled with x_post, and rejects inactive social accounts during validation instead of failing silently downstream. Shared services (PostMetricsFetcher, PostPreviewer, MediaAttacher) back both MCP tools and REST controllers so behaviour stays aligned. New Resources (PlatformContentTypesResource, PostMetricsResource, PostPreviewResource, PostMediaAttachResource) keep controllers free of inline model mapping. Suite: 1.332 passing, 0 failing — covers web (PostControllerTest), REST (PostApiTest, PlatformApiTest, PostMediaApiTest), MCP (66 tool tests), and the publish job (PublishToSocialPlatformTest). Removes /docs from git tracking and TIKTOK_REVIEW_VIDEO_SCRIPT.md.
2026-05-04 11:12:28 +00:00
/**
* Create a Post with optional platform selection.
*
* `platforms[]` enables specific social accounts. Each entry takes
* `social_account_id` and an optional `content_type` (defaults to the
* platform's default). Accounts not listed remain disabled but are still
* created via SyncPostPlatforms so the user can toggle them later in the
* editor.
*
fix: address PR review findings — publish, REST store, SSRF, race Code-review surfaced two correctness bugs and a security gap that needed to land before merging. - UpdatePost::execute disabled every platform when called without a `platforms` key. PublishPostTool relied on that path, so every publish-via-MCP queued a job whose handler then found nothing enabled to publish to. Wrap the platform toggle in `Arr::has($data, 'platforms')` (matches the existing label_ids guard a few lines up). Add a regression assertion to `PostPublishToolTest::publish post immediate dispatches PublishPost job` that the previously-enabled platform stays enabled. - StorePostRequest declared rules for only `platforms`, `scheduled_at`, and `status`. `validated()` then stripped `content`, `media`, and `label_ids`, so REST `POST /api/posts` silently created empty drafts. Added rules for content / media / label_ids (with workspace-scoped `Rule::exists` for labels) and dropped the unused `status` field — REST callers transition state via `PUT /posts/{id}`. Removed the dead `platforms.*.content` rule. Added a feature test that asserts content + media + labels roundtrip on create, plus a regression that an `is_active=false` social_account is rejected at validation. - CreatePost::execute now syncs label_ids itself so REST and MCP share the behavior. Removed the duplicate sync from CreatePostTool. - MCP UpdatePostTool didn't scope `platforms.*.id` to the post being updated, drifting from the REST UpdatePostRequest which adds `Rule::exists('post_platforms','id')->where('post_id', ...)`. Now it loads the post first (failing fast with `Post not found.` if the workspace check rejects), then uses the same Rule::exists. - MediaAttacher fetched any URL the caller passed, including loopback / link-local / private targets — classic SSRF pivot. Now `isPublicHttpUrl` rejects non-http(s) schemes, restricted IP ranges, and DNS hostnames whose A/AAAA records resolve into those ranges (covers DNS rebinding). Bypassed under `app()->runningUnitTests()` so `Http::fake()` keeps working. Streaming the response body lets us abort early once we exceed MAX_BYTES instead of buffering the full payload first; redirects are disabled so a 200→302 trick can't bypass the host check. - The `media[]` JSON column had a lost-update race in `attachFromUrls`: read `$post->media`, mutate in PHP, write back. Two concurrent calls clobbered each other. Now wrapped in a transaction with `lockForUpdate()`. - ESLint: `resources/js/actions/**` and `resources/js/routes/**` are auto-generated by Wayfinder on every build. Their import order matches PHP scan order, not alphabetical, so import/order fought eslint-fix forever. Added them to ignores.
2026-05-04 15:16:39 +00:00
* `label_ids[]` are attached after creation so the same set of UUIDs
* works for REST, MCP, and web callers.
*
feat: complete create + publish post flow via MCP and REST API Lets ChatGPT (MCP) and external clients (REST API) drive the full lifecycle of a post — create with platform selection, attach media from URLs, schedule or publish immediately, and fetch engagement metrics — without touching the web UI. MCP tools added: UpdatePostTool, PublishPostTool, AttachMediaFromUrlTool, ListContentTypesTool, GetPostMetricsTool, PreviewPostTool. CreatePostTool now accepts platforms[] + scheduled_at + label_ids; ListPostsTool gains status/search/limit filters. REST endpoints added: POST /api/posts/{post}/media, GET /api/posts/{post}/metrics, GET /api/posts/{post}/preview, GET /api/content-types. Also fixes a silent CreatePost::execute bug — the action validated platforms[] but ignored it, so REST callers never saw their selection persisted. Adds cross validation rules (ContentTypeMatchesPlatform / ContentTypeMatchesPostPlatform) so a LinkedIn account can't be saddled with x_post, and rejects inactive social accounts during validation instead of failing silently downstream. Shared services (PostMetricsFetcher, PostPreviewer, MediaAttacher) back both MCP tools and REST controllers so behaviour stays aligned. New Resources (PlatformContentTypesResource, PostMetricsResource, PostPreviewResource, PostMediaAttachResource) keep controllers free of inline model mapping. Suite: 1.332 passing, 0 failing — covers web (PostControllerTest), REST (PostApiTest, PlatformApiTest, PostMediaApiTest), MCP (66 tool tests), and the publish job (PublishToSocialPlatformTest). Removes /docs from git tracking and TIKTOK_REVIEW_VIDEO_SCRIPT.md.
2026-05-04 11:12:28 +00:00
* @param array{
* content?: ?string,
* media?: array<int, mixed>,
* date?: ?string,
* scheduled_at?: ?string,
* platforms?: array<int, array{social_account_id: string, content_type?: string, meta?: array<string, mixed>}>,
fix: address PR review findings — publish, REST store, SSRF, race Code-review surfaced two correctness bugs and a security gap that needed to land before merging. - UpdatePost::execute disabled every platform when called without a `platforms` key. PublishPostTool relied on that path, so every publish-via-MCP queued a job whose handler then found nothing enabled to publish to. Wrap the platform toggle in `Arr::has($data, 'platforms')` (matches the existing label_ids guard a few lines up). Add a regression assertion to `PostPublishToolTest::publish post immediate dispatches PublishPost job` that the previously-enabled platform stays enabled. - StorePostRequest declared rules for only `platforms`, `scheduled_at`, and `status`. `validated()` then stripped `content`, `media`, and `label_ids`, so REST `POST /api/posts` silently created empty drafts. Added rules for content / media / label_ids (with workspace-scoped `Rule::exists` for labels) and dropped the unused `status` field — REST callers transition state via `PUT /posts/{id}`. Removed the dead `platforms.*.content` rule. Added a feature test that asserts content + media + labels roundtrip on create, plus a regression that an `is_active=false` social_account is rejected at validation. - CreatePost::execute now syncs label_ids itself so REST and MCP share the behavior. Removed the duplicate sync from CreatePostTool. - MCP UpdatePostTool didn't scope `platforms.*.id` to the post being updated, drifting from the REST UpdatePostRequest which adds `Rule::exists('post_platforms','id')->where('post_id', ...)`. Now it loads the post first (failing fast with `Post not found.` if the workspace check rejects), then uses the same Rule::exists. - MediaAttacher fetched any URL the caller passed, including loopback / link-local / private targets — classic SSRF pivot. Now `isPublicHttpUrl` rejects non-http(s) schemes, restricted IP ranges, and DNS hostnames whose A/AAAA records resolve into those ranges (covers DNS rebinding). Bypassed under `app()->runningUnitTests()` so `Http::fake()` keeps working. Streaming the response body lets us abort early once we exceed MAX_BYTES instead of buffering the full payload first; redirects are disabled so a 200→302 trick can't bypass the host check. - The `media[]` JSON column had a lost-update race in `attachFromUrls`: read `$post->media`, mutate in PHP, write back. Two concurrent calls clobbered each other. Now wrapped in a transaction with `lockForUpdate()`. - ESLint: `resources/js/actions/**` and `resources/js/routes/**` are auto-generated by Wayfinder on every build. Their import order matches PHP scan order, not alphabetical, so import/order fought eslint-fix forever. Added them to ignores.
2026-05-04 15:16:39 +00:00
* label_ids?: array<int, string>
feat: complete create + publish post flow via MCP and REST API Lets ChatGPT (MCP) and external clients (REST API) drive the full lifecycle of a post — create with platform selection, attach media from URLs, schedule or publish immediately, and fetch engagement metrics — without touching the web UI. MCP tools added: UpdatePostTool, PublishPostTool, AttachMediaFromUrlTool, ListContentTypesTool, GetPostMetricsTool, PreviewPostTool. CreatePostTool now accepts platforms[] + scheduled_at + label_ids; ListPostsTool gains status/search/limit filters. REST endpoints added: POST /api/posts/{post}/media, GET /api/posts/{post}/metrics, GET /api/posts/{post}/preview, GET /api/content-types. Also fixes a silent CreatePost::execute bug — the action validated platforms[] but ignored it, so REST callers never saw their selection persisted. Adds cross validation rules (ContentTypeMatchesPlatform / ContentTypeMatchesPostPlatform) so a LinkedIn account can't be saddled with x_post, and rejects inactive social accounts during validation instead of failing silently downstream. Shared services (PostMetricsFetcher, PostPreviewer, MediaAttacher) back both MCP tools and REST controllers so behaviour stays aligned. New Resources (PlatformContentTypesResource, PostMetricsResource, PostPreviewResource, PostMediaAttachResource) keep controllers free of inline model mapping. Suite: 1.332 passing, 0 failing — covers web (PostControllerTest), REST (PostApiTest, PlatformApiTest, PostMediaApiTest), MCP (66 tool tests), and the publish job (PublishToSocialPlatformTest). Removes /docs from git tracking and TIKTOK_REVIEW_VIDEO_SCRIPT.md.
2026-05-04 11:12:28 +00:00
* } $data
*/
public static function execute(Workspace $workspace, User $user, array $data): Post
{
feat: complete create + publish post flow via MCP and REST API Lets ChatGPT (MCP) and external clients (REST API) drive the full lifecycle of a post — create with platform selection, attach media from URLs, schedule or publish immediately, and fetch engagement metrics — without touching the web UI. MCP tools added: UpdatePostTool, PublishPostTool, AttachMediaFromUrlTool, ListContentTypesTool, GetPostMetricsTool, PreviewPostTool. CreatePostTool now accepts platforms[] + scheduled_at + label_ids; ListPostsTool gains status/search/limit filters. REST endpoints added: POST /api/posts/{post}/media, GET /api/posts/{post}/metrics, GET /api/posts/{post}/preview, GET /api/content-types. Also fixes a silent CreatePost::execute bug — the action validated platforms[] but ignored it, so REST callers never saw their selection persisted. Adds cross validation rules (ContentTypeMatchesPlatform / ContentTypeMatchesPostPlatform) so a LinkedIn account can't be saddled with x_post, and rejects inactive social accounts during validation instead of failing silently downstream. Shared services (PostMetricsFetcher, PostPreviewer, MediaAttacher) back both MCP tools and REST controllers so behaviour stays aligned. New Resources (PlatformContentTypesResource, PostMetricsResource, PostPreviewResource, PostMediaAttachResource) keep controllers free of inline model mapping. Suite: 1.332 passing, 0 failing — covers web (PostControllerTest), REST (PostApiTest, PlatformApiTest, PostMediaApiTest), MCP (66 tool tests), and the publish job (PublishToSocialPlatformTest). Removes /docs from git tracking and TIKTOK_REVIEW_VIDEO_SCRIPT.md.
2026-05-04 11:12:28 +00:00
$scheduledAt = self::resolveScheduledAt($data);
$post = DB::transaction(function () use ($workspace, $user, $data, $scheduledAt): Post {
feat: complete create + publish post flow via MCP and REST API Lets ChatGPT (MCP) and external clients (REST API) drive the full lifecycle of a post — create with platform selection, attach media from URLs, schedule or publish immediately, and fetch engagement metrics — without touching the web UI. MCP tools added: UpdatePostTool, PublishPostTool, AttachMediaFromUrlTool, ListContentTypesTool, GetPostMetricsTool, PreviewPostTool. CreatePostTool now accepts platforms[] + scheduled_at + label_ids; ListPostsTool gains status/search/limit filters. REST endpoints added: POST /api/posts/{post}/media, GET /api/posts/{post}/metrics, GET /api/posts/{post}/preview, GET /api/content-types. Also fixes a silent CreatePost::execute bug — the action validated platforms[] but ignored it, so REST callers never saw their selection persisted. Adds cross validation rules (ContentTypeMatchesPlatform / ContentTypeMatchesPostPlatform) so a LinkedIn account can't be saddled with x_post, and rejects inactive social accounts during validation instead of failing silently downstream. Shared services (PostMetricsFetcher, PostPreviewer, MediaAttacher) back both MCP tools and REST controllers so behaviour stays aligned. New Resources (PlatformContentTypesResource, PostMetricsResource, PostPreviewResource, PostMediaAttachResource) keep controllers free of inline model mapping. Suite: 1.332 passing, 0 failing — covers web (PostControllerTest), REST (PostApiTest, PlatformApiTest, PostMediaApiTest), MCP (66 tool tests), and the publish job (PublishToSocialPlatformTest). Removes /docs from git tracking and TIKTOK_REVIEW_VIDEO_SCRIPT.md.
2026-05-04 11:12:28 +00:00
$post = $workspace->posts()->create([
'user_id' => $user->id,
'content' => data_get($data, 'content', ''),
'media' => data_get($data, 'media', []),
'status' => PostStatus::Draft,
'scheduled_at' => $scheduledAt,
]);
SyncPostPlatforms::execute($post);
foreach (data_get($data, 'platforms', []) as $platformData) {
$accountId = data_get($platformData, 'social_account_id');
if (! $accountId) {
continue;
}
$updates = ['enabled' => true];
if ($contentType = data_get($platformData, 'content_type')) {
$updates['content_type'] = $contentType;
}
$meta = data_get($platformData, 'meta');
if (is_array($meta) && $meta !== []) {
$existing = $post->postPlatforms()
->where('social_account_id', $accountId)
->first();
if ($existing) {
$updates['meta'] = array_merge($existing->meta ?? [], $meta);
}
}
feat: complete create + publish post flow via MCP and REST API Lets ChatGPT (MCP) and external clients (REST API) drive the full lifecycle of a post — create with platform selection, attach media from URLs, schedule or publish immediately, and fetch engagement metrics — without touching the web UI. MCP tools added: UpdatePostTool, PublishPostTool, AttachMediaFromUrlTool, ListContentTypesTool, GetPostMetricsTool, PreviewPostTool. CreatePostTool now accepts platforms[] + scheduled_at + label_ids; ListPostsTool gains status/search/limit filters. REST endpoints added: POST /api/posts/{post}/media, GET /api/posts/{post}/metrics, GET /api/posts/{post}/preview, GET /api/content-types. Also fixes a silent CreatePost::execute bug — the action validated platforms[] but ignored it, so REST callers never saw their selection persisted. Adds cross validation rules (ContentTypeMatchesPlatform / ContentTypeMatchesPostPlatform) so a LinkedIn account can't be saddled with x_post, and rejects inactive social accounts during validation instead of failing silently downstream. Shared services (PostMetricsFetcher, PostPreviewer, MediaAttacher) back both MCP tools and REST controllers so behaviour stays aligned. New Resources (PlatformContentTypesResource, PostMetricsResource, PostPreviewResource, PostMediaAttachResource) keep controllers free of inline model mapping. Suite: 1.332 passing, 0 failing — covers web (PostControllerTest), REST (PostApiTest, PlatformApiTest, PostMediaApiTest), MCP (66 tool tests), and the publish job (PublishToSocialPlatformTest). Removes /docs from git tracking and TIKTOK_REVIEW_VIDEO_SCRIPT.md.
2026-05-04 11:12:28 +00:00
$post->postPlatforms()
->where('social_account_id', $accountId)
->update($updates);
}
fix: address PR review findings — publish, REST store, SSRF, race Code-review surfaced two correctness bugs and a security gap that needed to land before merging. - UpdatePost::execute disabled every platform when called without a `platforms` key. PublishPostTool relied on that path, so every publish-via-MCP queued a job whose handler then found nothing enabled to publish to. Wrap the platform toggle in `Arr::has($data, 'platforms')` (matches the existing label_ids guard a few lines up). Add a regression assertion to `PostPublishToolTest::publish post immediate dispatches PublishPost job` that the previously-enabled platform stays enabled. - StorePostRequest declared rules for only `platforms`, `scheduled_at`, and `status`. `validated()` then stripped `content`, `media`, and `label_ids`, so REST `POST /api/posts` silently created empty drafts. Added rules for content / media / label_ids (with workspace-scoped `Rule::exists` for labels) and dropped the unused `status` field — REST callers transition state via `PUT /posts/{id}`. Removed the dead `platforms.*.content` rule. Added a feature test that asserts content + media + labels roundtrip on create, plus a regression that an `is_active=false` social_account is rejected at validation. - CreatePost::execute now syncs label_ids itself so REST and MCP share the behavior. Removed the duplicate sync from CreatePostTool. - MCP UpdatePostTool didn't scope `platforms.*.id` to the post being updated, drifting from the REST UpdatePostRequest which adds `Rule::exists('post_platforms','id')->where('post_id', ...)`. Now it loads the post first (failing fast with `Post not found.` if the workspace check rejects), then uses the same Rule::exists. - MediaAttacher fetched any URL the caller passed, including loopback / link-local / private targets — classic SSRF pivot. Now `isPublicHttpUrl` rejects non-http(s) schemes, restricted IP ranges, and DNS hostnames whose A/AAAA records resolve into those ranges (covers DNS rebinding). Bypassed under `app()->runningUnitTests()` so `Http::fake()` keeps working. Streaming the response body lets us abort early once we exceed MAX_BYTES instead of buffering the full payload first; redirects are disabled so a 200→302 trick can't bypass the host check. - The `media[]` JSON column had a lost-update race in `attachFromUrls`: read `$post->media`, mutate in PHP, write back. Two concurrent calls clobbered each other. Now wrapped in a transaction with `lockForUpdate()`. - ESLint: `resources/js/actions/**` and `resources/js/routes/**` are auto-generated by Wayfinder on every build. Their import order matches PHP scan order, not alphabetical, so import/order fought eslint-fix forever. Added them to ignores.
2026-05-04 15:16:39 +00:00
if ($labelIds = data_get($data, 'label_ids')) {
$post->labels()->sync($labelIds);
}
feat: complete create + publish post flow via MCP and REST API Lets ChatGPT (MCP) and external clients (REST API) drive the full lifecycle of a post — create with platform selection, attach media from URLs, schedule or publish immediately, and fetch engagement metrics — without touching the web UI. MCP tools added: UpdatePostTool, PublishPostTool, AttachMediaFromUrlTool, ListContentTypesTool, GetPostMetricsTool, PreviewPostTool. CreatePostTool now accepts platforms[] + scheduled_at + label_ids; ListPostsTool gains status/search/limit filters. REST endpoints added: POST /api/posts/{post}/media, GET /api/posts/{post}/metrics, GET /api/posts/{post}/preview, GET /api/content-types. Also fixes a silent CreatePost::execute bug — the action validated platforms[] but ignored it, so REST callers never saw their selection persisted. Adds cross validation rules (ContentTypeMatchesPlatform / ContentTypeMatchesPostPlatform) so a LinkedIn account can't be saddled with x_post, and rejects inactive social accounts during validation instead of failing silently downstream. Shared services (PostMetricsFetcher, PostPreviewer, MediaAttacher) back both MCP tools and REST controllers so behaviour stays aligned. New Resources (PlatformContentTypesResource, PostMetricsResource, PostPreviewResource, PostMediaAttachResource) keep controllers free of inline model mapping. Suite: 1.332 passing, 0 failing — covers web (PostControllerTest), REST (PostApiTest, PlatformApiTest, PostMediaApiTest), MCP (66 tool tests), and the publish job (PublishToSocialPlatformTest). Removes /docs from git tracking and TIKTOK_REVIEW_VIDEO_SCRIPT.md.
2026-05-04 11:12:28 +00:00
return $post;
});
PostCreated::dispatch($post);
return $post;
feat: complete create + publish post flow via MCP and REST API Lets ChatGPT (MCP) and external clients (REST API) drive the full lifecycle of a post — create with platform selection, attach media from URLs, schedule or publish immediately, and fetch engagement metrics — without touching the web UI. MCP tools added: UpdatePostTool, PublishPostTool, AttachMediaFromUrlTool, ListContentTypesTool, GetPostMetricsTool, PreviewPostTool. CreatePostTool now accepts platforms[] + scheduled_at + label_ids; ListPostsTool gains status/search/limit filters. REST endpoints added: POST /api/posts/{post}/media, GET /api/posts/{post}/metrics, GET /api/posts/{post}/preview, GET /api/content-types. Also fixes a silent CreatePost::execute bug — the action validated platforms[] but ignored it, so REST callers never saw their selection persisted. Adds cross validation rules (ContentTypeMatchesPlatform / ContentTypeMatchesPostPlatform) so a LinkedIn account can't be saddled with x_post, and rejects inactive social accounts during validation instead of failing silently downstream. Shared services (PostMetricsFetcher, PostPreviewer, MediaAttacher) back both MCP tools and REST controllers so behaviour stays aligned. New Resources (PlatformContentTypesResource, PostMetricsResource, PostPreviewResource, PostMediaAttachResource) keep controllers free of inline model mapping. Suite: 1.332 passing, 0 failing — covers web (PostControllerTest), REST (PostApiTest, PlatformApiTest, PostMediaApiTest), MCP (66 tool tests), and the publish job (PublishToSocialPlatformTest). Removes /docs from git tracking and TIKTOK_REVIEW_VIDEO_SCRIPT.md.
2026-05-04 11:12:28 +00:00
}
/**
* @param array<string, mixed> $data
*/
private static function resolveScheduledAt(array $data): Carbon
{
if ($scheduledAt = data_get($data, 'scheduled_at')) {
return Carbon::parse($scheduledAt)->utc();
}
$date = data_get($data, 'date') ?: Carbon::now('UTC')->format('Y-m-d');
feat: complete create + publish post flow via MCP and REST API Lets ChatGPT (MCP) and external clients (REST API) drive the full lifecycle of a post — create with platform selection, attach media from URLs, schedule or publish immediately, and fetch engagement metrics — without touching the web UI. MCP tools added: UpdatePostTool, PublishPostTool, AttachMediaFromUrlTool, ListContentTypesTool, GetPostMetricsTool, PreviewPostTool. CreatePostTool now accepts platforms[] + scheduled_at + label_ids; ListPostsTool gains status/search/limit filters. REST endpoints added: POST /api/posts/{post}/media, GET /api/posts/{post}/metrics, GET /api/posts/{post}/preview, GET /api/content-types. Also fixes a silent CreatePost::execute bug — the action validated platforms[] but ignored it, so REST callers never saw their selection persisted. Adds cross validation rules (ContentTypeMatchesPlatform / ContentTypeMatchesPostPlatform) so a LinkedIn account can't be saddled with x_post, and rejects inactive social accounts during validation instead of failing silently downstream. Shared services (PostMetricsFetcher, PostPreviewer, MediaAttacher) back both MCP tools and REST controllers so behaviour stays aligned. New Resources (PlatformContentTypesResource, PostMetricsResource, PostPreviewResource, PostMediaAttachResource) keep controllers free of inline model mapping. Suite: 1.332 passing, 0 failing — covers web (PostControllerTest), REST (PostApiTest, PlatformApiTest, PostMediaApiTest), MCP (66 tool tests), and the publish job (PublishToSocialPlatformTest). Removes /docs from git tracking and TIKTOK_REVIEW_VIDEO_SCRIPT.md.
2026-05-04 11:12:28 +00:00
return Carbon::parse($date, 'UTC')->setTime(9, 0)->utc();
}
}