Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,6 @@ export class PublicIntegrationsController {
}

const { accessToken } = data;

if (accessToken) {
getIntegration.token = accessToken;

Expand All @@ -451,4 +450,255 @@ export class PublicIntegrationsController {
}
}
}

// ===== Ghost-specific post management endpoints =====

@Put('/posts/:id/date')
async updatePostDate(
@GetOrgFromRequest() org: Organization,
@Param('id') id: string,
@Body() body: { date: string; action?: 'schedule' | 'update' }
) {
Sentry.metrics.count('public_api-request', 1);
const action = body.action || 'schedule';

// Validate date format
const newDate = new Date(body.date);
if (isNaN(newDate.getTime())) {
throw new HttpException({ msg: 'Invalid date format. Use ISO 8601.' }, 400);
}

// Update the publish date - PostsService.changeDate handles:
// - schedule: sets state to QUEUE (or DRAFT if it was draft), resets releaseId/releaseURL
// - update: just changes the date
const result = await this._postsService.changeDate(
org.id,
id,
body.date,
action
);

return { success: true, postId: id, publishDate: result.publishDate };
}

@Get('/integration/:id/post/:postId/status')
async getPostStatus(
@GetOrgFromRequest() org: Organization,
@Param('id') integrationId: string,
@Param('postId') providerPostId: string
) {
Sentry.metrics.count('public_api-request', 1);
const getIntegration = await this._integrationService.getIntegrationById(
org.id,
integrationId
);

if (!getIntegration) {
throw new HttpException({ msg: 'Integration not found' }, 404);
}

const provider = this._integrationManager.getSocialIntegration(
getIntegration.providerIdentifier
);

if (!provider) {
throw new HttpException({ msg: 'Integration provider not found' }, 404);
}

// Check if provider supports getStatus
// @ts-ignore
if (typeof provider.getStatus !== 'function') {
throw new HttpException(
{ msg: 'This integration does not support status queries' },
400
);
}

try {
// @ts-ignore
const status = await provider.getStatus(
getIntegration.token,
providerPostId,
getIntegration.internalId,
getIntegration
);
return { status };
} catch (err: any) {
if (err instanceof RefreshToken) {
const data = await this._refreshIntegrationService.refresh(
getIntegration
);
if (!data) {
throw new HttpException(
{ msg: 'Failed to refresh token' },
401
);
}
// Retry with refreshed token
// @ts-ignore
const status = await provider.getStatus(
data.accessToken,
providerPostId,
getIntegration.internalId,
getIntegration
);
return { status };
}
throw new HttpException(
{ msg: err.message || 'Failed to get post status' },
500
);
}
}

@Put('/integration/:id/post/:postId/status')
async changePostStatus(
@GetOrgFromRequest() org: Organization,
@Param('id') integrationId: string,
@Param('postId') providerPostId: string,
@Body() body: { status: 'draft' | 'published' | 'scheduled'; publishedAt?: string }
) {
Sentry.metrics.count('public_api-request', 1);
const getIntegration = await this._integrationService.getIntegrationById(
org.id,
integrationId
);

if (!getIntegration) {
throw new HttpException({ msg: 'Integration not found' }, 404);
}

const provider = this._integrationManager.getSocialIntegration(
getIntegration.providerIdentifier
);

if (!provider) {
throw new HttpException({ msg: 'Integration provider not found' }, 404);
}

// Check if provider supports changeStatus
// @ts-ignore
if (typeof provider.changeStatus !== 'function') {
throw new HttpException(
{ msg: 'This integration does not support status changes' },
400
);
}

const validStatuses = ['draft', 'published', 'scheduled'];
if (!validStatuses.includes(body.status)) {
throw new HttpException(
{ msg: `Invalid status. Must be one of: ${validStatuses.join(', ')}` },
400
);
}

try {
// @ts-ignore
const result = await provider.changeStatus(
getIntegration.token,
providerPostId,
body.status,
body.publishedAt,
getIntegration.internalId,
getIntegration
);
return { success: true, result };
} catch (err: any) {
if (err instanceof RefreshToken) {
const data = await this._refreshIntegrationService.refresh(
getIntegration
);
if (!data) {
throw new HttpException(
{ msg: 'Failed to refresh token' },
401
);
}
// @ts-ignore
const result = await provider.changeStatus(
data.accessToken,
providerPostId,
body.status,
body.publishedAt,
getIntegration.internalId,
getIntegration
);
return { success: true, result };
}
throw new HttpException(
{ msg: err.message || 'Failed to change post status' },
500
);
}
}

