add devto, medium and hashnode
This commit is contained in:
parent
004cf3c319
commit
45f0957e17
|
|
@ -44,7 +44,7 @@ const ActionControls = ({ store }: any) => {
|
|||
<Button
|
||||
loading={load}
|
||||
className="outline-none"
|
||||
innerClassName="invert outline-none"
|
||||
innerClassName="invert outline-none text-black"
|
||||
onClick={async () => {
|
||||
setLoad(true);
|
||||
const blob = await store.toBlob();
|
||||
|
|
@ -56,10 +56,10 @@ const ActionControls = ({ store }: any) => {
|
|||
body: formData,
|
||||
})
|
||||
).json();
|
||||
close.setMedia({
|
||||
close.setMedia([{
|
||||
id: data.id,
|
||||
path: data.path,
|
||||
});
|
||||
}]);
|
||||
close.close();
|
||||
}}
|
||||
>
|
||||
|
|
@ -69,7 +69,7 @@ const ActionControls = ({ store }: any) => {
|
|||
);
|
||||
};
|
||||
const Polonto: FC<{
|
||||
setMedia: (params: { id: string; path: string }) => void;
|
||||
setMedia: (params: { id: string; path: string }[]) => void;
|
||||
type?: 'image' | 'video';
|
||||
closeModal: () => void;
|
||||
width?: number;
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ export const Pagination: FC<{
|
|||
export const ShowMediaBoxModal: FC = () => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [callBack, setCallBack] =
|
||||
useState<(params: { id: string; path: string }) => void | undefined>();
|
||||
useState<(params: { id: string; path: string }[]) => void | undefined>();
|
||||
const closeModal = useCallback(() => {
|
||||
setShowModal(false);
|
||||
setCallBack(undefined);
|
||||
|
|
@ -155,7 +155,7 @@ export const showMediaBox = (
|
|||
};
|
||||
const CHUNK_SIZE = 1024 * 1024;
|
||||
export const MediaBox: FC<{
|
||||
setMedia: (params: { id: string; path: string }) => void;
|
||||
setMedia: (params: { id: string; path: string }[]) => void;
|
||||
type?: 'image' | 'video';
|
||||
closeModal: () => void;
|
||||
}> = (props) => {
|
||||
|
|
@ -767,12 +767,12 @@ export const MediaComponent: FC<{
|
|||
const showDesignModal = useCallback(() => {
|
||||
setMediaModal(true);
|
||||
}, [modal]);
|
||||
const changeMedia = useCallback((m: { path: string; id: string }) => {
|
||||
setCurrentMedia(m);
|
||||
const changeMedia = useCallback((m: { path: string; id: string }[]) => {
|
||||
setCurrentMedia(m[0]);
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: m,
|
||||
value: m[0],
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
|
@ -807,7 +807,7 @@ export const MediaComponent: FC<{
|
|||
<div className="my-[20px] cursor-pointer w-[200px] h-[200px] border-2 border-tableBorder">
|
||||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={mediaDirectory.set(currentMedia.path)}
|
||||
src={currentMedia.path}
|
||||
onClick={() => window.open(mediaDirectory.set(currentMedia.path))}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -90,8 +90,8 @@ export default withProvider({
|
|||
postComment: PostComment.COMMENT,
|
||||
minimumCharacters: [],
|
||||
SettingsComponent: DevtoSettings,
|
||||
CustomPreviewComponent: DevtoPreview,
|
||||
CustomPreviewComponent: undefined, // DevtoPreview,
|
||||
dto: DevToSettingsDto,
|
||||
checkValidity: undefined,
|
||||
maximumCharacters: undefined,
|
||||
maximumCharacters: 100000,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export const DevtoTags: FC<{
|
|||
}) => void;
|
||||
}> = (props) => {
|
||||
const { onChange, name, label } = props;
|
||||
const form = useSettings();
|
||||
const customFunc = useCustomProviderFunction();
|
||||
const [tags, setTags] = useState<any[]>([]);
|
||||
const { getValues } = useSettings();
|
||||
|
|
@ -24,14 +25,9 @@ export const DevtoTags: FC<{
|
|||
(tagIndex: number) => {
|
||||
const modify = tagValue.filter((_, i) => i !== tagIndex);
|
||||
setTagValue(modify);
|
||||
onChange({
|
||||
target: {
|
||||
value: modify,
|
||||
name,
|
||||
},
|
||||
});
|
||||
form.setValue(name, modify);
|
||||
},
|
||||
[tagValue]
|
||||
[tagValue, name, form]
|
||||
);
|
||||
const onAddition = useCallback(
|
||||
(newTag: any) => {
|
||||
|
|
@ -40,14 +36,9 @@ export const DevtoTags: FC<{
|
|||
}
|
||||
const modify = [...tagValue, newTag];
|
||||
setTagValue(modify);
|
||||
onChange({
|
||||
target: {
|
||||
value: modify,
|
||||
name,
|
||||
},
|
||||
});
|
||||
form.setValue(name, modify);
|
||||
},
|
||||
[tagValue]
|
||||
[tagValue, name, form]
|
||||
);
|
||||
useEffect(() => {
|
||||
customFunc.get('tags').then((data) => setTags(data));
|
||||
|
|
|
|||
|
|
@ -92,8 +92,8 @@ export default withProvider({
|
|||
postComment: PostComment.COMMENT,
|
||||
minimumCharacters: [],
|
||||
SettingsComponent: HashnodeSettings,
|
||||
CustomPreviewComponent: HashnodePreview,
|
||||
CustomPreviewComponent: undefined, // HashnodePreview,
|
||||
dto: HashnodeSettingsDto,
|
||||
checkValidity: undefined,
|
||||
maximumCharacters: undefined,
|
||||
maximumCharacters: 10000,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -67,11 +67,11 @@ const MediumSettings: FC = () => {
|
|||
);
|
||||
};
|
||||
export default withProvider({
|
||||
postComment: PostComment.COMMENT,
|
||||
postComment: PostComment.POST,
|
||||
minimumCharacters: [],
|
||||
SettingsComponent: MediumSettings,
|
||||
CustomPreviewComponent: MediumPreview,
|
||||
CustomPreviewComponent: undefined, //MediumPreview,
|
||||
dto: MediumSettingsDto,
|
||||
checkValidity: undefined,
|
||||
maximumCharacters: undefined,
|
||||
maximumCharacters: 100000,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ import { SlackDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-setting
|
|||
import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto';
|
||||
import { LinkedinDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/linkedin.dto';
|
||||
import { IsIn } from 'class-validator';
|
||||
import { MediumSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/medium.settings.dto';
|
||||
import { DevToSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto';
|
||||
import { HashnodeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/hashnode.settings.dto';
|
||||
|
||||
export type ProviderExtension<T extends string, M> = { __type: T } & M;
|
||||
export type AllProvidersSettings =
|
||||
|
|
@ -26,6 +29,9 @@ export type AllProvidersSettings =
|
|||
| ProviderExtension<'linkedin-page', LinkedinDto>
|
||||
| ProviderExtension<'instagram', InstagramDto>
|
||||
| ProviderExtension<'instagram-standalone', InstagramDto>
|
||||
| ProviderExtension<'medium', MediumSettingsDto>
|
||||
| ProviderExtension<'devto', DevToSettingsDto>
|
||||
| ProviderExtension<'hashnode', HashnodeSettingsDto>
|
||||
| ProviderExtension<'facebook', None>
|
||||
| ProviderExtension<'threads', None>
|
||||
| ProviderExtension<'mastodon', None>
|
||||
|
|
@ -52,6 +58,9 @@ export const allProviders = (setEmpty?: any) => {
|
|||
{ value: LinkedinDto, name: 'linkedin-page' },
|
||||
{ value: InstagramDto, name: 'instagram' },
|
||||
{ value: InstagramDto, name: 'instagram-standalone' },
|
||||
{ value: MediumSettingsDto, name: 'medium' },
|
||||
{ value: DevToSettingsDto, name: 'devto' },
|
||||
{ value: HashnodeSettingsDto, name: 'hashnode' },
|
||||
{ value: setEmpty, name: 'facebook' },
|
||||
{ value: setEmpty, name: 'threads' },
|
||||
{ value: setEmpty, name: 'mastodon' },
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export class DevToSettingsDto {
|
|||
|
||||
@IsArray()
|
||||
@ArrayMaxSize(4)
|
||||
@IsOptional()
|
||||
@Type(() => DevToTagsSettingsDto)
|
||||
@ValidateNested({ each: true })
|
||||
tags: DevToTagsSettingsDto[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,5 +53,7 @@ export class HashnodeSettingsDto {
|
|||
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@Type(() => HashnodeTagsSettings)
|
||||
@ValidateNested({ each: true })
|
||||
tags: HashnodeTagsSettings[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,104 +0,0 @@
|
|||
import { ArticleProvider } from '@gitroom/nestjs-libraries/integrations/article/article.integrations.interface';
|
||||
import { DevToSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto';
|
||||
|
||||
export class DevToProvider implements ArticleProvider {
|
||||
identifier = 'devto';
|
||||
name = 'Dev.to';
|
||||
async authenticate(token: string) {
|
||||
const { name, id, profile_image, username } = await (
|
||||
await fetch('https://dev.to/api/users/me', {
|
||||
headers: {
|
||||
'api-key': token,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
token,
|
||||
picture: profile_image,
|
||||
username,
|
||||
};
|
||||
}
|
||||
|
||||
async tags(token: string) {
|
||||
const tags = await (
|
||||
await fetch('https://dev.to/api/tags?per_page=1000&page=1', {
|
||||
headers: {
|
||||
'api-key': token,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
return tags.map((p: any) => ({ value: p.id, label: p.name }));
|
||||
}
|
||||
|
||||
async organizations(token: string) {
|
||||
const orgs = await (
|
||||
await fetch('https://dev.to/api/articles/me/all?per_page=1000', {
|
||||
headers: {
|
||||
'api-key': token,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
const allOrgs: string[] = [
|
||||
...new Set(
|
||||
orgs
|
||||
.flatMap((org: any) => org?.organization?.username)
|
||||
.filter((f: string) => f)
|
||||
),
|
||||
] as string[];
|
||||
const fullDetails = await Promise.all(
|
||||
allOrgs.map(async (org: string) => {
|
||||
return (
|
||||
await fetch(`https://dev.to/api/organizations/${org}`, {
|
||||
headers: {
|
||||
'api-key': token,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
})
|
||||
);
|
||||
|
||||
return fullDetails.map((org: any) => ({
|
||||
id: org.id,
|
||||
name: org.name,
|
||||
username: org.username,
|
||||
}));
|
||||
}
|
||||
|
||||
async post(token: string, content: string, settings: DevToSettingsDto) {
|
||||
const { id, url } = await (
|
||||
await fetch(`https://dev.to/api/articles`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
article: {
|
||||
title: settings.title,
|
||||
body_markdown: content,
|
||||
published: true,
|
||||
main_image: settings?.main_image?.path
|
||||
? `${
|
||||
settings?.main_image?.path.indexOf('http') === -1
|
||||
? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY}`
|
||||
: ``
|
||||
}${settings?.main_image?.path}`
|
||||
: undefined,
|
||||
tags: settings?.tags?.map((t) => t.label),
|
||||
organization_id: settings.organization,
|
||||
},
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'api-key': token,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
return {
|
||||
postId: String(id),
|
||||
releaseURL: url,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +1,90 @@
|
|||
import { ArticleProvider } from '@gitroom/nestjs-libraries/integrations/article/article.integrations.interface';
|
||||
import { MediumSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/medium.settings.dto';
|
||||
import {
|
||||
AuthTokenDetails,
|
||||
PostDetails,
|
||||
PostResponse,
|
||||
SocialProvider,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
|
||||
export class MediumProvider implements ArticleProvider {
|
||||
export class MediumProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'medium';
|
||||
name = 'Medium';
|
||||
isBetweenSteps = false;
|
||||
scopes = [] as string[];
|
||||
|
||||
async authenticate(token: string) {
|
||||
const {
|
||||
data: { name, id, imageUrl, username },
|
||||
} = await (
|
||||
await fetch('https://api.medium.com/v1/me', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
async generateAuthUrl() {
|
||||
const state = makeId(6);
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
token,
|
||||
picture: imageUrl,
|
||||
username,
|
||||
url: '',
|
||||
codeVerifier: makeId(10),
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
async publications(token: string) {
|
||||
const { id } = await this.authenticate(token);
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
return {
|
||||
refreshToken: '',
|
||||
expiresIn: 0,
|
||||
accessToken: '',
|
||||
id: '',
|
||||
name: '',
|
||||
picture: '',
|
||||
username: '',
|
||||
};
|
||||
}
|
||||
|
||||
async customFields() {
|
||||
return [
|
||||
{
|
||||
key: 'apiKey',
|
||||
label: 'API key',
|
||||
validation: `/^.{3,}$/`,
|
||||
type: 'password' as const,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async authenticate(params: {
|
||||
code: string;
|
||||
codeVerifier: string;
|
||||
refresh?: string;
|
||||
}) {
|
||||
const body = JSON.parse(Buffer.from(params.code, 'base64').toString());
|
||||
try {
|
||||
const {
|
||||
data: { name, id, imageUrl, username },
|
||||
} = await (
|
||||
await fetch('https://api.medium.com/v1/me', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${body.apiKey}`,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
return {
|
||||
refreshToken: '',
|
||||
expiresIn: dayjs().add(100, 'years').unix() - dayjs().unix(),
|
||||
accessToken: body.apiKey,
|
||||
id,
|
||||
name,
|
||||
picture: imageUrl,
|
||||
username,
|
||||
};
|
||||
} catch (err) {
|
||||
return 'Invalid credentials';
|
||||
}
|
||||
}
|
||||
|
||||
async publications(accessToken: string, _: any, id: string) {
|
||||
const { data } = await (
|
||||
await fetch(`https://api.medium.com/v1/users/${id}/publications`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
|
@ -38,8 +92,13 @@ export class MediumProvider implements ArticleProvider {
|
|||
return data;
|
||||
}
|
||||
|
||||
async post(token: string, content: string, settings: MediumSettingsDto) {
|
||||
const { id } = await this.authenticate(token);
|
||||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[],
|
||||
integration: Integration
|
||||
): Promise<PostResponse[]> {
|
||||
const { settings } = postDetails?.[0] || { settings: {} };
|
||||
const { data } = await (
|
||||
await fetch(
|
||||
settings?.publication
|
||||
|
|
@ -50,24 +109,28 @@ export class MediumProvider implements ArticleProvider {
|
|||
body: JSON.stringify({
|
||||
title: settings.title,
|
||||
contentFormat: 'markdown',
|
||||
content,
|
||||
content: postDetails?.[0].message,
|
||||
...(settings.canonical ? { canonicalUrl: settings.canonical } : {}),
|
||||
...(settings?.tags?.length
|
||||
? { tags: settings?.tags?.map((p) => p.value) }
|
||||
? { tags: settings?.tags?.map((p: any) => p.value) }
|
||||
: {}),
|
||||
publishStatus: settings?.publication ? 'draft' : 'public',
|
||||
}),
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
return {
|
||||
postId: data.id,
|
||||
releaseURL: data.url,
|
||||
};
|
||||
return [
|
||||
{
|
||||
id: postDetails?.[0].id,
|
||||
status: 'completed',
|
||||
postId: data.id,
|
||||
releaseURL: data.url,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import { XProvider } from '@gitroom/nestjs-libraries/integrations/social/x.provi
|
|||
import { SocialProvider } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { LinkedinProvider } from '@gitroom/nestjs-libraries/integrations/social/linkedin.provider';
|
||||
import { RedditProvider } from '@gitroom/nestjs-libraries/integrations/social/reddit.provider';
|
||||
import { DevToProvider } from '@gitroom/nestjs-libraries/integrations/article/dev.to.provider';
|
||||
import { HashnodeProvider } from '@gitroom/nestjs-libraries/integrations/article/hashnode.provider';
|
||||
import { DevToProvider } from '@gitroom/nestjs-libraries/integrations/social/dev.to.provider';
|
||||
import { HashnodeProvider } from '@gitroom/nestjs-libraries/integrations/social/hashnode.provider';
|
||||
import { MediumProvider } from '@gitroom/nestjs-libraries/integrations/article/medium.provider';
|
||||
import { ArticleProvider } from '@gitroom/nestjs-libraries/integrations/article/article.integrations.interface';
|
||||
import { FacebookProvider } from '@gitroom/nestjs-libraries/integrations/social/facebook.provider';
|
||||
|
|
@ -54,13 +54,13 @@ export const socialIntegrationList: SocialProvider[] = [
|
|||
new TelegramProvider(),
|
||||
new NostrProvider(),
|
||||
new VkProvider(),
|
||||
new MediumProvider(),
|
||||
new DevToProvider(),
|
||||
new HashnodeProvider(),
|
||||
// new MastodonCustomProvider(),
|
||||
];
|
||||
|
||||
const articleIntegrationList = [
|
||||
new DevToProvider(),
|
||||
new HashnodeProvider(),
|
||||
new MediumProvider(),
|
||||
const articleIntegrationList: ArticleProvider[] = [
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
import {
|
||||
AuthTokenDetails,
|
||||
PostDetails,
|
||||
PostResponse,
|
||||
SocialProvider,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
|
||||
export class DevToProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'devto';
|
||||
name = 'Dev.to';
|
||||
isBetweenSteps = false;
|
||||
scopes = [] as string[];
|
||||
|
||||
async generateAuthUrl() {
|
||||
const state = makeId(6);
|
||||
return {
|
||||
url: '',
|
||||
codeVerifier: makeId(10),
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
override handleErrors(body: string) {
|
||||
if (body.indexOf('Canonical url has already been taken') > -1) {
|
||||
return {
|
||||
type: 'bad-body' as const,
|
||||
value: 'Canonical URL already exists'
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
return {
|
||||
refreshToken: '',
|
||||
expiresIn: 0,
|
||||
accessToken: '',
|
||||
id: '',
|
||||
name: '',
|
||||
picture: '',
|
||||
username: '',
|
||||
};
|
||||
}
|
||||
|
||||
async customFields() {
|
||||
return [
|
||||
{
|
||||
key: 'apiKey',
|
||||
label: 'API key',
|
||||
validation: `/^.{3,}$/`,
|
||||
type: 'password' as const,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async authenticate(params: {
|
||||
code: string;
|
||||
codeVerifier: string;
|
||||
refresh?: string;
|
||||
}) {
|
||||
const body = JSON.parse(Buffer.from(params.code, 'base64').toString());
|
||||
try {
|
||||
const { name, id, profile_image, username } = await (
|
||||
await fetch('https://dev.to/api/users/me', {
|
||||
headers: {
|
||||
'api-key': body.apiKey,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
return {
|
||||
refreshToken: '',
|
||||
expiresIn: dayjs().add(100, 'years').unix() - dayjs().unix(),
|
||||
accessToken: body.apiKey,
|
||||
id,
|
||||
name,
|
||||
picture: profile_image,
|
||||
username,
|
||||
};
|
||||
} catch (err) {
|
||||
return 'Invalid credentials';
|
||||
}
|
||||
}
|
||||
|
||||
async tags(token: string) {
|
||||
const tags = await (
|
||||
await fetch('https://dev.to/api/tags?per_page=1000&page=1', {
|
||||
headers: {
|
||||
'api-key': token,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
return tags.map((p: any) => ({ value: p.id, label: p.name }));
|
||||
}
|
||||
|
||||
async organizations(token: string) {
|
||||
const orgs = await (
|
||||
await fetch('https://dev.to/api/articles/me/all?per_page=1000', {
|
||||
headers: {
|
||||
'api-key': token,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
const allOrgs: string[] = [
|
||||
...new Set(
|
||||
orgs
|
||||
.flatMap((org: any) => org?.organization?.username)
|
||||
.filter((f: string) => f)
|
||||
),
|
||||
] as string[];
|
||||
const fullDetails = await Promise.all(
|
||||
allOrgs.map(async (org: string) => {
|
||||
return (
|
||||
await fetch(`https://dev.to/api/organizations/${org}`, {
|
||||
headers: {
|
||||
'api-key': token,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
})
|
||||
);
|
||||
|
||||
return fullDetails.map((org: any) => ({
|
||||
id: org.id,
|
||||
name: org.name,
|
||||
username: org.username,
|
||||
}));
|
||||
}
|
||||
|
||||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[],
|
||||
integration: Integration
|
||||
): Promise<PostResponse[]> {
|
||||
const { settings } = postDetails?.[0] || { settings: {} };
|
||||
const { id: postId, url } = await (
|
||||
await this.fetch(`https://dev.to/api/articles`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
article: {
|
||||
title: settings.title,
|
||||
body_markdown: postDetails?.[0].message,
|
||||
published: true,
|
||||
...(settings?.main_image?.path
|
||||
? { main_image: settings?.main_image?.path }
|
||||
: {}),
|
||||
tags: settings?.tags?.map((t: any) => t.label),
|
||||
organization_id: settings.organization,
|
||||
...(settings.canonical
|
||||
? { canonical_url: settings.canonical }
|
||||
: {}),
|
||||
},
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'api-key': accessToken,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
return [
|
||||
{
|
||||
id: postDetails?.[0].id,
|
||||
status: 'completed',
|
||||
postId: String(postId),
|
||||
releaseURL: url,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,61 @@
|
|||
import { ArticleProvider } from '@gitroom/nestjs-libraries/integrations/article/article.integrations.interface';
|
||||
import {
|
||||
AuthTokenDetails,
|
||||
PostDetails,
|
||||
PostResponse,
|
||||
SocialProvider,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { tags } from '@gitroom/nestjs-libraries/integrations/article/hashnode.tags';
|
||||
import { jsonToGraphQLQuery } from 'json-to-graphql-query';
|
||||
import { HashnodeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/hashnode.settings.dto';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
|
||||
export class HashnodeProvider implements ArticleProvider {
|
||||
export class HashnodeProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'hashnode';
|
||||
name = 'Hashnode';
|
||||
async authenticate(token: string) {
|
||||
isBetweenSteps = false;
|
||||
scopes = [] as string[];
|
||||
|
||||
async generateAuthUrl() {
|
||||
const state = makeId(6);
|
||||
return {
|
||||
url: '',
|
||||
codeVerifier: makeId(10),
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
return {
|
||||
refreshToken: '',
|
||||
expiresIn: 0,
|
||||
accessToken: '',
|
||||
id: '',
|
||||
name: '',
|
||||
picture: '',
|
||||
username: '',
|
||||
};
|
||||
}
|
||||
|
||||
async customFields() {
|
||||
return [
|
||||
{
|
||||
key: 'apiKey',
|
||||
label: 'API key',
|
||||
validation: `/^.{3,}$/`,
|
||||
type: 'password' as const,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async authenticate(params: {
|
||||
code: string;
|
||||
codeVerifier: string;
|
||||
refresh?: string;
|
||||
}) {
|
||||
const body = JSON.parse(Buffer.from(params.code, 'base64').toString());
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
|
|
@ -17,7 +66,7 @@ export class HashnodeProvider implements ArticleProvider {
|
|||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `${token}`,
|
||||
Authorization: `${body.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
|
|
@ -35,20 +84,16 @@ export class HashnodeProvider implements ArticleProvider {
|
|||
).json();
|
||||
|
||||
return {
|
||||
refreshToken: '',
|
||||
expiresIn: dayjs().add(100, 'years').unix() - dayjs().unix(),
|
||||
accessToken: body.apiKey,
|
||||
id,
|
||||
name,
|
||||
token,
|
||||
picture: profilePicture,
|
||||
username,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
id: '',
|
||||
name: '',
|
||||
token: '',
|
||||
picture: '',
|
||||
username: '',
|
||||
};
|
||||
return 'Invalid credentials';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +101,7 @@ export class HashnodeProvider implements ArticleProvider {
|
|||
return tags.map((tag) => ({ value: tag.objectID, label: tag.name }));
|
||||
}
|
||||
|
||||
async publications(token: string) {
|
||||
async publications(accessToken: string) {
|
||||
const {
|
||||
data: {
|
||||
me: {
|
||||
|
|
@ -68,7 +113,7 @@ export class HashnodeProvider implements ArticleProvider {
|
|||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `${token}`,
|
||||
Authorization: `${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
|
|
@ -97,7 +142,13 @@ export class HashnodeProvider implements ArticleProvider {
|
|||
);
|
||||
}
|
||||
|
||||
async post(token: string, content: string, settings: HashnodeSettingsDto) {
|
||||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[],
|
||||
integration: Integration
|
||||
): Promise<PostResponse[]> {
|
||||
const { settings } = postDetails?.[0] || { settings: {} };
|
||||
const query = jsonToGraphQLQuery(
|
||||
{
|
||||
mutation: {
|
||||
|
|
@ -109,8 +160,8 @@ export class HashnodeProvider implements ArticleProvider {
|
|||
...(settings.canonical
|
||||
? { originalArticleURL: settings.canonical }
|
||||
: {}),
|
||||
contentMarkdown: content,
|
||||
tags: settings.tags.map((tag) => ({ id: tag.value })),
|
||||
contentMarkdown: postDetails?.[0].message,
|
||||
tags: settings.tags.map((tag: any) => ({ id: tag.value })),
|
||||
...(settings.subtitle ? { subtitle: settings.subtitle } : {}),
|
||||
...(settings.main_image
|
||||
? {
|
||||
|
|
@ -138,15 +189,15 @@ export class HashnodeProvider implements ArticleProvider {
|
|||
const {
|
||||
data: {
|
||||
publishPost: {
|
||||
post: { id, url },
|
||||
post: { id: postId, url },
|
||||
},
|
||||
},
|
||||
} = await (
|
||||
await fetch('https://gql.hashnode.com', {
|
||||
await this.fetch('https://gql.hashnode.com', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `${token}`,
|
||||
Authorization: `${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
|
|
@ -154,9 +205,13 @@ export class HashnodeProvider implements ArticleProvider {
|
|||
})
|
||||
).json();
|
||||
|
||||
return {
|
||||
postId: id,
|
||||
releaseURL: url,
|
||||
};
|
||||
return [
|
||||
{
|
||||
id: postDetails?.[0].id,
|
||||
status: 'completed',
|
||||
postId: postId,
|
||||
releaseURL: url,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -58,17 +58,17 @@ class CloudflareStorage implements IUploadProvider {
|
|||
}
|
||||
|
||||
async uploadSimple(path: string) {
|
||||
const loadImage = await axios.get(path, { responseType: 'arraybuffer' });
|
||||
const loadImage = await fetch(path);
|
||||
const contentType =
|
||||
loadImage?.headers?.['content-type'] ||
|
||||
loadImage?.headers?.['Content-Type'];
|
||||
loadImage?.headers?.get('content-type') ||
|
||||
loadImage?.headers?.get('Content-Type');
|
||||
const extension = getExtension(contentType)!;
|
||||
const id = makeId(10);
|
||||
|
||||
const params = {
|
||||
Bucket: this._bucketName,
|
||||
Key: `${id}.${extension}`,
|
||||
Body: loadImage.data,
|
||||
Body: Buffer.from(await loadImage.arrayBuffer()),
|
||||
ContentType: contentType,
|
||||
ChecksumMode: 'DISABLED',
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue