Merge branch 'main' into add-authentik-sso
This commit is contained in:
commit
2a87ba1959
|
|
@ -78,6 +78,7 @@ OPENAI_API_KEY=""
|
|||
NEXT_PUBLIC_DISCORD_SUPPORT=""
|
||||
NEXT_PUBLIC_POLOTNO=""
|
||||
NOT_SECURED=false
|
||||
API_LIMIT=30 # The limit of the public API hour limit
|
||||
|
||||
# Payment settings
|
||||
FEE_AMOUNT=0.05
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ name: Build
|
|||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import { SignatureController } from '@gitroom/backend/api/routes/signature.contr
|
|||
import { AutopostController } from '@gitroom/backend/api/routes/autopost.controller';
|
||||
import { McpService } from '@gitroom/nestjs-libraries/mcp/mcp.service';
|
||||
import { McpController } from '@gitroom/backend/api/routes/mcp.controller';
|
||||
import { McpSettings } from '@gitroom/nestjs-libraries/mcp/mcp.settings';
|
||||
|
||||
const authenticatedController = [
|
||||
UsersController,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export class CopilotController {
|
|||
req?.body?.variables?.data?.metadata?.requestType ===
|
||||
'TextareaCompletion'
|
||||
? 'gpt-4o-mini'
|
||||
: 'gpt-4o-2024-08-06',
|
||||
: 'gpt-4.1',
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {
|
|||
Controller,
|
||||
Get,
|
||||
Header,
|
||||
HttpException,
|
||||
Param,
|
||||
Post,
|
||||
RawBodyRequest,
|
||||
|
|
@ -52,27 +53,34 @@ export class StripeController {
|
|||
);
|
||||
|
||||
// Maybe it comes from another stripe webhook
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if (event?.data?.object?.metadata?.service !== 'gitroom' && event.type !== 'invoice.payment_succeeded') {
|
||||
if (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
event?.data?.object?.metadata?.service !== 'gitroom' &&
|
||||
event.type !== 'invoice.payment_succeeded'
|
||||
) {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case 'invoice.payment_succeeded':
|
||||
return this._stripeService.paymentSucceeded(event);
|
||||
case 'checkout.session.completed':
|
||||
return this._stripeService.updateOrder(event);
|
||||
case 'account.updated':
|
||||
return this._stripeService.updateAccount(event);
|
||||
case 'customer.subscription.created':
|
||||
return this._stripeService.createSubscription(event);
|
||||
case 'customer.subscription.updated':
|
||||
return this._stripeService.updateSubscription(event);
|
||||
case 'customer.subscription.deleted':
|
||||
return this._stripeService.deleteSubscription(event);
|
||||
default:
|
||||
return { ok: true };
|
||||
try {
|
||||
switch (event.type) {
|
||||
case 'invoice.payment_succeeded':
|
||||
return this._stripeService.paymentSucceeded(event);
|
||||
case 'checkout.session.completed':
|
||||
return this._stripeService.updateOrder(event);
|
||||
case 'account.updated':
|
||||
return this._stripeService.updateAccount(event);
|
||||
case 'customer.subscription.created':
|
||||
return this._stripeService.createSubscription(event);
|
||||
case 'customer.subscription.updated':
|
||||
return this._stripeService.updateSubscription(event);
|
||||
case 'customer.subscription.deleted':
|
||||
return this._stripeService.deleteSubscription(event);
|
||||
default:
|
||||
return { ok: true };
|
||||
}
|
||||
} catch (e) {
|
||||
throw new HttpException(e, 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import { McpModule } from '@gitroom/backend/mcp/mcp.module';
|
|||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: 3600000,
|
||||
limit: 30,
|
||||
limit: process.env.API_LIMIT ? Number(process.env.API_LIMIT) : 30,
|
||||
},
|
||||
]),
|
||||
],
|
||||
|
|
@ -40,8 +40,15 @@ import { McpModule } from '@gitroom/backend/mcp/mcp.module';
|
|||
useClass: PoliciesGuard,
|
||||
},
|
||||
],
|
||||
get exports() {
|
||||
return [...this.imports];
|
||||
},
|
||||
exports: [
|
||||
BullMqModule,
|
||||
DatabaseModule,
|
||||
ApiModule,
|
||||
PluginModule,
|
||||
PublicApiModule,
|
||||
AgentModule,
|
||||
McpModule,
|
||||
ThrottlerModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -22,6 +22,14 @@ export default async function Page({
|
|||
};
|
||||
}
|
||||
|
||||
if (provider === 'vk') {
|
||||
searchParams = {
|
||||
...searchParams,
|
||||
state: searchParams.state || '',
|
||||
code: searchParams.code + '&&&&' + searchParams.device_id
|
||||
};
|
||||
}
|
||||
|
||||
const data = await internalFetch(`/integrations/social/${provider}/connect`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(searchParams),
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export default async function AuthLayout({
|
|||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="dark !bg-black">
|
||||
<ReturnUrlComponent />
|
||||
<div className="absolute left-0 top-0 z-[0] h-[100vh] w-[100vw] overflow-hidden bg-loginBg bg-contain bg-no-repeat bg-left-top" />
|
||||
<div className="relative z-[1] px-3 lg:pr-[100px] xs:mt-[70px] flex justify-center lg:justify-end items-center h-[100vh] w-[100vw] overflow-hidden">
|
||||
|
|
@ -75,6 +75,6 @@ export default async function AuthLayout({
|
|||
<div className="absolute right-0 bg-gradient-to-l from-customColor9 h-[1px] -translate-y-[22px] w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -503,7 +503,7 @@ export const MainBillingComponent: FC<{
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
<PurchaseCrypto />
|
||||
{!subscription?.id && (<PurchaseCrypto />)}
|
||||
{!!subscription?.id && (
|
||||
<div className="flex justify-center mt-[20px] gap-[10px]">
|
||||
<Button onClick={updatePayment}>Update Payment Method / Invoices History</Button>
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ import { DropFiles } from '@gitroom/frontend/components/layout/drop.files';
|
|||
import { SelectCustomer } from '@gitroom/frontend/components/launches/select.customer';
|
||||
import { TagsComponent } from './tags.component';
|
||||
import { RepeatComponent } from '@gitroom/frontend/components/launches/repeat.component';
|
||||
import { MergePost } from '@gitroom/frontend/components/launches/merge.post';
|
||||
|
||||
function countCharacters(text: string, type: string): number {
|
||||
if (type !== 'x') {
|
||||
|
|
@ -136,6 +137,27 @@ export const AddEditModal: FC<{
|
|||
// hook to test if the top editor should be hidden
|
||||
const showHide = useHideTopEditor();
|
||||
|
||||
// merge all posts and delete all the comments
|
||||
const merge = useCallback(() => {
|
||||
setValue(
|
||||
value.reduce(
|
||||
(all, current) => {
|
||||
all[0].content = all[0].content + current.content + '\n';
|
||||
all[0].image = [...all[0].image, ...(current.image || [])];
|
||||
|
||||
return all;
|
||||
},
|
||||
[
|
||||
{
|
||||
content: '',
|
||||
id: value[0].id,
|
||||
image: [] as { id: string; path: string }[],
|
||||
},
|
||||
]
|
||||
)
|
||||
);
|
||||
}, [value]);
|
||||
|
||||
const [showError, setShowError] = useState(false);
|
||||
|
||||
// are we in edit mode?
|
||||
|
|
@ -536,7 +558,14 @@ export const AddEditModal: FC<{
|
|||
title: 'AI Content Assistant',
|
||||
}}
|
||||
className="!z-[499]"
|
||||
instructions="You are an assistant that help the user to schedule their social media posts, everytime somebody write something, try to use a function call, if not prompt the user that the request is invalid and you are here to assists with social media posts"
|
||||
instructions={`
|
||||
You are an assistant that help the user to schedule their social media posts,
|
||||
Here are the things you can do:
|
||||
- Add a new comment / post to the list of posts
|
||||
- Delete a comment / post from the list of posts
|
||||
- Add content to the comment / post
|
||||
- Activate or deactivate the comment / post
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
|
|
@ -651,16 +680,8 @@ export const AddEditModal: FC<{
|
|||
<Editor
|
||||
order={index}
|
||||
height={value.length > 1 ? 150 : 250}
|
||||
commands={
|
||||
[
|
||||
// ...commands
|
||||
// .getCommands()
|
||||
// .filter((f) => f.name === 'image'),
|
||||
// newImage,
|
||||
// postSelector(dateState),
|
||||
]
|
||||
}
|
||||
value={p.content}
|
||||
totalPosts={value.length}
|
||||
preview="edit"
|
||||
onPaste={pasteImages(index, p.image || [])}
|
||||
// @ts-ignore
|
||||
|
|
@ -731,6 +752,11 @@ export const AddEditModal: FC<{
|
|||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
{value.length > 1 && (
|
||||
<div>
|
||||
<MergePost merge={merge} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export const AddPostButton: FC<{ onClick: () => void; num: number }> = (
|
|||
|
||||
useCopilotAction({
|
||||
name: 'addPost_' + num,
|
||||
description: 'Add a post after the post number ' + (num + 1),
|
||||
description: 'Add a post after the post number ' + num,
|
||||
handler: () => {
|
||||
onClick();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,13 +15,19 @@ import { SignatureBox } from '@gitroom/frontend/components/signature';
|
|||
|
||||
export const Editor = forwardRef<
|
||||
RefMDEditor,
|
||||
MDEditorProps & { order: number; currentWatching: string; isGlobal: boolean }
|
||||
MDEditorProps & {
|
||||
order: number;
|
||||
currentWatching: string;
|
||||
isGlobal: boolean;
|
||||
totalPosts: number;
|
||||
}
|
||||
>(
|
||||
(
|
||||
props: MDEditorProps & {
|
||||
order: number;
|
||||
currentWatching: string;
|
||||
isGlobal: boolean;
|
||||
totalPosts: number;
|
||||
},
|
||||
ref: React.ForwardedRef<RefMDEditor>
|
||||
) => {
|
||||
|
|
@ -32,12 +38,16 @@ export const Editor = forwardRef<
|
|||
|
||||
useCopilotReadable({
|
||||
description: 'Content of the post number ' + (props.order + 1),
|
||||
value: props.value,
|
||||
value: JSON.stringify({
|
||||
content: props.value,
|
||||
order: props.order,
|
||||
allowAddContent: props?.value?.length === 0,
|
||||
}),
|
||||
});
|
||||
|
||||
useCopilotAction({
|
||||
name: 'editPost_' + props.order,
|
||||
description: `Edit the content of post number ${props.order + 1}`,
|
||||
description: `Edit the content of post number ${props.order}`,
|
||||
parameters: [
|
||||
{
|
||||
name: 'content',
|
||||
|
|
@ -93,7 +103,8 @@ export const Editor = forwardRef<
|
|||
disableBranding={true}
|
||||
ref={newRef}
|
||||
className={clsx(
|
||||
'!min-h-40 !max-h-80 p-2 overflow-x-hidden scrollbar scrollbar-thumb-[#612AD5] bg-customColor2 outline-none'
|
||||
'!min-h-40 p-2 overflow-x-hidden scrollbar scrollbar-thumb-[#612AD5] bg-customColor2 outline-none',
|
||||
props.totalPosts > 1 && '!max-h-80'
|
||||
)}
|
||||
value={props.value}
|
||||
onChange={(e) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import { Button } from '@gitroom/react/form/button';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { FC, useCallback } from 'react';
|
||||
|
||||
export const MergePost: FC<{merge: () => void}> = (props) => {
|
||||
const { merge } = props;
|
||||
|
||||
const notReversible = useCallback(async () => {
|
||||
if (await deleteDialog('Are you sure you want to merge all comments into one post? This action is not reversible.', 'Yes')) {
|
||||
merge();
|
||||
}
|
||||
}, [merge]);
|
||||
|
||||
return (
|
||||
<Button className="!h-[30px] !text-sm !bg-red-800" onClick={notReversible}>
|
||||
Merge comments into one post
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
|
@ -43,6 +43,7 @@ import { DropFiles } from '@gitroom/frontend/components/layout/drop.files';
|
|||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import useSWR from 'swr';
|
||||
import { InternalChannels } from '@gitroom/frontend/components/launches/internal.channels';
|
||||
import { MergePost } from '@gitroom/frontend/components/launches/merge.post';
|
||||
|
||||
// Simple component to change back to settings on after changing tab
|
||||
export const SetTab: FC<{ changeTab: () => void }> = (props) => {
|
||||
|
|
@ -176,6 +177,26 @@ export const withProvider = function <T extends object>(
|
|||
[InPlaceValue]
|
||||
);
|
||||
|
||||
const merge = useCallback(() => {
|
||||
setInPlaceValue(
|
||||
InPlaceValue.reduce(
|
||||
(all, current) => {
|
||||
all[0].content = all[0].content + current.content + '\n';
|
||||
all[0].image = [...all[0].image, ...(current.image || [])];
|
||||
|
||||
return all;
|
||||
},
|
||||
[
|
||||
{
|
||||
content: '',
|
||||
id: InPlaceValue[0].id,
|
||||
image: [] as { id: string; path: string }[],
|
||||
},
|
||||
]
|
||||
)
|
||||
);
|
||||
}, [InPlaceValue]);
|
||||
|
||||
const changeImage = useCallback(
|
||||
(index: number) =>
|
||||
(newValue: {
|
||||
|
|
@ -464,6 +485,7 @@ export const withProvider = function <T extends object>(
|
|||
order={index}
|
||||
height={InPlaceValue.length > 1 ? 200 : 250}
|
||||
value={val.content}
|
||||
totalPosts={InPlaceValue.length}
|
||||
commands={[
|
||||
// ...commands
|
||||
// .getCommands()
|
||||
|
|
@ -545,6 +567,11 @@ export const withProvider = function <T extends object>(
|
|||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
{InPlaceValue.length > 1 && (
|
||||
<div>
|
||||
<MergePost merge={merge} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</EditorWrapper>,
|
||||
document.querySelector('#renderEditor')!
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import LemmyProvider from '@gitroom/frontend/components/launches/providers/lemmy
|
|||
import WarpcastProvider from '@gitroom/frontend/components/launches/providers/warpcast/warpcast.provider';
|
||||
import TelegramProvider from '@gitroom/frontend/components/launches/providers/telegram/telegram.provider';
|
||||
import NostrProvider from '@gitroom/frontend/components/launches/providers/nostr/nostr.provider';
|
||||
import VkProvider from '@gitroom/frontend/components/launches/providers/vk/vk.provider';
|
||||
|
||||
export const Providers = [
|
||||
{identifier: 'devto', component: DevtoProvider},
|
||||
|
|
@ -46,6 +47,7 @@ export const Providers = [
|
|||
{identifier: 'wrapcast', component: WarpcastProvider},
|
||||
{identifier: 'telegram', component: TelegramProvider},
|
||||
{identifier: 'nostr', component: NostrProvider},
|
||||
{identifier: 'vk', component: VkProvider},
|
||||
];
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
|
||||
|
||||
export default withProvider(
|
||||
null,
|
||||
undefined,
|
||||
undefined,
|
||||
async (posts) => {
|
||||
return true;
|
||||
},
|
||||
2048
|
||||
);
|
||||
|
|
@ -21,13 +21,13 @@ const toolNode = new ToolNode(tools);
|
|||
|
||||
const model = new ChatOpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY || 'sk-proj-',
|
||||
model: 'gpt-4o-2024-08-06',
|
||||
model: 'gpt-4.1',
|
||||
temperature: 0.7,
|
||||
});
|
||||
|
||||
const dalle = new DallEAPIWrapper({
|
||||
apiKey: process.env.OPENAI_API_KEY || 'sk-proj-',
|
||||
model: 'dall-e-3',
|
||||
model: 'gpt-image-1',
|
||||
});
|
||||
|
||||
interface WorkflowChannelsState {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export class BullMqServer extends Server implements CustomTransportStrategy {
|
|||
});
|
||||
},
|
||||
{
|
||||
concurrency: 10,
|
||||
connection: ioRedis,
|
||||
removeOnComplete: {
|
||||
count: 0,
|
||||
|
|
|
|||
|
|
@ -33,13 +33,13 @@ interface WorkflowChannelsState {
|
|||
|
||||
const model = new ChatOpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY || 'sk-proj-',
|
||||
model: 'gpt-4o-2024-08-06',
|
||||
model: 'gpt-4.1',
|
||||
temperature: 0.7,
|
||||
});
|
||||
|
||||
const dalle = new DallEAPIWrapper({
|
||||
apiKey: process.env.OPENAI_API_KEY || 'sk-proj-',
|
||||
model: 'dall-e-3',
|
||||
model: 'gpt-image-1',
|
||||
});
|
||||
|
||||
const generateContent = z.object({
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { InstagramStandaloneProvider } from '@gitroom/nestjs-libraries/integrati
|
|||
import { FarcasterProvider } from '@gitroom/nestjs-libraries/integrations/social/farcaster.provider';
|
||||
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';
|
||||
|
||||
export const socialIntegrationList: SocialProvider[] = [
|
||||
new XProvider(),
|
||||
|
|
@ -48,6 +49,7 @@ export const socialIntegrationList: SocialProvider[] = [
|
|||
new FarcasterProvider(),
|
||||
new TelegramProvider(),
|
||||
new NostrProvider(),
|
||||
new VkProvider(),
|
||||
// new MastodonCustomProvider(),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -413,7 +413,7 @@ export class LinkedinPageProvider
|
|||
|
||||
if (totalLikes >= +fields.likesAmount) {
|
||||
await timer(2000);
|
||||
await this.fetch(`https://api.linkedin.com/v2/posts`, {
|
||||
await this.fetch(`https://api.linkedin.com/rest/posts`, {
|
||||
body: JSON.stringify({
|
||||
author: `urn:li:organization:${integration.internalId}`,
|
||||
commentary: '',
|
||||
|
|
@ -433,7 +433,7 @@ export class LinkedinPageProvider
|
|||
headers: {
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'Content-Type': 'application/json',
|
||||
'LinkedIn-Version': '202501',
|
||||
'LinkedIn-Version': '202504',
|
||||
Authorization: `Bearer ${integration.token}`,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -472,7 +472,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
isPersonal = true
|
||||
) {
|
||||
try {
|
||||
await this.fetch(`https://api.linkedin.com/v2/posts`, {
|
||||
await this.fetch(`https://api.linkedin.com/rest/posts`, {
|
||||
body: JSON.stringify({
|
||||
author:
|
||||
(isPersonal ? 'urn:li:person:' : `urn:li:organization:`) +
|
||||
|
|
@ -494,7 +494,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
headers: {
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'Content-Type': 'application/json',
|
||||
'LinkedIn-Version': '202501',
|
||||
'LinkedIn-Version': '202504',
|
||||
Authorization: `Bearer ${integration.token}`,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -145,178 +145,240 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
};
|
||||
}
|
||||
|
||||
private async createSingleMediaContent(
|
||||
userId: string,
|
||||
accessToken: string,
|
||||
media: { url: string },
|
||||
message: string,
|
||||
isCarouselItem = false,
|
||||
replyToId?: string
|
||||
): Promise<string> {
|
||||
const mediaType = media.url.indexOf('.mp4') > -1 ? 'video_url' : 'image_url';
|
||||
const mediaParams = new URLSearchParams({
|
||||
...(mediaType === 'video_url' ? { video_url: media.url } : {}),
|
||||
...(mediaType === 'image_url' ? { image_url: media.url } : {}),
|
||||
...(isCarouselItem ? { is_carousel_item: 'true' } : {}),
|
||||
...(replyToId ? { reply_to_id: replyToId } : {}),
|
||||
media_type: mediaType === 'video_url' ? 'VIDEO' : 'IMAGE',
|
||||
text: message,
|
||||
access_token: accessToken,
|
||||
});
|
||||
|
||||
console.log(mediaParams);
|
||||
|
||||
const { id: mediaId } = await (
|
||||
await this.fetch(
|
||||
`https://graph.threads.net/v1.0/${userId}/threads?${mediaParams.toString()}`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
return mediaId;
|
||||
}
|
||||
|
||||
private async createCarouselContent(
|
||||
userId: string,
|
||||
accessToken: string,
|
||||
media: { url: string }[],
|
||||
message: string,
|
||||
replyToId?: string
|
||||
): Promise<string> {
|
||||
// Create each media item
|
||||
const mediaIds = [];
|
||||
for (const mediaItem of media) {
|
||||
const mediaId = await this.createSingleMediaContent(
|
||||
userId,
|
||||
accessToken,
|
||||
mediaItem,
|
||||
message,
|
||||
true
|
||||
);
|
||||
mediaIds.push(mediaId);
|
||||
}
|
||||
|
||||
// Wait for all media to be loaded
|
||||
await Promise.all(
|
||||
mediaIds.map((id: string) => this.checkLoaded(id, accessToken))
|
||||
);
|
||||
|
||||
// Create carousel container
|
||||
const params = new URLSearchParams({
|
||||
text: message,
|
||||
media_type: 'CAROUSEL',
|
||||
children: mediaIds.join(','),
|
||||
...(replyToId ? { reply_to_id: replyToId } : {}),
|
||||
access_token: accessToken,
|
||||
});
|
||||
|
||||
const { id: containerId } = await (
|
||||
await this.fetch(
|
||||
`https://graph.threads.net/v1.0/${userId}/threads?${params.toString()}`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
return containerId;
|
||||
}
|
||||
|
||||
private async createTextContent(
|
||||
userId: string,
|
||||
accessToken: string,
|
||||
message: string,
|
||||
replyToId?: string
|
||||
): Promise<string> {
|
||||
const form = new FormData();
|
||||
form.append('media_type', 'TEXT');
|
||||
form.append('text', message);
|
||||
form.append('access_token', accessToken);
|
||||
|
||||
if (replyToId) {
|
||||
form.append('reply_to_id', replyToId);
|
||||
}
|
||||
|
||||
const { id: contentId } = await (
|
||||
await this.fetch(
|
||||
`https://graph.threads.net/v1.0/${userId}/threads`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: form,
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
return contentId;
|
||||
}
|
||||
|
||||
private async publishThread(
|
||||
userId: string,
|
||||
accessToken: string,
|
||||
creationId: string
|
||||
): Promise<{ threadId: string; permalink: string }> {
|
||||
await this.checkLoaded(creationId, accessToken);
|
||||
|
||||
const { id: threadId } = await (
|
||||
await this.fetch(
|
||||
`https://graph.threads.net/v1.0/${userId}/threads_publish?creation_id=${creationId}&access_token=${accessToken}`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
const { permalink } = await (
|
||||
await this.fetch(
|
||||
`https://graph.threads.net/v1.0/${threadId}?fields=id,permalink&access_token=${accessToken}`
|
||||
)
|
||||
).json();
|
||||
|
||||
return { threadId, permalink };
|
||||
}
|
||||
|
||||
private async createThreadContent(
|
||||
userId: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails,
|
||||
replyToId?: string
|
||||
): Promise<string> {
|
||||
// Handle content creation based on media type
|
||||
if (!postDetails.media || postDetails.media.length === 0) {
|
||||
// Text-only content
|
||||
return await this.createTextContent(
|
||||
userId,
|
||||
accessToken,
|
||||
postDetails.message,
|
||||
replyToId
|
||||
);
|
||||
} else if (postDetails.media.length === 1) {
|
||||
// Single media content
|
||||
return await this.createSingleMediaContent(
|
||||
userId,
|
||||
accessToken,
|
||||
postDetails.media[0],
|
||||
postDetails.message,
|
||||
false,
|
||||
replyToId
|
||||
);
|
||||
} else {
|
||||
// Carousel content
|
||||
return await this.createCarouselContent(
|
||||
userId,
|
||||
accessToken,
|
||||
postDetails.media,
|
||||
postDetails.message,
|
||||
replyToId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async post(
|
||||
id: string,
|
||||
userId: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[]
|
||||
): Promise<PostResponse[]> {
|
||||
const [firstPost, ...theRest] = postDetails;
|
||||
if (!postDetails.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let globalThread = '';
|
||||
let link = '';
|
||||
if (firstPost?.media?.length! <= 1) {
|
||||
const type = !firstPost?.media?.[0]?.url
|
||||
? undefined
|
||||
: firstPost?.media![0].url.indexOf('.mp4') > -1
|
||||
? 'video_url'
|
||||
: 'image_url';
|
||||
|
||||
const media = new URLSearchParams({
|
||||
...(type === 'video_url'
|
||||
? { video_url: firstPost?.media![0].url }
|
||||
: {}),
|
||||
...(type === 'image_url'
|
||||
? { image_url: firstPost?.media![0].url }
|
||||
: {}),
|
||||
media_type:
|
||||
type === 'video_url'
|
||||
? 'VIDEO'
|
||||
: type === 'image_url'
|
||||
? 'IMAGE'
|
||||
: 'TEXT',
|
||||
text: firstPost?.message,
|
||||
access_token: accessToken,
|
||||
});
|
||||
|
||||
const { id: containerId } = await (
|
||||
await this.fetch(
|
||||
`https://graph.threads.net/v1.0/${id}/threads?${media.toString()}`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
await this.checkLoaded(containerId, accessToken);
|
||||
|
||||
const { id: threadId } = await (
|
||||
await this.fetch(
|
||||
`https://graph.threads.net/v1.0/${id}/threads_publish?creation_id=${containerId}&access_token=${accessToken}`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
const { permalink, ...all } = await (
|
||||
await this.fetch(
|
||||
`https://graph.threads.net/v1.0/${threadId}?fields=id,permalink&access_token=${accessToken}`
|
||||
)
|
||||
).json();
|
||||
|
||||
globalThread = threadId;
|
||||
link = permalink;
|
||||
} else {
|
||||
const medias = [];
|
||||
for (const mediaLoad of firstPost.media!) {
|
||||
const type =
|
||||
mediaLoad.url.indexOf('.mp4') > -1 ? 'video_url' : 'image_url';
|
||||
|
||||
const media = new URLSearchParams({
|
||||
...(type === 'video_url' ? { video_url: mediaLoad.url } : {}),
|
||||
...(type === 'image_url' ? { image_url: mediaLoad.url } : {}),
|
||||
is_carousel_item: 'true',
|
||||
media_type:
|
||||
type === 'video_url'
|
||||
? 'VIDEO'
|
||||
: type === 'image_url'
|
||||
? 'IMAGE'
|
||||
: 'TEXT',
|
||||
text: firstPost?.message,
|
||||
access_token: accessToken,
|
||||
});
|
||||
|
||||
const { id: mediaId } = await (
|
||||
await this.fetch(
|
||||
`https://graph.threads.net/v1.0/${id}/threads?${media.toString()}`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
medias.push(mediaId);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
medias.map((p: string) => this.checkLoaded(p, accessToken))
|
||||
const [firstPost, ...replies] = postDetails;
|
||||
|
||||
// Create the initial thread
|
||||
const initialContentId = await this.createThreadContent(
|
||||
userId,
|
||||
accessToken,
|
||||
firstPost
|
||||
);
|
||||
|
||||
// Publish the thread
|
||||
const { threadId, permalink } = await this.publishThread(
|
||||
userId,
|
||||
accessToken,
|
||||
initialContentId
|
||||
);
|
||||
|
||||
// Track the responses
|
||||
const responses: PostResponse[] = [{
|
||||
id: firstPost.id,
|
||||
postId: threadId,
|
||||
status: 'success',
|
||||
releaseURL: permalink,
|
||||
}];
|
||||
|
||||
// Handle replies if any
|
||||
let lastReplyId = threadId;
|
||||
|
||||
for (const reply of replies) {
|
||||
// Create reply content
|
||||
const replyContentId = await this.createThreadContent(
|
||||
userId,
|
||||
accessToken,
|
||||
reply,
|
||||
lastReplyId
|
||||
);
|
||||
|
||||
const { id: containerId } = await (
|
||||
await this.fetch(
|
||||
`https://graph.threads.net/v1.0/${id}/threads?text=${
|
||||
firstPost?.message
|
||||
}&media_type=CAROUSEL&children=${medias.join(
|
||||
','
|
||||
)}&access_token=${accessToken}`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
await this.checkLoaded(containerId, accessToken);
|
||||
|
||||
const { id: threadId } = await (
|
||||
await this.fetch(
|
||||
`https://graph.threads.net/v1.0/${id}/threads_publish?creation_id=${containerId}&access_token=${accessToken}`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
const { permalink } = await (
|
||||
await this.fetch(
|
||||
`https://graph.threads.net/v1.0/${threadId}?fields=id,permalink&access_token=${accessToken}`
|
||||
)
|
||||
).json();
|
||||
|
||||
globalThread = threadId;
|
||||
link = permalink;
|
||||
}
|
||||
|
||||
let lastId = globalThread;
|
||||
for (const post of theRest) {
|
||||
const form = new FormData();
|
||||
form.append('media_type', 'TEXT');
|
||||
form.append('text', post.message);
|
||||
form.append('reply_to_id', lastId);
|
||||
form.append('access_token', accessToken);
|
||||
|
||||
const { id: replyId } = await (
|
||||
await this.fetch('https://graph.threads.net/v1.0/me/threads', {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
})
|
||||
).json();
|
||||
|
||||
const { id: threadMediaId } = await (
|
||||
await this.fetch(
|
||||
`https://graph.threads.net/v1.0/${id}/threads_publish?creation_id=${replyId}&access_token=${accessToken}`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
lastId = threadMediaId;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: firstPost.id,
|
||||
postId: String(globalThread),
|
||||
|
||||
// Publish the reply
|
||||
const { threadId: replyThreadId } = await this.publishThread(
|
||||
userId,
|
||||
accessToken,
|
||||
replyContentId
|
||||
);
|
||||
|
||||
// Update the last reply ID for chaining
|
||||
lastReplyId = replyThreadId;
|
||||
|
||||
// Add to responses
|
||||
responses.push({
|
||||
id: reply.id,
|
||||
postId: threadId, // Main thread ID
|
||||
status: 'success',
|
||||
releaseURL: link,
|
||||
},
|
||||
...theRest.map((p) => ({
|
||||
id: p.id,
|
||||
postId: String(globalThread),
|
||||
status: 'success',
|
||||
releaseURL: link,
|
||||
})),
|
||||
];
|
||||
releaseURL: permalink, // Main thread URL
|
||||
});
|
||||
}
|
||||
|
||||
return responses;
|
||||
}
|
||||
|
||||
async analytics(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,274 @@
|
|||
import {
|
||||
AuthTokenDetails,
|
||||
PostDetails,
|
||||
PostResponse,
|
||||
SocialProvider,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import dayjs from 'dayjs';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { createHash, randomBytes } from 'crypto';
|
||||
import axios from 'axios';
|
||||
import FormDataNew from 'form-data';
|
||||
import mime from 'mime-types';
|
||||
|
||||
export class VkProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'vk';
|
||||
name = 'VK';
|
||||
isBetweenSteps = false;
|
||||
scopes = [
|
||||
'vkid.personal_info',
|
||||
'email',
|
||||
'wall',
|
||||
'status',
|
||||
'docs',
|
||||
'photos',
|
||||
'video',
|
||||
];
|
||||
|
||||
async refreshToken(refresh: string): Promise<AuthTokenDetails> {
|
||||
const [oldRefreshToken, device_id] = refresh.split('&&&&');
|
||||
const formData = new FormData();
|
||||
formData.append('grant_type', 'refresh_token');
|
||||
formData.append('refresh_token', oldRefreshToken);
|
||||
formData.append('client_id', process.env.VK_ID!);
|
||||
formData.append('device_id', device_id);
|
||||
formData.append('state', makeId(32));
|
||||
formData.append('scope', this.scopes.join(' '));
|
||||
|
||||
const { access_token, refresh_token, expires_in } = await (
|
||||
await this.fetch('https://id.vk.com/oauth2/auth', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
).json();
|
||||
|
||||
const newFormData = new FormData();
|
||||
newFormData.append('client_id', process.env.VK_ID!);
|
||||
newFormData.append('access_token', access_token);
|
||||
|
||||
const {
|
||||
user: { user_id, first_name, last_name, avatar },
|
||||
} = await (
|
||||
await this.fetch('https://id.vk.com/oauth2/user_info', {
|
||||
method: 'POST',
|
||||
body: newFormData,
|
||||
})
|
||||
).json();
|
||||
|
||||
return {
|
||||
id: user_id,
|
||||
name: first_name + ' ' + last_name,
|
||||
accessToken: access_token,
|
||||
refreshToken: refresh_token + '&&&&' + device_id,
|
||||
expiresIn: dayjs().add(expires_in, 'seconds').unix() - dayjs().unix(),
|
||||
picture: avatar,
|
||||
username: first_name.toLowerCase(),
|
||||
};
|
||||
}
|
||||
|
||||
async generateAuthUrl() {
|
||||
const state = makeId(32);
|
||||
const codeVerifier = randomBytes(64).toString('base64url');
|
||||
const challenge = Buffer.from(
|
||||
createHash('sha256').update(codeVerifier).digest()
|
||||
)
|
||||
.toString('base64')
|
||||
.replace(/=*$/g, '')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
|
||||
return {
|
||||
url:
|
||||
'https://id.vk.com/authorize' +
|
||||
`?response_type=code` +
|
||||
`&client_id=${process.env.VK_ID}` +
|
||||
`&code_challenge_method=S256` +
|
||||
`&code_challenge=${challenge}` +
|
||||
`&redirect_uri=${encodeURIComponent(
|
||||
`${
|
||||
process?.env.FRONTEND_URL?.indexOf('https') == -1
|
||||
? `https://redirectmeto.com/${process?.env.FRONTEND_URL}`
|
||||
: `${process?.env.FRONTEND_URL}`
|
||||
}/integrations/social/vk`
|
||||
)}` +
|
||||
`&state=${state}` +
|
||||
`&scope=${encodeURIComponent(this.scopes.join(' '))}`,
|
||||
codeVerifier,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
async authenticate(params: {
|
||||
code: string;
|
||||
codeVerifier: string;
|
||||
refresh?: string;
|
||||
}) {
|
||||
const [code, device_id] = params.code.split('&&&&');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('client_id', process.env.VK_ID!);
|
||||
formData.append('grant_type', 'authorization_code');
|
||||
formData.append('code_verifier', params.codeVerifier);
|
||||
formData.append('device_id', device_id);
|
||||
formData.append('code', code);
|
||||
formData.append(
|
||||
'redirect_uri',
|
||||
`${
|
||||
process?.env.FRONTEND_URL?.indexOf('https') == -1
|
||||
? `https://redirectmeto.com/${process?.env.FRONTEND_URL}`
|
||||
: `${process?.env.FRONTEND_URL}`
|
||||
}/integrations/social/vk`
|
||||
);
|
||||
|
||||
const { access_token, scope, refresh_token, expires_in } = await (
|
||||
await this.fetch('https://id.vk.com/oauth2/auth', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
).json();
|
||||
|
||||
const newFormData = new FormData();
|
||||
newFormData.append('client_id', process.env.VK_ID!);
|
||||
newFormData.append('access_token', access_token);
|
||||
|
||||
const {
|
||||
user: { user_id, first_name, last_name, avatar },
|
||||
} = await (
|
||||
await this.fetch('https://id.vk.com/oauth2/user_info', {
|
||||
method: 'POST',
|
||||
body: newFormData,
|
||||
})
|
||||
).json();
|
||||
|
||||
return {
|
||||
id: user_id,
|
||||
name: first_name + ' ' + last_name,
|
||||
accessToken: access_token,
|
||||
refreshToken: refresh_token + '&&&&' + device_id,
|
||||
expiresIn: dayjs().add(expires_in, 'seconds').unix() - dayjs().unix(),
|
||||
picture: avatar,
|
||||
username: first_name.toLowerCase(),
|
||||
};
|
||||
}
|
||||
|
||||
async post(
|
||||
userId: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[]
|
||||
): Promise<PostResponse[]> {
|
||||
let replyTo = '';
|
||||
const values: PostResponse[] = [];
|
||||
|
||||
const uploading = await Promise.all(
|
||||
postDetails.map(async (post) => {
|
||||
return await Promise.all(
|
||||
(post?.media || []).map(async (media) => {
|
||||
const all = await (
|
||||
await this.fetch(
|
||||
media.url.indexOf('mp4') > -1
|
||||
? `https://api.vk.com/method/video.save?access_token=${accessToken}&v=5.251`
|
||||
: `https://api.vk.com/method/photos.getWallUploadServer?owner_id=${userId}&access_token=${accessToken}&v=5.251`
|
||||
)
|
||||
).json();
|
||||
|
||||
const { data } = await axios.get(media.url!, {
|
||||
responseType: 'stream',
|
||||
});
|
||||
|
||||
const slash = media.url.split('/').at(-1);
|
||||
|
||||
const formData = new FormDataNew();
|
||||
formData.append('photo', data, {
|
||||
filename: slash,
|
||||
contentType: mime.lookup(slash!) || '',
|
||||
});
|
||||
const value = (
|
||||
await axios.post(all.response.upload_url, formData, {
|
||||
headers: {
|
||||
...formData.getHeaders(),
|
||||
},
|
||||
})
|
||||
).data;
|
||||
|
||||
if (media.url.indexOf('mp4') > -1) {
|
||||
return {
|
||||
id: all.response.video_id,
|
||||
type: 'video',
|
||||
};
|
||||
}
|
||||
|
||||
const formSend = new FormData();
|
||||
formSend.append('photo', value.photo);
|
||||
formSend.append('server', value.server);
|
||||
formSend.append('hash', value.hash);
|
||||
|
||||
const { id } = (
|
||||
await (
|
||||
await fetch(
|
||||
`https://api.vk.com/method/photos.saveWallPhoto?access_token=${accessToken}&v=5.251`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: formSend,
|
||||
}
|
||||
)
|
||||
).json()
|
||||
).response[0];
|
||||
|
||||
return {
|
||||
id,
|
||||
type: 'photo',
|
||||
};
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
let i = 0;
|
||||
for (const post of postDetails) {
|
||||
const list = uploading?.[i] || [];
|
||||
|
||||
const body = new FormData();
|
||||
body.append('message', post.message);
|
||||
if (replyTo) {
|
||||
body.append('post_id', replyTo);
|
||||
}
|
||||
|
||||
if (list.length) {
|
||||
body.append(
|
||||
'attachments',
|
||||
list.map((p) => `${p.type}${userId}_${p.id}`).join(',')
|
||||
);
|
||||
}
|
||||
|
||||
const { response, ...all } = await (
|
||||
await this.fetch(
|
||||
`https://api.vk.com/method/${
|
||||
replyTo ? 'wall.createComment' : 'wall.post'
|
||||
}?v=5.251&access_token=${accessToken}&client_id=${process.env.VK_ID}`,
|
||||
{
|
||||
method: 'POST',
|
||||
body,
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
values.push({
|
||||
id: post.id,
|
||||
postId: String(response?.post_id || response?.comment_id),
|
||||
releaseURL: `https://vk.com/feed?w=wall${userId}_${
|
||||
response?.post_id || replyTo
|
||||
}`,
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
if (!replyTo) {
|
||||
replyTo = response.post_id;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ export class OpenaiService {
|
|||
await openai.images.generate({
|
||||
prompt,
|
||||
response_format: isUrl ? 'url' : 'b64_json',
|
||||
model: 'dall-e-3',
|
||||
model: 'gpt-image-1',
|
||||
})
|
||||
).data[0];
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ export class OpenaiService {
|
|||
return (
|
||||
(
|
||||
await openai.beta.chat.completions.parse({
|
||||
model: 'gpt-4o-2024-08-06',
|
||||
model: 'gpt-4.1',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
|
|
@ -64,7 +64,7 @@ export class OpenaiService {
|
|||
],
|
||||
n: 5,
|
||||
temperature: 1,
|
||||
model: 'gpt-4o',
|
||||
model: 'gpt-4.1',
|
||||
}),
|
||||
openai.chat.completions.create({
|
||||
messages: [
|
||||
|
|
@ -80,7 +80,7 @@ export class OpenaiService {
|
|||
],
|
||||
n: 5,
|
||||
temperature: 1,
|
||||
model: 'gpt-4o',
|
||||
model: 'gpt-4.1',
|
||||
}),
|
||||
])
|
||||
).flatMap((p) => p.choices);
|
||||
|
|
@ -118,7 +118,7 @@ export class OpenaiService {
|
|||
content,
|
||||
},
|
||||
],
|
||||
model: 'gpt-4o',
|
||||
model: 'gpt-4.1',
|
||||
});
|
||||
|
||||
const { content: articleContent } = websiteContent.choices[0].message;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -38,10 +38,10 @@
|
|||
"@aws-sdk/client-s3": "^3.787.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.787.0",
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@copilotkit/react-core": "^1.8.5",
|
||||
"@copilotkit/react-textarea": "^1.8.5",
|
||||
"@copilotkit/react-ui": "^1.8.5",
|
||||
"@copilotkit/runtime": "^1.8.5",
|
||||
"@copilotkit/react-core": "^1.8.9",
|
||||
"@copilotkit/react-textarea": "^1.8.9",
|
||||
"@copilotkit/react-ui": "^1.8.9",
|
||||
"@copilotkit/runtime": "^1.8.9",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@langchain/community": "^0.3.40",
|
||||
"@langchain/core": "^0.3.44",
|
||||
|
|
|
|||
Loading…
Reference in New Issue