Compare commits

..

10 commits

Author SHA1 Message Date
Nevo David
09088a5391 Merge remote-tracking branch 'origin/main'
Some checks failed
Build / build (22.12.0) (push) Has been cancelled
Code Quality Analysis / Analyze (javascript-typescript) (push) Has been cancelled
2026-05-18 20:49:30 +07:00
Nevo David
faeb89853b feat: threads error 2026-05-18 20:32:45 +07:00
Nevo David
6fc51da7e3
Merge pull request #1538 from gitroomhq/feat/list-view-post-filters
Add state filter (all/scheduled/draft/published) to list view
2026-05-18 20:24:39 +07:00
Santosh Bhandari
415c9c4ba8
Merge pull request #1537 from gitroomhq/tiktok-info
Show TikTok title/content restriction notice for video posts
2026-05-18 10:29:12 +00:00
Santosh Bhandari
e19c855da6
Merge pull request #1539 from gitroomhq/fix/tiktok-pending-share-error-message
Clarify TikTok pending-share error mentions the 24-hour window
2026-05-18 10:27:59 +00:00
Santosh Bhandari
7e0bb7075e fix: clarify TikTok pending-share error mentions 24-hour window
Some checks failed
Build / build (22.12.0) (push) Has been cancelled
TikTok's spam_risk_too_many_pending_share limit applies per 24-hour
period; the previous message did not state the window.
2026-05-18 15:28:03 +05:45
Nevo David
0b3328daeb feat: pinterest fixes 2026-05-18 16:39:38 +07:00
Santosh Bhandari
4811741e63 feat: filter list view by post state (all/scheduled/draft/published)
Adds a state filter to the calendar list view so users can see all
posts (default) or narrow to scheduled, draft, or published. The
backend repository now switches its WHERE/orderBy off the new query
param; 'all' includes ERROR posts so failed publishes remain visible.
2026-05-18 15:21:28 +05:45
Santosh Bhandari
7dda2812d7 feat: add TikTok restriction notice for video posts
Show an inline warning explaining title/content limitations for
direct-post vs upload-only video modes on TikTok.
2026-05-18 14:44:25 +05:45
Nevo David
2316a45388 feat: upgrade nextjs due to security risks 2026-05-18 13:55:12 +07:00
16 changed files with 216 additions and 747 deletions

View file

@ -26,6 +26,8 @@ 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'),
@ -78,6 +80,10 @@ export const CalendarContext = createContext({
setListPage: (page: number) => {
/** empty **/
},
listState: 'all' as ListStateFilter,
setListState: (state: ListStateFilter) => {
/** empty **/
},
});
export interface Integrations {
@ -144,6 +150,11 @@ 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');
@ -190,8 +201,9 @@ export const CalendarWeekProvider: FC<{
page: listPage.toString(),
limit: '100',
customer: filters?.customer?.toString() || '',
state: listState,
}).toString();
}, [listPage, filters.customer]);
}, [listPage, filters.customer, listState]);
const loadListData = useCallback(async () => {
const response = await fetch(`/posts/list?${listParams}`);
@ -341,6 +353,8 @@ export const CalendarWeekProvider: FC<{
listPage,
listTotalPages,
setListPage,
listState,
setListState,
}}
>
{children}

View file

@ -493,7 +493,15 @@ export const MonthView = () => {
export const ListView = () => {
const t = useT();
const user = useUser();
const { integrations, loading, listPosts } = useCalendar();
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');
// Use shared post actions hook
const { editPost, deletePost, copyDebugJson, openStatistics, openMissingRelease } = usePostActions();
@ -522,9 +530,7 @@ 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]">
{t('no_upcoming_posts', 'No upcoming posts scheduled')}
</div>
<div className="text-textColor text-[16px]">{emptyMessage}</div>
</div>
);
}

View file

@ -1,6 +1,6 @@
'use client';
import { useCalendar } from '@gitroom/frontend/components/launches/calendar.context';
import { useCalendar, ListStateFilter } from '@gitroom/frontend/components/launches/calendar.context';
import clsx from 'clsx';
import dayjs from 'dayjs';
import { useCallback } from 'react';
@ -259,6 +259,21 @@ 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);
@ -393,6 +408,21 @@ 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>
)}

View file

@ -35,13 +35,18 @@ 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';
}

View file