@Delete('/integration/:id/post/:postId')
async deleteProviderPost(
@GetOrgFromRequest() org: Organization,
@Param('id') integrationId: string,
@Param('postId') providerPostId: string
) {
Sentry.metrics.count('public_api-request', 1);
const getIntegration = await this._integrationService.getIntegrationById(
org.id,
integrationId
);

if (!getIntegration) {
throw new HttpException({ msg: 'Integration not found' }, 404);
}

const provider = this._integrationManager.getSocialIntegration(
getIntegration.providerIdentifier
);

if (!provider) {
throw new HttpException({ msg: 'Integration provider not found' }, 404);
}

// Check if provider supports delete
// @ts-ignore
if (typeof provider.delete !== 'function') {
throw new HttpException(
{ msg: 'This integration does not support post deletion' },
400
);
}

try {
// @ts-ignore
await provider.delete(
getIntegration.token,
providerPostId,
getIntegration.internalId,
getIntegration
);
return { success: true };
} catch (err: any) {
if (err instanceof RefreshToken) {
const data = await this._refreshIntegrationService.refresh(
getIntegration
);
if (!data) {
throw new HttpException(
{ msg: 'Failed to refresh token' },
401
);
}
// @ts-ignore
await provider.delete(
data.accessToken,
providerPostId,
getIntegration.internalId,
getIntegration
);
return { success: true };
}
throw new HttpException(
{ msg: err.message || 'Failed to delete post' },
500
);
}
}
}
Binary file added apps/frontend/public/icons/platforms/ghost.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use client';

import { FC, useCallback, useEffect, useState } from 'react';
import { ReactTags, Tag, TagSelected } from 'react-tag-autocomplete';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';

interface GhostAuthor {
value: string;
label: string;
}

export const GhostAuthors: FC<{
name: string;
label: string;
onChange: (event: {
target: {
value: string[];
name: string;
};
}) => void;
}> = (props) => {
const { name, label } = props;
const form = useSettings();
const { getValues } = useSettings();
const customFunc = useCustomProviderFunction();
const [suggestions, setSuggestions] = useState<GhostAuthor[]>([]);
const [tagValue, setTagValue] = useState<TagSelected[]>([]);
const [isInitialized, setIsInitialized] = useState(false);

const onDelete = useCallback(
(tagIndex: number) => {
const modify = tagValue.filter((_, i) => i !== tagIndex);
setTagValue(modify);
form.setValue(
name,
modify.map((t) => String(t.value))
);
},
[tagValue, name, form]
);

const onAddition = useCallback(
(newTag: Tag) => {
const modify = [...tagValue, newTag];
setTagValue(modify);
form.setValue(
name,
modify.map((t) => String(t.value))
);
},
[tagValue, name, form]
);

useEffect(() => {
// Fetch available authors from Ghost
customFunc.get('authors').then((data: GhostAuthor[]) => {
setSuggestions(data || []);

// Now that we have suggestions, restore existing values
const settings = getValues()[name];
if (settings && Array.isArray(settings)) {
// Map author IDs back to their display names using suggestions
const restoredTags = settings.map((id: string) => {
const authorFound = (data || []).find((a) => a.value === id);
return {
value: id,
label: authorFound ? authorFound.label : id,
};
});
setTagValue(restoredTags);
}
setIsInitialized(true);
});
}, [name, customFunc, getValues, form]);

if (!isInitialized) {
return (
<div>
<div className="text-[14px] mb-[6px]">{label}</div>
<div className="text-[12px] text-white/60">Loading authors...</div>
</div>
);
}

return (
<div>
<div className="text-[14px] mb-[6px]">{label}</div>
<ReactTags
suggestions={suggestions}
selected={tagValue}
onAdd={onAddition}
onDelete={onDelete}
placeholderText="Select an author..."
/>
</div>
);
};
Loading
Loading