feat: new provider - listmonk

This commit is contained in:
Nevo David 2025-09-17 22:49:21 +07:00
parent ba6d035839
commit df0077771a
18 changed files with 554 additions and 11 deletions

View File

@ -50,6 +50,10 @@ GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET="" GITHUB_CLIENT_SECRET=""
BEEHIIVE_API_KEY="" BEEHIIVE_API_KEY=""
BEEHIIVE_PUBLICATION_ID="" BEEHIIVE_PUBLICATION_ID=""
LISTMONK_DOMAIN=""
LISTMONK_USER=""
LISTMONK_API_KEY=""
LISTMONK_LIST_ID=""
THREADS_APP_ID="" THREADS_APP_ID=""
THREADS_APP_SECRET="" THREADS_APP_SECRET=""
FACEBOOK_APP_ID="" FACEBOOK_APP_ID=""

View File

@ -7,10 +7,10 @@ import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/o
import { AuthService as AuthChecker } from '@gitroom/helpers/auth/auth.service'; import { AuthService as AuthChecker } from '@gitroom/helpers/auth/auth.service';
import { ProvidersFactory } from '@gitroom/backend/services/auth/providers/providers.factory'; import { ProvidersFactory } from '@gitroom/backend/services/auth/providers/providers.factory';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { NewsletterService } from '@gitroom/nestjs-libraries/services/newsletter.service';
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service'; import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
import { ForgotReturnPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot-return.password.dto'; import { ForgotReturnPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot-return.password.dto';
import { EmailService } from '@gitroom/nestjs-libraries/services/email.service'; import { EmailService } from '@gitroom/nestjs-libraries/services/email.service';
import { NewsletterService } from '@gitroom/nestjs-libraries/newsletter/newsletter.service';
@Injectable() @Injectable()
export class AuthService { export class AuthService {

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -258,6 +258,7 @@ export const CustomVariables: FC<{
}); });
const submit = useCallback( const submit = useCallback(
async (data: FieldValues) => { async (data: FieldValues) => {
modals.closeAll();
gotoUrl( gotoUrl(
`/integrations/social/${identifier}?state=nostate&code=${Buffer.from( `/integrations/social/${identifier}?state=nostate&code=${Buffer.from(
JSON.stringify(data) JSON.stringify(data)

View File

@ -0,0 +1,47 @@
'use client';
import {
PostComment,
withProvider,
} from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
import { ListmonkDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/listmonk.dto';
import { Input } from '@gitroom/react/form/input';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { SelectList } from '@gitroom/frontend/components/new-launch/providers/listmonk/select.list';
import { SelectTemplates } from '@gitroom/frontend/components/new-launch/providers/listmonk/select.templates';
const SettingsComponent = () => {
const form = useSettings();
return (
<>
<Input label="Subject" {...form.register('subject')} />
<Input label="Preview" {...form.register('preview')} />
<SelectList {...form.register('list')} />
<SelectTemplates {...form.register('templates')} />
</>
);
};
export default withProvider({
postComment: PostComment.POST,
minimumCharacters: [],
SettingsComponent: SettingsComponent,
CustomPreviewComponent: undefined,
dto: ListmonkDto,
checkValidity: async (posts) => {
if (
posts.some(
(p) => p.some((a) => a.path.indexOf('mp4') > -1) && p.length > 1
)
) {
return 'You can only upload one video per post.';
}
if (posts.some((p) => p.length > 4)) {
return 'There can be maximum 4 pictures in a post.';
}
return true;
},
maximumCharacters: 300000,
});

View File

@ -0,0 +1,57 @@
'use client';
import { FC, useEffect, useState } from 'react';
import { Select } from '@gitroom/react/form/select';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
export const SelectList: FC<{
name: string;
onChange: (event: {
target: {
value: string;
name: string;
};
}) => void;
}> = (props) => {
const { onChange, name } = props;
const t = useT();
const customFunc = useCustomProviderFunction();
const [orgs, setOrgs] = useState([]);
const { getValues } = useSettings();
const [currentMedia, setCurrentMedia] = useState<string | undefined>();
const onChangeInner = (event: {
target: {
value: string;
name: string;
};
}) => {
setCurrentMedia(event.target.value);
onChange(event);
};
useEffect(() => {
customFunc.get('list').then((data) => setOrgs(data));
const settings = getValues()[props.name];
if (settings) {
setCurrentMedia(settings);
}
}, []);
if (!orgs.length) {
return null;
}
return (
<Select
name={name}
label="Select List"
onChange={onChangeInner}
value={currentMedia}
>
<option value="">{t('select_1', '--Select--')}</option>
{orgs.map((org: any) => (
<option key={org.id} value={org.id}>
{org.name}
</option>
))}
</Select>
);
};

View File

@ -0,0 +1,57 @@
'use client';
import { FC, useEffect, useState } from 'react';
import { Select } from '@gitroom/react/form/select';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
export const SelectTemplates: FC<{
name: string;
onChange: (event: {
target: {
value: string;
name: string;
};
}) => void;
}> = (props) => {
const { onChange, name } = props;
const t = useT();
const customFunc = useCustomProviderFunction();
const [orgs, setOrgs] = useState([]);
const { getValues } = useSettings();
const [currentMedia, setCurrentMedia] = useState<string | undefined>();
const onChangeInner = (event: {
target: {
value: string;
name: string;
};
}) => {
setCurrentMedia(event.target.value);
onChange(event);
};
useEffect(() => {
customFunc.get('templates').then((data) => setOrgs(data));
const settings = getValues()[props.name];
if (settings) {
setCurrentMedia(settings);
}
}, []);
if (!orgs.length) {
return null;
}
return (
<Select
name={name}
label="Select Template"
onChange={onChangeInner}
value={currentMedia}
>
<option value="">{t('select_1', '--Select--')}</option>
{orgs.map((org: any) => (
<option key={org.id} value={org.id}>
{org.name}
</option>
))}
</Select>
);
};

View File

@ -31,6 +31,7 @@ import { Button } from '@gitroom/react/form/button';
import { useT } from '@gitroom/react/translation/get.transation.service.client'; import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { PostComment } from '@gitroom/frontend/components/new-launch/providers/high.order.provider'; import { PostComment } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
import WordpressProvider from '@gitroom/frontend/components/new-launch/providers/wordpress/wordpress.provider'; import WordpressProvider from '@gitroom/frontend/components/new-launch/providers/wordpress/wordpress.provider';
import ListmonkProvider from '@gitroom/frontend/components/new-launch/providers/listmonk/listmonk.provider';
export const Providers = [ export const Providers = [
{ {
@ -133,6 +134,10 @@ export const Providers = [
identifier: 'wordpress', identifier: 'wordpress',
component: WordpressProvider, component: WordpressProvider,
}, },
{
identifier: 'listmonk',
component: ListmonkProvider,
},
]; ];
export const ShowAllProviders = forwardRef((props, ref) => { export const ShowAllProviders = forwardRef((props, ref) => {
const { date, current, global, selectedIntegrations, allIntegrations } = const { date, current, global, selectedIntegrations, allIntegrations } =

View File

@ -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 { 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 { 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 { 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<T extends string, M> = { __type: T } & M; export type ProviderExtension<T extends string, M> = { __type: T } & M;
export type AllProvidersSettings = export type AllProvidersSettings =
@ -34,6 +35,7 @@ export type AllProvidersSettings =
| ProviderExtension<'devto', DevToSettingsDto> | ProviderExtension<'devto', DevToSettingsDto>
| ProviderExtension<'hashnode', HashnodeSettingsDto> | ProviderExtension<'hashnode', HashnodeSettingsDto>
| ProviderExtension<'wordpress', WordpressDto> | ProviderExtension<'wordpress', WordpressDto>
| ProviderExtension<'listmonk', ListmonkDto>
| ProviderExtension<'facebook', None> | ProviderExtension<'facebook', None>
| ProviderExtension<'threads', None> | ProviderExtension<'threads', None>
| ProviderExtension<'mastodon', None> | ProviderExtension<'mastodon', None>
@ -64,6 +66,7 @@ export const allProviders = (setEmpty?: any) => {
{ value: DevToSettingsDto, name: 'devto' }, { value: DevToSettingsDto, name: 'devto' },
{ value: WordpressDto, name: 'wordpress' }, { value: WordpressDto, name: 'wordpress' },
{ value: HashnodeSettingsDto, name: 'hashnode' }, { value: HashnodeSettingsDto, name: 'hashnode' },
{ value: ListmonkDto, name: 'listmonk' },
{ value: setEmpty, name: 'facebook' }, { value: setEmpty, name: 'facebook' },
{ value: setEmpty, name: 'threads' }, { value: setEmpty, name: 'threads' },
{ value: setEmpty, name: 'mastodon' }, { value: setEmpty, name: 'mastodon' },

View File

@ -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;
}

View File

@ -27,6 +27,7 @@ import { TelegramProvider } from '@gitroom/nestjs-libraries/integrations/social/
import { NostrProvider } from '@gitroom/nestjs-libraries/integrations/social/nostr.provider'; import { NostrProvider } from '@gitroom/nestjs-libraries/integrations/social/nostr.provider';
import { VkProvider } from '@gitroom/nestjs-libraries/integrations/social/vk.provider'; import { VkProvider } from '@gitroom/nestjs-libraries/integrations/social/vk.provider';
import { WordpressProvider } from '@gitroom/nestjs-libraries/integrations/social/wordpress.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[] = [ export const socialIntegrationList: SocialProvider[] = [
new XProvider(), new XProvider(),
@ -54,6 +55,7 @@ export const socialIntegrationList: SocialProvider[] = [
new DevToProvider(), new DevToProvider(),
new HashnodeProvider(), new HashnodeProvider(),
new WordpressProvider(), new WordpressProvider(),
new ListmonkProvider(),
// new MastodonCustomProvider(), // new MastodonCustomProvider(),
]; ];

View File

@ -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<AuthTokenDetails> {
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<ListmonkDto>[],
integration: Integration
): Promise<PostResponse[]> {
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 = `
<style>
.content {
padding: 20px;
font-size: 15px;
line-height: 1.6;
}
</style>
<div class="hidden-preheader"
style="display:none !important; visibility:hidden; opacity:0; overflow:hidden;
max-height:0; max-width:0; line-height:1px; font-size:1px; color:transparent;
mso-hide:all;">
<!-- A short visible decoy (optional): shows as "." or short text in preview -->
${postDetails?.[0]?.settings?.preview || ''}
<!-- Then invisible padding to eat up preview characters -->
&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;
&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;
&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;
<!-- Repeat the trio (zero-width space, zero-width non-joiner, nbsp, BOM) a bunch of times -->
&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;
&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;
&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;
&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;
&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;
&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;
&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;
&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;
&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;
&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;
&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;
&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;
&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;
&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;
&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;
&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;
&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;&#8203;&zwnj;&nbsp;&#65279;
</div>
<div class="content">
${postDetails[0].message}
</div>
`;
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,
},
];
}
}

View File

@ -0,0 +1,4 @@
export interface NewsletterInterface {
name: string;
register(email: string): Promise<void>;
}

View File

@ -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);
}
}

View File

@ -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(),
];

View File

@ -1,13 +1,8 @@
export class NewsletterService { import { NewsletterInterface } from '@gitroom/nestjs-libraries/newsletter/newsletter.interface';
static async register(email: string) {
if ( export class BeehiivProvider implements NewsletterInterface {
!process.env.BEEHIIVE_API_KEY || name = 'beehiiv';
!process.env.BEEHIIVE_PUBLICATION_ID || async register(email: string) {
process.env.NODE_ENV === 'development' ||
email.indexOf('@') === -1
) {
return;
}
const body = { const body = {
email, email,
reactivate_existing: false, reactivate_existing: false,

View File

@ -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);
}
}

View File

@ -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) {}
}
}