Merge branch 'main' of https://github.com/gitroomhq/postiz-app
This commit is contained in:
commit
4cb7aa2a51
|
|
@ -49,5 +49,68 @@
|
|||
|
||||
---
|
||||
|
||||
# Logs
|
||||
|
||||
- Where logs are used, ensure Sentry is imported using `import * as Sentry from "@sentry/nextjs"`
|
||||
- Enable logging in Sentry using `Sentry.init({ enableLogs: true })`
|
||||
- Reference the logger using `const { logger } = Sentry`
|
||||
- Sentry offers a `consoleLoggingIntegration` that can be used to log specific console error types automatically without instrumenting the individual logger calls
|
||||
|
||||
## Configuration
|
||||
|
||||
The Sentry initialization needs to be updated to enable the logs feature.
|
||||
|
||||
### Baseline
|
||||
|
||||
```javascript
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
|
||||
enableLogs: true,
|
||||
});
|
||||
```
|
||||
|
||||
### Logger Integration
|
||||
|
||||
```javascript
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
integrations: [
|
||||
// send console.log, console.error, and console.warn calls as logs to Sentry
|
||||
Sentry.consoleLoggingIntegration({ levels: ["log", "error", "warn"] }),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Logger Examples
|
||||
|
||||
`logger.fmt` is a template literal function that should be used to bring variables into the structured logs.
|
||||
|
||||
```javascript
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const { logger } = Sentry;
|
||||
|
||||
logger.trace("Starting database connection", { database: "users" });
|
||||
logger.debug(logger.fmt`Cache miss for user: ${userId}`);
|
||||
logger.info("Updated profile", { profileId: 345 });
|
||||
logger.warn("Rate limit reached for endpoint", {
|
||||
endpoint: "/api/results/",
|
||||
isEnterprise: false,
|
||||
});
|
||||
logger.error("Failed to process payment", {
|
||||
orderId: "order_123",
|
||||
amount: 99.99,
|
||||
});
|
||||
logger.fatal("Database connection pool exhausted", {
|
||||
database: "users",
|
||||
activeConnections: 100,
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For questions or unclear conventions, check the main README or ask for clarification in your PR description.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ on:
|
|||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export class AuthController {
|
|||
async register(
|
||||
@Req() req: Request,
|
||||
@Body() body: CreateOrgUserDto,
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
@Res({ passthrough: false }) response: Response,
|
||||
@RealIP() ip: string,
|
||||
@UserAgent() userAgent: string
|
||||
) {
|
||||
|
|
@ -114,7 +114,7 @@ export class AuthController {
|
|||
async login(
|
||||
@Req() req: Request,
|
||||
@Body() body: LoginUserDto,
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
@Res({ passthrough: false }) response: Response,
|
||||
@RealIP() ip: string,
|
||||
@UserAgent() userAgent: string
|
||||
) {
|
||||
|
|
@ -204,11 +204,11 @@ export class AuthController {
|
|||
@Post('/activate')
|
||||
async activate(
|
||||
@Body('code') code: string,
|
||||
@Res({ passthrough: true }) response: Response
|
||||
@Res({ passthrough: false }) response: Response
|
||||
) {
|
||||
const activate = await this._authService.activate(code);
|
||||
if (!activate) {
|
||||
return response.status(200).send({ can: false });
|
||||
return response.status(200).json({ can: false });
|
||||
}
|
||||
|
||||
response.cookie('auth', activate, {
|
||||
|
|
@ -228,16 +228,18 @@ export class AuthController {
|
|||
}
|
||||
|
||||
response.header('onboarding', 'true');
|
||||
return response.status(200).send({ can: true });
|
||||
|
||||
return response.status(200).json({ can: true });
|
||||
}
|
||||
|
||||
@Post('/oauth/:provider/exists')
|
||||
async oauthExists(
|
||||
@Body('code') code: string,
|
||||
@Param('provider') provider: string,
|
||||
@Res({ passthrough: true }) response: Response
|
||||
@Res({ passthrough: false }) response: Response
|
||||
) {
|
||||
const { jwt, token } = await this._authService.checkExists(provider, code);
|
||||
|
||||
if (token) {
|
||||
return response.json({ token });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import {
|
|||
AuthorizationActions,
|
||||
Sections,
|
||||
} from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
import { uniqBy } from 'lodash';
|
||||
|
||||
@ApiTags('Integrations')
|
||||
@Controller('/integrations')
|
||||
|
|
@ -246,11 +247,59 @@ export class IntegrationsController {
|
|||
) {
|
||||
return this._integrationService.setTimes(org.id, id, body);
|
||||
}
|
||||
|
||||
@Post('/mentions')
|
||||
async mentions(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: IntegrationFunctionDto
|
||||
) {
|
||||
const getIntegration = await this._integrationService.getIntegrationById(
|
||||
org.id,
|
||||
body.id
|
||||
);
|
||||
if (!getIntegration) {
|
||||
throw new Error('Invalid integration');
|
||||
}
|
||||
|
||||
const list = await this._integrationService.getMentions(
|
||||
getIntegration.providerIdentifier,
|
||||
body?.data?.query
|
||||
);
|
||||
|
||||
let newList = [];
|
||||
try {
|
||||
newList = await this.functionIntegration(org, body);
|
||||
} catch (err) {}
|
||||
|
||||
if (newList.length) {
|
||||
await this._integrationService.insertMentions(
|
||||
getIntegration.providerIdentifier,
|
||||
newList.map((p: any) => ({
|
||||
name: p.label || '',
|
||||
username: p.id || '',
|
||||
image: p.image || '',
|
||||
})).filter((f: any) => f.name)
|
||||
);
|
||||
}
|
||||
|
||||
return uniqBy(
|
||||
[
|
||||
...list.map((p) => ({
|
||||
id: p.username,
|
||||
image: p.image,
|
||||
label: p.name,
|
||||
})),
|
||||
...newList,
|
||||
],
|
||||
(p) => p.id
|
||||
).filter(f => f.label && f.image && f.id);
|
||||
}
|
||||
|
||||
@Post('/function')
|
||||
async functionIntegration(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: IntegrationFunctionDto
|
||||
) {
|
||||
): Promise<any> {
|
||||
const getIntegration = await this._integrationService.getIntegrationById(
|
||||
org.id,
|
||||
body.id
|
||||
|
|
@ -266,8 +315,10 @@ export class IntegrationsController {
|
|||
throw new Error('Invalid provider');
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (integrationProvider[body.name]) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const load = await integrationProvider[body.name](
|
||||
getIntegration.token,
|
||||
body.data,
|
||||
|
|
|
|||
|
|
@ -36,10 +36,13 @@ export class AuthService {
|
|||
addToOrg?: boolean | { orgId: string; role: 'USER' | 'ADMIN'; id: string }
|
||||
) {
|
||||
if (provider === Provider.LOCAL) {
|
||||
if (process.env.DISALLOW_PLUS && body.email.includes('+')) {
|
||||
throw new Error('Email with plus sign is not allowed');
|
||||
}
|
||||
const user = await this._userService.getUserByEmail(body.email);
|
||||
if (body instanceof CreateOrgUserDto) {
|
||||
if (user) {
|
||||
throw new Error('User already exists');
|
||||
throw new Error('Email already exists');
|
||||
}
|
||||
|
||||
if (!(await this.canRegister(provider))) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,19 @@ const nextConfig = {
|
|||
},
|
||||
reactStrictMode: false,
|
||||
transpilePackages: ['crypto-hash'],
|
||||
// Enable production sourcemaps for Sentry
|
||||
productionBrowserSourceMaps: true,
|
||||
|
||||
// Custom webpack config to ensure sourcemaps are generated properly
|
||||
webpack: (config, { buildId, dev, isServer, defaultLoaders }) => {
|
||||
// Enable sourcemaps for both client and server in production
|
||||
if (!dev) {
|
||||
config.devtool = isServer ? 'source-map' : 'hidden-source-map';
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
|
|
@ -42,9 +55,54 @@ const nextConfig = {
|
|||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default withSentryConfig(nextConfig, {
|
||||
org: process.env.SENTRY_ORG,
|
||||
project: process.env.SENTRY_PROJECT,
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
|
||||
// Sourcemap configuration optimized for monorepo
|
||||
sourcemaps: {
|
||||
disable: false,
|
||||
// More comprehensive asset patterns for monorepo
|
||||
assets: [
|
||||
".next/static/**/*.js",
|
||||
".next/static/**/*.js.map",
|
||||
".next/server/**/*.js",
|
||||
".next/server/**/*.js.map",
|
||||
],
|
||||
ignore: [
|
||||
"**/node_modules/**",
|
||||
"**/*hot-update*",
|
||||
"**/_buildManifest.js",
|
||||
"**/_ssgManifest.js",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.js",
|
||||
],
|
||||
deleteSourcemapsAfterUpload: true,
|
||||
},
|
||||
|
||||
// Release configuration
|
||||
release: {
|
||||
create: true,
|
||||
finalize: true,
|
||||
// Use git commit hash for releases in monorepo
|
||||
name: process.env.VERCEL_GIT_COMMIT_SHA || process.env.GITHUB_SHA || undefined,
|
||||
},
|
||||
|
||||
// NextJS specific optimizations for monorepo
|
||||
widenClientFileUpload: true,
|
||||
|
||||
// Additional configuration
|
||||
telemetry: false,
|
||||
silent: process.env.NODE_ENV === 'production',
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
|
||||
// Error handling for CI/CD
|
||||
errorHandler: (error) => {
|
||||
console.warn("Sentry build error occurred:", error.message);
|
||||
console.warn("This might be due to missing Sentry environment variables or network issues");
|
||||
// Don't fail the build if Sentry upload fails in monorepo context
|
||||
return;
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"scripts": {
|
||||
"dev": "dotenv -e ../../.env -- next dev -p 4200",
|
||||
"build": "next build",
|
||||
"build:sentry": "dotenv -e ../../.env -- next build",
|
||||
"start": "dotenv -e ../../.env -- next start -p 4200",
|
||||
"pm2": "pm2 start pnpm --name frontend -- start"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -626,3 +626,44 @@ html[dir='rtl'] [dir='ltr'] {
|
|||
.mantine-Overlay-root {
|
||||
background: rgba(65, 64, 66, 0.3) !important;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
@apply shadow-menu;
|
||||
background: var(--new-bgColorInner);
|
||||
border: 1px solid var(--new-bgLineColor);
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
|
||||
button {
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
display: flex;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
|
||||
&:hover,
|
||||
&:hover.is-selected {
|
||||
background-color: var(--new-bgLineColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tiptap {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mention {
|
||||
background-color: var(--purple-light);
|
||||
border-radius: 0.4rem;
|
||||
box-decoration-break: clone;
|
||||
color: #ae8afc;
|
||||
&::after {
|
||||
content: '\200B';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export function RegisterAfter({
|
|||
...data,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
.then(async (response) => {
|
||||
setLoading(false);
|
||||
if (response.status === 200) {
|
||||
fireEvents('register');
|
||||
|
|
@ -129,7 +129,7 @@ export function RegisterAfter({
|
|||
});
|
||||
} else {
|
||||
form.setError('email', {
|
||||
message: getHelpfulReasonForRegistrationFailure(response.status),
|
||||
message: await response.text(),
|
||||
});
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -245,6 +245,10 @@ export const MediaBox: FC<{
|
|||
|
||||
const dragAndDrop = useCallback(
|
||||
async (event: ClipboardEvent<HTMLDivElement> | File[]) => {
|
||||
if (!ref?.current?.setOptions) {
|
||||
return ;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const clipboardItems = event.map((p) => ({
|
||||
kind: 'file',
|
||||
|
|
|
|||
|
|
@ -75,9 +75,9 @@ export const BoldText: FC<{
|
|||
currentValue: string;
|
||||
}> = ({ editor }) => {
|
||||
const mark = () => {
|
||||
editor.commands.unsetUnderline();
|
||||
editor.commands.toggleBold();
|
||||
editor.commands.focus();
|
||||
editor?.commands?.unsetUnderline();
|
||||
editor?.commands?.toggleBold();
|
||||
editor?.commands?.focus();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export const Bullets: FC<{
|
|||
currentValue: string;
|
||||
}> = ({ editor }) => {
|
||||
const bullet = () => {
|
||||
editor.commands.toggleBulletList();
|
||||
editor?.commands?.toggleBulletList();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -50,6 +50,12 @@ import { BulletList, ListItem } from '@tiptap/extension-list';
|
|||
import { Bullets } from '@gitroom/frontend/components/new-launch/bullets.component';
|
||||
import Heading from '@tiptap/extension-heading';
|
||||
import { HeadingComponent } from '@gitroom/frontend/components/new-launch/heading.component';
|
||||
import Mention from '@tiptap/extension-mention';
|
||||
import { suggestion } from '@gitroom/frontend/components/new-launch/mention.component';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
|
||||
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
const InterceptBoldShortcut = Extension.create({
|
||||
name: 'preventBoldWithUnderline',
|
||||
|
|
@ -58,8 +64,8 @@ const InterceptBoldShortcut = Extension.create({
|
|||
return {
|
||||
'Mod-b': () => {
|
||||
// For example, toggle bold while removing underline
|
||||
this.editor.commands.unsetUnderline();
|
||||
return this.editor.commands.toggleBold();
|
||||
this?.editor?.commands?.unsetUnderline();
|
||||
return this?.editor?.commands?.toggleBold();
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
@ -72,58 +78,13 @@ const InterceptUnderlineShortcut = Extension.create({
|
|||
return {
|
||||
'Mod-u': () => {
|
||||
// For example, toggle bold while removing underline
|
||||
this.editor.commands.unsetBold();
|
||||
return this.editor.commands.toggleUnderline();
|
||||
this?.editor?.commands?.unsetBold();
|
||||
return this?.editor?.commands?.toggleUnderline();
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const Span = Node.create({
|
||||
name: 'mention',
|
||||
|
||||
inline: true,
|
||||
group: 'inline',
|
||||
selectable: false,
|
||||
atom: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
linkedinId: {
|
||||
default: null,
|
||||
},
|
||||
label: {
|
||||
default: '',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'span[data-linkedin-id]',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
'span',
|
||||
mergeAttributes(
|
||||
// Exclude linkedinId from HTMLAttributes to avoid duplication
|
||||
Object.fromEntries(
|
||||
Object.entries(HTMLAttributes).filter(([key]) => key !== 'linkedinId')
|
||||
),
|
||||
{
|
||||
'data-linkedin-id': HTMLAttributes.linkedinId,
|
||||
class: 'mention',
|
||||
}
|
||||
),
|
||||
`@${HTMLAttributes.label}`,
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
export const EditorWrapper: FC<{
|
||||
totalPosts: number;
|
||||
value: string;
|
||||
|
|
@ -544,29 +505,12 @@ export const Editor: FC<{
|
|||
|
||||
const addText = useCallback(
|
||||
(emoji: string) => {
|
||||
editorRef?.current?.editor.commands.insertContent(emoji);
|
||||
editorRef?.current?.editor.commands.focus();
|
||||
editorRef?.current?.editor?.commands?.insertContent(emoji);
|
||||
editorRef?.current?.editor?.commands?.focus();
|
||||
},
|
||||
[props.value, id]
|
||||
);
|
||||
|
||||
const addLinkedinTag = useCallback((text: string) => {
|
||||
const id = text.split('(')[1].split(')')[0];
|
||||
const name = text.split('[')[1].split(']')[0];
|
||||
|
||||
editorRef?.current?.editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContent({
|
||||
type: 'mention',
|
||||
attrs: {
|
||||
linkedinId: id,
|
||||
label: name,
|
||||
},
|
||||
})
|
||||
.run();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="relative bg-bigStrip" id={id}>
|
||||
|
|
@ -580,7 +524,7 @@ export const Editor: FC<{
|
|||
editor={editorRef?.current?.editor}
|
||||
currentValue={props.value!}
|
||||
/>
|
||||
{(editorType === 'markdown' || editorType === 'html') && (
|
||||
{(editorType === 'markdown' || editorType === 'html') && identifier !== 'telegram' && (
|
||||
<>
|
||||
<Bullets
|
||||
editor={editorRef?.current?.editor}
|
||||
|
|
@ -598,9 +542,6 @@ export const Editor: FC<{
|
|||
>
|
||||
{'\uD83D\uDE00'}
|
||||
</div>
|
||||
{identifier === 'linkedin' || identifier === 'linkedin-page' ? (
|
||||
<LinkedinCompanyPop addText={addLinkedinTag} />
|
||||
) : null}
|
||||
<div className="relative">
|
||||
<div className="absolute z-[200] top-[35px] -start-[50px]">
|
||||
<EmojiPicker
|
||||
|
|
@ -717,6 +658,43 @@ export const OnlyEditor = forwardRef<
|
|||
paste?: (event: ClipboardEvent | File[]) => void;
|
||||
}
|
||||
>(({ editorType, value, onChange, paste }, ref) => {
|
||||
const fetch = useFetch();
|
||||
const { internal } = useLaunchStore(
|
||||
useShallow((state) => ({
|
||||
internal: state.internal.find((p) => p.integration.id === state.current),
|
||||
}))
|
||||
);
|
||||
|
||||
const loadList = useCallback(
|
||||
async (query: string) => {
|
||||
if (query.length < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!internal?.integration.id) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const load = await fetch('/integrations/mentions', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: 'mention',
|
||||
id: internal.integration.id,
|
||||
data: { query },
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await load.json();
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error loading mentions:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
[internal, fetch]
|
||||
);
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
Document,
|
||||
|
|
@ -726,9 +704,28 @@ export const OnlyEditor = forwardRef<
|
|||
Bold,
|
||||
InterceptBoldShortcut,
|
||||
InterceptUnderlineShortcut,
|
||||
Span,
|
||||
BulletList,
|
||||
ListItem,
|
||||
...(internal?.integration?.id
|
||||
? [
|
||||
Mention.configure({
|
||||
HTMLAttributes: {
|
||||
class: 'mention',
|
||||
},
|
||||
renderHTML({ options, node }) {
|
||||
return [
|
||||
'span',
|
||||
mergeAttributes(options.HTMLAttributes, {
|
||||
'data-mention-id': node.attrs.id || '',
|
||||
'data-mention-label': node.attrs.label || '',
|
||||
}),
|
||||
`@${node.attrs.label}`,
|
||||
];
|
||||
},
|
||||
suggestion: suggestion(loadList),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
Heading.configure({
|
||||
levels: [1, 2, 3],
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export const HeadingComponent: FC<{
|
|||
currentValue: string;
|
||||
}> = ({ editor }) => {
|
||||
const setHeading = (level: number) => () => {
|
||||
editor.commands.toggleHeading({ level })
|
||||
editor?.commands?.toggleHeading({ level })
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,248 @@
|
|||
import React, { FC, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { computePosition, flip, shift } from '@floating-ui/dom';
|
||||
import { posToDOMRect, ReactRenderer } from '@tiptap/react';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
|
||||
// Debounce utility for TipTap
|
||||
const debounce = <T extends any[]>(
|
||||
func: (...args: any[]) => Promise<T>,
|
||||
wait: number
|
||||
) => {
|
||||
let timeout: NodeJS.Timeout;
|
||||
return (...args: any[]): Promise<T> => {
|
||||
clearTimeout(timeout);
|
||||
return new Promise((resolve) => {
|
||||
timeout = setTimeout(async () => {
|
||||
try {
|
||||
const result = await func(...args);
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
console.error('Debounced function error:', error);
|
||||
resolve([] as T);
|
||||
}
|
||||
}, wait);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const MentionList: FC = (props: any) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const selectItem = (index: number) => {
|
||||
const item = props.items[index];
|
||||
|
||||
if (item) {
|
||||
props.command(item);
|
||||
}
|
||||
};
|
||||
|
||||
const upHandler = () => {
|
||||
setSelectedIndex(
|
||||
(selectedIndex + props.items.length - 1) % props.items.length
|
||||
);
|
||||
};
|
||||
|
||||
const downHandler = () => {
|
||||
setSelectedIndex((selectedIndex + 1) % props.items.length);
|
||||
};
|
||||
|
||||
const enterHandler = () => {
|
||||
selectItem(selectedIndex);
|
||||
};
|
||||
|
||||
useEffect(() => setSelectedIndex(0), [props.items]);
|
||||
|
||||
useImperativeHandle(props.ref, () => ({
|
||||
onKeyDown: ({ event }: { event: any }) => {
|
||||
if (event.key === 'ArrowUp') {
|
||||
upHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
downHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
enterHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
}));
|
||||
|
||||
if (props?.stop) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dropdown-menu bg-white border border-gray-200 rounded-lg shadow-lg max-h-60 overflow-y-auto p-2">
|
||||
{props?.items?.none ? (
|
||||
<div className="flex items-center justify-center p-2 text-gray-500">
|
||||
We don't have autocomplete for this social media
|
||||
</div>
|
||||
) : props?.loading ? (
|
||||
<div className="flex items-center justify-center p-2 text-gray-500">
|
||||
Loading...
|
||||
</div>
|
||||
) : props?.items ? (
|
||||
props.items.length === 0 ? (
|
||||
<div className="p-2 text-gray-500 text-center">No results found</div>
|
||||
) : (
|
||||
props.items.map((item: any, index: any) => (
|
||||
<button
|
||||
className={`flex gap-[10px] w-full p-2 text-left rounded hover:bg-gray-100 ${
|
||||
index === selectedIndex ? 'bg-blue-100' : ''
|
||||
}`}
|
||||
key={item.id || index}
|
||||
onClick={() => selectItem(index)}
|
||||
>
|
||||
<img
|
||||
src={item.image}
|
||||
alt={item.label}
|
||||
className="w-[30px] h-[30px] rounded-full object-cover"
|
||||
/>
|
||||
<div className="flex-1 text-gray-800">{item.label}</div>
|
||||
</button>
|
||||
))
|
||||
)
|
||||
) : (
|
||||
<div className="p-2 text-gray-500 text-center">Loading...</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const updatePosition = (editor: any, element: any) => {
|
||||
const virtualElement = {
|
||||
getBoundingClientRect: () =>
|
||||
posToDOMRect(
|
||||
editor.view,
|
||||
editor.state.selection.from,
|
||||
editor.state.selection.to
|
||||
),
|
||||
};
|
||||
|
||||
computePosition(virtualElement, element, {
|
||||
placement: 'bottom-start',
|
||||
strategy: 'absolute',
|
||||
middleware: [shift(), flip()],
|
||||
}).then(({ x, y, strategy }) => {
|
||||
element.style.width = 'max-content';
|
||||
element.style.position = strategy;
|
||||
element.style.left = `${x}px`;
|
||||
element.style.top = `${y}px`;
|
||||
element.style.zIndex = '1000';
|
||||
});
|
||||
};
|
||||
|
||||
export const suggestion = (
|
||||
loadList: (
|
||||
query: string
|
||||
) => Promise<{ image: string; label: string; id: string }[]>
|
||||
) => {
|
||||
// Create debounced version of loadList once
|
||||
const debouncedLoadList = debounce(loadList, 500);
|
||||
let component: any;
|
||||
|
||||
return {
|
||||
items: async ({ query }: { query: string }) => {
|
||||
if (!query || query.length < 2) {
|
||||
component.updateProps({ loading: true, stop: true });
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
component.updateProps({ loading: true, stop: false });
|
||||
const result = await debouncedLoadList(query);
|
||||
return result;
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
render: () => {
|
||||
let currentQuery = '';
|
||||
let isLoadingQuery = false;
|
||||
|
||||
return {
|
||||
onBeforeStart: (props: any) => {
|
||||
component = new ReactRenderer(MentionList, {
|
||||
props: {
|
||||
...props,
|
||||
loading: true,
|
||||
},
|
||||
editor: props.editor,
|
||||
});
|
||||
component.updateProps({ ...props, loading: true, stop: false });
|
||||
updatePosition(props.editor, component.element);
|
||||
},
|
||||
onStart: (props: any) => {
|
||||
currentQuery = props.query || '';
|
||||
isLoadingQuery = currentQuery.length >= 2;
|
||||
|
||||
if (!props.clientRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
component.element.style.position = 'absolute';
|
||||
component.element.style.zIndex = '1000';
|
||||
|
||||
const container =
|
||||
document.querySelector('.mantine-Paper-root') || document.body;
|
||||
container.appendChild(component.element);
|
||||
|
||||
updatePosition(props.editor, component.element);
|
||||
component.updateProps({ ...props, loading: true });
|
||||
},
|
||||
|
||||
onUpdate(props: any) {
|
||||
const newQuery = props.query || '';
|
||||
const queryChanged = newQuery !== currentQuery;
|
||||
currentQuery = newQuery;
|
||||
|
||||
// If query changed and is valid, we're loading until results come in
|
||||
if (queryChanged && newQuery.length >= 2) {
|
||||
isLoadingQuery = true;
|
||||
}
|
||||
|
||||
// If we have results, we're no longer loading
|
||||
if (props.items && props.items.length > 0) {
|
||||
isLoadingQuery = false;
|
||||
}
|
||||
|
||||
// Show loading if we have a valid query but no results yet
|
||||
const shouldShowLoading =
|
||||
isLoadingQuery &&
|
||||
newQuery.length >= 2 &&
|
||||
(!props.items || props.items.length === 0);
|
||||
|
||||
component.updateProps({ ...props, loading: false, stop: false });
|
||||
|
||||
if (!props.clientRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
updatePosition(props.editor, component.element);
|
||||
},
|
||||
|
||||
onKeyDown(props: any) {
|
||||
if (props.event.key === 'Escape') {
|
||||
component.destroy();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return component.ref?.onKeyDown(props);
|
||||
},
|
||||
|
||||
onExit() {
|
||||
component.element.remove();
|
||||
component.destroy();
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
@ -148,7 +148,7 @@ const TikTokSettings: FC<{
|
|||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<CheckTikTokValidity picture={props?.values?.[0]?.image?.[0]?.path} />
|
||||
{/*<CheckTikTokValidity picture={props?.values?.[0]?.image?.[0]?.path} />*/}
|
||||
{isTitle && (
|
||||
<Input label="Title" {...register('title')} maxLength={90} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -75,9 +75,9 @@ export const UText: FC<{
|
|||
currentValue: string;
|
||||
}> = ({ editor }) => {
|
||||
const mark = () => {
|
||||
editor.commands.unsetBold();
|
||||
editor.commands.toggleUnderline();
|
||||
editor.commands.focus();
|
||||
editor?.commands?.unsetBold();
|
||||
editor?.commands?.toggleUnderline();
|
||||
editor?.commands?.focus();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ export const SignatureBox: FC<{
|
|||
setShowModal(true);
|
||||
}, [showModal]);
|
||||
const appendValue = (val: string) => {
|
||||
editor?.commands.insertContent("\n\n" + val);
|
||||
editor?.commands.focus();
|
||||
editor?.commands?.insertContent("\n\n" + val);
|
||||
editor?.commands?.focus();
|
||||
setShowModal(false);
|
||||
};
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import striptags from 'striptags';
|
||||
import { NodeHtmlMarkdown } from 'node-html-markdown';
|
||||
import TurndownService from 'turndown';
|
||||
const turndownService = new TurndownService();
|
||||
|
||||
const bold = {
|
||||
a: '𝗮',
|
||||
|
|
@ -135,7 +136,8 @@ export const stripHtmlValidation = (
|
|||
type: 'none' | 'normal' | 'markdown' | 'html',
|
||||
value: string,
|
||||
replaceBold = false,
|
||||
none = false
|
||||
none = false,
|
||||
convertMentionFunction?: (idOrHandle: string, name: string) => string,
|
||||
): string => {
|
||||
if (type === 'html') {
|
||||
return striptags(value, [
|
||||
|
|
@ -152,7 +154,7 @@ export const stripHtmlValidation = (
|
|||
}
|
||||
|
||||
if (type === 'markdown') {
|
||||
return NodeHtmlMarkdown.translate(value);
|
||||
return turndownService.turndown(value);
|
||||
}
|
||||
|
||||
if (value.indexOf('<p>') === -1 && !none) {
|
||||
|
|
@ -171,18 +173,16 @@ export const stripHtmlValidation = (
|
|||
}
|
||||
|
||||
if (replaceBold) {
|
||||
const processedHtml = convertLinkedinMention(
|
||||
const processedHtml = convertMention(
|
||||
convertToAscii(
|
||||
html
|
||||
.replace(/<ul>/, "\n<ul>")
|
||||
.replace(/<\/ul>\n/, "</ul>")
|
||||
.replace(
|
||||
/<li.*?>([.\s\S]*?)<\/li.*?>/gm,
|
||||
(match, p1) => {
|
||||
.replace(/<ul>/, '\n<ul>')
|
||||
.replace(/<\/ul>\n/, '</ul>')
|
||||
.replace(/<li.*?>([.\s\S]*?)<\/li.*?>/gm, (match, p1) => {
|
||||
return `<li><p>- ${p1.replace(/\n/gm, '')}\n</p></li>`;
|
||||
}
|
||||
)
|
||||
)
|
||||
})
|
||||
),
|
||||
convertMentionFunction
|
||||
);
|
||||
|
||||
return striptags(processedHtml, ['h1', 'h2', 'h3']);
|
||||
|
|
@ -192,11 +192,18 @@ export const stripHtmlValidation = (
|
|||
return striptags(html, ['ul', 'li', 'h1', 'h2', 'h3']);
|
||||
};
|
||||
|
||||
export const convertLinkedinMention = (value: string) => {
|
||||
export const convertMention = (
|
||||
value: string,
|
||||
process?: (idOrHandle: string, name: string) => string
|
||||
) => {
|
||||
if (!process) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return value.replace(
|
||||
/<span.+?data-linkedin-id="(.+?)".+?>(.+?)<\/span>/gi,
|
||||
/<span.*?data-mention-id="(.*?)".*?>(.*?)<\/span>/gi,
|
||||
(match, id, name) => {
|
||||
return `@[${name.replace('@', '')}](${id})`;
|
||||
return `<span>` + process(id, name) + `</span>`;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,9 +15,59 @@ export class IntegrationRepository {
|
|||
private _posts: PrismaRepository<'post'>,
|
||||
private _plugs: PrismaRepository<'plugs'>,
|
||||
private _exisingPlugData: PrismaRepository<'exisingPlugData'>,
|
||||
private _customers: PrismaRepository<'customer'>
|
||||
private _customers: PrismaRepository<'customer'>,
|
||||
private _mentions: PrismaRepository<'mentions'>
|
||||
) {}
|
||||
|
||||
getMentions(platform: string, q: string) {
|
||||
return this._mentions.model.mentions.findMany({
|
||||
where: {
|
||||
platform,
|
||||
OR: [
|
||||
{
|
||||
name: {
|
||||
contains: q,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
{
|
||||
username: {
|
||||
contains: q,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
take: 100,
|
||||
select: {
|
||||
name: true,
|
||||
username: true,
|
||||
image: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
insertMentions(
|
||||
platform: string,
|
||||
mentions: { name: string; username: string; image: string }[]
|
||||
) {
|
||||
if (mentions.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return this._mentions.model.mentions.createMany({
|
||||
data: mentions.map((mention) => ({
|
||||
platform,
|
||||
name: mention.name,
|
||||
username: mention.username,
|
||||
image: mention.image,
|
||||
})),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
|
||||
updateProviderSettings(org: string, id: string, settings: string) {
|
||||
return this._integration.model.integration.update({
|
||||
where: {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,17 @@ export class IntegrationService {
|
|||
return true;
|
||||
}
|
||||
|
||||
getMentions(platform: string, q: string) {
|
||||
return this._integrationRepository.getMentions(platform, q);
|
||||
}
|
||||
|
||||
insertMentions(
|
||||
platform: string,
|
||||
mentions: { name: string; username: string; image: string }[]
|
||||
) {
|
||||
return this._integrationRepository.insertMentions(platform, mentions);
|
||||
}
|
||||
|
||||
async setTimes(
|
||||
orgId: string,
|
||||
integrationId: string,
|
||||
|
|
@ -163,7 +174,11 @@ export class IntegrationService {
|
|||
await this.informAboutRefreshError(orgId, integration);
|
||||
}
|
||||
|
||||
async informAboutRefreshError(orgId: string, integration: Integration, err = '') {
|
||||
async informAboutRefreshError(
|
||||
orgId: string,
|
||||
integration: Integration,
|
||||
err = ''
|
||||
) {
|
||||
await this._notificationService.inAppNotification(
|
||||
orgId,
|
||||
`Could not refresh your ${integration.providerIdentifier} channel ${err}`,
|
||||
|
|
|
|||
|
|
@ -394,7 +394,7 @@ export class PostsRepository {
|
|||
where: {
|
||||
orgId: orgId,
|
||||
name: {
|
||||
in: tags.map((tag) => tag.label).filter(f => f),
|
||||
in: tags.map((tag) => tag.label).filter((f) => f),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -378,7 +378,9 @@ export class PostsService {
|
|||
return post;
|
||||
}
|
||||
|
||||
const ids = (extract || []).map((e) => e.replace('(post:', '').replace(')', ''));
|
||||
const ids = (extract || []).map((e) =>
|
||||
e.replace('(post:', '').replace(')', '')
|
||||
);
|
||||
const urls = await this._postRepository.getPostUrls(orgId, ids);
|
||||
const newPlainText = ids.reduce((acc, value) => {
|
||||
const findUrl = urls?.find?.((u) => u.id === value)?.releaseURL || '';
|
||||
|
|
@ -467,7 +469,13 @@ export class PostsService {
|
|||
await Promise.all(
|
||||
(newPosts || []).map(async (p) => ({
|
||||
id: p.id,
|
||||
message: stripHtmlValidation(getIntegration.editor, p.content, true),
|
||||
message: stripHtmlValidation(
|
||||
getIntegration.editor,
|
||||
p.content,
|
||||
true,
|
||||
false,
|
||||
getIntegration.mentionFormat
|
||||
),
|
||||
settings: JSON.parse(p.settings || '{}'),
|
||||
media: await this.updateMedia(
|
||||
p.id,
|
||||
|
|
@ -535,7 +543,12 @@ export class PostsService {
|
|||
throw err;
|
||||
}
|
||||
|
||||
throw new BadBody(integration.providerIdentifier, JSON.stringify(err), {} as any, '');
|
||||
throw new BadBody(
|
||||
integration.providerIdentifier,
|
||||
JSON.stringify(err),
|
||||
{} as any,
|
||||
''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -658,6 +658,18 @@ model Errors {
|
|||
@@index([createdAt])
|
||||
}
|
||||
|
||||
model Mentions {
|
||||
name String
|
||||
username String
|
||||
platform String
|
||||
image String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@id([name, username, platform, image])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
enum OrderStatus {
|
||||
PENDING
|
||||
ACCEPTED
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { concurrencyService } from '@gitroom/helpers/utils/concurrency.service';
|
||||
import { Integration } from '@prisma/client';
|
||||
|
||||
export class RefreshToken {
|
||||
constructor(
|
||||
|
|
@ -31,6 +32,10 @@ export abstract class SocialAbstract {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
public async mention(token: string, d: { query: string }, id: string, integration: Integration): Promise<{ id: string; label: string; image: string }[] | {none: true}> {
|
||||
return {none: true};
|
||||
}
|
||||
|
||||
async runInConcurrent<T>(func: (...args: any[]) => Promise<T>) {
|
||||
const value = await concurrencyService<any>(this.identifier.split('-')[0], async () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@ import {
|
|||
RefreshToken,
|
||||
SocialAbstract,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import {
|
||||
BskyAgent,
|
||||
RichText,
|
||||
import {
|
||||
BskyAgent,
|
||||
RichText,
|
||||
AppBskyEmbedVideo,
|
||||
AppBskyVideoDefs,
|
||||
AtpAgent,
|
||||
BlobRef
|
||||
BlobRef,
|
||||
} from '@atproto/api';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integration } from '@prisma/client';
|
||||
|
|
@ -59,16 +59,19 @@ async function reduceImageBySize(url: string, maxSizeKB = 976) {
|
|||
}
|
||||
}
|
||||
|
||||
async function uploadVideo(agent: AtpAgent, videoPath: string): Promise<AppBskyEmbedVideo.Main> {
|
||||
const { data: serviceAuth } = await agent.com.atproto.server.getServiceAuth(
|
||||
{
|
||||
aud: `did:web:${agent.dispatchUrl.host}`,
|
||||
lxm: "com.atproto.repo.uploadBlob",
|
||||
exp: Date.now() / 1000 + 60 * 30, // 30 minutes
|
||||
},
|
||||
);
|
||||
async function uploadVideo(
|
||||
agent: AtpAgent,
|
||||
videoPath: string
|
||||
): Promise<AppBskyEmbedVideo.Main> {
|
||||
const { data: serviceAuth } = await agent.com.atproto.server.getServiceAuth({
|
||||
aud: `did:web:${agent.dispatchUrl.host}`,
|
||||
lxm: 'com.atproto.repo.uploadBlob',
|
||||
exp: Date.now() / 1000 + 60 * 30, // 30 minutes
|
||||
});
|
||||
|
||||
async function downloadVideo(url: string): Promise<{ video: Buffer, size: number }> {
|
||||
async function downloadVideo(
|
||||
url: string
|
||||
): Promise<{ video: Buffer; size: number }> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch video: ${response.statusText}`);
|
||||
|
|
@ -81,35 +84,37 @@ async function uploadVideo(agent: AtpAgent, videoPath: string): Promise<AppBskyE
|
|||
|
||||
const video = await downloadVideo(videoPath);
|
||||
|
||||
console.log("Downloaded video", videoPath, video.size);
|
||||
|
||||
const uploadUrl = new URL("https://video.bsky.app/xrpc/app.bsky.video.uploadVideo");
|
||||
uploadUrl.searchParams.append("did", agent.session!.did);
|
||||
uploadUrl.searchParams.append("name", videoPath.split("/").pop()!);
|
||||
|
||||
console.log('Downloaded video', videoPath, video.size);
|
||||
|
||||
const uploadUrl = new URL(
|
||||
'https://video.bsky.app/xrpc/app.bsky.video.uploadVideo'
|
||||
);
|
||||
uploadUrl.searchParams.append('did', agent.session!.did);
|
||||
uploadUrl.searchParams.append('name', videoPath.split('/').pop()!);
|
||||
|
||||
const uploadResponse = await fetch(uploadUrl, {
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceAuth.token}`,
|
||||
"Content-Type": "video/mp4",
|
||||
"Content-Length": video.size.toString(),
|
||||
'Content-Type': 'video/mp4',
|
||||
'Content-Length': video.size.toString(),
|
||||
},
|
||||
body: video.video
|
||||
body: video.video,
|
||||
});
|
||||
|
||||
|
||||
const jobStatus = (await uploadResponse.json()) as AppBskyVideoDefs.JobStatus;
|
||||
console.log("JobId:", jobStatus.jobId);
|
||||
console.log('JobId:', jobStatus.jobId);
|
||||
let blob: BlobRef | undefined = jobStatus.blob;
|
||||
const videoAgent = new AtpAgent({ service: "https://video.bsky.app" });
|
||||
|
||||
const videoAgent = new AtpAgent({ service: 'https://video.bsky.app' });
|
||||
|
||||
while (!blob) {
|
||||
const { data: status } = await videoAgent.app.bsky.video.getJobStatus(
|
||||
{ jobId: jobStatus.jobId },
|
||||
);
|
||||
const { data: status } = await videoAgent.app.bsky.video.getJobStatus({
|
||||
jobId: jobStatus.jobId,
|
||||
});
|
||||
console.log(
|
||||
"Status:",
|
||||
'Status:',
|
||||
status.jobStatus.state,
|
||||
status.jobStatus.progress || "",
|
||||
status.jobStatus.progress || ''
|
||||
);
|
||||
if (status.jobStatus.blob) {
|
||||
blob = status.jobStatus.blob;
|
||||
|
|
@ -117,11 +122,11 @@ async function uploadVideo(agent: AtpAgent, videoPath: string): Promise<AppBskyE
|
|||
// wait a second
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
console.log("posting video...");
|
||||
|
||||
console.log('posting video...');
|
||||
|
||||
return {
|
||||
$type: "app.bsky.embed.video",
|
||||
$type: 'app.bsky.embed.video',
|
||||
video: blob,
|
||||
} satisfies AppBskyEmbedVideo.Main;
|
||||
}
|
||||
|
|
@ -243,8 +248,10 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
const cidUrl = [] as { cid: string; url: string; rev: string }[];
|
||||
for (const post of postDetails) {
|
||||
// Separate images and videos
|
||||
const imageMedia = post.media?.filter((p) => p.path.indexOf('mp4') === -1) || [];
|
||||
const videoMedia = post.media?.filter((p) => p.path.indexOf('mp4') !== -1) || [];
|
||||
const imageMedia =
|
||||
post.media?.filter((p) => p.path.indexOf('mp4') === -1) || [];
|
||||
const videoMedia =
|
||||
post.media?.filter((p) => p.path.indexOf('mp4') !== -1) || [];
|
||||
|
||||
// Upload images
|
||||
const images = await Promise.all(
|
||||
|
|
@ -313,7 +320,11 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
if (postDetails?.[0]?.settings?.active_thread_finisher) {
|
||||
const rt = new RichText({
|
||||
text: stripHtmlValidation('normal', postDetails?.[0]?.settings?.thread_finisher, true),
|
||||
text: stripHtmlValidation(
|
||||
'normal',
|
||||
postDetails?.[0]?.settings?.thread_finisher,
|
||||
true
|
||||
),
|
||||
});
|
||||
|
||||
await rt.detectFacets(agent);
|
||||
|
|
@ -487,4 +498,38 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
override async mention(
|
||||
token: string,
|
||||
d: { query: string },
|
||||
id: string,
|
||||
integration: Integration
|
||||
) {
|
||||
const body = JSON.parse(
|
||||
AuthService.fixedDecryption(integration.customInstanceDetails!)
|
||||
);
|
||||
|
||||
const agent = new BskyAgent({
|
||||
service: body.service,
|
||||
});
|
||||
|
||||
await agent.login({
|
||||
identifier: body.identifier,
|
||||
password: body.password,
|
||||
});
|
||||
|
||||
const list = await agent.searchActors({
|
||||
q: d.query,
|
||||
});
|
||||
|
||||
return list.data.actors.map((p) => ({
|
||||
label: p.displayName,
|
||||
id: p.handle,
|
||||
image: p.avatar,
|
||||
}));
|
||||
}
|
||||
|
||||
mentionFormat(idOrHandle: string, name: string) {
|
||||
return `@${idOrHandle}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -715,4 +715,32 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
override async mention(token: string, data: { query: string }) {
|
||||
const { elements } = await (
|
||||
await this.fetch(
|
||||
`https://api.linkedin.com/v2/organizations?q=vanityName&vanityName=${encodeURIComponent(
|
||||
data.query
|
||||
)}&projection=(elements*(id,localizedName,logoV2(original~:playableStreams)))`,
|
||||
{
|
||||
headers: {
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'Content-Type': 'application/json',
|
||||
'LinkedIn-Version': '202504',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
return elements.map((p: any) => ({
|
||||
id: String(p.id),
|
||||
label: p.localizedName,
|
||||
image: p.logoV2?.['original~']?.elements?.[0]?.identifiers?.[0]?.identifier || '',
|
||||
}));
|
||||
}
|
||||
|
||||
mentionFormat(idOrHandle: string, name: string) {
|
||||
return `@[${name.replace('@', '')}](urn:li:organization:${idOrHandle})`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,24 @@ export class PinterestProvider
|
|||
|
||||
editor = 'normal' as const;
|
||||
|
||||
public override handleErrors(body: string):
|
||||
| {
|
||||
type: 'refresh-token' | 'bad-body';
|
||||
value: string;
|
||||
}
|
||||
| undefined {
|
||||
|
||||
if (body.indexOf('cover_image_url or cover_image_content_type') > -1) {
|
||||
return {
|
||||
type: 'bad-body' as const,
|
||||
value:
|
||||
'When uploading a video, you must add also an image to be used as a cover image.',
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
const { access_token, expires_in } = await (
|
||||
await this.fetch('https://api.pinterest.com/v5/oauth/token', {
|
||||
|
|
@ -212,57 +230,52 @@ export class PinterestProvider
|
|||
path: m.path,
|
||||
}));
|
||||
|
||||
try {
|
||||
const { id: pId } = await (
|
||||
await this.fetch('https://api.pinterest.com/v5/pins', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...(postDetails?.[0]?.settings.link
|
||||
? { link: postDetails?.[0]?.settings.link }
|
||||
: {}),
|
||||
...(postDetails?.[0]?.settings.title
|
||||
? { title: postDetails?.[0]?.settings.title }
|
||||
: {}),
|
||||
description: postDetails?.[0]?.message,
|
||||
...(postDetails?.[0]?.settings.dominant_color
|
||||
? { dominant_color: postDetails?.[0]?.settings.dominant_color }
|
||||
: {}),
|
||||
board_id: postDetails?.[0]?.settings.board,
|
||||
media_source: mediaId
|
||||
? {
|
||||
source_type: 'video_id',
|
||||
media_id: mediaId,
|
||||
cover_image_url: picture?.path,
|
||||
}
|
||||
: mapImages?.length === 1
|
||||
? {
|
||||
source_type: 'image_url',
|
||||
url: mapImages?.[0]?.path,
|
||||
}
|
||||
: {
|
||||
source_type: 'multiple_image_urls',
|
||||
items: mapImages,
|
||||
},
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
return [
|
||||
{
|
||||
id: postDetails?.[0]?.id,
|
||||
postId: pId,
|
||||
releaseURL: `https://www.pinterest.com/pin/${pId}`,
|
||||
status: 'success',
|
||||
const { id: pId } = await (
|
||||
await this.fetch('https://api.pinterest.com/v5/pins', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
];
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return [];
|
||||
}
|
||||
body: JSON.stringify({
|
||||
...(postDetails?.[0]?.settings.link
|
||||
? { link: postDetails?.[0]?.settings.link }
|
||||
: {}),
|
||||
...(postDetails?.[0]?.settings.title
|
||||
? { title: postDetails?.[0]?.settings.title }
|
||||
: {}),
|
||||
description: postDetails?.[0]?.message,
|
||||
...(postDetails?.[0]?.settings.dominant_color
|
||||
? { dominant_color: postDetails?.[0]?.settings.dominant_color }
|
||||
: {}),
|
||||
board_id: postDetails?.[0]?.settings.board,
|
||||
media_source: mediaId
|
||||
? {
|
||||
source_type: 'video_id',
|
||||
media_id: mediaId,
|
||||
cover_image_url: picture?.path,
|
||||
}
|
||||
: mapImages?.length === 1
|
||||
? {
|
||||
source_type: 'image_url',
|
||||
url: mapImages?.[0]?.path,
|
||||
}
|
||||
: {
|
||||
source_type: 'multiple_image_urls',
|
||||
items: mapImages,
|
||||
},
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
return [
|
||||
{
|
||||
id: postDetails?.[0]?.id,
|
||||
postId: pId,
|
||||
releaseURL: `https://www.pinterest.com/pin/${pId}`,
|
||||
status: 'success',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async analytics(
|
||||
|
|
|
|||
|
|
@ -132,4 +132,8 @@ export interface SocialProvider
|
|||
externalUrl?: (
|
||||
url: string
|
||||
) => Promise<{ client_id: string; client_secret: string }>;
|
||||
mention?: (
|
||||
token: string, data: { query: string }, id: string, integration: Integration
|
||||
) => Promise<{ id: string; label: string; image: string }[] | {none: true}>;
|
||||
mentionFormat?(idOrHandle: string, name: string): string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.ab
|
|||
import mime from 'mime';
|
||||
import TelegramBot from 'node-telegram-bot-api';
|
||||
import { Integration } from '@prisma/client';
|
||||
import striptags from 'striptags';
|
||||
|
||||
const telegramBot = new TelegramBot(process.env.TELEGRAM_TOKEN!);
|
||||
// Added to support local storage posting
|
||||
|
|
@ -23,7 +24,7 @@ export class TelegramProvider extends SocialAbstract implements SocialProvider {
|
|||
isBetweenSteps = false;
|
||||
isWeb3 = true;
|
||||
scopes = [] as string[];
|
||||
editor = 'markdown' as const;
|
||||
editor = 'html' as const;
|
||||
|
||||
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
|
||||
return {
|
||||
|
|
@ -145,7 +146,14 @@ export class TelegramProvider extends SocialAbstract implements SocialProvider {
|
|||
for (const message of postDetails) {
|
||||
let messageId: number | null = null;
|
||||
const mediaFiles = message.media || [];
|
||||
const text = message.message || '';
|
||||
const text = striptags(message.message || '', [
|
||||
'u',
|
||||
'strong',
|
||||
'p',
|
||||
])
|
||||
.replace(/<strong>/g, '<b>')
|
||||
.replace(/<\/strong>/g, '</b>')
|
||||
.replace(/<p>(.*?)<\/p>/g, '$1\n')
|
||||
// check if media is local to modify url
|
||||
const processedMedia = mediaFiles.map((media) => {
|
||||
let mediaUrl = media.path;
|
||||
|
|
@ -176,7 +184,9 @@ export class TelegramProvider extends SocialAbstract implements SocialProvider {
|
|||
});
|
||||
// if there's no media, bot sends a text message only
|
||||
if (processedMedia.length === 0) {
|
||||
const response = await telegramBot.sendMessage(accessToken, text);
|
||||
const response = await telegramBot.sendMessage(accessToken, text, {
|
||||
parse_mode: 'HTML',
|
||||
});
|
||||
messageId = response.message_id;
|
||||
}
|
||||
// if there's only one media, bot sends the media with the text message as caption
|
||||
|
|
@ -187,20 +197,20 @@ export class TelegramProvider extends SocialAbstract implements SocialProvider {
|
|||
? await telegramBot.sendVideo(
|
||||
accessToken,
|
||||
media.media,
|
||||
{ caption: text, parse_mode: 'Markdown' },
|
||||
{ caption: text, parse_mode: 'HTML' },
|
||||
media.fileOptions
|
||||
)
|
||||
: media.type === 'photo'
|
||||
? await telegramBot.sendPhoto(
|
||||
accessToken,
|
||||
media.media,
|
||||
{ caption: text, parse_mode: 'Markdown' },
|
||||
{ caption: text, parse_mode: 'HTML' },
|
||||
media.fileOptions
|
||||
)
|
||||
: await telegramBot.sendDocument(
|
||||
accessToken,
|
||||
media.media,
|
||||
{ caption: text, parse_mode: 'Markdown' },
|
||||
{ caption: text, parse_mode: 'HTML' },
|
||||
media.fileOptions
|
||||
);
|
||||
messageId = response.message_id;
|
||||
|
|
@ -213,7 +223,7 @@ export class TelegramProvider extends SocialAbstract implements SocialProvider {
|
|||
type: m.type === 'document' ? 'document' : m.type, // Documents are not allowed in media groups
|
||||
media: m.media,
|
||||
caption: i === 0 && index === 0 ? text : undefined,
|
||||
parse_mode: 'Markdown'
|
||||
parse_mode: 'HTML',
|
||||
}));
|
||||
|
||||
const response = await telegramBot.sendMediaGroup(
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ 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 { TwitterApi } from 'twitter-api-v2';
|
||||
|
||||
export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'threads';
|
||||
|
|
@ -23,6 +24,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
'threads_content_publish',
|
||||
'threads_manage_replies',
|
||||
'threads_manage_insights',
|
||||
// 'threads_profile_discovery',
|
||||
];
|
||||
|
||||
editor = 'normal' as const;
|
||||
|
|
@ -413,8 +415,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
{
|
||||
id: makeId(10),
|
||||
media: [],
|
||||
message:
|
||||
postDetails?.[0]?.settings?.thread_finisher,
|
||||
message: postDetails?.[0]?.settings?.thread_finisher,
|
||||
settings: {},
|
||||
},
|
||||
lastReplyId,
|
||||
|
|
@ -526,4 +527,29 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
// override async mention(
|
||||
// token: string,
|
||||
// data: { query: string },
|
||||
// id: string,
|
||||
// integration: Integration
|
||||
// ) {
|
||||
// const p = await (
|
||||
// await fetch(
|
||||
// `https://graph.threads.net/v1.0/profile_lookup?username=${data.query}&access_token=${integration.token}`
|
||||
// )
|
||||
// ).json();
|
||||
//
|
||||
// return [
|
||||
// {
|
||||
// id: String(p.id),
|
||||
// label: p.name,
|
||||
// image: p.profile_picture_url,
|
||||
// },
|
||||
// ];
|
||||
// }
|
||||
//
|
||||
// mentionFormat(idOrHandle: string, name: string) {
|
||||
// return `@${idOrHandle}`;
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -315,7 +315,10 @@ export class XProvider extends SocialAbstract implements SocialProvider {
|
|||
const media_ids = (uploadAll[post.id] || []).filter((f) => f);
|
||||
|
||||
// @ts-ignore
|
||||
const { data }: { data: { id: string } } = await this.runInConcurrent( async () => client.v2.tweet({
|
||||
const { data }: { data: { id: string } } = await this.runInConcurrent(
|
||||
async () =>
|
||||
// @ts-ignore
|
||||
client.v2.tweet({
|
||||
...(!postDetails?.[0]?.settings?.who_can_reply_post ||
|
||||
postDetails?.[0]?.settings?.who_can_reply_post === 'everyone'
|
||||
? {}
|
||||
|
|
@ -492,4 +495,39 @@ export class XProvider extends SocialAbstract implements SocialProvider {
|
|||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
override async mention(token: string, d: { query: string }) {
|
||||
const [accessTokenSplit, accessSecretSplit] = token.split(':');
|
||||
const client = new TwitterApi({
|
||||
appKey: process.env.X_API_KEY!,
|
||||
appSecret: process.env.X_API_SECRET!,
|
||||
accessToken: accessTokenSplit,
|
||||
accessSecret: accessSecretSplit,
|
||||
});
|
||||
|
||||
try {
|
||||
const data = await client.v2.userByUsername(d.query, {
|
||||
'user.fields': ['username', 'name', 'profile_image_url'],
|
||||
});
|
||||
|
||||
if (!data?.data?.username) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: data.data.username,
|
||||
image: data.data.profile_image_url,
|
||||
label: data.data.name,
|
||||
},
|
||||
];
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
mentionFormat(idOrHandle: string, name: string) {
|
||||
return `@${idOrHandle}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,12 +86,14 @@
|
|||
"@tiptap/extension-heading": "^3.0.7",
|
||||
"@tiptap/extension-history": "^3.0.7",
|
||||
"@tiptap/extension-list": "^3.0.7",
|
||||
"@tiptap/extension-mention": "^3.0.7",
|
||||
"@tiptap/extension-paragraph": "^3.0.6",
|
||||
"@tiptap/extension-text": "^3.0.6",
|
||||
"@tiptap/extension-underline": "^3.0.6",
|
||||
"@tiptap/pm": "^3.0.6",
|
||||
"@tiptap/react": "^3.0.6",
|
||||
"@tiptap/starter-kit": "^3.0.6",
|
||||
"@tiptap/suggestion": "^3.0.7",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/concat-stream": "^2.0.3",
|
||||
"@types/facebook-nodejs-business-sdk": "^20.0.2",
|
||||
|
|
@ -107,6 +109,7 @@
|
|||
"@types/sha256": "^0.2.2",
|
||||
"@types/stripe": "^8.0.417",
|
||||
"@types/striptags": "^0.0.5",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"@types/yup": "^0.32.0",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@uiw/react-md-editor": "^4.0.3",
|
||||
|
|
@ -165,7 +168,6 @@
|
|||
"next": "^14.2.30",
|
||||
"next-plausible": "^3.12.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"node-html-markdown": "^1.3.0",
|
||||
"node-telegram-bot-api": "^0.66.0",
|
||||
"nodemailer": "^6.9.15",
|
||||
"nostr-tools": "^2.10.4",
|
||||
|
|
@ -207,11 +209,13 @@
|
|||
"tailwind-scrollbar": "^3.1.0",
|
||||
"tailwindcss": "3.4.17",
|
||||
"tailwindcss-rtl": "^0.9.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tldts": "^6.1.47",
|
||||
"transloadit": "^3.0.2",
|
||||
"tslib": "^2.3.0",
|
||||
"turndown": "^7.2.0",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"twitter-api-v2": "^1.23.2",
|
||||
"twitter-api-v2": "^1.24.0",
|
||||
"twitter-text": "^3.1.0",
|
||||
"use-debounce": "^10.0.0",
|
||||
"utf-8-validate": "^5.0.10",
|
||||
|
|
|
|||
|
|
@ -138,6 +138,9 @@ importers:
|
|||
'@tiptap/extension-list':
|
||||
specifier: ^3.0.7
|
||||
version: 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)
|
||||
'@tiptap/extension-mention':
|
||||
specifier: ^3.0.7
|
||||
version: 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)(@tiptap/suggestion@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6))
|
||||
'@tiptap/extension-paragraph':
|
||||
specifier: ^3.0.6
|
||||
version: 3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))
|
||||
|
|
@ -156,6 +159,9 @@ importers:
|
|||
'@tiptap/starter-kit':
|
||||
specifier: ^3.0.6
|
||||
version: 3.0.6
|
||||
'@tiptap/suggestion':
|
||||
specifier: ^3.0.7
|
||||
version: 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)
|
||||
'@types/bcrypt':
|
||||
specifier: ^5.0.2
|
||||
version: 5.0.2
|
||||
|
|
@ -201,6 +207,9 @@ importers:
|
|||
'@types/striptags':
|
||||
specifier: ^0.0.5
|
||||
version: 0.0.5
|
||||
'@types/turndown':
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.5
|
||||
'@types/yup':
|
||||
specifier: ^0.32.0
|
||||
version: 0.32.0
|
||||
|
|
@ -375,9 +384,6 @@ importers:
|
|||
node-fetch:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
node-html-markdown:
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
node-telegram-bot-api:
|
||||
specifier: ^0.66.0
|
||||
version: 0.66.0(request@2.88.2)
|
||||
|
|
@ -501,6 +507,9 @@ importers:
|
|||
tailwindcss-rtl:
|
||||
specifier: ^0.9.0
|
||||
version: 0.9.0
|
||||
tippy.js:
|
||||
specifier: ^6.3.7
|
||||
version: 6.3.7
|
||||
tldts:
|
||||
specifier: ^6.1.47
|
||||
version: 6.1.86
|
||||
|
|
@ -510,11 +519,14 @@ importers:
|
|||
tslib:
|
||||
specifier: ^2.3.0
|
||||
version: 2.8.1
|
||||
turndown:
|
||||
specifier: ^7.2.0
|
||||
version: 7.2.0
|
||||
tweetnacl:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
twitter-api-v2:
|
||||
specifier: ^1.23.2
|
||||
specifier: ^1.24.0
|
||||
version: 1.24.0
|
||||
twitter-text:
|
||||
specifier: ^3.1.0
|
||||
|
|
@ -2989,6 +3001,9 @@ packages:
|
|||
'@microsoft/tsdoc@0.15.1':
|
||||
resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==}
|
||||
|
||||
'@mixmark-io/domino@2.2.0':
|
||||
resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==}
|
||||
|
||||
'@modelcontextprotocol/sdk@1.15.0':
|
||||
resolution: {integrity: sha512-67hnl/ROKdb03Vuu0YOr+baKTvf1/5YBHBm9KnZdjdAh8hjt4FRCPD5ucwxGB237sBpzlqQsLy1PFu7z/ekZ9Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -5921,6 +5936,13 @@ packages:
|
|||
'@tiptap/core': ^3.0.7
|
||||
'@tiptap/pm': ^3.0.7
|
||||
|
||||
'@tiptap/extension-mention@3.0.7':
|
||||
resolution: {integrity: sha512-PHEx6NdmarjvPPvTd8D9AqK1JIaVYTsnQLxJUERakOLzujgUCToZ7FpMQDhPj97YLvF0t3jeyjZOPmFuj5kw4w==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^3.0.7
|
||||
'@tiptap/pm': ^3.0.7
|
||||
'@tiptap/suggestion': ^3.0.7
|
||||
|
||||
'@tiptap/extension-ordered-list@3.0.6':
|
||||
resolution: {integrity: sha512-9SbeGO6kGKoX8GwhaSgpFNCGxlzfGu5otK5DE+Unn5F8/gIYGBJkXTZE1tj8XzPmH6lWhmKJQPudANnW6yuKqg==}
|
||||
peerDependencies:
|
||||
|
|
@ -5966,6 +5988,12 @@ packages:
|
|||
'@tiptap/starter-kit@3.0.6':
|
||||
resolution: {integrity: sha512-7xqcx5hwa+o0J6vpqJRSQNxKHOO6/vSwwicmaHxZ4zdGtlUjJrdreeYaaUpCf0wvpBT1DAQlRnancuD6DJkkPg==}
|
||||
|
||||
'@tiptap/suggestion@3.0.7':
|
||||
resolution: {integrity: sha512-HSMvzAejdvcnVaRZOhXJWAvQqaQs3UYDZaA0ZnzgiJ/sNSbtTyn9XVbX6MfVNYrbtBua4iKaXuJwp6CP0KdHQg==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^3.0.7
|
||||
'@tiptap/pm': ^3.0.7
|
||||
|
||||
'@tokenizer/inflate@0.2.7':
|
||||
resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -6374,6 +6402,9 @@ packages:
|
|||
'@types/trusted-types@2.0.7':
|
||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||
|
||||
'@types/turndown@5.0.5':
|
||||
resolution: {integrity: sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==}
|
||||
|
||||
'@types/unist@2.0.11':
|
||||
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
||||
|
||||
|
|
@ -9707,10 +9738,6 @@ packages:
|
|||
hastscript@9.0.1:
|
||||
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
|
||||
|
||||
he@1.2.0:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
|
||||
header-case@2.0.4:
|
||||
resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==}
|
||||
|
||||
|
|
@ -11886,13 +11913,6 @@ packages:
|
|||
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||
hasBin: true
|
||||
|
||||
node-html-markdown@1.3.0:
|
||||
resolution: {integrity: sha512-OeFi3QwC/cPjvVKZ114tzzu+YoR+v9UXW5RwSXGUqGb0qCl0DvP406tzdL7SFn8pZrMyzXoisfG2zcuF9+zw4g==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
node-html-parser@6.1.13:
|
||||
resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==}
|
||||
|
||||
node-int64@0.4.0:
|
||||
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
|
||||
|
||||
|
|
@ -14277,6 +14297,9 @@ packages:
|
|||
resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tippy.js@6.3.7:
|
||||
resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
|
||||
|
||||
tlds@1.259.0:
|
||||
resolution: {integrity: sha512-AldGGlDP0PNgwppe2quAvuBl18UcjuNtOnDuUkqhd6ipPqrYYBt3aTxK1QTsBVknk97lS2JcafWMghjGWFtunw==}
|
||||
hasBin: true
|
||||
|
|
@ -14504,6 +14527,9 @@ packages:
|
|||
tunnel-agent@0.6.0:
|
||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||
|
||||
turndown@7.2.0:
|
||||
resolution: {integrity: sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==}
|
||||
|
||||
tus-js-client@2.3.2:
|
||||
resolution: {integrity: sha512-5a2rm7gp+G7Z+ZB0AO4PzD/dwczB3n1fZeWO5W8AWLJ12RRk1rY4Aeb2VAYX9oKGE+/rGPrdxoFPA/vDSVKnpg==}
|
||||
|
||||
|
|
@ -18685,6 +18711,8 @@ snapshots:
|
|||
|
||||
'@microsoft/tsdoc@0.15.1': {}
|
||||
|
||||
'@mixmark-io/domino@2.2.0': {}
|
||||
|
||||
'@modelcontextprotocol/sdk@1.15.0':
|
||||
dependencies:
|
||||
ajv: 6.12.6
|
||||
|
|
@ -22336,6 +22364,12 @@ snapshots:
|
|||
'@tiptap/core': 3.0.6(@tiptap/pm@3.0.6)
|
||||
'@tiptap/pm': 3.0.6
|
||||
|
||||
'@tiptap/extension-mention@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)(@tiptap/suggestion@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.0.6(@tiptap/pm@3.0.6)
|
||||
'@tiptap/pm': 3.0.6
|
||||
'@tiptap/suggestion': 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)
|
||||
|
||||
'@tiptap/extension-ordered-list@3.0.6(@tiptap/extension-list@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6))':
|
||||
dependencies:
|
||||
'@tiptap/extension-list': 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)
|
||||
|
|
@ -22424,6 +22458,11 @@ snapshots:
|
|||
'@tiptap/extensions': 3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)
|
||||
'@tiptap/pm': 3.0.6
|
||||
|
||||
'@tiptap/suggestion@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.0.6(@tiptap/pm@3.0.6)
|
||||
'@tiptap/pm': 3.0.6
|
||||
|
||||
'@tokenizer/inflate@0.2.7':
|
||||
dependencies:
|
||||
debug: 4.4.1(supports-color@5.5.0)
|
||||
|
|
@ -22934,6 +22973,8 @@ snapshots:
|
|||
|
||||
'@types/trusted-types@2.0.7': {}
|
||||
|
||||
'@types/turndown@5.0.5': {}
|
||||
|
||||
'@types/unist@2.0.11': {}
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
|
|
@ -27561,8 +27602,6 @@ snapshots:
|
|||
property-information: 7.1.0
|
||||
space-separated-tokens: 2.0.2
|
||||
|
||||
he@1.2.0: {}
|
||||
|
||||
header-case@2.0.4:
|
||||
dependencies:
|
||||
capital-case: 1.0.4
|
||||
|
|
@ -30473,15 +30512,6 @@ snapshots:
|
|||
|
||||
node-gyp-build@4.8.4: {}
|
||||
|
||||
node-html-markdown@1.3.0:
|
||||
dependencies:
|
||||
node-html-parser: 6.1.13
|
||||
|
||||
node-html-parser@6.1.13:
|
||||
dependencies:
|
||||
css-select: 5.2.2
|
||||
he: 1.2.0
|
||||
|
||||
node-int64@0.4.0: {}
|
||||
|
||||
node-mock-http@1.0.1: {}
|
||||
|
|
@ -33414,6 +33444,10 @@ snapshots:
|
|||
|
||||
tinyspy@3.0.2: {}
|
||||
|
||||
tippy.js@6.3.7:
|
||||
dependencies:
|
||||
'@popperjs/core': 2.11.8
|
||||
|
||||
tlds@1.259.0: {}
|
||||
|
||||
tldts-core@6.1.86: {}
|
||||
|
|
@ -33643,6 +33677,10 @@ snapshots:
|
|||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
turndown@7.2.0:
|
||||
dependencies:
|
||||
'@mixmark-io/domino': 2.2.0
|
||||
|
||||
tus-js-client@2.3.2:
|
||||
dependencies:
|
||||
buffer-from: 1.1.2
|
||||
|
|
|
|||
Loading…
Reference in New Issue