From df0077771a2c5d7dc52951e58cfc60f67556a2ec Mon Sep 17 00:00:00 2001 From: Nevo David Date: Wed, 17 Sep 2025 22:49:21 +0700 Subject: [PATCH] feat: new provider - listmonk --- .env.example | 4 + .../backend/src/services/auth/auth.service.ts | 2 +- .../public/icons/platforms/listmonk.png | Bin 0 -> 2580 bytes .../launches/add.provider.component.tsx | 1 + .../providers/listmonk/listmonk.provider.tsx | 47 +++ .../providers/listmonk/select.list.tsx | 57 ++++ .../providers/listmonk/select.templates.tsx | 57 ++++ .../providers/show.all.providers.tsx | 5 + .../all.providers.settings.ts | 3 + .../posts/providers-settings/listmonk.dto.ts | 17 ++ .../src/integrations/integration.manager.ts | 2 + .../integrations/social/listmonk.provider.ts | 269 ++++++++++++++++++ .../src/newsletter/newsletter.interface.ts | 4 + .../src/newsletter/newsletter.service.ts | 20 ++ .../src/newsletter/providers.ts | 9 + .../providers/beehiiv.provider.ts} | 15 +- .../providers/email-empty.provider.ts | 8 + .../newsletter/providers/listmonk.provider.ts | 45 +++ 18 files changed, 554 insertions(+), 11 deletions(-) create mode 100644 apps/frontend/public/icons/platforms/listmonk.png create mode 100644 apps/frontend/src/components/new-launch/providers/listmonk/listmonk.provider.tsx create mode 100644 apps/frontend/src/components/new-launch/providers/listmonk/select.list.tsx create mode 100644 apps/frontend/src/components/new-launch/providers/listmonk/select.templates.tsx create mode 100644 libraries/nestjs-libraries/src/dtos/posts/providers-settings/listmonk.dto.ts create mode 100644 libraries/nestjs-libraries/src/integrations/social/listmonk.provider.ts create mode 100644 libraries/nestjs-libraries/src/newsletter/newsletter.interface.ts create mode 100644 libraries/nestjs-libraries/src/newsletter/newsletter.service.ts create mode 100644 libraries/nestjs-libraries/src/newsletter/providers.ts rename libraries/nestjs-libraries/src/{services/newsletter.service.ts => newsletter/providers/beehiiv.provider.ts} (65%) create mode 100644 libraries/nestjs-libraries/src/newsletter/providers/email-empty.provider.ts create mode 100644 libraries/nestjs-libraries/src/newsletter/providers/listmonk.provider.ts diff --git a/.env.example b/.env.example index 9a017b3a..61d2a020 100644 --- a/.env.example +++ b/.env.example @@ -50,6 +50,10 @@ GITHUB_CLIENT_ID="" GITHUB_CLIENT_SECRET="" BEEHIIVE_API_KEY="" BEEHIIVE_PUBLICATION_ID="" +LISTMONK_DOMAIN="" +LISTMONK_USER="" +LISTMONK_API_KEY="" +LISTMONK_LIST_ID="" THREADS_APP_ID="" THREADS_APP_SECRET="" FACEBOOK_APP_ID="" diff --git a/apps/backend/src/services/auth/auth.service.ts b/apps/backend/src/services/auth/auth.service.ts index 31e889e6..59c4d11a 100644 --- a/apps/backend/src/services/auth/auth.service.ts +++ b/apps/backend/src/services/auth/auth.service.ts @@ -7,10 +7,10 @@ import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/o import { AuthService as AuthChecker } from '@gitroom/helpers/auth/auth.service'; import { ProvidersFactory } from '@gitroom/backend/services/auth/providers/providers.factory'; 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 { ForgotReturnPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot-return.password.dto'; import { EmailService } from '@gitroom/nestjs-libraries/services/email.service'; +import { NewsletterService } from '@gitroom/nestjs-libraries/newsletter/newsletter.service'; @Injectable() export class AuthService { diff --git a/apps/frontend/public/icons/platforms/listmonk.png b/apps/frontend/public/icons/platforms/listmonk.png new file mode 100644 index 0000000000000000000000000000000000000000..8c90e6da8743f76deb5d3875530ead74c9fee482 GIT binary patch literal 2580 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 { + modals.closeAll(); gotoUrl( `/integrations/social/${identifier}?state=nostate&code=${Buffer.from( JSON.stringify(data) diff --git a/apps/frontend/src/components/new-launch/providers/listmonk/listmonk.provider.tsx b/apps/frontend/src/components/new-launch/providers/listmonk/listmonk.provider.tsx new file mode 100644 index 00000000..130192eb --- /dev/null +++ b/apps/frontend/src/components/new-launch/providers/listmonk/listmonk.provider.tsx @@ -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 ( + <> + + + + + + ); +}; + +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, +}); diff --git a/apps/frontend/src/components/new-launch/providers/listmonk/select.list.tsx b/apps/frontend/src/components/new-launch/providers/listmonk/select.list.tsx new file mode 100644 index 00000000..e8d76be7 --- /dev/null +++ b/apps/frontend/src/components/new-launch/providers/listmonk/select.list.tsx @@ -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(); + 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 ( + + ); +}; diff --git a/apps/frontend/src/components/new-launch/providers/listmonk/select.templates.tsx b/apps/frontend/src/components/new-launch/providers/listmonk/select.templates.tsx new file mode 100644 index 00000000..a24ac4bc --- /dev/null +++ b/apps/frontend/src/components/new-launch/providers/listmonk/select.templates.tsx @@ -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(); + 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 ( + + ); +}; diff --git a/apps/frontend/src/components/new-launch/providers/show.all.providers.tsx b/apps/frontend/src/components/new-launch/providers/show.all.providers.tsx index 6bf6ab36..9b1c9e51 100644 --- a/apps/frontend/src/components/new-launch/providers/show.all.providers.tsx +++ b/apps/frontend/src/components/new-launch/providers/show.all.providers.tsx @@ -31,6 +31,7 @@ import { Button } from '@gitroom/react/form/button'; import { useT } from '@gitroom/react/translation/get.transation.service.client'; 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 ListmonkProvider from '@gitroom/frontend/components/new-launch/providers/listmonk/listmonk.provider'; export const Providers = [ { @@ -133,6 +134,10 @@ export const Providers = [ identifier: 'wordpress', component: WordpressProvider, }, + { + identifier: 'listmonk', + component: ListmonkProvider, + }, ]; export const ShowAllProviders = forwardRef((props, ref) => { const { date, current, global, selectedIntegrations, allIntegrations } = 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/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/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/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) {} + } +}