@ -29,12 +29,29 @@ 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',
@ -83,6 +100,25 @@ 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?')}

View file

@ -4,7 +4,6 @@ import {
ActivityMethod,
TemporalService,
} from 'nestjs-temporal-core';
import * as Sentry from '@sentry/nestjs';
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
import {
NotificationService,
@ -77,7 +76,7 @@ export class PostActivity {
for (const post of list) {
await this._temporalService.client
.getRawClient()
.workflow.signalWithStart('postWorkflowV106', {
.workflow.signalWithStart('postWorkflowV105', {
workflowId: `post_${post.id}`,
taskQueue: 'main',
signal: 'poke',
@ -414,227 +413,4 @@ export class PostActivity {
return false;
}
}
@ActivityMethod()
async postSocialV106(integration: Integration, posts: Post[]) {
const ctx = {
provider: integration.providerIdentifier,
integrationId: integration.id,
organizationId: integration.organizationId,
postIds: posts.map((p) => p.id),
};
try {
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
);
const newPosts = await this._postService.updateTags(
integration.organizationId,
posts
);
Sentry.logger.info('Posting to social channel', ctx);
const postNow = await getIntegration.post(
integration.internalId,
integration.token,
await Promise.all(
(newPosts || []).map(async (p) => ({
id: p.id,
message: stripHtmlValidation(
getIntegration.editor,
p.content,
true,
false,
!/<\/?[a-z][\s\S]*>/i.test(p.content),
getIntegration.mentionFormat
),
settings: JSON.parse(p.settings || '{}'),
media: await this._postService.updateMedia(
p.id,
JSON.parse(p.image || '[]'),
getIntegration?.convertToJPEG || false
),
}))
),
integration
);
Sentry.logger.info('Posted to social channel successfully', {
...ctx,
published: postNow.length,
});
await this._temporalService.client
.getRawClient()
.workflow.start('streakWorkflow', {
args: [{ organizationId: integration.organizationId }],
workflowId: `streak_${integration.organizationId}`,
taskQueue: 'main',
workflowIdConflictPolicy: 'TERMINATE_EXISTING',
typedSearchAttributes: new TypedSearchAttributes([
{
key: organizationId,
value: integration.organizationId,
},
]),
});
return postNow;
} catch (err) {
Sentry.logger.error('Failed to post to social channel', {
...ctx,
error: err instanceof Error ? err.message : String(err),
errorType: err instanceof Error ? err.constructor.name : typeof err,
});
Sentry.captureException(err, {
tags: {
provider: integration.providerIdentifier,
activity: 'postSocial',
},
contexts: { post: ctx },
});
throw err;
}
}
@ActivityMethod()
async postCommentV106(
postId: string,
lastPostId: string | undefined,
integration: Integration,
posts: Post[]
) {
const ctx = {
provider: integration.providerIdentifier,
integrationId: integration.id,
organizationId: integration.organizationId,
parentPostId: postId,
postIds: posts.map((p) => p.id),
};
try {
const getIntegration = this._integrationManager.getSocialIntegration(
integration.providerIdentifier
);
const newPosts = await this._postService.updateTags(
integration.organizationId,
posts
);
Sentry.logger.info('Posting comment to social channel', ctx);
const result = await getIntegration.comment(
integration.internalId,
postId,
lastPostId,
integration.token,
await Promise.all(
(newPosts || []).map(async (p) => ({
id: p.id,
message: stripHtmlValidation(
getIntegration.editor,
p.content,
true,
false,
!/<\/?[a-z][\s\S]*>/i.test(p.content),
getIntegration.mentionFormat
),
settings: JSON.parse(p.settings || '{}'),
media: await this._postService.updateMedia(
p.id,
JSON.parse(p.image || '[]'),
getIntegration?.convertToJPEG || false
),
}))
),
integration
);
Sentry.logger.info('Posted comment successfully', ctx);
return result;
} catch (err) {
Sentry.logger.error('Failed to post comment to social channel', {
...ctx,
error: err instanceof Error ? err.message : String(err),
errorType: err instanceof Error ? err.constructor.name : typeof err,
});
Sentry.captureException(err, {
tags: {
provider: integration.providerIdentifier,
activity: 'postComment',
},
contexts: { post: ctx },
});
throw err;
}
}
@ActivityMethod()
async refreshTokenWithCauseV106(
integration: Integration,
cause: string
): Promise<false | AuthTokenDetails> {
const getIntegration = this._integrationManager.getSocialIntegration(
integration.providerIdentifier
);
const ctx = {
provider: integration.providerIdentifier,
integrationId: integration.id,
organizationId: integration.organizationId,
cause,
};
try {
Sentry.logger.info('Refreshing integration token', ctx);
const refresh = await this._refreshIntegrationService.refresh(
integration,
cause
);
if (!refresh) {
Sentry.logger.warn('Token refresh returned no credentials', ctx);
return false;
}
if (getIntegration.refreshWait) {
await timer(10000);
}
Sentry.logger.info('Refreshed integration token successfully', ctx);
return refresh;
} catch (err) {
Sentry.logger.error('Failed to refresh integration token', {
...ctx,
error: err instanceof Error ? err.message : String(err),
errorType: err instanceof Error ? err.constructor.name : typeof err,
});
Sentry.captureException(err, {
tags: {
provider: integration.providerIdentifier,
activity: 'refreshTokenWithCause',
},
contexts: { integration: ctx },
});
await this._refreshIntegrationService.setBetweenSteps(integration, cause);
return false;
}
}
}

View file

@ -3,7 +3,6 @@ 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 './post-workflows/post.workflow.v1.0.6';
export * from './autopost.workflow';
export * from './digest.email.workflow';
export * from './missing.post.workflow';

View file

@ -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 postWorkflowV106({
taskQueue,
postId,
organizationId,
postNow = false,
}: {
taskQueue: string;
postId: string;
organizationId: string;
postNow?: boolean;
}) {
// Dynamic task queue, for concurrency
const {
postSocialV106,
postCommentV106,
getIntegrationById,
refreshTokenWithCauseV106,
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 postSocialV106(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 postCommentV106(
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 refreshTokenWithCauseV106(
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 refreshTokenWithCauseV106(
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 refreshTokenWithCauseV106(
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(postWorkflowV106, {
parentClosePolicy: 'ABANDON',
args: [
{
taskQueue,
postId,
organizationId,
postNow: true,
},
],
workflowId: `post_${post.id}_${makeId(10)}`,
typedSearchAttributes: new TypedSearchAttributes([
{
key: postIdSearchParam,
value: postId,
},
]),
});
}
}
}

View file

@ -224,6 +224,26 @@ 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: [
{
@ -233,12 +253,8 @@ export class PostsRepository {
},
],
},
{
publishDate: {
gte: dayjs.utc().toDate(),
},
},
],
...stateAndDate,
deletedAt: null as Date | null,
parentPostId: null as string | null,
intervalInDays: null as number | null,
@ -257,7 +273,7 @@ export class PostsRepository {
skip,
take: limit,
orderBy: {
publishDate: 'asc',
publishDate: orderDirection,
},
select: {
id: true,

View file

@ -722,7 +722,7 @@ export class PostsService {
try {
await this._temporalService.client
.getRawClient()
?.workflow.start('postWorkflowV106', {
?.workflow.start('postWorkflowV105', {
workflowId: `post_${postId}`,
taskQueue: 'main',
workflowIdConflictPolicy: 'TERMINATE_EXISTING',

View file

@ -4,9 +4,12 @@ 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()
@ -24,4 +27,8 @@ export class GetPostsListDto {
@IsOptional()
@IsString()
customer?: string;
@IsOptional()
@IsIn(['all', 'scheduled', 'draft', 'published'])
state?: PostListStateFilter = 'all';
}

View file

@ -20,7 +20,7 @@ import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorato
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'
'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'
)
export class PinterestProvider
extends SocialAbstract
@ -51,6 +51,12 @@ 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,
@ -185,8 +191,8 @@ 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) =>
hasExtension(p.path, 'mp4')
);
const picture = postDetails?.[0]?.media?.find(
(p) => !hasExtension(p.path, 'mp4')
@ -292,7 +298,9 @@ export class PinterestProvider
}
: {
source_type: 'multiple_image_urls',
items: mapImages,
items: mapImages.map((m) => ({
url: m.path,
})),
},
}),
})

View file

@ -45,6 +45,12 @@ 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',
@ -189,8 +195,9 @@ 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 = hasExtension(media.path, 'mp4')
? 'video_url'
: 'image_url';
const mediaParams = new URLSearchParams({
...(mediaType === 'video_url' ? { video_url: media.path } : {}),
...(mediaType === 'image_url' ? { image_url: media.path } : {}),

View file

@ -147,7 +147,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
return {
type: 'bad-body' as const,
value:
'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',
'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.',
};
}

View file

@ -189,7 +189,7 @@
"nestjs-command": "^3.1.4",
"nestjs-real-ip": "^3.0.1",
"nestjs-temporal-core": "^3.2.0",
"next": "16.2.1",
"next": "16.2.6",
"next-plausible": "^3.12.0",
"node-fetch": "^3.3.2",
"node-telegram-bot-api": "^0.66.0",
@ -286,7 +286,7 @@
"babel-jest": "29.7.0",
"cross-env": "^10.0.0",
"eslint": "8.57.0",
"eslint-config-next": "16.2.1",
"eslint-config-next": "16.2.6",
"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.1",
"next": "16.2.6",
"react": "19.2.4",
"react-dom": "19.2.4",
"@types/react": "19.1.8",

121
pnpm-lock.yaml generated
View file

@ -6,7 +6,7 @@ settings:
injectWorkspacePackages: true
overrides:
next: 16.2.1
next: 16.2.6
react: 19.2.4
react-dom: 19.2.4
'@types/react': 19.1.8
@ -144,7 +144,7 @@ importers:
version: 10.45.0(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)
'@sentry/nextjs':
specifier: ^10.26.0
version: 10.45.0(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react@19.2.4)(webpack@5.105.4(@swc/core@1.5.7(@swc/helpers@0.5.13))(esbuild@0.27.7))
version: 10.45.0(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react@19.2.4)(webpack@5.105.4(@swc/core@1.5.7(@swc/helpers@0.5.13))(esbuild@0.27.7))
'@sentry/profiling-node':
specifier: ^10.25.0
version: 10.45.0
@ -464,11 +464,11 @@ importers:
specifier: ^3.2.0
version: 3.2.3(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(@temporalio/client@1.15.0)(@temporalio/common@1.15.0)(@temporalio/worker@1.15.0(@swc/helpers@0.5.13)(esbuild@0.27.7)(tslib@2.8.1))(@temporalio/workflow@1.15.0)(reflect-metadata@0.2.2)(rxjs@7.8.2)
next:
specifier: 16.2.1
version: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)
specifier: 16.2.6
version: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)
next-plausible:
specifier: ^3.12.0
version: 3.12.5(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
version: 3.12.5(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
node-fetch:
specifier: ^3.3.2
version: 3.3.2
@ -750,8 +750,8 @@ importers:
specifier: 8.57.0
version: 8.57.0
eslint-config-next:
specifier: 16.2.1
version: 16.2.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4)
specifier: 16.2.6
version: 16.2.6(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4)
eslint-config-prettier:
specifier: ^9.0.0
version: 9.1.2(eslint@8.57.0)
@ -5001,56 +5001,56 @@ packages:
'@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
reflect-metadata: ^0.1.13 || ^0.2.0
'@next/env@16.2.1':
resolution: {integrity: sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg==}
'@next/env@16.2.6':
resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==}
'@next/eslint-plugin-next@16.2.1':
resolution: {integrity: sha512-r0epZGo24eT4g08jJlg2OEryBphXqO8aL18oajoTKLzHJ6jVr6P6FI58DLMug04MwD3j8Fj0YK0slyzneKVyzA==}
'@next/eslint-plugin-next@16.2.6':
resolution: {integrity: sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw==}
'@next/swc-darwin-arm64@16.2.1':
resolution: {integrity: sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q==}
'@next/swc-darwin-arm64@16.2.6':
resolution: {integrity: sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@16.2.1':
resolution: {integrity: sha512-/vrcE6iQSJq3uL3VGVHiXeaKbn8Es10DGTGRJnRZlkNQQk3kaNtAJg8Y6xuAlrx/6INKVjkfi5rY0iEXorZ6uA==}
'@next/swc-darwin-x64@16.2.6':
resolution: {integrity: sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@16.2.1':
resolution: {integrity: sha512-uLn+0BK+C31LTVbQ/QU+UaVrV0rRSJQ8RfniQAHPghDdgE+SlroYqcmFnO5iNjNfVWCyKZHYrs3Nl0mUzWxbBw==}
'@next/swc-linux-arm64-gnu@16.2.6':
resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@16.2.1':
resolution: {integrity: sha512-ssKq6iMRnHdnycGp9hCuGnXJZ0YPr4/wNwrfE5DbmvEcgl9+yv97/Kq3TPVDfYome1SW5geciLB9aiEqKXQjlQ==}
'@next/swc-linux-arm64-musl@16.2.6':
resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-x64-gnu@16.2.1':
resolution: {integrity: sha512-HQm7SrHRELJ30T1TSmT706IWovFFSRGxfgUkyWJZF/RKBMdbdRWJuFrcpDdE5vy9UXjFOx6L3mRdqH04Mmx0hg==}
'@next/swc-linux-x64-gnu@16.2.6':
resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@16.2.1':
resolution: {integrity: sha512-aV2iUaC/5HGEpbBkE+4B8aHIudoOy5DYekAKOMSHoIYQ66y/wIVeaRx8MS2ZMdxe/HIXlMho4ubdZs/J8441Tg==}
'@next/swc-linux-x64-musl@16.2.6':
resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-win32-arm64-msvc@16.2.1':
resolution: {integrity: sha512-IXdNgiDHaSk0ZUJ+xp0OQTdTgnpx1RCfRTalhn3cjOP+IddTMINwA7DXZrwTmGDO8SUr5q2hdP/du4DcrB1GxA==}
'@next/swc-win32-arm64-msvc@16.2.6':
resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@16.2.1':
resolution: {integrity: sha512-qvU+3a39Hay+ieIztkGSbF7+mccbbg1Tk25hc4JDylf8IHjYmY/Zm64Qq1602yPyQqvie+vf5T/uPwNxDNIoeg==}
'@next/swc-win32-x64-msvc@16.2.6':
resolution: {integrity: sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@ -6982,7 +6982,7 @@ packages:
resolution: {integrity: sha512-4LE+UvnfdOYyG8YEb/9TWaJQzMPuGLlph/iqowvsMdxaW6la+mvADiuzNTXly4QfsjeD3KIb7dKlGTqiVV0Ttw==}
engines: {node: '>=18'}
peerDependencies:
next: 16.2.1
next: 16.2.6
'@sentry/node-core@10.45.0':
resolution: {integrity: sha512-KQZEvLKM344+EqXiA9HIzWbW5hzq6/9nnFUQ8niaBPoOgR9AiJhrccfIscfgb8vjkriiEtzE03OW/4h1CTgZ3Q==}
@ -11248,8 +11248,8 @@ packages:
engines: {node: '>=6.0'}
hasBin: true
eslint-config-next@16.2.1:
resolution: {integrity: sha512-qhabwjQZ1Mk53XzXvmogf8KQ0tG0CQXF0CZ56+2/lVhmObgmaqj7x5A1DSrWdZd3kwI7GTPGUjFne+krRxYmFg==}
eslint-config-next@16.2.6:
resolution: {integrity: sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q==}
peerDependencies:
eslint: '>=9.0.0'
typescript: '>=3.3.1'
@ -14393,12 +14393,12 @@ packages:
next-plausible@3.12.5:
resolution: {integrity: sha512-l1YMuTI9akb2u7z4hyTuxXpudy8KfSteRNXCYpWpnhAoBjaWQlv6sITai1TwcR7wWvVW8DFbLubvMQAsirAjcA==}
peerDependencies:
next: 16.2.1
next: 16.2.6
react: 19.2.4
react-dom: 19.2.4
next@16.2.1:
resolution: {integrity: sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q==}
next@16.2.6:
resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
@ -17647,6 +17647,7 @@ packages:
uuid@10.0.0:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
hasBin: true
uuid@11.1.0:
@ -17664,10 +17665,12 @@ packages:
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
hasBin: true
uuid@9.0.1:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
hasBin: true
uuidv4@6.2.13:
@ -22960,34 +22963,34 @@ snapshots:
'@nestjs/core': 11.1.21(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.21)(@nestjs/platform-express@11.1.21)(reflect-metadata@0.2.2)(rxjs@7.8.2)
reflect-metadata: 0.2.2
'@next/env@16.2.1': {}
'@next/env@16.2.6': {}
'@next/eslint-plugin-next@16.2.1':
'@next/eslint-plugin-next@16.2.6':
dependencies:
fast-glob: 3.3.1
'@next/swc-darwin-arm64@16.2.1':
'@next/swc-darwin-arm64@16.2.6':
optional: true
'@next/swc-darwin-x64@16.2.1':
'@next/swc-darwin-x64@16.2.6':
optional: true
'@next/swc-linux-arm64-gnu@16.2.1':
'@next/swc-linux-arm64-gnu@16.2.6':
optional: true
'@next/swc-linux-arm64-musl@16.2.1':
'@next/swc-linux-arm64-musl@16.2.6':
optional: true
'@next/swc-linux-x64-gnu@16.2.1':
'@next/swc-linux-x64-gnu@16.2.6':
optional: true
'@next/swc-linux-x64-musl@16.2.1':
'@next/swc-linux-x64-musl@16.2.6':
optional: true
'@next/swc-win32-arm64-msvc@16.2.1':
'@next/swc-win32-arm64-msvc@16.2.6':
optional: true
'@next/swc-win32-x64-msvc@16.2.1':
'@next/swc-win32-x64-msvc@16.2.6':
optional: true
'@neynar/nodejs-sdk@3.137.0(@nestjs/microservices@11.1.21)(@nestjs/platform-express@11.1.21)(bufferutil@4.1.0)(class-transformer@0.5.1)(class-validator@0.14.4)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.25.76)':
@ -25500,7 +25503,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@sentry/nextjs@10.45.0(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react@19.2.4)(webpack@5.105.4(@swc/core@1.5.7(@swc/helpers@0.5.13))(esbuild@0.27.7))':
'@sentry/nextjs@10.45.0(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react@19.2.4)(webpack@5.105.4(@swc/core@1.5.7(@swc/helpers@0.5.13))(esbuild@0.27.7))':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/semantic-conventions': 1.40.0
@ -25513,7 +25516,7 @@ snapshots:
'@sentry/react': 10.45.0(react@19.2.4)
'@sentry/vercel-edge': 10.45.0
'@sentry/webpack-plugin': 5.1.1(webpack@5.105.4(@swc/core@1.5.7(@swc/helpers@0.5.13))(esbuild@0.27.7))
next: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)
next: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)
rollup: 4.59.0
stacktrace-parser: 0.1.11
transitivePeerDependencies:
@ -31148,9 +31151,9 @@ snapshots:
optionalDependencies:
source-map: 0.6.1
eslint-config-next@16.2.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4):
eslint-config-next@16.2.6(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4):
dependencies:
'@next/eslint-plugin-next': 16.2.1
'@next/eslint-plugin-next': 16.2.6
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.0)
@ -35340,15 +35343,15 @@ snapshots:
netmask@2.0.2: {}
next-plausible@3.12.5(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
next-plausible@3.12.5(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
next: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)
next: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3):
next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3):
dependencies:
'@next/env': 16.2.1
'@next/env': 16.2.6
'@swc/helpers': 0.5.15
baseline-browser-mapping: 2.10.0
caniuse-lite: 1.0.30001777
@ -35357,14 +35360,14 @@ snapshots:
react-dom: 19.2.4(react@19.2.4)
styled-jsx: 5.1.6(@babel/core@7.29.0)(babel-plugin-macros@3.1.0)(react@19.2.4)
optionalDependencies:
'@next/swc-darwin-arm64': 16.2.1
'@next/swc-darwin-x64': 16.2.1
'@next/swc-linux-arm64-gnu': 16.2.1
'@next/swc-linux-arm64-musl': 16.2.1
'@next/swc-linux-x64-gnu': 16.2.1
'@next/swc-linux-x64-musl': 16.2.1
'@next/swc-win32-arm64-msvc': 16.2.1
'@next/swc-win32-x64-msvc': 16.2.1
'@next/swc-darwin-arm64': 16.2.6
'@next/swc-darwin-x64': 16.2.6
'@next/swc-linux-arm64-gnu': 16.2.6
'@next/swc-linux-arm64-musl': 16.2.6
'@next/swc-linux-x64-gnu': 16.2.6
'@next/swc-linux-x64-musl': 16.2.6
'@next/swc-win32-arm64-msvc': 16.2.6
'@next/swc-win32-x64-msvc': 16.2.6
'@opentelemetry/api': 1.9.0
'@playwright/test': 1.58.2
sass: 1.97.3