Compare commits
1 commit
main
...
fix/tiktok
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4c947b89a |
66 changed files with 883 additions and 2737 deletions
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -18,5 +18,5 @@ Put a "X" in the boxes below to indicate you have followed the checklist;
|
|||
|
||||
- [ ] I have read the [CONTRIBUTING](https://github.com/gitroomhq/postiz-app/blob/main/CONTRIBUTING.md) guide.
|
||||
- [ ] I confirm I have not used AI to submit this PR or generate code for it.
|
||||
- [ ] I checked that there were no similar issues or PRs already open for this.
|
||||
- [ ] This PR fixes just ONE issue
|
||||
- [ ] I checked that there were not similar issues or PRs already open for this.
|
||||
- [ ] This PR fixes just ONE issue (do not include multiple issues or types of change in the same PR) For example, don't try and fix a UI issue and include new dependencies in the same PR.
|
||||
|
|
|
|||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
|
|
@ -3,8 +3,6 @@ name: Build
|
|||
|
||||
on:
|
||||
push:
|
||||
merge_group:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
@ -29,7 +27,7 @@ jobs:
|
|||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 10
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
|
|
|
|||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
name: "Code Quality Analysis"
|
||||
name: "Code Quality Analysis"
|
||||
|
||||
on:
|
||||
push:
|
||||
|
|
@ -9,8 +9,6 @@ on:
|
|||
- apps/**
|
||||
- '!apps/docs/**'
|
||||
- libraries/**
|
||||
merge_group:
|
||||
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -58,6 +58,3 @@ Thumbs.db
|
|||
.secrets/
|
||||
libraries/plugins/src/plugins.ts
|
||||
i18n.cache
|
||||
|
||||
# Generated by apps/frontend/scripts/fetch-gtm.mjs on install
|
||||
apps/frontend/public/g.js
|
||||
|
|
|
|||
|
|
@ -101,7 +101,6 @@ export class IntegrationsController {
|
|||
internalId: p.internalId,
|
||||
disabled: p.disabled,
|
||||
editor: findIntegration.editor,
|
||||
stripLinks: !!findIntegration?.stripLinks?.(),
|
||||
picture: p.picture || '/no-picture.jpg',
|
||||
identifier: p.providerIdentifier,
|
||||
inBetweenSteps: p.inBetweenSteps,
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ export class PostsController {
|
|||
) {
|
||||
console.log(JSON.stringify(rawBody, null, 2));
|
||||
const body = await this._postsService.mapTypeToPost(rawBody, org.id);
|
||||
return this._postsService.createPost(org.id, body, 'WEB');
|
||||
return this._postsService.createPost(org.id, body);
|
||||
}
|
||||
|
||||
@Post('/generator/draft')
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
|||
throttlers: [
|
||||
{
|
||||
ttl: 3600000,
|
||||
limit: process.env.API_LIMIT ? Number(process.env.API_LIMIT) : 90,
|
||||
limit: process.env.API_LIMIT ? Number(process.env.API_LIMIT) : 30,
|
||||
},
|
||||
],
|
||||
storage: new ThrottlerStorageRedisService(ioRedis),
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ async function start() {
|
|||
})
|
||||
);
|
||||
|
||||
app.use(['/copilot/{*splat}', '/posts'], (req: any, res: any, next: any) => {
|
||||
app.use(['/copilot/*', '/posts'], (req: any, res: any, next: any) => {
|
||||
json({ limit: '50mb' })(req, res, next);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -188,15 +188,8 @@ export class PublicIntegrationsController {
|
|||
);
|
||||
}
|
||||
|
||||
const allowedCreationMethods = ['CLI', 'API'] as const;
|
||||
const creationMethod = allowedCreationMethods.includes(
|
||||
rawBody.creationMethod
|
||||
)
|
||||
? (rawBody.creationMethod as 'CLI' | 'API')
|
||||
: 'API';
|
||||
|
||||
console.log(JSON.stringify(body, null, 2));
|
||||
return this._postsService.createPost(org.id, body, creationMethod);
|
||||
return this._postsService.createPost(org.id, body);
|
||||
}
|
||||
|
||||
@Delete('/posts/:id')
|
||||
|
|
|
|||
|
|
@ -43,9 +43,6 @@ export class AuthService {
|
|||
if (process.env.DISALLOW_PLUS && body.email.includes('+')) {
|
||||
throw new Error('Email with plus sign is not allowed');
|
||||
}
|
||||
if (body instanceof CreateOrgUserDto) {
|
||||
body.email = body.email.toLowerCase();
|
||||
}
|
||||
const user = await this._userService.getUserByEmail(body.email);
|
||||
if (body instanceof CreateOrgUserDto) {
|
||||
if (user) {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "dotenv -e ../../.env -- next dev -p 4200",
|
||||
"fetch-gtm": "node scripts/fetch-gtm.mjs",
|
||||
"postinstall": "node scripts/fetch-gtm.mjs",
|
||||
"build": "next build",
|
||||
"build:sentry": "dotenv -e ../../.env -- next build",
|
||||
"start": "dotenv -e ../../.env -- next start -p 4200",
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
import { writeFile, mkdir, readFile } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const envPath = resolve(__dirname, '..', '..', '..', '.env');
|
||||
const outPath = resolve(__dirname, '..', 'public', 'g.js');
|
||||
|
||||
if (!process.env.NEXT_PUBLIC_GTM_ID && existsSync(envPath)) {
|
||||
const content = await readFile(envPath, 'utf8');
|
||||
for (const raw of content.split('\n')) {
|
||||
const line = raw.trim();
|
||||
if (!line || line.startsWith('#')) continue;
|
||||
const eq = line.indexOf('=');
|
||||
if (eq === -1) continue;
|
||||
const key = line.slice(0, eq).trim();
|
||||
let value = line.slice(eq + 1).trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
if (!process.env[key]) process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const id = process.env.NEXT_PUBLIC_GTM_ID;
|
||||
if (!id) {
|
||||
console.log('[fetch-gtm] NEXT_PUBLIC_GTM_ID not set, skipping');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const url = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(id)}`;
|
||||
try {
|
||||
console.log(`[fetch-gtm] fetching ${url}`);
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
console.warn(`[fetch-gtm] non-OK response ${res.status}, skipping`);
|
||||
process.exit(0);
|
||||
}
|
||||
const body = await res.text();
|
||||
await mkdir(dirname(outPath), { recursive: true });
|
||||
await writeFile(outPath, body, 'utf8');
|
||||
console.log(`[fetch-gtm] wrote ${outPath} (${body.length} bytes)`);
|
||||
} catch (err) {
|
||||
console.warn(`[fetch-gtm] failed: ${err?.message || err}, skipping`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
|
@ -12,7 +12,6 @@ import { VideoOrImage } from '@gitroom/react/helpers/video.or.image';
|
|||
import { CopyClient } from '@gitroom/frontend/components/preview/copy.client';
|
||||
import { getT } from '@gitroom/react/translation/get.translation.service.backend';
|
||||
import { RenderPreviewDateClient } from '@gitroom/frontend/components/preview/render.preview.date.client';
|
||||
import { CreationMethodBadge } from '@gitroom/frontend/components/launches/creation.method.badge';
|
||||
|
||||
dayjs.extend(utc);
|
||||
export const metadata: Metadata = {
|
||||
|
|
@ -143,12 +142,6 @@ export default async function Auth(
|
|||
<span className="text-sm text-gray-500">
|
||||
@{post[0].integration.profile}
|
||||
</span>
|
||||
{index === 0 && (
|
||||
<CreationMethodBadge
|
||||
creationMethod={p.creationMethod}
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-[20px]">
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import { PHProvider } from '@gitroom/react/helpers/posthog';
|
|||
import UtmSaver from '@gitroom/helpers/utils/utm.saver';
|
||||
import { DubAnalytics } from '@gitroom/frontend/components/layout/dubAnalytics';
|
||||
import { FacebookComponent } from '@gitroom/frontend/components/layout/facebook.component';
|
||||
import { GoogleTagManagerComponent } from '@gitroom/frontend/components/layout/gtm.component';
|
||||
import { cookies } from 'next/headers';
|
||||
import {
|
||||
cookieName,
|
||||
|
|
@ -82,8 +81,6 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
|
|||
disableXAnalytics={!!process.env.DISABLE_X_ANALYTICS}
|
||||
sentryDsn={process.env.NEXT_PUBLIC_SENTRY_DSN!}
|
||||
extensionId={process.env.EXTENSION_ID || ''}
|
||||
googleAdsId={process.env.NEXT_PUBLIC_GTM_ID}
|
||||
googleAdsTrialTracking={process.env.NEXT_PUBLIC_TRACKING_TRIAL}
|
||||
language={language}
|
||||
transloadit={
|
||||
process.env.TRANSLOADIT_AUTH && process.env.TRANSLOADIT_TEMPLATE
|
||||
|
|
@ -99,7 +96,6 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
|
|||
<HtmlComponent />
|
||||
<DubAnalytics />
|
||||
<FacebookComponent />
|
||||
<GoogleTagManagerComponent gtmId={process.env.NEXT_PUBLIC_GTM_ID} />
|
||||
<Plausible
|
||||
domain={!!process.env.IS_GENERAL ? 'postiz.com' : 'gitroom.com'}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import dayjs from 'dayjs';
|
|||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { ExistingDataContextProvider } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
|
||||
|
||||
export const AgentChat: FC = () => {
|
||||
const { backendUrl } = useVariables();
|
||||
|
|
@ -163,7 +162,7 @@ const NewInput: FC<InputProps> = (props) => {
|
|||
? '\n[--Media--]' +
|
||||
media
|
||||
.map((m) =>
|
||||
hasExtension(m.path, 'mp4')
|
||||
m.path.indexOf('mp4') > -1
|
||||
? `Video: ${m.path}`
|
||||
: `Image: ${m.path}`
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@ import { expandPostsList, expandPosts } from '@gitroom/helpers/utils/posts.list.
|
|||
extend(isoWeek);
|
||||
extend(weekOfYear);
|
||||
|
||||
export type ListStateFilter = 'all' | 'scheduled' | 'draft' | 'published';
|
||||
|
||||
export const CalendarContext = createContext({
|
||||
startDate: newDayjs().startOf('isoWeek').format('YYYY-MM-DD'),
|
||||
endDate: newDayjs().endOf('isoWeek').format('YYYY-MM-DD'),
|
||||
|
|
@ -80,10 +78,6 @@ export const CalendarContext = createContext({
|
|||
setListPage: (page: number) => {
|
||||
/** empty **/
|
||||
},
|
||||
listState: 'all' as ListStateFilter,
|
||||
setListState: (state: ListStateFilter) => {
|
||||
/** empty **/
|
||||
},
|
||||
});
|
||||
|
||||
export interface Integrations {
|
||||
|
|
@ -92,7 +86,6 @@ export interface Integrations {
|
|||
disabled?: boolean;
|
||||
inBetweenSteps: boolean;
|
||||
editor: 'none' | 'normal' | 'markdown' | 'html';
|
||||
stripLinks?: boolean;
|
||||
display: string;
|
||||
identifier: string;
|
||||
type: string;
|
||||
|
|
@ -150,11 +143,6 @@ export const CalendarWeekProvider: FC<{
|
|||
|
||||
// List view state
|
||||
const [listPage, setListPage] = useState(0);
|
||||
const [listState, setListStateRaw] = useState<ListStateFilter>('all');
|
||||
const setListState = useCallback((next: ListStateFilter) => {
|
||||
setListStateRaw(next);
|
||||
setListPage(0);
|
||||
}, []);
|
||||
|
||||
// Initialize with current date range based on URL params or defaults
|
||||
const initStartDate = searchParams.get('startDate');
|
||||
|
|
@ -201,9 +189,8 @@ export const CalendarWeekProvider: FC<{
|
|||
page: listPage.toString(),
|
||||
limit: '100',
|
||||
customer: filters?.customer?.toString() || '',
|
||||
state: listState,
|
||||
}).toString();
|
||||
}, [listPage, filters.customer, listState]);
|
||||
}, [listPage, filters.customer]);
|
||||
|
||||
const loadListData = useCallback(async () => {
|
||||
const response = await fetch(`/posts/list?${listParams}`);
|
||||
|
|
@ -353,8 +340,6 @@ export const CalendarWeekProvider: FC<{
|
|||
listPage,
|
||||
listTotalPages,
|
||||
setListPage,
|
||||
listState,
|
||||
setListState,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ import { MissingReleaseModal } from '@gitroom/frontend/components/launches/missi
|
|||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import i18next from 'i18next';
|
||||
import { AddEditModal } from '@gitroom/frontend/components/new-launch/add.edit.modal';
|
||||
import { CreationMethodBadge } from '@gitroom/frontend/components/launches/creation.method.badge';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import copy from 'copy-to-clipboard';
|
||||
|
|
@ -493,15 +492,7 @@ export const MonthView = () => {
|
|||
export const ListView = () => {
|
||||
const t = useT();
|
||||
const user = useUser();
|
||||
const { integrations, loading, listPosts, listState } = useCalendar();
|
||||
const emptyMessage =
|
||||
listState === 'scheduled'
|
||||
? t('no_upcoming_posts', 'No upcoming posts scheduled')
|
||||
: listState === 'draft'
|
||||
? t('no_draft_posts', 'No draft posts')
|
||||
: listState === 'published'
|
||||
? t('no_published_posts', 'No published posts')
|
||||
: t('no_posts', 'No posts');
|
||||
const { integrations, loading, listPosts } = useCalendar();
|
||||
|
||||
// Use shared post actions hook
|
||||
const { editPost, deletePost, copyDebugJson, openStatistics, openMissingRelease } = usePostActions();
|
||||
|
|
@ -530,7 +521,9 @@ export const ListView = () => {
|
|||
if (listPosts.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 items-center justify-center">
|
||||
<div className="text-textColor text-[16px]">{emptyMessage}</div>
|
||||
<div className="text-textColor text-[16px]">
|
||||
{t('no_upcoming_posts', 'No upcoming posts scheduled')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1012,11 +1005,6 @@ const CalendarItem: FC<{
|
|||
missingRelease,
|
||||
} = props;
|
||||
const { disableXAnalytics } = useVariables();
|
||||
const user = useUser();
|
||||
const showCreationMethodBadge =
|
||||
user?.impersonate &&
|
||||
post.creationMethod &&
|
||||
post.creationMethod !== 'UNKNOWN';
|
||||
const preview = useCallback(() => {
|
||||
window.open(`/p/` + post.id + '?share=true', '_blank');
|
||||
}, [post]);
|
||||
|
|
@ -1056,14 +1044,6 @@ const CalendarItem: FC<{
|
|||
!
|
||||
</div>
|
||||
)}
|
||||
{showCreationMethodBadge && (
|
||||
<div className="absolute -bottom-[4px] -right-[4px] z-10">
|
||||
<CreationMethodBadge
|
||||
creationMethod={post.creationMethod}
|
||||
ringColor="var(--new-bgColor)"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'text-white text-[11px] max-h-[24px] h-[24px] min-h-[24px] w-full rounded-tr-[10px] rounded-tl-[10px] flex items-center justify-center gap-[10px] px-[5px] bg-btnPrimary'
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
import { FC } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
type CreationMethod = 'UNKNOWN' | 'WEB' | 'API' | 'MCP' | 'AUTOPOST' | 'CLI';
|
||||
|
||||
interface Props {
|
||||
creationMethod?: CreationMethod | string | null;
|
||||
size?: 'xs' | 'sm' | 'md';
|
||||
className?: string;
|
||||
ringColor?: string;
|
||||
}
|
||||
|
||||
const tooltipFor = (m: string) =>
|
||||
m === 'AUTOPOST' ? 'Auto-posted by system' : `Created via ${m}`;
|
||||
|
||||
export const CreationMethodBadge: FC<Props> = ({
|
||||
creationMethod,
|
||||
size = 'xs',
|
||||
className,
|
||||
ringColor,
|
||||
}) => {
|
||||
if (!creationMethod || creationMethod === 'UNKNOWN') return null;
|
||||
|
||||
const sizeClasses =
|
||||
size === 'xs'
|
||||
? 'h-[12px] px-[4px] text-[7px]'
|
||||
: size === 'md'
|
||||
? 'h-[22px] px-[10px] text-[12px]'
|
||||
: 'h-[18px] px-[8px] text-[10px]';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'inline-flex items-center justify-center rounded-full text-white font-bold uppercase tracking-wide leading-none cursor-default',
|
||||
sizeClasses,
|
||||
creationMethod === 'WEB' && 'bg-[#6b7280]',
|
||||
creationMethod === 'API' && 'bg-[#2563eb]',
|
||||
creationMethod === 'MCP' && 'bg-[#9333ea]',
|
||||
creationMethod === 'AUTOPOST' && 'bg-[#d97706]',
|
||||
creationMethod === 'CLI' && 'bg-[#0f766e]',
|
||||
className
|
||||
)}
|
||||
style={ringColor ? { boxShadow: `0 0 0 2px ${ringColor}` } : undefined}
|
||||
data-tooltip-id="tooltip"
|
||||
data-tooltip-content={tooltipFor(creationMethod)}
|
||||
>
|
||||
{creationMethod}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useCalendar, ListStateFilter } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import { useCalendar } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCallback } from 'react';
|
||||
|
|
@ -259,21 +259,6 @@ export const Filters = () => {
|
|||
|
||||
const isListView = calendar.display === 'list';
|
||||
|
||||
const setListStateFilter = useCallback(
|
||||
(next: ListStateFilter) => () => {
|
||||
if (calendar.listState === next) return;
|
||||
calendar.setListState(next);
|
||||
},
|
||||
[calendar]
|
||||
);
|
||||
|
||||
const listStateOptions: { value: ListStateFilter; label: string }[] = [
|
||||
{ value: 'all', label: t('all', 'All') },
|
||||
{ value: 'scheduled', label: t('scheduled', 'Scheduled') },
|
||||
{ value: 'draft', label: t('draft', 'Draft') },
|
||||
{ value: 'published', label: t('published', 'Published') },
|
||||
];
|
||||
|
||||
const previousPage = useCallback(() => {
|
||||
if (calendar.listPage > 0) {
|
||||
calendar.setListPage(calendar.listPage - 1);
|
||||
|
|
@ -408,21 +393,6 @@ export const Filters = () => {
|
|||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row p-[4px] border border-newTableBorder rounded-[8px] text-[14px] font-[500]">
|
||||
{listStateOptions.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
onClick={setListStateFilter(option.value)}
|
||||
className={clsx(
|
||||
'pt-[6px] pb-[5px] cursor-pointer min-w-[80px] px-[12px] text-center rounded-[6px]',
|
||||
calendar.listState === option.value &&
|
||||
'text-textItemFocused bg-boxFocused'
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { EventEmitter } from 'events';
|
|||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
|
||||
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
const postUrlEmitter = new EventEmitter();
|
||||
|
|
@ -376,7 +375,7 @@ export const MediaComponentInner: FC<{
|
|||
className="w-full px-3 py-2 bg-fifth border border-tableBorder rounded-lg text-textColor placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-forth focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
{hasExtension(media?.path, 'mp4') && (
|
||||
{media?.path.indexOf('mp4') > -1 && (
|
||||
<>
|
||||
{/* Alt Text Input */}
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ export const ImportDebugPostModal: FC<{ close: () => void }> = ({ close }) => {
|
|||
<div className="text-[13px] font-[600] text-textColor">
|
||||
{t('debug_info', 'Debug Info')}
|
||||
</div>
|
||||
<div className="text-[12px] text-textColor/70 flex flex-col gap-[4px] min-w-0 break-all">
|
||||
<div className="text-[12px] text-textColor/70 flex flex-col gap-[4px]">
|
||||
<div>
|
||||
<span className="font-[500]">
|
||||
{t('provider', 'Provider')}:
|
||||
|
|
@ -175,7 +175,7 @@ export const ImportDebugPostModal: FC<{ close: () => void }> = ({ close }) => {
|
|||
<span className="font-[500]">
|
||||
{t('error_details', 'Error Details')}:
|
||||
</span>
|
||||
<div className="mt-[4px] max-h-[100px] overflow-y-auto bg-newBgColor p-[8px] rounded-[4px] text-[11px] font-mono break-all whitespace-pre-wrap">
|
||||
<div className="mt-[4px] max-h-[100px] overflow-y-auto bg-newBgColor p-[8px] rounded-[4px] text-[11px] font-mono">
|
||||
{parsed._debug.errors.map((err, i) => (
|
||||
<div key={i} className="mb-[4px]">
|
||||
[{err.platform}] {err.message}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import clsx from 'clsx';
|
|||
import SafeImage from '@gitroom/react/helpers/safe.image';
|
||||
import { capitalize } from 'lodash';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { hasLinks } from '@gitroom/helpers/utils/strip.links';
|
||||
|
||||
const Valid: FC = () => {
|
||||
return (
|
||||
|
|
@ -60,36 +59,15 @@ export const InformationComponent: FC<{
|
|||
totalChars: number;
|
||||
totalAllowedChars: number;
|
||||
isPicture: boolean;
|
||||
text?: string;
|
||||
}> = ({ totalChars, totalAllowedChars, chars, isPicture, text }) => {
|
||||
}> = ({ totalChars, totalAllowedChars, chars, isPicture }) => {
|
||||
const t = useT();
|
||||
const { isGlobal, selectedIntegrations, internal, currentIntegration } =
|
||||
useLaunchStore(
|
||||
useShallow((state) => ({
|
||||
isGlobal: state.current === 'global',
|
||||
selectedIntegrations: state.selectedIntegrations,
|
||||
internal: state.internal,
|
||||
currentIntegration: state.integrations.find(
|
||||
(p) => p.id === state.current
|
||||
),
|
||||
}))
|
||||
);
|
||||
|
||||
const stripLinkNames = useMemo(() => {
|
||||
if (!hasLinks(text)) {
|
||||
return [] as string[];
|
||||
}
|
||||
|
||||
if (!isGlobal) {
|
||||
return currentIntegration?.stripLinks ? [currentIntegration.name] : [];
|
||||
}
|
||||
|
||||
return selectedIntegrations
|
||||
.filter((p) => p.integration.stripLinks)
|
||||
.map((p) => p.integration.name);
|
||||
}, [text, isGlobal, currentIntegration, selectedIntegrations]);
|
||||
|
||||
const showStripLinkWarning = stripLinkNames.length > 0;
|
||||
const { isGlobal, selectedIntegrations, internal } = useLaunchStore(
|
||||
useShallow((state) => ({
|
||||
isGlobal: state.current === 'global',
|
||||
selectedIntegrations: state.selectedIntegrations,
|
||||
internal: state.internal,
|
||||
}))
|
||||
);
|
||||
|
||||
const isInternal = useMemo(() => {
|
||||
if (!isGlobal) {
|
||||
|
|
@ -105,10 +83,6 @@ export const InformationComponent: FC<{
|
|||
}, [isGlobal, internal, selectedIntegrations]);
|
||||
|
||||
const isValid = useMemo(() => {
|
||||
if (showStripLinkWarning) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isPicture && !totalChars) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -134,14 +108,7 @@ export const InformationComponent: FC<{
|
|||
}
|
||||
|
||||
return true;
|
||||
}, [
|
||||
totalAllowedChars,
|
||||
totalChars,
|
||||
isInternal,
|
||||
isPicture,
|
||||
chars,
|
||||
showStripLinkWarning,
|
||||
]);
|
||||
}, [totalAllowedChars, totalChars, isInternal, isPicture, chars]);
|
||||
|
||||
const globalDisplayLimit = useMemo(() => {
|
||||
if (!isGlobal || !selectedIntegrations.length) {
|
||||
|
|
@ -263,19 +230,6 @@ export const InformationComponent: FC<{
|
|||
))}
|
||||
</div>
|
||||
)}
|
||||
{showStripLinkWarning && (
|
||||
<div
|
||||
className={clsx(
|
||||
'text-sm text-[#FF3F3F] whitespace-nowrap',
|
||||
((isGlobal && selectedIntegrations.length) ||
|
||||
(!isPicture && !totalChars)) &&
|
||||
'mt-[12px]'
|
||||
)}
|
||||
>
|
||||
{t('links_will_be_removed_from', 'Links will be removed from')}:{' '}
|
||||
{stripLinkNames.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import Script from 'next/script';
|
||||
import { FC, useEffect } from 'react';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
|
||||
export const TrialTracker: FC = () => {
|
||||
const user = useUser();
|
||||
const { googleAdsId, googleAdsTrialTracking } = useVariables();
|
||||
useEffect(() => {
|
||||
if (
|
||||
typeof window === 'undefined' ||
|
||||
!user?.id ||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
!window.gtag ||
|
||||
!googleAdsId ||
|
||||
!googleAdsTrialTracking
|
||||
)
|
||||
return;
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('onboarding') !== 'true') return;
|
||||
const key = `gtm_start_trial_${user?.id}`;
|
||||
if (sessionStorage.getItem(key)) return;
|
||||
sessionStorage.setItem(key, '1');
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
gtag('event', 'conversion', {
|
||||
send_to: `${googleAdsId}/${googleAdsTrialTracking}`,
|
||||
});
|
||||
}, [user]);
|
||||
return null;
|
||||
};
|
||||
|
||||
export const GoogleTagManagerComponent: FC<{ gtmId?: string }> = ({
|
||||
gtmId,
|
||||
}) => {
|
||||
if (!gtmId) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Script src="/g.js" strategy="afterInteractive" />
|
||||
|
||||
<Script id="google-ads-gtag" strategy="afterInteractive">
|
||||
{`
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${gtmId}');
|
||||
`}
|
||||
</Script>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -433,7 +433,6 @@ const ImportDebugPost = () => {
|
|||
const handleClick = useCallback(() => {
|
||||
openModal({
|
||||
title: t('import_debug_post', 'Import Debug Post'),
|
||||
maxSize: 800,
|
||||
children: (close) => <ImportDebugPostModal close={close} />,
|
||||
});
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ interface OpenModalInterface {
|
|||
modal?: string;
|
||||
};
|
||||
size?: string | number;
|
||||
maxSize?: string | number;
|
||||
height?: string | number;
|
||||
id?: string;
|
||||
}
|
||||
|
|
@ -201,11 +200,10 @@ export const Component: FC<{
|
|||
modal.size ? '' : 'min-w-[600px]',
|
||||
modal.fullScreen && 'h-full'
|
||||
)}
|
||||
{...((!!modal.size || !!modal.height || !!modal.maxSize) && {
|
||||
{...((!!modal.size || !!modal.height) && {
|
||||
style: {
|
||||
...(modal.size ? { width: modal.size } : {}),
|
||||
...(modal.height ? { height: modal.height } : {}),
|
||||
...(modal.maxSize ? { maxWidth: modal.maxSize } : {}),
|
||||
},
|
||||
})}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,8 @@ import dayjs, { ConfigType } from 'dayjs';
|
|||
import { FC, useEffect } from 'react';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
const { utc: originalUtc } = dayjs;
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import React, {
|
|||
import { Button } from '@gitroom/react/form/button';
|
||||
import useSWR from 'swr';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
|
||||
import { Media } from '@prisma/client';
|
||||
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
|
||||
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
|
||||
|
|
@ -352,7 +351,7 @@ export const MediaBox: FC<{
|
|||
top: 10,
|
||||
children: (
|
||||
<div className="w-full h-full p-[50px]">
|
||||
{hasExtension(media.path, 'mp4') ? (
|
||||
{media.path.indexOf('mp4') > -1 ? (
|
||||
<VideoFrame
|
||||
autoplay={true}
|
||||
url={mediaDirectory.set(media.path)}
|
||||
|
|
@ -526,9 +525,9 @@ export const MediaBox: FC<{
|
|||
{data?.results
|
||||
?.filter((f: any) => {
|
||||
if (type === 'video') {
|
||||
return hasExtension(f.path, 'mp4');
|
||||
return f.path.indexOf('mp4') > -1;
|
||||
} else if (type === 'image') {
|
||||
return !hasExtension(f.path, 'mp4');
|
||||
return f.path.indexOf('mp4') === -1;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
|
|
@ -580,7 +579,7 @@ export const MediaBox: FC<{
|
|||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
{hasExtension(media.path, 'mp4') ? (
|
||||
{media.path.indexOf('mp4') > -1 ? (
|
||||
<VideoFrame url={mediaDirectory.set(media.path)} />
|
||||
) : (
|
||||
<img
|
||||
|
|
@ -804,7 +803,7 @@ export const MultiMediaComponent: FC<{
|
|||
>
|
||||
<MediaSettingsIcon className="cursor-pointer relative z-[200]" />
|
||||
</div>
|
||||
{hasExtension(media?.path, 'mp4') ? (
|
||||
{media?.path?.indexOf('mp4') > -1 ? (
|
||||
<VideoFrame url={mediaDirectory.set(media?.path)} />
|
||||
) : (
|
||||
<img
|
||||
|
|
|
|||
|
|
@ -772,7 +772,6 @@ export const Editor: FC<{
|
|||
chars={chars}
|
||||
totalChars={valueWithoutHtml.length}
|
||||
totalAllowedChars={props.totalChars}
|
||||
text={valueWithoutHtml}
|
||||
/>
|
||||
}
|
||||
toolBar={
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import { SelectCustomer } from '@gitroom/frontend/components/launches/select.cus
|
|||
import { CopilotPopup } from '@copilotkit/react-ui';
|
||||
import { DummyCodeComponent } from '@gitroom/frontend/components/new-launch/dummy.code.component';
|
||||
import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation';
|
||||
import { CreationMethodBadge } from '@gitroom/frontend/components/launches/creation.method.badge';
|
||||
import {
|
||||
SettingsIcon,
|
||||
ChevronDownIcon,
|
||||
|
|
@ -342,12 +341,9 @@ export const ManageModal: FC<AddEditModalProps> = (props) => {
|
|||
await fetch('/posts/should-shortlink', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
messages: checkAllValid
|
||||
// platforms that remove links won't keep shortlinks either
|
||||
.filter((p: any) => !p?.integration?.stripLinks)
|
||||
.flatMap((p: any) =>
|
||||
p.values.flatMap((a: any) => a.content)
|
||||
),
|
||||
messages: checkAllValid.flatMap((p: any) =>
|
||||
p.values.flatMap((a: any) => a.content)
|
||||
),
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
|
@ -451,12 +447,8 @@ export const ManageModal: FC<AddEditModalProps> = (props) => {
|
|||
<div className="flex flex-1 bg-newBgColorInner rounded-[20px] flex-col">
|
||||
<div className="flex-1 flex">
|
||||
<div className="flex flex-col flex-1 border-e border-newBorder">
|
||||
<div className="bg-newBgColor h-[65px] rounded-s-[20px] !rounded-b-[0] flex items-center gap-[12px] px-[20px] text-[20px] font-[600]">
|
||||
<div className="bg-newBgColor h-[65px] rounded-s-[20px] !rounded-b-[0] flex items-center px-[20px] text-[20px] font-[600]">
|
||||
{t('create_post_title', 'Create Post')}
|
||||
<CreationMethodBadge
|
||||
creationMethod={existingData?.posts?.[0]?.creationMethod}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-[16px]">
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -35,18 +35,13 @@ export default withProvider({
|
|||
CustomPreviewComponent: PinterestPreview,
|
||||
dto: PinterestSettingsDto,
|
||||
checkValidity: async ([firstItem, ...otherItems] = []) => {
|
||||
const isMp4 = firstItem?.find(
|
||||
(item) => (item?.path?.indexOf?.('mp4') ?? -1) > -1
|
||||
);
|
||||
const isMp4 = firstItem?.find((item) => (item?.path?.indexOf?.('mp4') ?? -1) > -1);
|
||||
const isPicture = firstItem?.find(
|
||||
(item) => (item?.path?.indexOf?.('mp4') ?? -1) === -1
|
||||
);
|
||||
if ((firstItem?.length ?? 0) === 0) {
|
||||
return 'Requires at least one media';
|
||||
}
|
||||
if ((firstItem?.length ?? 0) > 5) {
|
||||
return 'You can only have up to 5 media items';
|
||||
}
|
||||
if (isMp4 && firstItem?.length !== 2 && !isPicture) {
|
||||
return 'If posting a video you have to also include a cover image as second media';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,29 +29,12 @@ const TikTokSettings: FC<{
|
|||
return value?.[0]?.image?.some((p) => (p?.path?.indexOf?.('mp4') ?? -1) === -1);
|
||||
}, [value]);
|
||||
|
||||
const hasMedia = (value?.[0]?.image?.length ?? 0) > 0;
|
||||
const isVideo = hasMedia && !isTitle;
|
||||
|
||||
const disclose = watch('disclose');
|
||||
const brand_organic_toggle = watch('brand_organic_toggle');
|
||||
const brand_content_toggle = watch('brand_content_toggle');
|
||||
const content_posting_method = watch('content_posting_method');
|
||||
const isUploadMode = content_posting_method === 'UPLOAD';
|
||||
|
||||
const tiktokRestrictionNotice = useMemo(() => {
|
||||
if (!hasMedia || !isVideo) return null;
|
||||
if (!isUploadMode) {
|
||||
return t(
|
||||
'tiktok_restriction_direct_video',
|
||||
'TikTok restriction: For direct post with video, your post content is used as the title. A separate title field is not available.'
|
||||
);
|
||||
}
|
||||
return t(
|
||||
'tiktok_restriction_upload_video',
|
||||
'TikTok restriction: For upload-only video, TikTok does not accept a title or message. The content will default to "#Postiz" and you can edit it inside the TikTok app before publishing.'
|
||||
);
|
||||
}, [hasMedia, isUploadMode, isVideo, t]);
|
||||
|
||||
const privacyLevel = [
|
||||
{
|
||||
value: 'PUBLIC_TO_EVERYONE',
|
||||
|
|
@ -100,25 +83,6 @@ const TikTokSettings: FC<{
|
|||
return (
|
||||
<div className="flex flex-col">
|
||||
{/*<CheckTikTokValidity picture={props?.values?.[0]?.image?.[0]?.path} />*/}
|
||||
{tiktokRestrictionNotice && (
|
||||
<div className="bg-tableBorder p-[10px] mb-[18px] rounded-[10px] flex gap-[10px] items-start text-[13px] text-balance">
|
||||
<div className="shrink-0 mt-[2px]">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M22.201 17.6335L14.0026 3.39569C13.7977 3.04687 13.5052 2.75764 13.1541 2.55668C12.803 2.35572 12.4055 2.25 12.001 2.25C11.5965 2.25 11.199 2.35572 10.8479 2.55668C10.4968 2.75764 10.2043 3.04687 9.99944 3.39569L1.80101 17.6335C1.60388 17.9709 1.5 18.3546 1.5 18.7454C1.5 19.1361 1.60388 19.5199 1.80101 19.8572C2.00325 20.2082 2.29523 20.499 2.64697 20.6998C2.99871 20.9006 3.39755 21.0043 3.80257 21.0001H20.1994C20.6041 21.0039 21.0026 20.9001 21.354 20.6993C21.7054 20.4985 21.997 20.2079 22.1991 19.8572C22.3965 19.52 22.5007 19.1364 22.5011 18.7456C22.5014 18.3549 22.3978 17.9711 22.201 17.6335ZM11.251 9.75006C11.251 9.55115 11.33 9.36038 11.4707 9.21973C11.6113 9.07908 11.8021 9.00006 12.001 9.00006C12.1999 9.00006 12.3907 9.07908 12.5313 9.21973C12.672 9.36038 12.751 9.55115 12.751 9.75006V13.5001C12.751 13.699 12.672 13.8897 12.5313 14.0304C12.3907 14.171 12.1999 14.2501 12.001 14.2501C11.8021 14.2501 11.6113 14.171 11.4707 14.0304C11.33 13.8897 11.251 13.699 11.251 13.5001V9.75006ZM12.001 18.0001C11.7785 18.0001 11.561 17.9341 11.376 17.8105C11.191 17.6868 11.0468 17.5111 10.9616 17.3056C10.8765 17.1 10.8542 16.8738 10.8976 16.6556C10.941 16.4374 11.0482 16.2369 11.2055 16.0796C11.3628 15.9222 11.5633 15.8151 11.7815 15.7717C11.9998 15.7283 12.226 15.7505 12.4315 15.8357C12.6371 15.9208 12.8128 16.065 12.9364 16.25C13.06 16.4351 13.126 16.6526 13.126 16.8751C13.126 17.1734 13.0075 17.4596 12.7965 17.6706C12.5855 17.8815 12.2994 18.0001 12.001 18.0001Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>{tiktokRestrictionNotice}</div>
|
||||
</div>
|
||||
)}
|
||||
{isTitle && <Input label="Title" {...register('title')} maxLength={89} />}
|
||||
<Select
|
||||
label={t('label_who_can_see_this_video', 'Who can see this video?')}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ import { StreakComponent } from '@gitroom/frontend/components/layout/streak.comp
|
|||
import { PreConditionComponent } from '@gitroom/frontend/components/layout/pre-condition.component';
|
||||
import { AttachToFeedbackIcon } from '@gitroom/frontend/components/new-layout/sentry.feedback.component';
|
||||
import { FirstBillingComponent } from '@gitroom/frontend/components/billing/first.billing.component';
|
||||
import { TrialTracker } from '@gitroom/frontend/components/layout/gtm.component';
|
||||
|
||||
const jakartaSans = Plus_Jakarta_Sans({
|
||||
weight: ['600', '500', '700'],
|
||||
|
|
@ -79,7 +78,6 @@ export const LayoutComponent = ({ children }: { children: ReactNode }) => {
|
|||
<MantineWrapper>
|
||||
<ToolTip />
|
||||
<Toaster />
|
||||
<TrialTracker />
|
||||
<CheckPayment check={searchParams.get('check') || ''} mutate={mutate}>
|
||||
<ShowMediaBoxModal />
|
||||
<ShowLinkedinCompany />
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
|||
import useSWR from 'swr';
|
||||
import { FC, useCallback, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import { useClickAway } from '@uidotdev/usehooks';
|
||||
import ReactLoading from '@gitroom/frontend/components/layout/loading';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
|
|
@ -27,29 +26,16 @@ export const ShowNotification: FC<{
|
|||
const [newNotification] = useState(
|
||||
new Date(notification.createdAt) > new Date(props.lastReadNotification)
|
||||
);
|
||||
const createdAt = dayjs(notification.createdAt);
|
||||
const isWithin24h = dayjs().diff(createdAt, 'hour') < 24;
|
||||
const fullDate = createdAt.format('MMM D, YYYY h:mm A');
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
`text-textColor px-[16px] py-[10px] border-b border-tableBorder last:border-b-0 transition-colors`,
|
||||
`text-textColor px-[16px] py-[10px] border-b border-tableBorder last:border-b-0 transition-colors overflow-hidden text-ellipsis`,
|
||||
newNotification && 'font-bold bg-seventh animate-newMessages'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="break-words"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceLinks(notification.content),
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="text-[11px] mt-[4px] opacity-60 font-normal"
|
||||
title={isWithin24h ? fullDate : undefined}
|
||||
>
|
||||
{isWithin24h ? createdAt.fromNow() : fullDate}
|
||||
</div>
|
||||
</div>
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceLinks(notification.content),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const NotificationOpenComponent = () => {
|
||||
|
|
@ -71,7 +57,7 @@ export const NotificationOpenComponent = () => {
|
|||
{t('notifications', 'Notifications')}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col max-h-[400px] overflow-y-auto scrollbar scrollbar-thumb-fifth scrollbar-track-newBgColor">
|
||||
<div className="flex flex-col">
|
||||
{isLoading && (
|
||||
<div className="flex-1 flex justify-center pt-12">
|
||||
<ReactLoading type="spin" color="#fff" width={36} height={36} />
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { Toaster } from '@gitroom/react/toaster/toaster';
|
|||
import { MantineWrapper } from '@gitroom/react/helpers/mantine.wrapper';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { CopilotKit } from '@copilotkit/react-core';
|
||||
import { ToolTip } from '@gitroom/frontend/components/layout/top.tip';
|
||||
export const PreviewWrapper = ({ children }: { children: ReactNode }) => {
|
||||
const fetch = useFetch();
|
||||
const { backendUrl } = useVariables();
|
||||
|
|
@ -31,7 +30,6 @@ export const PreviewWrapper = ({ children }: { children: ReactNode }) => {
|
|||
>
|
||||
<MantineWrapper>
|
||||
<Toaster />
|
||||
<ToolTip />
|
||||
{children}
|
||||
</MantineWrapper>
|
||||
</CopilotKit>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { useFormContext } from 'react-hook-form';
|
|||
import { useVideo } from '@gitroom/frontend/components/videos/video.context.wrapper';
|
||||
import { Textarea } from '@gitroom/react/form/textarea';
|
||||
import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component';
|
||||
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
|
||||
|
||||
export interface Voice {
|
||||
id: string;
|
||||
|
|
@ -47,7 +46,7 @@ const VEO3Settings: FC = () => {
|
|||
setValue(
|
||||
'images',
|
||||
val.target.value
|
||||
.filter((f) => !hasExtension(f.path, 'mp4'))
|
||||
.filter((f) => f.path.indexOf('mp4') === -1)
|
||||
.slice(0, 3)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export class PostActivity {
|
|||
for (const post of list) {
|
||||
await this._temporalService.client
|
||||
.getRawClient()
|
||||
.workflow.signalWithStart('postWorkflowV105', {
|
||||
.workflow.signalWithStart('postWorkflowV102', {
|
||||
workflowId: `post_${post.id}`,
|
||||
taskQueue: 'main',
|
||||
signal: 'poke',
|
||||
|
|
@ -110,25 +110,10 @@ export class PostActivity {
|
|||
await this._postService.updatePost(id, postId, releaseURL);
|
||||
}
|
||||
|
||||
@ActivityMethod()
|
||||
async getPost(orgId: string, postId: string) {
|
||||
if (process.env.STRIPE_SECRET_KEY) {
|
||||
const subscription = await this._subscriptionService.getSubscription(
|
||||
orgId
|
||||
);
|
||||
if (!subscription) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return this._postService.getPostById(postId, orgId);
|
||||
}
|
||||
|
||||
@ActivityMethod()
|
||||
async getPostsList(orgId: string, postId: string) {
|
||||
if (process.env.STRIPE_SECRET_KEY) {
|
||||
const subscription = await this._subscriptionService.getSubscription(
|
||||
orgId
|
||||
);
|
||||
const subscription = await this._subscriptionService.getSubscription(orgId);
|
||||
if (!subscription) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -201,16 +186,6 @@ export class PostActivity {
|
|||
|
||||
@ActivityMethod()
|
||||
async postSocial(integration: Integration, posts: Post[]) {
|
||||
if (process.env.STRIPE_SECRET_KEY) {
|
||||
const subscription = await this._subscriptionService.getSubscription(
|
||||
integration.organizationId
|
||||
);
|
||||
|
||||
if (!subscription) {
|
||||
throw new Error('No active subscription found for this organization.');
|
||||
}
|
||||
}
|
||||
|
||||
const getIntegration = this._integrationManager.getSocialIntegration(
|
||||
integration.providerIdentifier
|
||||
);
|
||||
|
|
@ -409,7 +384,10 @@ export class PostActivity {
|
|||
|
||||
return refresh;
|
||||
} catch (err) {
|
||||
await this._refreshIntegrationService.setBetweenSteps(integration, cause);
|
||||
await this._refreshIntegrationService.setBetweenSteps(
|
||||
integration,
|
||||
cause
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
export * from './post-workflows/post.workflow.v1.0.1';
|
||||
export * from './post-workflows/post.workflow.v1.0.2';
|
||||
export * from './post-workflows/post.workflow.v1.0.3';
|
||||
export * from './post-workflows/post.workflow.v1.0.4';
|
||||
export * from './post-workflows/post.workflow.v1.0.5';
|
||||
export * from './autopost.workflow';
|
||||
export * from './digest.email.workflow';
|
||||
export * from './missing.post.workflow';
|
||||
|
|
|
|||
|
|
@ -1,430 +0,0 @@
|
|||
import { PostActivity } from '@gitroom/orchestrator/activities/post.activity';
|
||||
import {
|
||||
ActivityFailure,
|
||||
ApplicationFailure,
|
||||
startChild,
|
||||
proxyActivities,
|
||||
sleep,
|
||||
defineSignal,
|
||||
setHandler,
|
||||
} from '@temporalio/workflow';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { capitalize, sortBy } from 'lodash';
|
||||
import { PostResponse } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { TypedSearchAttributes } from '@temporalio/common';
|
||||
import { postId as postIdSearchParam } from '@gitroom/nestjs-libraries/temporal/temporal.search.attribute';
|
||||
|
||||
const proxyTaskQueue = (taskQueue: string) => {
|
||||
return proxyActivities<PostActivity>({
|
||||
startToCloseTimeout: '10 minute',
|
||||
taskQueue,
|
||||
retry: {
|
||||
maximumAttempts: 3,
|
||||
backoffCoefficient: 1,
|
||||
initialInterval: '2 minutes',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
getPostsList,
|
||||
inAppNotification,
|
||||
changeState,
|
||||
updatePost,
|
||||
sendWebhooks,
|
||||
isCommentable,
|
||||
} = proxyActivities<PostActivity>({
|
||||
startToCloseTimeout: '10 minute',
|
||||
retry: {
|
||||
maximumAttempts: 3,
|
||||
backoffCoefficient: 1,
|
||||
initialInterval: '2 minutes',
|
||||
},
|
||||
});
|
||||
|
||||
const poke = defineSignal('poke');
|
||||
|
||||
const iterate = Array.from({ length: 5 });
|
||||
|
||||
export async function postWorkflowV103({
|
||||
taskQueue,
|
||||
postId,
|
||||
organizationId,
|
||||
postNow = false,
|
||||
}: {
|
||||
taskQueue: string;
|
||||
postId: string;
|
||||
organizationId: string;
|
||||
postNow?: boolean;
|
||||
}) {
|
||||
// Dynamic task queue, for concurrency
|
||||
const {
|
||||
postSocial,
|
||||
postComment,
|
||||
getIntegrationById,
|
||||
refreshTokenWithCause,
|
||||
internalPlugs,
|
||||
globalPlugs,
|
||||
processInternalPlug,
|
||||
processPlug,
|
||||
} = proxyTaskQueue(taskQueue);
|
||||
|
||||
let poked = false;
|
||||
setHandler(poke, () => {
|
||||
poked = true;
|
||||
});
|
||||
|
||||
const startTime = new Date();
|
||||
// get all the posts and comments to post
|
||||
const postsListBefore = await getPostsList(organizationId, postId);
|
||||
const [post] = postsListBefore;
|
||||
|
||||
// in case doesn't exists for some reason, fail it
|
||||
if (!post || (!postNow && post.state !== 'QUEUE')) {
|
||||
await changeState(
|
||||
postsListBefore[0].id,
|
||||
'ERROR',
|
||||
'Already posted',
|
||||
postsListBefore
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// if it's a repeatable post, we should ignore this.
|
||||
if (!postNow) {
|
||||
await sleep(
|
||||
dayjs(post.publishDate).isBefore(dayjs())
|
||||
? 0
|
||||
: dayjs(post.publishDate).diff(dayjs(), 'millisecond')
|
||||
);
|
||||
}
|
||||
|
||||
// if refresh is needed from last time, let's inform the user
|
||||
if (post.integration?.refreshNeeded) {
|
||||
await inAppNotification(
|
||||
post.organizationId,
|
||||
`We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name}`,
|
||||
`We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name} because you need to reconnect it. Please enable it and try again.`,
|
||||
true,
|
||||
false,
|
||||
'info'
|
||||
);
|
||||
|
||||
await changeState(
|
||||
postsListBefore[0].id,
|
||||
'ERROR',
|
||||
'Refresh channel needed',
|
||||
postsListBefore
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// if it's disabled, inform the user
|
||||
if (post.integration?.disabled) {
|
||||
await inAppNotification(
|
||||
post.organizationId,
|
||||
`We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name}`,
|
||||
`We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name} because it's disabled. Please enable it and try again.`,
|
||||
true,
|
||||
false,
|
||||
'info'
|
||||
);
|
||||
|
||||
await changeState(
|
||||
postsListBefore[0].id,
|
||||
'ERROR',
|
||||
'Channel disabled',
|
||||
postsListBefore
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Do we need to post comment for this social?
|
||||
const toComment: boolean =
|
||||
postsListBefore.length === 1
|
||||
? false
|
||||
: await isCommentable(post.integration);
|
||||
|
||||
const postsList = toComment ? postsListBefore : [postsListBefore[0]];
|
||||
|
||||
// list of all the saved results
|
||||
const postsResults: PostResponse[] = [];
|
||||
|
||||
// iterate over the posts
|
||||
for (let i = 0; i < postsList.length; i++) {
|
||||
const before = postsResults.length;
|
||||
// this is a small trick to repeat an action in case of token refresh
|
||||
for (const _ of iterate) {
|
||||
try {
|
||||
// first post the main post
|
||||
if (i === 0) {
|
||||
postsResults.push(
|
||||
...(await postSocial(post.integration as Integration, [
|
||||
postsList[i],
|
||||
]))
|
||||
);
|
||||
|
||||
// then post the comments if any
|
||||
} else {
|
||||
if (postsList[i].delay) {
|
||||
await sleep(60000 * Math.max(0, Number(postsList[i].delay ?? 0)));
|
||||
}
|
||||
|
||||
postsResults.push(
|
||||
...(await postComment(
|
||||
postsResults[0].postId,
|
||||
postsResults.length === 1
|
||||
? undefined
|
||||
: postsResults[i - 1].postId,
|
||||
post.integration,
|
||||
[postsList[i]]
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
// mark post as successful
|
||||
await updatePost(
|
||||
postsList[i].id,
|
||||
postsResults[i].postId,
|
||||
postsResults[i].releaseURL
|
||||
);
|
||||
|
||||
if (i === 0) {
|
||||
// send notification on a sucessful post
|
||||
await inAppNotification(
|
||||
post.integration.organizationId,
|
||||
`Your post has been published on ${capitalize(
|
||||
post.integration.providerIdentifier
|
||||
)}`,
|
||||
`Your post has been published on ${capitalize(
|
||||
post.integration.providerIdentifier
|
||||
)} at ${postsResults[0].releaseURL}`,
|
||||
true,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// break the current while to move to the next post
|
||||
break;
|
||||
} catch (err) {
|
||||
// if token refresh is needed, do it and repeat
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'refresh_token'
|
||||
) {
|
||||
const refresh = await refreshTokenWithCause(
|
||||
post.integration,
|
||||
err?.cause?.message || ''
|
||||
);
|
||||
if (!refresh || !refresh.accessToken) {
|
||||
await changeState(postsList[0].id, 'ERROR', err, postsList);
|
||||
return false;
|
||||
}
|
||||
|
||||
post.integration.token = refresh.accessToken;
|
||||
continue;
|
||||
}
|
||||
|
||||
// for other errors, change state and inform the user if needed
|
||||
await changeState(postsList[0].id, 'ERROR', err, postsList);
|
||||
|
||||
// specific case for bad body errors
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'bad_body'
|
||||
) {
|
||||
await inAppNotification(
|
||||
post.organizationId,
|
||||
`Error posting${i === 0 ? ' ' : ' comments '}on ${
|
||||
post.integration?.providerIdentifier
|
||||
} for ${post?.integration?.name}`,
|
||||
`An error occurred while posting${i === 0 ? ' ' : ' comments '}on ${
|
||||
post.integration?.providerIdentifier
|
||||
}${err?.cause?.message ? `: ${err?.cause?.message}` : ``}`,
|
||||
true,
|
||||
false,
|
||||
'fail'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (postsResults.length === before) {
|
||||
// all retries exhausted without success
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// send webhooks for the post
|
||||
await sendWebhooks(
|
||||
postsResults[0].postId,
|
||||
post.organizationId,
|
||||
post.integration.id
|
||||
);
|
||||
|
||||
// load internal plugs like repost by other users
|
||||
const internalPlugsList = await internalPlugs(
|
||||
post.integration,
|
||||
JSON.parse(post.settings)
|
||||
);
|
||||
|
||||
// load global plugs, like repost a post if it gets to a certain number of likes
|
||||
const globalPlugsList = (await globalPlugs(post.integration)).reduce(
|
||||
(all, current) => {
|
||||
for (let i = 1; i <= current.totalRuns; i++) {
|
||||
all.push({
|
||||
...current,
|
||||
delay: current.delay * i,
|
||||
});
|
||||
}
|
||||
|
||||
return all;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Check if the post is repeatable
|
||||
const repeatPost = !post.intervalInDays
|
||||
? []
|
||||
: [
|
||||
{
|
||||
type: 'repeat-post',
|
||||
delay:
|
||||
post.intervalInDays * 24 * 60 * 60 * 1000 -
|
||||
(new Date().getTime() - startTime.getTime()),
|
||||
},
|
||||
];
|
||||
|
||||
// Sort all the actions by delay, so we can process them in order
|
||||
const list = sortBy(
|
||||
[...internalPlugsList, ...globalPlugsList, ...repeatPost],
|
||||
'delay'
|
||||
);
|
||||
|
||||
// process all the plugs in order, we are using while because in some cases we need to remove items from the list
|
||||
while (list.length > 0) {
|
||||
// get the next to process
|
||||
const todo = list.shift();
|
||||
|
||||
// wait for the delay
|
||||
await sleep(Math.max(0, Number(todo.delay ?? 0)));
|
||||
|
||||
// process internal plug
|
||||
if (todo.type === 'internal-plug') {
|
||||
for (const _ of iterate) {
|
||||
try {
|
||||
await processInternalPlug({ ...todo, post: postsResults[0].postId });
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'refresh_token'
|
||||
) {
|
||||
const refresh = await refreshTokenWithCause(
|
||||
await getIntegrationById(organizationId, todo.integration),
|
||||
err?.cause?.message || ''
|
||||
);
|
||||
if (!refresh || !refresh.accessToken) {
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'bad_body'
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// process global plug
|
||||
if (todo.type === 'global') {
|
||||
for (const _ of iterate) {
|
||||
try {
|
||||
const process = await processPlug({
|
||||
...todo,
|
||||
postId: postsResults[0].postId,
|
||||
});
|
||||
if (process) {
|
||||
const toDelete = list
|
||||
.reduce((all, current, index) => {
|
||||
if (current.plugId === todo.plugId) {
|
||||
all.push(index);
|
||||
}
|
||||
|
||||
return all;
|
||||
}, [])
|
||||
.reverse();
|
||||
|
||||
for (const index of toDelete) {
|
||||
list.splice(index, 1);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'refresh_token'
|
||||
) {
|
||||
const refresh = await refreshTokenWithCause(
|
||||
post.integration,
|
||||
err?.cause?.message || ''
|
||||
);
|
||||
if (!refresh || !refresh.accessToken) {
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'bad_body'
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// process repeat post in a new workflow, this is important so the other plugs can keep running
|
||||
if (todo.type === 'repeat-post') {
|
||||
await startChild(postWorkflowV103, {
|
||||
parentClosePolicy: 'ABANDON',
|
||||
args: [
|
||||
{
|
||||
taskQueue,
|
||||
postId,
|
||||
organizationId,
|
||||
postNow: true,
|
||||
},
|
||||
],
|
||||
workflowId: `post_${post.id}_${makeId(10)}`,
|
||||
typedSearchAttributes: new TypedSearchAttributes([
|
||||
{
|
||||
key: postIdSearchParam,
|
||||
value: postId,
|
||||
},
|
||||
]),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,439 +0,0 @@
|
|||
import { PostActivity } from '@gitroom/orchestrator/activities/post.activity';
|
||||
import {
|
||||
ActivityFailure,
|
||||
ApplicationFailure,
|
||||
startChild,
|
||||
proxyActivities,
|
||||
sleep,
|
||||
defineSignal,
|
||||
setHandler,
|
||||
} from '@temporalio/workflow';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { capitalize, sortBy } from 'lodash';
|
||||
import { PostResponse } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { TypedSearchAttributes } from '@temporalio/common';
|
||||
import { postId as postIdSearchParam } from '@gitroom/nestjs-libraries/temporal/temporal.search.attribute';
|
||||
|
||||
const proxyTaskQueue = (taskQueue: string) => {
|
||||
return proxyActivities<PostActivity>({
|
||||
startToCloseTimeout: '10 minute',
|
||||
taskQueue,
|
||||
retry: {
|
||||
maximumAttempts: 3,
|
||||
backoffCoefficient: 1,
|
||||
initialInterval: '2 minutes',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
getPostsList,
|
||||
inAppNotification,
|
||||
changeState,
|
||||
updatePost,
|
||||
sendWebhooks,
|
||||
isCommentable,
|
||||
} = proxyActivities<PostActivity>({
|
||||
startToCloseTimeout: '10 minute',
|
||||
retry: {
|
||||
maximumAttempts: 3,
|
||||
backoffCoefficient: 1,
|
||||
initialInterval: '2 minutes',
|
||||
},
|
||||
});
|
||||
|
||||
const poke = defineSignal('poke');
|
||||
|
||||
const iterate = Array.from({ length: 5 });
|
||||
|
||||
export async function postWorkflowV104({
|
||||
taskQueue,
|
||||
postId,
|
||||
organizationId,
|
||||
postNow = false,
|
||||
}: {
|
||||
taskQueue: string;
|
||||
postId: string;
|
||||
organizationId: string;
|
||||
postNow?: boolean;
|
||||
}) {
|
||||
// Dynamic task queue, for concurrency
|
||||
const {
|
||||
postSocial,
|
||||
postComment,
|
||||
getIntegrationById,
|
||||
refreshTokenWithCause,
|
||||
internalPlugs,
|
||||
globalPlugs,
|
||||
processInternalPlug,
|
||||
processPlug,
|
||||
} = proxyTaskQueue(taskQueue);
|
||||
|
||||
let poked = false;
|
||||
setHandler(poke, () => {
|
||||
poked = true;
|
||||
});
|
||||
|
||||
const startTime = new Date();
|
||||
// get all the posts and comments to post
|
||||
const postsListBefore = await getPostsList(organizationId, postId);
|
||||
const [post] = postsListBefore;
|
||||
|
||||
// in case doesn't exists for some reason, fail it
|
||||
if (!post) {
|
||||
await changeState(
|
||||
postId,
|
||||
'ERROR',
|
||||
'No Post'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!postNow && post.state !== 'QUEUE') {
|
||||
await changeState(
|
||||
postsListBefore[0].id,
|
||||
'ERROR',
|
||||
'Already posted',
|
||||
postsListBefore
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// if it's a repeatable post, we should ignore this.
|
||||
if (!postNow) {
|
||||
await sleep(
|
||||
dayjs(post.publishDate).isBefore(dayjs())
|
||||
? 0
|
||||
: dayjs(post.publishDate).diff(dayjs(), 'millisecond')
|
||||
);
|
||||
}
|
||||
|
||||
// if refresh is needed from last time, let's inform the user
|
||||
if (post.integration?.refreshNeeded) {
|
||||
await inAppNotification(
|
||||
post.organizationId,
|
||||
`We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name}`,
|
||||
`We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name} because you need to reconnect it. Please enable it and try again.`,
|
||||
true,
|
||||
false,
|
||||
'info'
|
||||
);
|
||||
|
||||
await changeState(
|
||||
postsListBefore[0].id,
|
||||
'ERROR',
|
||||
'Refresh channel needed',
|
||||
postsListBefore
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// if it's disabled, inform the user
|
||||
if (post.integration?.disabled) {
|
||||
await inAppNotification(
|
||||
post.organizationId,
|
||||
`We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name}`,
|
||||
`We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name} because it's disabled. Please enable it and try again.`,
|
||||
true,
|
||||
false,
|
||||
'info'
|
||||
);
|
||||
|
||||
await changeState(
|
||||
postsListBefore[0].id,
|
||||
'ERROR',
|
||||
'Channel disabled',
|
||||
postsListBefore
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Do we need to post comment for this social?
|
||||
const toComment: boolean =
|
||||
postsListBefore.length === 1
|
||||
? false
|
||||
: await isCommentable(post.integration);
|
||||
|
||||
const postsList = toComment ? postsListBefore : [postsListBefore[0]];
|
||||
|
||||
// list of all the saved results
|
||||
const postsResults: PostResponse[] = [];
|
||||
|
||||
// iterate over the posts
|
||||
for (let i = 0; i < postsList.length; i++) {
|
||||
const before = postsResults.length;
|
||||
// this is a small trick to repeat an action in case of token refresh
|
||||
for (const _ of iterate) {
|
||||
try {
|
||||
// first post the main post
|
||||
if (i === 0) {
|
||||
postsResults.push(
|
||||
...(await postSocial(post.integration as Integration, [
|
||||
postsList[i],
|
||||
]))
|
||||
);
|
||||
|
||||
// then post the comments if any
|
||||
} else {
|
||||
if (postsList[i].delay) {
|
||||
await sleep(60000 * Math.max(0, Number(postsList[i].delay ?? 0)));
|
||||
}
|
||||
|
||||
postsResults.push(
|
||||
...(await postComment(
|
||||
postsResults[0].postId,
|
||||
postsResults.length === 1
|
||||
? undefined
|
||||
: postsResults[i - 1].postId,
|
||||
post.integration,
|
||||
[postsList[i]]
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
// mark post as successful
|
||||
await updatePost(
|
||||
postsList[i].id,
|
||||
postsResults[i].postId,
|
||||
postsResults[i].releaseURL
|
||||
);
|
||||
|
||||
if (i === 0) {
|
||||
// send notification on a sucessful post
|
||||
await inAppNotification(
|
||||
post.integration.organizationId,
|
||||
`Your post has been published on ${capitalize(
|
||||
post.integration.providerIdentifier
|
||||
)}`,
|
||||
`Your post has been published on ${capitalize(
|
||||
post.integration.providerIdentifier
|
||||
)} at ${postsResults[0].releaseURL}`,
|
||||
true,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// break the current while to move to the next post
|
||||
break;
|
||||
} catch (err) {
|
||||
// if token refresh is needed, do it and repeat
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'refresh_token'
|
||||
) {
|
||||
const refresh = await refreshTokenWithCause(
|
||||
post.integration,
|
||||
err?.cause?.message || ''
|
||||
);
|
||||
if (!refresh || !refresh.accessToken) {
|
||||
await changeState(postsList[0].id, 'ERROR', err, postsList);
|
||||
return false;
|
||||
}
|
||||
|
||||
post.integration.token = refresh.accessToken;
|
||||
continue;
|
||||
}
|
||||
|
||||
// for other errors, change state and inform the user if needed
|
||||
await changeState(postsList[0].id, 'ERROR', err, postsList);
|
||||
|
||||
// specific case for bad body errors
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'bad_body'
|
||||
) {
|
||||
await inAppNotification(
|
||||
post.organizationId,
|
||||
`Error posting${i === 0 ? ' ' : ' comments '}on ${
|
||||
post.integration?.providerIdentifier
|
||||
} for ${post?.integration?.name}`,
|
||||
`An error occurred while posting${i === 0 ? ' ' : ' comments '}on ${
|
||||
post.integration?.providerIdentifier
|
||||
}${err?.cause?.message ? `: ${err?.cause?.message}` : ``}`,
|
||||
true,
|
||||
false,
|
||||
'fail'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (postsResults.length === before) {
|
||||
// all retries exhausted without success
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// send webhooks for the post
|
||||
await sendWebhooks(
|
||||
postsResults[0].postId,
|
||||
post.organizationId,
|
||||
post.integration.id
|
||||
);
|
||||
|
||||
// load internal plugs like repost by other users
|
||||
const internalPlugsList = await internalPlugs(
|
||||
post.integration,
|
||||
JSON.parse(post.settings)
|
||||
);
|
||||
|
||||
// load global plugs, like repost a post if it gets to a certain number of likes
|
||||
const globalPlugsList = (await globalPlugs(post.integration)).reduce(
|
||||
(all, current) => {
|
||||
for (let i = 1; i <= current.totalRuns; i++) {
|
||||
all.push({
|
||||
...current,
|
||||
delay: current.delay * i,
|
||||
});
|
||||
}
|
||||
|
||||
return all;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Check if the post is repeatable
|
||||
const repeatPost = !post.intervalInDays
|
||||
? []
|
||||
: [
|
||||
{
|
||||
type: 'repeat-post',
|
||||
delay:
|
||||
post.intervalInDays * 24 * 60 * 60 * 1000 -
|
||||
(new Date().getTime() - startTime.getTime()),
|
||||
},
|
||||
];
|
||||
|
||||
// Sort all the actions by delay, so we can process them in order
|
||||
const list = sortBy(
|
||||
[...internalPlugsList, ...globalPlugsList, ...repeatPost],
|
||||
'delay'
|
||||
);
|
||||
|
||||
// process all the plugs in order, we are using while because in some cases we need to remove items from the list
|
||||
while (list.length > 0) {
|
||||
// get the next to process
|
||||
const todo = list.shift();
|
||||
|
||||
// wait for the delay
|
||||
await sleep(Math.max(0, Number(todo.delay ?? 0)));
|
||||
|
||||
// process internal plug
|
||||
if (todo.type === 'internal-plug') {
|
||||
for (const _ of iterate) {
|
||||
try {
|
||||
await processInternalPlug({ ...todo, post: postsResults[0].postId });
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'refresh_token'
|
||||
) {
|
||||
const refresh = await refreshTokenWithCause(
|
||||
await getIntegrationById(organizationId, todo.integration),
|
||||
err?.cause?.message || ''
|
||||
);
|
||||
if (!refresh || !refresh.accessToken) {
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'bad_body'
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// process global plug
|
||||
if (todo.type === 'global') {
|
||||
for (const _ of iterate) {
|
||||
try {
|
||||
const process = await processPlug({
|
||||
...todo,
|
||||
postId: postsResults[0].postId,
|
||||
});
|
||||
if (process) {
|
||||
const toDelete = list
|
||||
.reduce((all, current, index) => {
|
||||
if (current.plugId === todo.plugId) {
|
||||
all.push(index);
|
||||
}
|
||||
|
||||
return all;
|
||||
}, [])
|
||||
.reverse();
|
||||
|
||||
for (const index of toDelete) {
|
||||
list.splice(index, 1);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'refresh_token'
|
||||
) {
|
||||
const refresh = await refreshTokenWithCause(
|
||||
post.integration,
|
||||
err?.cause?.message || ''
|
||||
);
|
||||
if (!refresh || !refresh.accessToken) {
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'bad_body'
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// process repeat post in a new workflow, this is important so the other plugs can keep running
|
||||
if (todo.type === 'repeat-post') {
|
||||
await startChild(postWorkflowV104, {
|
||||
parentClosePolicy: 'ABANDON',
|
||||
args: [
|
||||
{
|
||||
taskQueue,
|
||||
postId,
|
||||
organizationId,
|
||||
postNow: true,
|
||||
},
|
||||
],
|
||||
workflowId: `post_${post.id}_${makeId(10)}`,
|
||||
typedSearchAttributes: new TypedSearchAttributes([
|
||||
{
|
||||
key: postIdSearchParam,
|
||||
value: postId,
|
||||
},
|
||||
]),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,438 +0,0 @@
|
|||
import { PostActivity } from '@gitroom/orchestrator/activities/post.activity';
|
||||
import {
|
||||
ActivityFailure,
|
||||
ApplicationFailure,
|
||||
startChild,
|
||||
proxyActivities,
|
||||
sleep,
|
||||
defineSignal,
|
||||
setHandler,
|
||||
} from '@temporalio/workflow';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { capitalize, sortBy } from 'lodash';
|
||||
import { PostResponse } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { TypedSearchAttributes } from '@temporalio/common';
|
||||
import { postId as postIdSearchParam } from '@gitroom/nestjs-libraries/temporal/temporal.search.attribute';
|
||||
|
||||
const proxyTaskQueue = (taskQueue: string) => {
|
||||
return proxyActivities<PostActivity>({
|
||||
startToCloseTimeout: '10 minute',
|
||||
taskQueue,
|
||||
retry: {
|
||||
maximumAttempts: 3,
|
||||
backoffCoefficient: 1,
|
||||
initialInterval: '2 minutes',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
getPostsList,
|
||||
getPost,
|
||||
inAppNotification,
|
||||
changeState,
|
||||
updatePost,
|
||||
sendWebhooks,
|
||||
isCommentable,
|
||||
} = proxyActivities<PostActivity>({
|
||||
startToCloseTimeout: '10 minute',
|
||||
retry: {
|
||||
maximumAttempts: 3,
|
||||
backoffCoefficient: 1,
|
||||
initialInterval: '2 minutes',
|
||||
},
|
||||
});
|
||||
|
||||
const poke = defineSignal('poke');
|
||||
|
||||
const iterate = Array.from({ length: 5 });
|
||||
|
||||
export async function postWorkflowV105({
|
||||
taskQueue,
|
||||
postId,
|
||||
organizationId,
|
||||
postNow = false,
|
||||
}: {
|
||||
taskQueue: string;
|
||||
postId: string;
|
||||
organizationId: string;
|
||||
postNow?: boolean;
|
||||
}) {
|
||||
// Dynamic task queue, for concurrency
|
||||
const {
|
||||
postSocial,
|
||||
postComment,
|
||||
getIntegrationById,
|
||||
refreshTokenWithCause,
|
||||
internalPlugs,
|
||||
globalPlugs,
|
||||
processInternalPlug,
|
||||
processPlug,
|
||||
} = proxyTaskQueue(taskQueue);
|
||||
|
||||
let poked = false;
|
||||
setHandler(poke, () => {
|
||||
poked = true;
|
||||
});
|
||||
|
||||
const startTime = new Date();
|
||||
// get all the posts and comments to post
|
||||
const firstPost = await getPost(organizationId, postId);
|
||||
|
||||
// in case doesn't exists for some reason, fail it
|
||||
if (!firstPost) {
|
||||
await changeState(postId, 'ERROR', 'No Post');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!postNow && firstPost.state !== 'QUEUE') {
|
||||
await changeState(firstPost.id, 'ERROR', 'Already posted', [firstPost]);
|
||||
return;
|
||||
}
|
||||
|
||||
// if it's a repeatable post, we should ignore this.
|
||||
if (!postNow) {
|
||||
await sleep(
|
||||
dayjs(firstPost.publishDate).isBefore(dayjs())
|
||||
? 0
|
||||
: dayjs(firstPost.publishDate).diff(dayjs(), 'millisecond')
|
||||
);
|
||||
}
|
||||
|
||||
const postsListBefore = await getPostsList(organizationId, postId);
|
||||
const [post] = postsListBefore;
|
||||
|
||||
if (!post) {
|
||||
await changeState(postId, 'ERROR', 'No Post');
|
||||
return;
|
||||
}
|
||||
|
||||
// if refresh is needed from last time, let's inform the user
|
||||
if (post.integration?.refreshNeeded) {
|
||||
await inAppNotification(
|
||||
post.organizationId,
|
||||
`We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name}`,
|
||||
`We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name} because you need to reconnect it. Please enable it and try again.`,
|
||||
true,
|
||||
false,
|
||||
'info'
|
||||
);
|
||||
|
||||
await changeState(
|
||||
postsListBefore[0].id,
|
||||
'ERROR',
|
||||
'Refresh channel needed',
|
||||
postsListBefore
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// if it's disabled, inform the user
|
||||
if (post.integration?.disabled) {
|
||||
await inAppNotification(
|
||||
post.organizationId,
|
||||
`We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name}`,
|
||||
`We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name} because it's disabled. Please enable it and try again.`,
|
||||
true,
|
||||
false,
|
||||
'info'
|
||||
);
|
||||
|
||||
await changeState(
|
||||
postsListBefore[0].id,
|
||||
'ERROR',
|
||||
'Channel disabled',
|
||||
postsListBefore
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Do we need to post comment for this social?
|
||||
const toComment: boolean =
|
||||
postsListBefore.length === 1
|
||||
? false
|
||||
: await isCommentable(post.integration);
|
||||
|
||||
const postsList = toComment ? postsListBefore : [postsListBefore[0]];
|
||||
|
||||
// list of all the saved results
|
||||
const postsResults: PostResponse[] = [];
|
||||
|
||||
// iterate over the posts
|
||||
for (let i = 0; i < postsList.length; i++) {
|
||||
const before = postsResults.length;
|
||||
// this is a small trick to repeat an action in case of token refresh
|
||||
for (const _ of iterate) {
|
||||
try {
|
||||
// first post the main post
|
||||
if (i === 0) {
|
||||
postsResults.push(
|
||||
...(await postSocial(post.integration as Integration, [
|
||||
postsList[i],
|
||||
]))
|
||||
);
|
||||
|
||||
// then post the comments if any
|
||||
} else {
|
||||
if (postsList[i].delay) {
|
||||
await sleep(60000 * Math.max(0, Number(postsList[i].delay ?? 0)));
|
||||
}
|
||||
|
||||
postsResults.push(
|
||||
...(await postComment(
|
||||
postsResults[0].postId,
|
||||
postsResults.length === 1
|
||||
? undefined
|
||||
: postsResults[i - 1].postId,
|
||||
post.integration,
|
||||
[postsList[i]]
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
// mark post as successful
|
||||
await updatePost(
|
||||
postsList[i].id,
|
||||
postsResults[i].postId,
|
||||
postsResults[i].releaseURL
|
||||
);
|
||||
|
||||
if (i === 0) {
|
||||
// send notification on a sucessful post
|
||||
await inAppNotification(
|
||||
post.integration.organizationId,
|
||||
`Your post has been published on ${capitalize(
|
||||
post.integration.providerIdentifier
|
||||
)}`,
|
||||
`Your post has been published on ${capitalize(
|
||||
post.integration.providerIdentifier
|
||||
)} at ${postsResults[0].releaseURL}`,
|
||||
true,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// break the current while to move to the next post
|
||||
break;
|
||||
} catch (err) {
|
||||
// if token refresh is needed, do it and repeat
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'refresh_token'
|
||||
) {
|
||||
const refresh = await refreshTokenWithCause(
|
||||
post.integration,
|
||||
err?.cause?.message || ''
|
||||
);
|
||||
if (!refresh || !refresh.accessToken) {
|
||||
await changeState(postsList[0].id, 'ERROR', err, postsList);
|
||||
return false;
|
||||
}
|
||||
|
||||
post.integration.token = refresh.accessToken;
|
||||
continue;
|
||||
}
|
||||
|
||||
// for other errors, change state and inform the user if needed
|
||||
await changeState(postsList[0].id, 'ERROR', err, postsList);
|
||||
|
||||
// specific case for bad body errors
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'bad_body'
|
||||
) {
|
||||
await inAppNotification(
|
||||
post.organizationId,
|
||||
`Error posting${i === 0 ? ' ' : ' comments '}on ${
|
||||
post.integration?.providerIdentifier
|
||||
} for ${post?.integration?.name}`,
|
||||
`An error occurred while posting${i === 0 ? ' ' : ' comments '}on ${
|
||||
post.integration?.providerIdentifier
|
||||
}${err?.cause?.message ? `: ${err?.cause?.message}` : ``}`,
|
||||
true,
|
||||
false,
|
||||
'fail'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (postsResults.length === before) {
|
||||
// all retries exhausted without success
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// send webhooks for the post
|
||||
await sendWebhooks(
|
||||
postsResults[0].postId,
|
||||
post.organizationId,
|
||||
post.integration.id
|
||||
);
|
||||
|
||||
// load internal plugs like repost by other users
|
||||
const internalPlugsList = await internalPlugs(
|
||||
post.integration,
|
||||
JSON.parse(post.settings)
|
||||
);
|
||||
|
||||
// load global plugs, like repost a post if it gets to a certain number of likes
|
||||
const globalPlugsList = (await globalPlugs(post.integration)).reduce(
|
||||
(all, current) => {
|
||||
for (let i = 1; i <= current.totalRuns; i++) {
|
||||
all.push({
|
||||
...current,
|
||||
delay: current.delay * i,
|
||||
});
|
||||
}
|
||||
|
||||
return all;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Check if the post is repeatable
|
||||
const repeatPost = !post.intervalInDays
|
||||
? []
|
||||
: [
|
||||
{
|
||||
type: 'repeat-post',
|
||||
delay:
|
||||
post.intervalInDays * 24 * 60 * 60 * 1000 -
|
||||
(new Date().getTime() - startTime.getTime()),
|
||||
},
|
||||
];
|
||||
|
||||
// Sort all the actions by delay, so we can process them in order
|
||||
const list = sortBy(
|
||||
[...internalPlugsList, ...globalPlugsList, ...repeatPost],
|
||||
'delay'
|
||||
);
|
||||
|
||||
// process all the plugs in order, we are using while because in some cases we need to remove items from the list
|
||||
while (list.length > 0) {
|
||||
// get the next to process
|
||||
const todo = list.shift();
|
||||
|
||||
// wait for the delay
|
||||
await sleep(Math.max(0, Number(todo.delay ?? 0)));
|
||||
|
||||
// process internal plug
|
||||
if (todo.type === 'internal-plug') {
|
||||
for (const _ of iterate) {
|
||||
try {
|
||||
await processInternalPlug({ ...todo, post: postsResults[0].postId });
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'refresh_token'
|
||||
) {
|
||||
const refresh = await refreshTokenWithCause(
|
||||
await getIntegrationById(organizationId, todo.integration),
|
||||
err?.cause?.message || ''
|
||||
);
|
||||
if (!refresh || !refresh.accessToken) {
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'bad_body'
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// process global plug
|
||||
if (todo.type === 'global') {
|
||||
for (const _ of iterate) {
|
||||
try {
|
||||
const process = await processPlug({
|
||||
...todo,
|
||||
postId: postsResults[0].postId,
|
||||
});
|
||||
if (process) {
|
||||
const toDelete = list
|
||||
.reduce((all, current, index) => {
|
||||
if (current.plugId === todo.plugId) {
|
||||
all.push(index);
|
||||
}
|
||||
|
||||
return all;
|
||||
}, [])
|
||||
.reverse();
|
||||
|
||||
for (const index of toDelete) {
|
||||
list.splice(index, 1);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'refresh_token'
|
||||
) {
|
||||
const refresh = await refreshTokenWithCause(
|
||||
post.integration,
|
||||
err?.cause?.message || ''
|
||||
);
|
||||
if (!refresh || !refresh.accessToken) {
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'bad_body'
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// process repeat post in a new workflow, this is important so the other plugs can keep running
|
||||
if (todo.type === 'repeat-post') {
|
||||
await startChild(postWorkflowV105, {
|
||||
parentClosePolicy: 'ABANDON',
|
||||
args: [
|
||||
{
|
||||
taskQueue,
|
||||
postId,
|
||||
organizationId,
|
||||
postNow: true,
|
||||
},
|
||||
],
|
||||
workflowId: `post_${post.id}_${makeId(10)}`,
|
||||
typedSearchAttributes: new TypedSearchAttributes([
|
||||
{
|
||||
key: postIdSearchParam,
|
||||
value: postId,
|
||||
},
|
||||
]),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
export const hasExtension = (
|
||||
path: string | undefined | null,
|
||||
extension: string
|
||||
): boolean => {
|
||||
if (!path) {
|
||||
return false;
|
||||
}
|
||||
const ext = extension.startsWith('.') ? extension : `.${extension}`;
|
||||
return path.toLowerCase().indexOf(ext.toLowerCase()) > -1;
|
||||
};
|
||||
|
|
@ -25,7 +25,6 @@ const POST_ITEM_KEYS: Record<string, string> = {
|
|||
integration: 'n',
|
||||
intervalInDays: 'iv',
|
||||
actualDate: 'ad',
|
||||
creationMethod: 'cm',
|
||||
};
|
||||
|
||||
const INTEGRATION_KEYS: Record<string, string> = {
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
// Keep this in sync with the URL detection used by the short linking service
|
||||
const urlRegex = () =>
|
||||
/(https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*))/gm;
|
||||
|
||||
export function hasLinks(text?: string | null): boolean {
|
||||
return !!(text || '').match(urlRegex());
|
||||
}
|
||||
|
||||
export function stripLinks(text?: string | null): string {
|
||||
return (text || '')
|
||||
.replace(urlRegex(), '')
|
||||
// collapse the whitespace / empty anchor leftovers the removed link left behind
|
||||
.replace(/<a\b[^>]*>\s*<\/a>/gi, '')
|
||||
.replace(/[ \t]{2,}/g, ' ')
|
||||
.replace(/ +\n/g, '\n')
|
||||
.trim();
|
||||
}
|
||||
|
|
@ -223,7 +223,7 @@ If the tools return errors, you would need to rerun it with the right parameters
|
|||
})),
|
||||
},
|
||||
],
|
||||
}, 'MCP');
|
||||
});
|
||||
finalOutput.push(...output);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ export class AutopostService {
|
|||
},
|
||||
],
|
||||
})),
|
||||
}, 'AUTOPOST');
|
||||
});
|
||||
}
|
||||
|
||||
async updateUrl(state: WorkflowChannelsState) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Post as PostBody } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
|
||||
import {
|
||||
APPROVED_SUBMIT_FOR_ORDER,
|
||||
CreationMethod,
|
||||
Post,
|
||||
State,
|
||||
} from '@prisma/client';
|
||||
import { APPROVED_SUBMIT_FOR_ORDER, Post, State } from '@prisma/client';
|
||||
import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
|
||||
import { GetPostsListDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.list.dto';
|
||||
import dayjs from 'dayjs';
|
||||
|
|
@ -40,11 +35,10 @@ export class PostsRepository {
|
|||
refreshNeeded: false,
|
||||
inBetweenSteps: false,
|
||||
disabled: false,
|
||||
deletedAt: null,
|
||||
},
|
||||
publishDate: {
|
||||
gte: dayjs.utc().subtract(2, 'day').toDate(),
|
||||
lt: dayjs.utc().toDate(),
|
||||
gte: dayjs.utc().subtract(2, 'hour').toDate(),
|
||||
lt: dayjs.utc().add(2, 'hour').toDate(),
|
||||
},
|
||||
state: 'QUEUE',
|
||||
deletedAt: null,
|
||||
|
|
@ -179,7 +173,6 @@ export class PostsRepository {
|
|||
state: true,
|
||||
intervalInDays: true,
|
||||
group: true,
|
||||
creationMethod: true,
|
||||
tags: {
|
||||
select: {
|
||||
tag: true,
|
||||
|
|
@ -224,26 +217,6 @@ export class PostsRepository {
|
|||
const limit = query.limit || 20;
|
||||
const skip = page * limit;
|
||||
|
||||
const stateFilter = query.state || 'all';
|
||||
const stateAndDate =
|
||||
stateFilter === 'scheduled'
|
||||
? {
|
||||
state: State.QUEUE,
|
||||
publishDate: { gte: dayjs.utc().toDate() },
|
||||
}
|
||||
: stateFilter === 'draft'
|
||||
? { state: State.DRAFT }
|
||||
: stateFilter === 'published'
|
||||
? { state: State.PUBLISHED }
|
||||
: {
|
||||
state: {
|
||||
in: [State.QUEUE, State.DRAFT, State.PUBLISHED, State.ERROR],
|
||||
},
|
||||
};
|
||||
|
||||
const orderDirection: 'asc' | 'desc' =
|
||||
stateFilter === 'published' ? 'desc' : 'asc';
|
||||
|
||||
const where = {
|
||||
AND: [
|
||||
{
|
||||
|
|
@ -253,8 +226,12 @@ export class PostsRepository {
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
publishDate: {
|
||||
gte: dayjs.utc().toDate(),
|
||||
},
|
||||
},
|
||||
],
|
||||
...stateAndDate,
|
||||
deletedAt: null as Date | null,
|
||||
parentPostId: null as string | null,
|
||||
intervalInDays: null as number | null,
|
||||
|
|
@ -273,7 +250,7 @@ export class PostsRepository {
|
|||
skip,
|
||||
take: limit,
|
||||
orderBy: {
|
||||
publishDate: orderDirection,
|
||||
publishDate: 'asc',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
|
|
@ -283,7 +260,6 @@ export class PostsRepository {
|
|||
releaseId: true,
|
||||
state: true,
|
||||
group: true,
|
||||
creationMethod: true,
|
||||
tags: {
|
||||
select: {
|
||||
tag: true,
|
||||
|
|
@ -507,7 +483,6 @@ export class PostsRepository {
|
|||
date: string,
|
||||
body: PostBody,
|
||||
tags: { value: string; label: string }[],
|
||||
creationMethod: CreationMethod,
|
||||
inter?: number
|
||||
) {
|
||||
const posts: Post[] = [];
|
||||
|
|
@ -542,7 +517,6 @@ export class PostsRepository {
|
|||
group: uuid,
|
||||
intervalInDays: inter ? +inter : null,
|
||||
approvedSubmitForOrder: APPROVED_SUBMIT_FOR_ORDER.NO,
|
||||
...(type === 'create' ? { creationMethod } : {}),
|
||||
...(state === 'update'
|
||||
? {}
|
||||
: {
|
||||
|
|
|
|||
|
|
@ -7,14 +7,7 @@ import { PostsRepository } from '@gitroom/nestjs-libraries/database/prisma/posts
|
|||
import { CreatePostDto } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
|
||||
import dayjs from 'dayjs';
|
||||
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import {
|
||||
Integration,
|
||||
Post,
|
||||
Media,
|
||||
From,
|
||||
CreationMethod,
|
||||
State,
|
||||
} from '@prisma/client';
|
||||
import { Integration, Post, Media, From, State } from '@prisma/client';
|
||||
import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
|
||||
import { GetPostsListDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.list.dto';
|
||||
import { shuffle } from 'lodash';
|
||||
|
|
@ -25,10 +18,7 @@ import utc from 'dayjs/plugin/utc';
|
|||
import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
|
||||
import { ShortLinkService } from '@gitroom/nestjs-libraries/short-linking/short.link.service';
|
||||
import { CreateTagDto } from '@gitroom/nestjs-libraries/dtos/posts/create.tag.dto';
|
||||
import {
|
||||
minifyPostsList,
|
||||
minifyPosts,
|
||||
} from '@gitroom/helpers/utils/posts.list.minify';
|
||||
import { minifyPostsList, minifyPosts } from '@gitroom/helpers/utils/posts.list.minify';
|
||||
import axios from 'axios';
|
||||
import sharp from 'sharp';
|
||||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
|
|
@ -47,8 +37,6 @@ import { timer } from '@gitroom/helpers/utils/timer';
|
|||
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
||||
import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service';
|
||||
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
|
||||
import { stripLinks } from '@gitroom/helpers/utils/strip.links';
|
||||
|
||||
type PostWithConditionals = Post & {
|
||||
integration?: Integration;
|
||||
|
|
@ -137,10 +125,6 @@ export class PostsService {
|
|||
return [];
|
||||
}
|
||||
|
||||
async getPostById(postId: string, orgId: string) {
|
||||
return this._postRepository.getPostById(postId, orgId);
|
||||
}
|
||||
|
||||
async updateReleaseId(orgId: string, postId: string, releaseId: string) {
|
||||
return this._postRepository.updateReleaseId(postId, orgId, releaseId);
|
||||
}
|
||||
|
|
@ -375,7 +359,7 @@ export class PostsService {
|
|||
return m;
|
||||
}
|
||||
|
||||
if (hasExtension(m.path, 'png')) {
|
||||
if (m.path.indexOf('.png') > -1) {
|
||||
imageUpdateNeeded = true;
|
||||
const response = await axios.get(m.url, {
|
||||
responseType: 'arraybuffer',
|
||||
|
|
@ -722,7 +706,7 @@ export class PostsService {
|
|||
try {
|
||||
await this._temporalService.client
|
||||
.getRawClient()
|
||||
?.workflow.start('postWorkflowV105', {
|
||||
?.workflow.start('postWorkflowV102', {
|
||||
workflowId: `post_${postId}`,
|
||||
taskQueue: 'main',
|
||||
workflowIdConflictPolicy: 'TERMINATE_EXISTING',
|
||||
|
|
@ -747,31 +731,17 @@ export class PostsService {
|
|||
} catch (err) {}
|
||||
}
|
||||
|
||||
async createPost(
|
||||
orgId: string,
|
||||
body: CreatePostDto,
|
||||
creationMethod: CreationMethod
|
||||
): Promise<any[]> {
|
||||
async createPost(orgId: string, body: CreatePostDto): Promise<any[]> {
|
||||
const postList = [];
|
||||
for (const post of body.posts) {
|
||||
const provider = this._integrationManager.getSocialIntegration(
|
||||
(post.settings as any)?.__type
|
||||
);
|
||||
const removeLinks = !!provider?.stripLinks?.();
|
||||
|
||||
const messages = (post.value || []).map((p) => p.content);
|
||||
// No point shortlinking links on platforms that strip them out anyway
|
||||
const updateContent =
|
||||
!body.shortLink || removeLinks
|
||||
? messages
|
||||
: await this._shortLinkService.convertTextToShortLinks(
|
||||
orgId,
|
||||
messages
|
||||
);
|
||||
const updateContent = !body.shortLink
|
||||
? messages
|
||||
: await this._shortLinkService.convertTextToShortLinks(orgId, messages);
|
||||
|
||||
post.value = (post.value || []).map((p, i) => ({
|
||||
...p,
|
||||
content: removeLinks ? stripLinks(updateContent[i]) : updateContent[i],
|
||||
content: updateContent[i],
|
||||
}));
|
||||
|
||||
const { posts } = await this._postRepository.createOrUpdatePost(
|
||||
|
|
@ -780,7 +750,6 @@ export class PostsService {
|
|||
body.type === 'now' ? dayjs().format('YYYY-MM-DDTHH:mm:00') : body.date,
|
||||
post,
|
||||
body.tags,
|
||||
creationMethod,
|
||||
body.inter
|
||||
);
|
||||
|
||||
|
|
@ -861,9 +830,7 @@ export class PostsService {
|
|||
if (action === 'schedule') {
|
||||
try {
|
||||
await this.startWorkflow(
|
||||
getPostById.integration.providerIdentifier
|
||||
.split('-')[0]
|
||||
.toLowerCase(),
|
||||
getPostById.integration.providerIdentifier.split('-')[0].toLowerCase(),
|
||||
getPostById.id,
|
||||
orgId,
|
||||
getPostById.state === 'DRAFT' ? 'DRAFT' : 'QUEUE'
|
||||
|
|
@ -914,47 +881,43 @@ export class PostsService {
|
|||
const group = makeId(10);
|
||||
const randomDate = findTime();
|
||||
|
||||
await this.createPost(
|
||||
orgId,
|
||||
{
|
||||
type: 'draft',
|
||||
date: randomDate,
|
||||
order: '',
|
||||
shortLink: false,
|
||||
tags: [],
|
||||
posts: [
|
||||
{
|
||||
group,
|
||||
integration: {
|
||||
id: integration.id,
|
||||
},
|
||||
settings: {
|
||||
__type: integration.providerIdentifier as any,
|
||||
title: '',
|
||||
tags: [],
|
||||
subreddit: [],
|
||||
},
|
||||
value: [
|
||||
...toPost.list.map((l) => ({
|
||||
id: '',
|
||||
content: l.post,
|
||||
delay: 0,
|
||||
image: [],
|
||||
})),
|
||||
{
|
||||
id: '',
|
||||
delay: 0,
|
||||
content: `Check out the full story here:\n${
|
||||
body.postId || body.url
|
||||
}`,
|
||||
image: [],
|
||||
},
|
||||
],
|
||||
await this.createPost(orgId, {
|
||||
type: 'draft',
|
||||
date: randomDate,
|
||||
order: '',
|
||||
shortLink: false,
|
||||
tags: [],
|
||||
posts: [
|
||||
{
|
||||
group,
|
||||
integration: {
|
||||
id: integration.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
'WEB'
|
||||
);
|
||||
settings: {
|
||||
__type: integration.providerIdentifier as any,
|
||||
title: '',
|
||||
tags: [],
|
||||
subreddit: [],
|
||||
},
|
||||
value: [
|
||||
...toPost.list.map((l) => ({
|
||||
id: '',
|
||||
content: l.post,
|
||||
delay: 0,
|
||||
image: [],
|
||||
})),
|
||||
{
|
||||
id: '',
|
||||
delay: 0,
|
||||
content: `Check out the full story here:\n${
|
||||
body.postId || body.url
|
||||
}`,
|
||||
image: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -409,7 +409,6 @@ model Post {
|
|||
submittedForOrderId String?
|
||||
submittedForOrganizationId String?
|
||||
approvedSubmitForOrder APPROVED_SUBMIT_FOR_ORDER @default(NO)
|
||||
creationMethod CreationMethod @default(UNKNOWN)
|
||||
lastMessageId String?
|
||||
intervalInDays Int?
|
||||
error String?
|
||||
|
|
@ -437,7 +436,6 @@ model Post {
|
|||
@@index([submittedForOrderId])
|
||||
@@index([intervalInDays])
|
||||
@@index([approvedSubmitForOrder])
|
||||
@@index([creationMethod])
|
||||
@@index([lastMessageId])
|
||||
@@index([createdAt])
|
||||
@@index([updatedAt])
|
||||
|
|
@ -940,15 +938,6 @@ enum APPROVED_SUBMIT_FOR_ORDER {
|
|||
YES
|
||||
}
|
||||
|
||||
enum CreationMethod {
|
||||
UNKNOWN
|
||||
WEB
|
||||
MCP
|
||||
API
|
||||
AUTOPOST
|
||||
CLI
|
||||
}
|
||||
|
||||
enum ShortLinkPreference {
|
||||
ASK
|
||||
YES
|
||||
|
|
|
|||
|
|
@ -4,12 +4,9 @@ import {
|
|||
IsNumber,
|
||||
Min,
|
||||
Max,
|
||||
IsIn,
|
||||
} from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export type PostListStateFilter = 'all' | 'scheduled' | 'draft' | 'published';
|
||||
|
||||
export class GetPostsListDto {
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
|
|
@ -27,8 +24,4 @@ export class GetPostsListDto {
|
|||
@IsOptional()
|
||||
@IsString()
|
||||
customer?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['all', 'scheduled', 'draft', 'published'])
|
||||
state?: PostListStateFilter = 'all';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import { timer } from '@gitroom/helpers/utils/timer';
|
|||
import axios from 'axios';
|
||||
import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
|
||||
|
||||
async function reduceImageBySize(url: string, maxSizeKB = 976) {
|
||||
try {
|
||||
|
|
@ -267,9 +266,9 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
): Promise<{ embed: any; images: any[] }> {
|
||||
// Separate images and videos
|
||||
const imageMedia =
|
||||
post.media?.filter((p) => !hasExtension(p.path, 'mp4')) || [];
|
||||
post.media?.filter((p) => p.path.indexOf('mp4') === -1) || [];
|
||||
const videoMedia =
|
||||
post.media?.filter((p) => hasExtension(p.path, 'mp4')) || [];
|
||||
post.media?.filter((p) => p.path.indexOf('mp4') !== -1) || [];
|
||||
|
||||
// Upload images
|
||||
const images = await Promise.all(
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.ab
|
|||
import { FacebookDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/facebook.dto';
|
||||
import { DribbbleDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dribbble.dto';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
|
||||
|
||||
export class FacebookProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'facebook';
|
||||
|
|
@ -416,7 +415,7 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
let finalId = '';
|
||||
let finalUrl = '';
|
||||
if (hasExtension(firstPost?.media?.[0]?.path, 'mp4')) {
|
||||
if ((firstPost?.media?.[0]?.path?.indexOf('mp4') || -2) > -1) {
|
||||
const {
|
||||
id: videoId,
|
||||
permalink_url,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.ab
|
|||
import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
|
||||
|
||||
@Rules(
|
||||
"Instagram should have at least one attachment, if it's a story, it can have only one picture"
|
||||
|
|
@ -545,21 +544,22 @@ export class InstagramProvider
|
|||
(firstPost?.media?.length || 0) > 1 && !isStory
|
||||
? `&is_carousel_item=true`
|
||||
: ``;
|
||||
const mediaType = hasExtension(m.path, 'mp4')
|
||||
? firstPost?.media?.length === 1
|
||||
? isStory
|
||||
const mediaType =
|
||||
m.path.indexOf('.mp4') > -1
|
||||
? firstPost?.media?.length === 1
|
||||
? isStory
|
||||
? `video_url=${m.path}&media_type=STORIES`
|
||||
: `video_url=${m.path}&media_type=REELS&thumb_offset=${
|
||||
m?.thumbnailTimestamp || 0
|
||||
}`
|
||||
: isStory
|
||||
? `video_url=${m.path}&media_type=STORIES`
|
||||
: `video_url=${m.path}&media_type=REELS&thumb_offset=${
|
||||
: `video_url=${m.path}&media_type=VIDEO&thumb_offset=${
|
||||
m?.thumbnailTimestamp || 0
|
||||
}`
|
||||
: isStory
|
||||
? `video_url=${m.path}&media_type=STORIES`
|
||||
: `video_url=${m.path}&media_type=VIDEO&thumb_offset=${
|
||||
m?.thumbnailTimestamp || 0
|
||||
}`
|
||||
: isStory
|
||||
? `image_url=${m.path}&media_type=STORIES`
|
||||
: `image_url=${m.path}`;
|
||||
? `image_url=${m.path}&media_type=STORIES`
|
||||
: `image_url=${m.path}`;
|
||||
|
||||
const trialParams = isTrialReel
|
||||
? `&trial_params=${encodeURIComponent(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
|||
import sharp from 'sharp';
|
||||
import { lookup } from 'mime-types';
|
||||
import { readOrFetch } from '@gitroom/helpers/utils/read.or.fetch';
|
||||
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { PostPlug } from '@gitroom/helpers/decorators/post.plug';
|
||||
|
|
@ -235,8 +234,8 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
type = 'personal' as 'company' | 'personal'
|
||||
) {
|
||||
// Determine the appropriate endpoint based on file type
|
||||
const isVideo = hasExtension(fileName, 'mp4');
|
||||
const isPdf = hasExtension(fileName, 'pdf');
|
||||
const isVideo = fileName.indexOf('mp4') > -1;
|
||||
const isPdf = fileName.toLowerCase().indexOf('pdf') > -1;
|
||||
|
||||
let endpoint: string;
|
||||
if (isVideo) {
|
||||
|
|
@ -478,7 +477,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
}
|
||||
|
||||
private async prepareMediaBuffer(mediaUrl: string): Promise<Buffer> {
|
||||
const isVideo = hasExtension(mediaUrl, 'mp4');
|
||||
const isVideo = mediaUrl.indexOf('mp4') > -1;
|
||||
const isGif = lookup(mediaUrl) === 'image/gif';
|
||||
|
||||
if (isVideo || isGif) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import dayjs from 'dayjs';
|
|||
import { Integration } from '@prisma/client';
|
||||
import { MeweDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/mewe.dto';
|
||||
import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator';
|
||||
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
|
||||
|
||||
export class MeweProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'mewe';
|
||||
|
|
@ -245,7 +244,7 @@ export class MeweProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
// Upload photos if present (exclude videos)
|
||||
const imageMedia =
|
||||
firstPost.media?.filter((m) => !m.path || !hasExtension(m.path, 'mp4')) ||
|
||||
firstPost.media?.filter((m) => !m.path || m.path.indexOf('mp4') === -1) ||
|
||||
[];
|
||||
|
||||
const uploadedPhotoIds: string[] = [];
|
||||
|
|
|
|||
|
|
@ -10,17 +10,13 @@ import { PinterestSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/provi
|
|||
import axios from 'axios';
|
||||
import FormData from 'form-data';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import {
|
||||
BadBody,
|
||||
SocialAbstract,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import dayjs from 'dayjs';
|
||||
import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
|
||||
|
||||
@Rules(
|
||||
'Pinterest requires at least one media, if posting a video, you must have two attachment, one for video, one for the cover picture, When posting a video, there can be only one, if posting images, there can be maximum 5'
|
||||
'Pinterest requires at least one media, if posting a video, you must have two attachment, one for video, one for the cover picture, When posting a video, there can be only one'
|
||||
)
|
||||
export class PinterestProvider
|
||||
extends SocialAbstract
|
||||
|
|
@ -51,12 +47,6 @@ export class PinterestProvider
|
|||
value: string;
|
||||
}
|
||||
| undefined {
|
||||
if (body.indexOf('constraint: maxItems=5') > -1) {
|
||||
return {
|
||||
type: 'bad-body' as const,
|
||||
value: 'You can upload a maximum of 5 images per post on Pinterest.',
|
||||
};
|
||||
}
|
||||
if (body.indexOf('cover_image_url or cover_image_content_type') > -1) {
|
||||
return {
|
||||
type: 'bad-body' as const,
|
||||
|
|
@ -191,11 +181,11 @@ export class PinterestProvider
|
|||
postDetails: PostDetails<PinterestSettingsDto>[]
|
||||
): Promise<PostResponse[]> {
|
||||
let mediaId = '';
|
||||
const findMp4 = postDetails?.[0]?.media?.find((p) =>
|
||||
hasExtension(p.path, 'mp4')
|
||||
const findMp4 = postDetails?.[0]?.media?.find(
|
||||
(p) => (p.path?.indexOf('mp4') || -1) > -1
|
||||
);
|
||||
const picture = postDetails?.[0]?.media?.find(
|
||||
(p) => !hasExtension(p.path, 'mp4')
|
||||
(p) => (p.path?.indexOf('mp4') || -1) === -1
|
||||
);
|
||||
|
||||
if (findMp4) {
|
||||
|
|
@ -246,15 +236,6 @@ export class PinterestProvider
|
|||
)
|
||||
).json();
|
||||
|
||||
if (mediafile.status === 'failed') {
|
||||
throw new BadBody(
|
||||
'pinterest',
|
||||
JSON.stringify({}),
|
||||
{} as any,
|
||||
'The file is corrupted and cannot be uploaded'
|
||||
);
|
||||
}
|
||||
|
||||
await timer(30000);
|
||||
statusCode = mediafile.status;
|
||||
}
|
||||
|
|
@ -298,9 +279,7 @@ export class PinterestProvider
|
|||
}
|
||||
: {
|
||||
source_type: 'multiple_image_urls',
|
||||
items: mapImages.map((m) => ({
|
||||
url: m.path,
|
||||
})),
|
||||
items: mapImages,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
|
@ -435,9 +414,7 @@ export class PinterestProvider
|
|||
result.push({
|
||||
label: 'Outbound Clicks',
|
||||
percentageChange: 0,
|
||||
data: [
|
||||
{ total: String(lifetimeMetrics.OUTBOUND_CLICK), date: today },
|
||||
],
|
||||
data: [{ total: String(lifetimeMetrics.OUTBOUND_CLICK), date: today }],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import axios from 'axios';
|
|||
import WebSocket from 'ws';
|
||||
import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
|
||||
|
||||
// @ts-ignore
|
||||
global.WebSocket = WebSocket;
|
||||
|
|
@ -187,7 +186,7 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
|
|||
for (const firstPostSettings of post.settings.subreddit) {
|
||||
const kind =
|
||||
firstPostSettings.value.type === 'media'
|
||||
? hasExtension(post.media[0].path, 'mp4')
|
||||
? post.media[0].path.indexOf('mp4') > -1
|
||||
? 'video'
|
||||
: 'image'
|
||||
: firstPostSettings.value.type;
|
||||
|
|
@ -212,7 +211,7 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken,
|
||||
post.media[0].path
|
||||
),
|
||||
...(hasExtension(post.media[0].path, 'mp4')
|
||||
...(post.media[0].path.indexOf('mp4') > -1
|
||||
? {
|
||||
video_poster_url: await this.uploadFileToReddit(
|
||||
accessToken,
|
||||
|
|
|
|||
|
|
@ -141,7 +141,6 @@ export interface SocialProvider
|
|||
identifier: string;
|
||||
refreshWait?: boolean;
|
||||
convertToJPEG?: boolean;
|
||||
stripLinks?: () => boolean;
|
||||
refreshCron?: boolean;
|
||||
dto?: any;
|
||||
maxLength: (additionalSettings?: any) => number;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import { capitalize, chunk } from 'lodash';
|
|||
import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation';
|
||||
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
|
||||
|
||||
export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'threads';
|
||||
|
|
@ -45,12 +44,6 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
return { type: 'refresh-token', value: 'Threads access token expired' };
|
||||
}
|
||||
|
||||
if (body.includes('The media could not be fetched from this URI')) {
|
||||
return {
|
||||
type: 'bad-body',
|
||||
value: 'One of the media URLs is invalid or inaccessible, make sure it\'s being uploaded to Postiz first',
|
||||
};
|
||||
}
|
||||
if (body.includes('text must be at most 500 characters')) {
|
||||
return {
|
||||
type: 'bad-body',
|
||||
|
|
@ -195,9 +188,8 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
isCarouselItem = false,
|
||||
replyToId?: string
|
||||
): Promise<string> {
|
||||
const mediaType = hasExtension(media.path, 'mp4')
|
||||
? 'video_url'
|
||||
: 'image_url';
|
||||
const mediaType =
|
||||
media.path.indexOf('.mp4') > -1 ? 'video_url' : 'image_url';
|
||||
const mediaParams = new URLSearchParams({
|
||||
...(mediaType === 'video_url' ? { video_url: media.path } : {}),
|
||||
...(mediaType === 'image_url' ? { image_url: media.path } : {}),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import {
|
|||
} from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { TikTokDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/tiktok.dto';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
|
||||
|
|
@ -147,7 +146,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
return {
|
||||
type: 'bad-body' as const,
|
||||
value:
|
||||
'TikTok limits pending posts to 5 within any 24-hour period. Please check your TikTok inbox in the TikTok mobile app and try again after 24 hours.',
|
||||
'TikTok limit the maximum of pending posts to 5, TikTok limits you for now, please check your TikTok inbox at your TikTok mobile app and try again later',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +184,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
if (body.indexOf('url_ownership_unverified') > -1) {
|
||||
return {
|
||||
type: 'bad-body' as const,
|
||||
value: 'You have to upload the picture/video to Postiz when sending a URL',
|
||||
value: 'URL ownership not verified, please verify domain ownership',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -247,7 +246,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
};
|
||||
|
||||
const { access_token, refresh_token, ...all } = await (
|
||||
await fetch('https://open.tiktokapis.com/v2/oauth/token/', {
|
||||
await this.fetch('https://open.tiktokapis.com/v2/oauth/token/', {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
|
|
@ -261,7 +260,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
user: { avatar_url, display_name, open_id, username },
|
||||
},
|
||||
} = await (
|
||||
await fetch(
|
||||
await this.fetch(
|
||||
'https://open.tiktokapis.com/v2/user/info/?fields=open_id,avatar_url,display_name,union_id,username',
|
||||
{
|
||||
method: 'GET',
|
||||
|
|
@ -324,7 +323,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
};
|
||||
|
||||
const { access_token, refresh_token, scope } = await (
|
||||
await fetch('https://open.tiktokapis.com/v2/oauth/token/', {
|
||||
await this.fetch('https://open.tiktokapis.com/v2/oauth/token/', {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
|
|
@ -340,7 +339,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
user: { avatar_url, display_name, open_id, username },
|
||||
},
|
||||
} = await (
|
||||
await fetch(
|
||||
await this.fetch(
|
||||
'https://open.tiktokapis.com/v2/user/info/?fields=open_id,avatar_url,display_name,union_id,username',
|
||||
{
|
||||
method: 'GET',
|
||||
|
|
@ -366,7 +365,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
const {
|
||||
data: { max_video_post_duration_sec },
|
||||
} = await (
|
||||
await fetch(
|
||||
await this.fetch(
|
||||
'https://open.tiktokapis.com/v2/post/publish/creator_info/query/',
|
||||
{
|
||||
method: 'POST',
|
||||
|
|
@ -458,7 +457,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
}
|
||||
|
||||
private buildTikokPostInfoBody(firstPost: PostDetails<TikTokDto>) {
|
||||
const isPhoto = !hasExtension(firstPost?.media?.[0]?.path, 'mp4');
|
||||
const isPhoto = (firstPost?.media?.[0]?.path?.indexOf('mp4') || -1) === -1;
|
||||
const method = firstPost?.settings?.content_posting_method;
|
||||
|
||||
if (method === 'DIRECT_POST') {
|
||||
|
|
@ -508,7 +507,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
}
|
||||
|
||||
private buildTikokSourceInfoBody(firstPost: PostDetails<TikTokDto>) {
|
||||
const isPhoto = !hasExtension(firstPost?.media?.[0]?.path, 'mp4');
|
||||
const isPhoto = (firstPost?.media?.[0]?.path?.indexOf('mp4') || -1) === -1;
|
||||
|
||||
if (isPhoto) {
|
||||
return {
|
||||
|
|
@ -546,7 +545,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
integration: Integration
|
||||
): Promise<PostResponse[]> {
|
||||
const [firstPost] = postDetails;
|
||||
const isPhoto = !hasExtension(firstPost?.media?.[0]?.path, 'mp4');
|
||||
const isPhoto = (firstPost?.media?.[0]?.path?.indexOf('mp4') || -1) === -1;
|
||||
|
||||
console.log({
|
||||
...this.buildTikokPostInfoBody(firstPost),
|
||||
|
|
@ -558,7 +557,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
await this.fetch(
|
||||
`https://open.tiktokapis.com/v2/post/publish${this.postingMethod(
|
||||
firstPost.settings.content_posting_method,
|
||||
!hasExtension(firstPost?.media?.[0]?.path, 'mp4')
|
||||
(firstPost?.media?.[0]?.path?.indexOf('mp4') || -1) === -1
|
||||
)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import axios from 'axios';
|
|||
import FormDataNew from 'form-data';
|
||||
import mime from 'mime-types';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
|
||||
|
||||
export class VkProvider extends SocialAbstract implements SocialProvider {
|
||||
override maxConcurrentJob = 2; // VK has moderate API limits
|
||||
|
|
@ -169,7 +168,7 @@ export class VkProvider extends SocialAbstract implements SocialProvider {
|
|||
(post?.media || []).map(async (media) => {
|
||||
const all = await (
|
||||
await this.fetch(
|
||||
hasExtension(media.path, 'mp4')
|
||||
media.path.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`
|
||||
)
|
||||
|
|
@ -194,7 +193,7 @@ export class VkProvider extends SocialAbstract implements SocialProvider {
|
|||
})
|
||||
).data;
|
||||
|
||||
if (hasExtension(media.path, 'mp4')) {
|
||||
if (media.path.indexOf('mp4') > -1) {
|
||||
return {
|
||||
id: all.response.video_id,
|
||||
type: 'video',
|
||||
|
|
|
|||
|
|
@ -18,10 +18,8 @@ import { PostPlug } from '@gitroom/helpers/decorators/post.plug';
|
|||
import dayjs from 'dayjs';
|
||||
import { uniqBy } from 'lodash';
|
||||
import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation';
|
||||
import { stripLinks as removeLinks } from '@gitroom/helpers/utils/strip.links';
|
||||
import { XDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/x.dto';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
|
||||
|
||||
@Rules(
|
||||
'X can have maximum 4 pictures, or maximum one video, it can also be without attachments'
|
||||
|
|
@ -31,7 +29,6 @@ export class XProvider extends SocialAbstract implements SocialProvider {
|
|||
name = 'X';
|
||||
isBetweenSteps = false;
|
||||
scopes = [] as string[];
|
||||
stripLinks = () => !!process.env.STRIP_LINKS_FROM_X_POSTS;
|
||||
override maxConcurrentJob = 1; // X has strict rate limits (300 posts per 3 hours)
|
||||
toolTip =
|
||||
'You will be logged in into your current account, if you would like a different account, change it first on X';
|
||||
|
|
@ -230,9 +227,8 @@ export class XProvider extends SocialAbstract implements SocialProvider {
|
|||
) {
|
||||
await timer(2000);
|
||||
|
||||
const plugText = stripHtmlValidation('normal', fields.post, true);
|
||||
await client.v2.tweet({
|
||||
text: this.stripLinks() ? removeLinks(plugText) : plugText,
|
||||
text: stripHtmlValidation('normal', fields.post, true),
|
||||
reply: { in_reply_to_tweet_id: id },
|
||||
});
|
||||
return true;
|
||||
|
|
@ -391,7 +387,7 @@ export class XProvider extends SocialAbstract implements SocialProvider {
|
|||
id: await this.runInConcurrent(
|
||||
async () =>
|
||||
client.v2.uploadMedia(
|
||||
hasExtension(m.path, 'mp4')
|
||||
m.path.indexOf('mp4') > -1
|
||||
? Buffer.from(await readOrFetch(m.path))
|
||||
: await sharp(await readOrFetch(m.path), {
|
||||
animated: lookup(m.path) === 'image/gif',
|
||||
|
|
@ -473,9 +469,7 @@ export class XProvider extends SocialAbstract implements SocialProvider {
|
|||
firstPost?.settings?.community?.split('/').pop() || '',
|
||||
}
|
||||
: {}),
|
||||
text: this.stripLinks()
|
||||
? removeLinks(firstPost.message)
|
||||
: firstPost.message,
|
||||
text: firstPost.message,
|
||||
...(media_ids.length ? { media: { media_ids } } : {}),
|
||||
made_with_ai: !!firstPost?.settings?.made_with_ai,
|
||||
paid_partnership: !!firstPost?.settings?.paid_partnership,
|
||||
|
|
@ -542,9 +536,7 @@ export class XProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
const tweetUrl = 'https://api.x.com/2/tweets';
|
||||
const tweetBody = {
|
||||
text: this.stripLinks()
|
||||
? removeLinks(commentPost.message)
|
||||
: commentPost.message,
|
||||
text: commentPost.message,
|
||||
...(media_ids.length ? { media: { media_ids } } : {}),
|
||||
reply: { in_reply_to_tweet_id: replyToId },
|
||||
made_with_ai: !!commentPost?.settings?.made_with_ai,
|
||||
|
|
|
|||
|
|
@ -71,7 +71,10 @@ export class StripeService {
|
|||
currency: 'usd',
|
||||
payment_method: latestMethod.id,
|
||||
customer: event.data.object.customer as string,
|
||||
off_session: true,
|
||||
automatic_payment_methods: {
|
||||
allow_redirects: 'never',
|
||||
enabled: true,
|
||||
},
|
||||
capture_method: 'manual', // Authorize without capturing
|
||||
confirm: true, // Confirm the PaymentIntent
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,8 +29,6 @@ interface VariableContextInterface {
|
|||
transloadit: string[];
|
||||
sentryDsn: string;
|
||||
extensionId: string;
|
||||
googleAdsId?: string;
|
||||
googleAdsTrialTracking?: string;
|
||||
}
|
||||
const VariableContext = createContext({
|
||||
stripeClient: '',
|
||||
|
|
@ -38,8 +36,6 @@ const VariableContext = createContext({
|
|||
isGeneral: true,
|
||||
genericOauth: false,
|
||||
oauthLogoUrl: '',
|
||||
googleAdsId: '',
|
||||
googleAdsTrialTracking: '',
|
||||
oauthDisplayName: '',
|
||||
mcpUrl: '',
|
||||
cloudflareUrl: '',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { FC } from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
|
||||
export const VideoOrImage: FC<{
|
||||
src: string;
|
||||
autoplay: boolean;
|
||||
|
|
@ -9,7 +8,7 @@ export const VideoOrImage: FC<{
|
|||
videoClassName?: string;
|
||||
}> = (props) => {
|
||||
const { src, autoplay, isContain, imageClassName, videoClassName } = props;
|
||||
if (hasExtension(src, 'mp4')) {
|
||||
if (src?.indexOf('mp4') > -1) {
|
||||
return (
|
||||
<video
|
||||
src={src}
|
||||
|
|
|
|||
28
package.json
28
package.json
|
|
@ -68,14 +68,14 @@
|
|||
"@meronex/icons": "^4.0.0",
|
||||
"@modelcontextprotocol/sdk": "^1.22.0",
|
||||
"@nest-lab/throttler-storage-redis": "^1.2.0",
|
||||
"@nestjs/cli": "^11.0.21",
|
||||
"@nestjs/common": "^11.1.21",
|
||||
"@nestjs/core": "^11.1.21",
|
||||
"@nestjs/microservices": "^11.1.21",
|
||||
"@nestjs/platform-express": "^11.1.21",
|
||||
"@nestjs/schedule": "^6.1.3",
|
||||
"@nestjs/swagger": "^11.4.3",
|
||||
"@nestjs/throttler": "^6.5.0",
|
||||
"@nestjs/cli": "10.0.2",
|
||||
"@nestjs/common": "^10.0.2",
|
||||
"@nestjs/core": "^10.0.2",
|
||||
"@nestjs/microservices": "^10.3.1",
|
||||
"@nestjs/platform-express": "^10.0.2",
|
||||
"@nestjs/schedule": "^4.0.0",
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"@nestjs/throttler": "^6.3.0",
|
||||
"@neynar/nodejs-sdk": "^3.112.0",
|
||||
"@neynar/react": "^1.2.22",
|
||||
"@pigment-css/react": "^0.0.30",
|
||||
|
|
@ -189,7 +189,7 @@
|
|||
"nestjs-command": "^3.1.4",
|
||||
"nestjs-real-ip": "^3.0.1",
|
||||
"nestjs-temporal-core": "^3.2.0",
|
||||
"next": "16.2.6",
|
||||
"next": "16.2.1",
|
||||
"next-plausible": "^3.12.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"node-telegram-bot-api": "^0.66.0",
|
||||
|
|
@ -217,7 +217,7 @@
|
|||
"react-use-cookie": "^1.6.1",
|
||||
"react-use-keypress": "^1.3.1",
|
||||
"redis": "^4.6.12",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"resend": "^3.2.0",
|
||||
"rss-parser": "^3.13.0",
|
||||
|
|
@ -256,8 +256,8 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@crxjs/vite-plugin": "^2.0.0-beta.32",
|
||||
"@nestjs/schematics": "^11.1.0",
|
||||
"@nestjs/testing": "^11.1.21",
|
||||
"@nestjs/schematics": "^10.0.1",
|
||||
"@nestjs/testing": "^10.0.2",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
|
||||
"@svgr/webpack": "^8.0.1",
|
||||
"@swc-node/register": "1.9.2",
|
||||
|
|
@ -286,7 +286,7 @@
|
|||
"babel-jest": "29.7.0",
|
||||
"cross-env": "^10.0.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-next": "16.2.6",
|
||||
"eslint-config-next": "16.2.1",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "6.7.1",
|
||||
|
|
@ -321,7 +321,7 @@
|
|||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"next": "16.2.6",
|
||||
"next": "16.2.1",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"@types/react": "19.1.8",
|
||||
|
|
|
|||
1407
pnpm-lock.yaml
generated
1407
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue