({
diff --git a/apps/workers/src/app/plugs.controller.ts b/apps/workers/src/app/plugs.controller.ts
index f730323c..a7d1134b 100644
--- a/apps/workers/src/app/plugs.controller.ts
+++ b/apps/workers/src/app/plugs.controller.ts
@@ -18,7 +18,7 @@ export class PlugsController {
return await this._integrationService.processPlugs(data);
} catch (err) {
console.log(
- "Unhandled error, let's avoid crashing the plugs worker",
+ "Unhandled error, let's avoid crashing the plug worker",
err
);
}
diff --git a/libraries/helpers/src/utils/strip.html.validation.ts b/libraries/helpers/src/utils/strip.html.validation.ts
index 7ad3591c..16f97042 100644
--- a/libraries/helpers/src/utils/strip.html.validation.ts
+++ b/libraries/helpers/src/utils/strip.html.validation.ts
@@ -207,7 +207,9 @@ export const stripHtmlValidation = (
.replace(/ /gi, ' ')
.replace(/^
]*>/i, '')
.replace(/
]*>/gi, '\n')
- .replace(/<\/p>/gi, '');
+ .replace(/<\/p>/gi, '')
+ .replace(/>/gi, '>')
+ .replace(/</gi, '<')
if (none) {
return striptags(html);
diff --git a/libraries/nestjs-libraries/src/agent/agent.categories.ts b/libraries/nestjs-libraries/src/agent/agent.categories.ts
index 62225de2..20329b7e 100644
--- a/libraries/nestjs-libraries/src/agent/agent.categories.ts
+++ b/libraries/nestjs-libraries/src/agent/agent.categories.ts
@@ -30,3 +30,4 @@ export const agentCategories = [
'Update',
'Trend',
];
+
diff --git a/libraries/nestjs-libraries/src/database/prisma/prisma.service.ts b/libraries/nestjs-libraries/src/database/prisma/prisma.service.ts
index eeb4ebc3..089afc5d 100644
--- a/libraries/nestjs-libraries/src/database/prisma/prisma.service.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/prisma.service.ts
@@ -1,8 +1,8 @@
-import { Injectable, OnModuleInit } from '@nestjs/common';
+import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
-export class PrismaService extends PrismaClient implements OnModuleInit {
+export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
constructor() {
super({
log: [
@@ -16,6 +16,10 @@ export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
+
+ async onModuleDestroy() {
+ await this.$disconnect();
+ }
}
@Injectable()
@@ -26,7 +30,6 @@ export class PrismaRepository {
}
}
-
@Injectable()
export class PrismaTransaction {
public model: Pick;
diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma
index 0958133a..333e6e7d 100644
--- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma
+++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma
@@ -3,6 +3,7 @@
generator client {
provider = "prisma-client-js"
+ runtime = "nodejs"
}
datasource db {
diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts
index 3caf5e5f..0e892e60 100644
--- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts
+++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts
@@ -14,6 +14,7 @@ import { MediumSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/provider
import { DevToSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto';
import { HashnodeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/hashnode.settings.dto';
import { WordpressDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/wordpress.dto';
+import { ListmonkDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/listmonk.dto';
export type ProviderExtension = { __type: T } & M;
export type AllProvidersSettings =
@@ -34,6 +35,7 @@ export type AllProvidersSettings =
| ProviderExtension<'devto', DevToSettingsDto>
| ProviderExtension<'hashnode', HashnodeSettingsDto>
| ProviderExtension<'wordpress', WordpressDto>
+ | ProviderExtension<'listmonk', ListmonkDto>
| ProviderExtension<'facebook', None>
| ProviderExtension<'threads', None>
| ProviderExtension<'mastodon', None>
@@ -64,6 +66,7 @@ export const allProviders = (setEmpty?: any) => {
{ value: DevToSettingsDto, name: 'devto' },
{ value: WordpressDto, name: 'wordpress' },
{ value: HashnodeSettingsDto, name: 'hashnode' },
+ { value: ListmonkDto, name: 'listmonk' },
{ value: setEmpty, name: 'facebook' },
{ value: setEmpty, name: 'threads' },
{ value: setEmpty, name: 'mastodon' },
diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/listmonk.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/listmonk.dto.ts
new file mode 100644
index 00000000..218af81e
--- /dev/null
+++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/listmonk.dto.ts
@@ -0,0 +1,17 @@
+import { IsOptional, IsString, MinLength } from 'class-validator';
+
+export class ListmonkDto {
+ @IsString()
+ @MinLength(1)
+ subject: string;
+
+ @IsString()
+ preview: string;
+
+ @IsString()
+ list: string;
+
+ @IsString()
+ @IsOptional()
+ template: string;
+}
diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/reddit.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/reddit.dto.ts
index 8785ddbe..49c7462f 100644
--- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/reddit.dto.ts
+++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/reddit.dto.ts
@@ -55,6 +55,7 @@ export class RedditSettingsDtoInner {
@ValidateIf((e) => e.is_flair_required)
@IsDefined()
@ValidateNested()
+ @Type(() => RedditFlairDto)
flair: RedditFlairDto;
}
diff --git a/libraries/nestjs-libraries/src/integrations/integration.manager.ts b/libraries/nestjs-libraries/src/integrations/integration.manager.ts
index c1ca5e73..db7cc7cf 100644
--- a/libraries/nestjs-libraries/src/integrations/integration.manager.ts
+++ b/libraries/nestjs-libraries/src/integrations/integration.manager.ts
@@ -27,6 +27,7 @@ import { TelegramProvider } from '@gitroom/nestjs-libraries/integrations/social/
import { NostrProvider } from '@gitroom/nestjs-libraries/integrations/social/nostr.provider';
import { VkProvider } from '@gitroom/nestjs-libraries/integrations/social/vk.provider';
import { WordpressProvider } from '@gitroom/nestjs-libraries/integrations/social/wordpress.provider';
+import { ListmonkProvider } from '@gitroom/nestjs-libraries/integrations/social/listmonk.provider';
export const socialIntegrationList: SocialProvider[] = [
new XProvider(),
@@ -54,6 +55,7 @@ export const socialIntegrationList: SocialProvider[] = [
new DevToProvider(),
new HashnodeProvider(),
new WordpressProvider(),
+ new ListmonkProvider(),
// new MastodonCustomProvider(),
];
diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts
index 0b4d26b1..94bb51b0 100644
--- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts
+++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts
@@ -29,7 +29,9 @@ export abstract class SocialAbstract {
public handleErrors(
body: string
- ): { type: 'refresh-token' | 'bad-body'; value: string } | undefined {
+ ):
+ | { type: 'refresh-token' | 'bad-body' | 'retry'; value: string }
+ | undefined {
return undefined;
}
@@ -38,11 +40,17 @@ export abstract class SocialAbstract {
d: { query: string },
id: string,
integration: Integration
- ): Promise<{ id: string; label: string; image: string, doNotCache?: boolean }[] | { none: true }> {
+ ): Promise<
+ | { id: string; label: string; image: string; doNotCache?: boolean }[]
+ | { none: true }
+ > {
return { none: true };
}
- async runInConcurrent(func: (...args: any[]) => Promise) {
+ async runInConcurrent(
+ func: (...args: any[]) => Promise,
+ ignoreConcurrency?: boolean
+ ) {
const value = await concurrency(
this.identifier,
this.maxConcurrentJob,
@@ -54,7 +62,8 @@ export abstract class SocialAbstract {
const handle = this.handleErrors(JSON.stringify(err));
return { err: true, ...(handle || {}) };
}
- }
+ },
+ ignoreConcurrency
);
if (value && value?.err && value?.value) {
@@ -100,11 +109,16 @@ export abstract class SocialAbstract {
json.includes('Rate limit')
) {
await timer(5000);
- return this.fetch(url, options, identifier, totalRetries + 1);
+ return this.fetch(url, options, identifier, totalRetries + 1, ignoreConcurrency);
}
const handleError = this.handleErrors(json || '{}');
+ if (handleError?.type === 'retry') {
+ await timer(5000);
+ return this.fetch(url, options, identifier, totalRetries + 1, ignoreConcurrency);
+ }
+
if (
request.status === 401 &&
(handleError?.type === 'refresh-token' || !handleError)
diff --git a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts
index 1803992b..0e3f9e5c 100644
--- a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts
@@ -6,6 +6,7 @@ import {
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import {
+ BadBody,
RefreshToken,
SocialAbstract,
} from '@gitroom/nestjs-libraries/integrations/social.abstract';
@@ -119,8 +120,17 @@ async function uploadVideo(
if (status.jobStatus.blob) {
blob = status.jobStatus.blob;
}
- // wait a second
- await new Promise((resolve) => setTimeout(resolve, 1000));
+
+ if (status.jobStatus.state === 'JOB_STATE_FAILED') {
+ throw new BadBody(
+ 'bluesky',
+ JSON.stringify({}),
+ {} as any,
+ 'Could not upload video, job failed'
+ );
+ }
+
+ await timer(30000);
}
console.log('posting video...');
@@ -295,7 +305,7 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
aspectRatio: {
width: p.width,
height: p.height,
- }
+ },
})),
};
}
diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts
index 2bdf2889..0b7ada53 100644
--- a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts
@@ -46,11 +46,18 @@ export class InstagramProvider
public override handleErrors(body: string):
| {
- type: 'refresh-token' | 'bad-body';
+ type: 'refresh-token' | 'bad-body' | 'retry';
value: string;
}
| undefined {
+ if (body.indexOf('An unknown error occurred') > -1) {
+ return {
+ type: 'retry' as const,
+ value: 'An unknown error occurred, please try again later',
+ };
+ }
+
if (body.indexOf('REVOKED_ACCESS_TOKEN') > -1) {
return {
type: 'refresh-token' as const,
diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts
index 68938b17..523bcda2 100644
--- a/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts
@@ -30,7 +30,7 @@ export class InstagramStandaloneProvider
editor = 'normal' as const;
- public override handleErrors(body: string): { type: "refresh-token" | "bad-body"; value: string } | undefined {
+ public override handleErrors(body: string): { type: "refresh-token" | "bad-body" | "retry"; value: string } | undefined {
return instagramProvider.handleErrors(body);
}
diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts
index d4731735..da870fd1 100644
--- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts
@@ -254,20 +254,26 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
const etags = [];
for (let i = 0; i < picture.length; i += 1024 * 1024 * 2) {
- const upload = await this.fetch(sendUrlRequest, {
- method: 'PUT',
- headers: {
- 'X-Restli-Protocol-Version': '2.0.0',
- 'LinkedIn-Version': '202501',
- Authorization: `Bearer ${accessToken}`,
- ...(isVideo
- ? { 'Content-Type': 'application/octet-stream' }
- : isPdf
- ? { 'Content-Type': 'application/pdf' }
- : {}),
+ const upload = await this.fetch(
+ sendUrlRequest,
+ {
+ method: 'PUT',
+ headers: {
+ 'X-Restli-Protocol-Version': '2.0.0',
+ 'LinkedIn-Version': '202501',
+ Authorization: `Bearer ${accessToken}`,
+ ...(isVideo
+ ? { 'Content-Type': 'application/octet-stream' }
+ : isPdf
+ ? { 'Content-Type': 'application/pdf' }
+ : {}),
+ },
+ body: picture.slice(i, i + 1024 * 1024 * 2),
},
- body: picture.slice(i, i + 1024 * 1024 * 2),
- });
+ 'linkedin',
+ 0,
+ true
+ );
etags.push(upload.headers.get('etag'));
}
@@ -737,7 +743,9 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
return elements.map((p: any) => ({
id: String(p.id),
label: p.localizedName,
- image: p.logoV2?.['original~']?.elements?.[0]?.identifiers?.[0]?.identifier || '',
+ image:
+ p.logoV2?.['original~']?.elements?.[0]?.identifiers?.[0]?.identifier ||
+ '',
}));
}
diff --git a/libraries/nestjs-libraries/src/integrations/social/listmonk.provider.ts b/libraries/nestjs-libraries/src/integrations/social/listmonk.provider.ts
new file mode 100644
index 00000000..f6864b1a
--- /dev/null
+++ b/libraries/nestjs-libraries/src/integrations/social/listmonk.provider.ts
@@ -0,0 +1,269 @@
+import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
+import { SocialAbstract } from '../social.abstract';
+import {
+ AuthTokenDetails,
+ PostDetails,
+ PostResponse,
+ SocialProvider,
+} from './social.integrations.interface';
+import dayjs from 'dayjs';
+import { Integration } from '@prisma/client';
+import { ListmonkDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/listmonk.dto';
+import { AuthService } from '@gitroom/helpers/auth/auth.service';
+import slugify from 'slugify';
+
+export class ListmonkProvider extends SocialAbstract implements SocialProvider {
+ override maxConcurrentJob = 100; // Bluesky has moderate rate limits
+ identifier = 'listmonk';
+ name = 'ListMonk';
+ isBetweenSteps = false;
+ scopes = [] as string[];
+ editor = 'html' as const;
+
+ async customFields() {
+ return [
+ {
+ key: 'url',
+ label: 'URL',
+ defaultValue: '',
+ validation: `/^(https?:\\/\\/)(?:\\S+(?::\\S*)?@)?(?:(?:localhost)|(?:\\d{1,3}(?:\\.\\d{1,3}){3})|(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63})(?::\\d{2,5})?(?:\\/[^\\s?#]*)?(?:\\?[^\\s#]*)?(?:#[^\\s]*)?$/`,
+ type: 'text' as const,
+ },
+ {
+ key: 'username',
+ label: 'Username',
+ validation: `/^.+$/`,
+ type: 'text' as const,
+ },
+ {
+ key: 'password',
+ label: 'Password',
+ validation: `/^.{3,}$/`,
+ type: 'password' as const,
+ },
+ ];
+ }
+
+ async refreshToken(refreshToken: string): Promise {
+ return {
+ refreshToken: '',
+ expiresIn: 0,
+ accessToken: '',
+ id: '',
+ name: '',
+ picture: '',
+ username: '',
+ };
+ }
+
+ async generateAuthUrl() {
+ const state = makeId(6);
+ return {
+ url: '',
+ codeVerifier: makeId(10),
+ state,
+ };
+ }
+
+ async authenticate(params: {
+ code: string;
+ codeVerifier: string;
+ refresh?: string;
+ }) {
+ const body: { url: string; username: string; password: string } =
+ JSON.parse(Buffer.from(params.code, 'base64').toString());
+
+ console.log(body);
+ try {
+ const basic = Buffer.from(body.username + ':' + body.password).toString(
+ 'base64'
+ );
+
+ const { data } = await (
+ await this.fetch(body.url + '/api/settings', {
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ Authorization: 'Basic ' + basic,
+ },
+ })
+ ).json();
+
+ return {
+ refreshToken: basic,
+ expiresIn: dayjs().add(100, 'years').unix() - dayjs().unix(),
+ accessToken: basic,
+ id: Buffer.from(body.url).toString('base64'),
+ name: data['app.site_name'],
+ picture: data['app.logo_url'] || '',
+ username: data['app.site_name'],
+ };
+ } catch (e) {
+ console.log(e);
+ return 'Invalid credentials';
+ }
+ }
+
+ async list(
+ token: string,
+ data: any,
+ internalId: string,
+ integration: Integration
+ ) {
+ const body: { url: string; username: string; password: string } =
+ JSON.parse(
+ AuthService.fixedDecryption(integration.customInstanceDetails!)
+ );
+
+ const auth = Buffer.from(`${body.username}:${body.password}`).toString(
+ 'base64'
+ );
+
+ const postTypes = await (
+ await this.fetch(`${body.url}/api/lists`, {
+ headers: {
+ Authorization: `Basic ${auth}`,
+ },
+ })
+ ).json();
+
+ return postTypes.data.results.map((p: any) => ({ id: p.id, name: p.name }));
+ }
+
+ async templates(
+ token: string,
+ data: any,
+ internalId: string,
+ integration: Integration
+ ) {
+ const body: { url: string; username: string; password: string } =
+ JSON.parse(
+ AuthService.fixedDecryption(integration.customInstanceDetails!)
+ );
+
+ const auth = Buffer.from(`${body.username}:${body.password}`).toString(
+ 'base64'
+ );
+
+ const postTypes = await (
+ await this.fetch(`${body.url}/api/templates`, {
+ headers: {
+ Authorization: `Basic ${auth}`,
+ },
+ })
+ ).json();
+
+ return [
+ { id: 0, name: 'Default' },
+ ...postTypes.data.map((p: any) => ({ id: p.id, name: p.name })),
+ ];
+ }
+
+ async post(
+ id: string,
+ accessToken: string,
+ postDetails: PostDetails[],
+ integration: Integration
+ ): Promise {
+ const body: { url: string; username: string; password: string } =
+ JSON.parse(
+ AuthService.fixedDecryption(integration.customInstanceDetails!)
+ );
+
+ const auth = Buffer.from(`${body.username}:${body.password}`).toString(
+ 'base64'
+ );
+
+ const sendBody = `
+
+
+
+
+ ${postDetails[0].message}
+
+`;
+
+ const {
+ data: { uuid: postId, id: campaignId },
+ } = await (
+ await this.fetch(body.url + '/api/campaigns', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ Authorization: `Basic ${auth}`,
+ },
+ body: JSON.stringify({
+ name: slugify(postDetails[0].settings.subject, {
+ lower: true,
+ strict: true,
+ trim: true,
+ }),
+ type: 'regular',
+ content_type: 'html',
+ subject: postDetails[0].settings.subject,
+ lists: [+postDetails[0].settings.list],
+ body: sendBody,
+ ...(+postDetails?.[0]?.settings?.template
+ ? { template_id: +postDetails[0].settings.template }
+ : {}),
+ }),
+ })
+ ).json();
+
+ await this.fetch(body.url + `/api/campaigns/${campaignId}/status`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ Authorization: `Basic ${auth}`,
+ },
+ body: JSON.stringify({
+ status: 'running',
+ }),
+ });
+
+ return [
+ {
+ id,
+ status: 'completed',
+ releaseURL: `${body.url}/api/campaigns/${campaignId}/preview`,
+ postId,
+ },
+ ];
+ }
+}
diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
index ca32f23c..1467352e 100644
--- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
@@ -38,8 +38,7 @@ export class XProvider extends SocialAbstract implements SocialProvider {
if (body.includes('usage-capped')) {
return {
type: 'refresh-token',
- value:
- 'Posting failed - capped reached. Please try again later',
+ value: 'Posting failed - capped reached. Please try again later',
};
}
if (body.includes('duplicate-rules')) {
@@ -55,6 +54,17 @@ export class XProvider extends SocialAbstract implements SocialProvider {
value: 'The Tweet contains a URL that is not allowed on X',
};
}
+ if (
+ body.includes(
+ 'This user is not allowed to post a video longer than 2 minutes'
+ )
+ ) {
+ return {
+ type: 'bad-body',
+ value:
+ 'The video you are trying to post is longer than 2 minutes, which is not allowed for this account',
+ };
+ }
return undefined;
}
@@ -307,22 +317,24 @@ export class XProvider extends SocialAbstract implements SocialProvider {
postDetails.flatMap((p) =>
p?.media?.flatMap(async (m) => {
return {
- id: await this.runInConcurrent(async () =>
- client.v1.uploadMedia(
- m.path.indexOf('mp4') > -1
- ? Buffer.from(await readOrFetch(m.path))
- : await sharp(await readOrFetch(m.path), {
- animated: lookup(m.path) === 'image/gif',
- })
- .resize({
- width: 1000,
+ id: await this.runInConcurrent(
+ async () =>
+ client.v1.uploadMedia(
+ m.path.indexOf('mp4') > -1
+ ? Buffer.from(await readOrFetch(m.path))
+ : await sharp(await readOrFetch(m.path), {
+ animated: lookup(m.path) === 'image/gif',
})
- .gif()
- .toBuffer(),
- {
- mimeType: lookup(m.path) || '',
- }
- )
+ .resize({
+ width: 1000,
+ })
+ .gif()
+ .toBuffer(),
+ {
+ mimeType: lookup(m.path) || '',
+ }
+ ),
+ true
),
postId: p.id,
};
diff --git a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts
index 7b6e6fcf..93caff82 100644
--- a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts
@@ -222,7 +222,8 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider {
media: {
body: response.data,
},
- })
+ }),
+ true
);
if (settings?.thumbnail?.path) {
diff --git a/libraries/nestjs-libraries/src/newsletter/newsletter.interface.ts b/libraries/nestjs-libraries/src/newsletter/newsletter.interface.ts
new file mode 100644
index 00000000..200ad05d
--- /dev/null
+++ b/libraries/nestjs-libraries/src/newsletter/newsletter.interface.ts
@@ -0,0 +1,4 @@
+export interface NewsletterInterface {
+ name: string;
+ register(email: string): Promise;
+}
\ No newline at end of file
diff --git a/libraries/nestjs-libraries/src/newsletter/newsletter.service.ts b/libraries/nestjs-libraries/src/newsletter/newsletter.service.ts
new file mode 100644
index 00000000..7980a6d0
--- /dev/null
+++ b/libraries/nestjs-libraries/src/newsletter/newsletter.service.ts
@@ -0,0 +1,20 @@
+import { newsletterProviders } from '@gitroom/nestjs-libraries/newsletter/providers';
+
+export class NewsletterService {
+ static getProvider() {
+ if (process.env.BEEHIIVE_API_KEY) {
+ return newsletterProviders.find((p) => p.name === 'beehiiv')!;
+ }
+ if (process.env.LISTMONK_API_KEY) {
+ return newsletterProviders.find((p) => p.name === 'listmonk')!;
+ }
+
+ return newsletterProviders.find((p) => p.name === 'empty')!;
+ }
+ static async register(email: string) {
+ if (email.indexOf('@') === -1) {
+ return;
+ }
+ return NewsletterService.getProvider().register(email);
+ }
+}
diff --git a/libraries/nestjs-libraries/src/newsletter/providers.ts b/libraries/nestjs-libraries/src/newsletter/providers.ts
new file mode 100644
index 00000000..c43fdca3
--- /dev/null
+++ b/libraries/nestjs-libraries/src/newsletter/providers.ts
@@ -0,0 +1,9 @@
+import { BeehiivProvider } from '@gitroom/nestjs-libraries/newsletter/providers/beehiiv.provider';
+import { EmailEmptyProvider } from '@gitroom/nestjs-libraries/newsletter/providers/email-empty.provider';
+import { ListmonkProvider } from '@gitroom/nestjs-libraries/newsletter/providers/listmonk.provider';
+
+export const newsletterProviders = [
+ new BeehiivProvider(),
+ new ListmonkProvider(),
+ new EmailEmptyProvider(),
+];
diff --git a/libraries/nestjs-libraries/src/services/newsletter.service.ts b/libraries/nestjs-libraries/src/newsletter/providers/beehiiv.provider.ts
similarity index 65%
rename from libraries/nestjs-libraries/src/services/newsletter.service.ts
rename to libraries/nestjs-libraries/src/newsletter/providers/beehiiv.provider.ts
index db2c7939..f7d0e885 100644
--- a/libraries/nestjs-libraries/src/services/newsletter.service.ts
+++ b/libraries/nestjs-libraries/src/newsletter/providers/beehiiv.provider.ts
@@ -1,13 +1,8 @@
-export class NewsletterService {
- static async register(email: string) {
- if (
- !process.env.BEEHIIVE_API_KEY ||
- !process.env.BEEHIIVE_PUBLICATION_ID ||
- process.env.NODE_ENV === 'development' ||
- email.indexOf('@') === -1
- ) {
- return;
- }
+import { NewsletterInterface } from '@gitroom/nestjs-libraries/newsletter/newsletter.interface';
+
+export class BeehiivProvider implements NewsletterInterface {
+ name = 'beehiiv';
+ async register(email: string) {
const body = {
email,
reactivate_existing: false,
diff --git a/libraries/nestjs-libraries/src/newsletter/providers/email-empty.provider.ts b/libraries/nestjs-libraries/src/newsletter/providers/email-empty.provider.ts
new file mode 100644
index 00000000..2abf9307
--- /dev/null
+++ b/libraries/nestjs-libraries/src/newsletter/providers/email-empty.provider.ts
@@ -0,0 +1,8 @@
+import { NewsletterInterface } from '@gitroom/nestjs-libraries/newsletter/newsletter.interface';
+
+export class EmailEmptyProvider implements NewsletterInterface {
+ name = 'empty';
+ async register(email: string) {
+ console.log('Could have registered to newsletter:', email);
+ }
+}
diff --git a/libraries/nestjs-libraries/src/newsletter/providers/listmonk.provider.ts b/libraries/nestjs-libraries/src/newsletter/providers/listmonk.provider.ts
new file mode 100644
index 00000000..ae022df1
--- /dev/null
+++ b/libraries/nestjs-libraries/src/newsletter/providers/listmonk.provider.ts
@@ -0,0 +1,45 @@
+import { NewsletterInterface } from '@gitroom/nestjs-libraries/newsletter/newsletter.interface';
+
+export class ListmonkProvider implements NewsletterInterface {
+ name = 'listmonk';
+ async register(email: string) {
+ const body = {
+ email,
+ status: 'enabled',
+ lists: [+process.env.LISTMONK_LIST_ID].filter((f) => f),
+ };
+
+ const authString = `${process.env.LISTMONK_USER}:${process.env.LISTMONK_API_KEY}`;
+ const headers = new Headers();
+ headers.set('Content-Type', 'application/json');
+ headers.set('Accept', 'application/json');
+ headers.set(
+ 'Authorization',
+ 'Basic ' + Buffer.from(authString).toString('base64')
+ );
+
+ try {
+ const {
+ data: { id },
+ } = await (
+ await fetch(`${process.env.LISTMONK_DOMAIN}/api/subscribers`, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify(body),
+ })
+ ).json();
+
+ const welcomeEmail = {
+ subscriber_id: id,
+ template_id: +process.env.LISTMONK_WELCOME_TEMPLATE_ID,
+ subject: 'Welcome to Postiz 🚀',
+ };
+
+ await fetch(`${process.env.LISTMONK_DOMAIN}/api/tx`, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify(welcomeEmail),
+ });
+ } catch (err) {}
+ }
+}
diff --git a/libraries/nestjs-libraries/src/sentry/initialize.sentry.ts b/libraries/nestjs-libraries/src/sentry/initialize.sentry.ts
index 4291dbd5..c79f407c 100644
--- a/libraries/nestjs-libraries/src/sentry/initialize.sentry.ts
+++ b/libraries/nestjs-libraries/src/sentry/initialize.sentry.ts
@@ -31,8 +31,12 @@ export const initializeSentry = (appName: string, allowLogs = false) => {
recordOutputs: true,
}),
],
- tracesSampleRate: process.env.NODE_ENV === 'development' ? 1.0 : 0.3,
+ tracesSampleRate: process.env.NODE_ENV === 'development' ? 1.0 : 0.25,
enableLogs: true,
+
+ // Profiling
+ profileSessionSampleRate: process.env.NODE_ENV === 'development' ? 1.0 : 0.35,
+ profileLifecycle: 'trace',
});
} catch (err) {
console.log(err);
diff --git a/libraries/nestjs-libraries/src/services/email.service.ts b/libraries/nestjs-libraries/src/services/email.service.ts
index 0e0943dc..8c4fc989 100644
--- a/libraries/nestjs-libraries/src/services/email.service.ts
+++ b/libraries/nestjs-libraries/src/services/email.service.ts
@@ -96,16 +96,18 @@ export class EmailService {
`;
- const sends = await concurrency('send-email', 1, () =>
- this.emailService.sendEmail(
+ try {
+ const sends = await this.emailService.sendEmail(
to,
subject,
modifiedHtml,
process.env.EMAIL_FROM_NAME,
process.env.EMAIL_FROM_ADDRESS,
replyTo
- )
- );
- console.log(sends);
+ );
+ console.log(sends);
+ } catch (err) {
+ console.log(err);
+ }
}
}
diff --git a/libraries/nestjs-libraries/src/upload/cloudflare.storage.ts b/libraries/nestjs-libraries/src/upload/cloudflare.storage.ts
index 2e46744b..93050f09 100644
--- a/libraries/nestjs-libraries/src/upload/cloudflare.storage.ts
+++ b/libraries/nestjs-libraries/src/upload/cloudflare.storage.ts
@@ -80,31 +80,36 @@ class CloudflareStorage implements IUploadProvider {
}
async uploadFile(file: Express.Multer.File): Promise