feat: media

This commit is contained in:
Nevo David 2025-07-17 19:25:20 +07:00
parent 3ef1bf8365
commit a9c6fc6f29
11 changed files with 375 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -30,6 +30,7 @@ import { IntegrationContext } from '@gitroom/frontend/components/launches/helper
import { Button } from '@gitroom/react/form/button';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { PostComment } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
import WordpressProvider from '@gitroom/frontend/components/new-launch/providers/wordpress/wordpress.provider';
export const Providers = [
{
@ -128,6 +129,10 @@ export const Providers = [
identifier: 'vk',
component: VkProvider,
},
{
identifier: 'wordpress',
component: WordpressProvider,
},
];
export const ShowAllProviders = forwardRef((props, ref) => {
const { date, current, global, selectedIntegrations, allIntegrations } =

View File

@ -0,0 +1,57 @@
'use client';
import { FC, useEffect, useState } from 'react';
import { Select } from '@gitroom/react/form/select';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
export const WordpressPostType: FC<{
name: string;
onChange: (event: {
target: {
value: string;
name: string;
};
}) => void;
}> = (props) => {
const { onChange, name } = props;
const t = useT();
const customFunc = useCustomProviderFunction();
const [orgs, setOrgs] = useState([]);
const { getValues } = useSettings();
const [currentMedia, setCurrentMedia] = useState<string | undefined>();
const onChangeInner = (event: {
target: {
value: string;
name: string;
};
}) => {
setCurrentMedia(event.target.value);
onChange(event);
};
useEffect(() => {
customFunc.get('postTypes').then((data) => setOrgs(data));
const settings = getValues()[props.name];
if (settings) {
setCurrentMedia(settings);
}
}, []);
if (!orgs.length) {
return null;
}
return (
<Select
name={name}
label="Select type"
onChange={onChangeInner}
value={currentMedia}
>
<option value="">{t('select_1', '--Select--')}</option>
{orgs.map((org: any) => (
<option key={org.id} value={org.id}>
{org.name}
</option>
))}
</Select>
);
};

View File

@ -0,0 +1,35 @@
'use client';
import { FC } from 'react';
import {
PostComment,
withProvider,
} from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
import { Input } from '@gitroom/react/form/input';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { WordpressPostType } from '@gitroom/frontend/components/new-launch/providers/wordpress/wordpress.post.type';
import { MediaComponent } from '@gitroom/frontend/components/media/media.component';
const WordpressSettings: FC = () => {
const form = useSettings();
return (
<>
<Input label="Title" {...form.register('title')} />
<WordpressPostType {...form.register('type')} />
<MediaComponent
label="Cover picture"
description="Add a cover picture"
{...form.register('main_image')}
/>
</>
);
};
export default withProvider({
postComment: PostComment.COMMENT,
minimumCharacters: [],
SettingsComponent: WordpressSettings,
CustomPreviewComponent: undefined, // WordpressPreview,
dto: undefined,
checkValidity: undefined,
maximumCharacters: 100000,
});

View File

@ -13,6 +13,7 @@ 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';
import { WordpressDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/wordpress.dto';
export type ProviderExtension<T extends string, M> = { __type: T } & M;
export type AllProvidersSettings =
@ -32,6 +33,7 @@ export type AllProvidersSettings =
| ProviderExtension<'medium', MediumSettingsDto>
| ProviderExtension<'devto', DevToSettingsDto>
| ProviderExtension<'hashnode', HashnodeSettingsDto>
| ProviderExtension<'wordpress', WordpressDto>
| ProviderExtension<'facebook', None>
| ProviderExtension<'threads', None>
| ProviderExtension<'mastodon', None>
@ -60,6 +62,7 @@ export const allProviders = (setEmpty?: any) => {
{ value: InstagramDto, name: 'instagram-standalone' },
{ value: MediumSettingsDto, name: 'medium' },
{ value: DevToSettingsDto, name: 'devto' },
{ value: WordpressDto, name: 'wordpress' },
{ value: HashnodeSettingsDto, name: 'hashnode' },
{ value: setEmpty, name: 'facebook' },
{ value: setEmpty, name: 'threads' },

View File

@ -0,0 +1,25 @@
import {
IsDefined,
IsOptional,
IsString,
MinLength,
ValidateNested,
} from 'class-validator';
import { MediaDto } from '@gitroom/nestjs-libraries/dtos/media/media.dto';
import { Type } from 'class-transformer';
export class WordpressDto {
@IsString()
@MinLength(2)
@IsDefined()
title: string;
@IsOptional()
@ValidateNested()
@Type(() => MediaDto)
main_image?: MediaDto;
@IsString()
@IsDefined()
type: string;
}

View File

@ -26,6 +26,7 @@ import { FarcasterProvider } from '@gitroom/nestjs-libraries/integrations/social
import { TelegramProvider } from '@gitroom/nestjs-libraries/integrations/social/telegram.provider';
import { NostrProvider } from '@gitroom/nestjs-libraries/integrations/social/nostr.provider';
import { VkProvider } from '@gitroom/nestjs-libraries/integrations/social/vk.provider';
import { WordpressProvider } from '@gitroom/nestjs-libraries/integrations/social/wordpress.provider';
export const socialIntegrationList: SocialProvider[] = [
new XProvider(),
@ -52,6 +53,7 @@ export const socialIntegrationList: SocialProvider[] = [
new MediumProvider(),
new DevToProvider(),
new HashnodeProvider(),
new WordpressProvider(),
// new MastodonCustomProvider(),
];

View File

@ -75,6 +75,7 @@ export abstract class SocialAbstract {
json = '{}';
}
console.log(json);
if (json.includes('rate_limit_exceeded') || json.includes('Rate limit')) {
await timer(5000);
console.log('rate limit trying again');

View File

@ -0,0 +1,237 @@
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';
import { WordpressDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/wordpress.dto';
import slugify from 'slugify';
import FormData from 'form-data';
import axios from 'axios';
export class WordpressProvider
extends SocialAbstract
implements SocialProvider
{
identifier = 'wordpress';
name = 'WordPress';
isBetweenSteps = false;
editor = 'html' as const;
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: 'domain',
label: 'Domain URL',
validation: `/^https?:\\/\\/(?:www\\.)?[\\w\\-]+(\\.[\\w\\-]+)+([\\/?#][^\\s]*)?$/`,
type: 'text' as const,
},
{
key: 'username',
label: 'Username',
validation: `/.+/`,
type: 'text' as const,
},
{
key: 'password',
label: 'Password',
validation: `/.+/`,
type: 'password' as const,
},
];
}
async authenticate(params: {
code: string;
codeVerifier: string;
refresh?: string;
}) {
const body = JSON.parse(Buffer.from(params.code, 'base64').toString()) as {
domain: string;
username: string;
password: string;
};
try {
const auth = Buffer.from(`${body.username}:${body.password}`).toString(
'base64'
);
const { id, name, avatar_urls } = await (
await fetch(`${body.domain}/wp-json/wp/v2/users/me`, {
headers: {
Authorization: `Basic ${auth}`,
},
})
).json();
const biggestImage = Object.entries(avatar_urls).reduce(
(all, current) => {
if (all > Number(current[0])) {
return all;
}
return Number(current[0]);
},
0
);
return {
refreshToken: '',
expiresIn: dayjs().add(100, 'years').unix() - dayjs().unix(),
accessToken: params.code,
id: body.domain + '_' + id,
name,
picture: avatar_urls?.[String(biggestImage)] || '',
username: body.username,
};
} catch (err) {
console.log(err);
return 'Invalid credentials';
}
}
async postTypes(token: string) {
const body = JSON.parse(Buffer.from(token, 'base64').toString()) as {
domain: string;
username: string;
password: string;
};
const auth = Buffer.from(`${body.username}:${body.password}`).toString(
'base64'
);
const postTypes = await (
await this.fetch(`${body.domain}/wp-json/wp/v2/types`, {
headers: {
Authorization: `Basic ${auth}`,
},
})
).json();
return Object.entries<any>(postTypes).reduce((all, [key, value]) => {
if (
key.indexOf('wp_') > -1 ||
key.indexOf('nav_') > -1 ||
key === 'attachment'
) {
return all;
}
all.push({
id: value.rest_base,
name: value.name,
});
return all;
}, []);
}
async post(
id: string,
accessToken: string,
postDetails: PostDetails<WordpressDto>[],
integration: Integration
): Promise<PostResponse[]> {
const body = JSON.parse(Buffer.from(accessToken, 'base64').toString()) as {
domain: string;
username: string;
password: string;
};
const auth = Buffer.from(`${body.username}:${body.password}`).toString(
'base64'
);
let mediaId = '';
if (postDetails?.[0]?.settings?.main_image?.path) {
console.log('Uploading image to WordPress', postDetails[0].settings.main_image.path);
const imageData = await axios.get(postDetails[0].settings.main_image.path, {
responseType: 'stream',
});
const form = new FormData();
form.append('file', imageData.data, {
filename: postDetails[0].settings.main_image.path.split('/').pop(), // You can customize the filename
contentType: imageData.headers['content-type'],
});
if (postDetails[0].settings.main_image?.alt) {
form.append('alt_text', postDetails[0].settings.main_image.alt);
}
const mediaResponse = await axios.post(
`${body.domain}/wp-json/wp/v2/media`,
{
method: 'POST',
body: form,
},
{
headers: {
Authorization: `Basic ${auth}`,
'Content-Type': 'application/json',
},
}
);
console.log('nevo', mediaResponse);
mediaId = mediaResponse.data.id;
}
const submit = await (
await this.fetch(
`https://cms.postiz.com/wp-json/wp/v2/${postDetails?.[0]?.settings?.type}`,
{
headers: {
Authorization: `Basic ${auth}`,
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify({
title: postDetails?.[0]?.settings?.title,
content: postDetails?.[0]?.message,
slug: slugify(postDetails?.[0]?.settings?.title, {
lower: true,
strict: true,
trim: true,
}),
status: 'publish',
...(mediaId ? { featured_media: mediaId } : {}),
}),
}
)
).json();
return [
{
id: postDetails?.[0].id,
status: 'completed',
postId: String(submit.id),
releaseURL: submit.link,
},
];
}
}

View File

@ -195,6 +195,7 @@
"sha256": "^0.2.0",
"sharp": "^0.33.4",
"simple-statistics": "^7.8.3",
"slugify": "^1.6.6",
"stripe": "^15.5.0",
"striptags": "^3.2.0",
"subtitle": "4.2.2-alpha.0",

View File

@ -462,6 +462,9 @@ importers:
simple-statistics:
specifier: ^7.8.3
version: 7.8.8
slugify:
specifier: ^1.6.6
version: 1.6.6
stripe:
specifier: ^15.5.0
version: 15.12.0
@ -13260,6 +13263,10 @@ packages:
slate@0.94.1:
resolution: {integrity: sha512-GH/yizXr1ceBoZ9P9uebIaHe3dC/g6Plpf9nlUwnvoyf6V1UOYrRwkabtOCd3ZfIGxomY4P7lfgLr7FPH8/BKA==}
slugify@1.6.6:
resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==}
engines: {node: '>=8.0.0'}
smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
@ -31592,6 +31599,8 @@ snapshots:
is-plain-object: 5.0.0
tiny-warning: 1.0.3
slugify@1.6.6: {}
smart-buffer@4.2.0: {}
snake-case@3.0.4: