From 3b35ca3ac87b78a3bed503a8973d9d55a726a325 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 4 Oct 2024 12:23:01 +0700 Subject: [PATCH 1/2] feat: external social media --- .../src/api/routes/integrations.controller.ts | 59 +++++- .../icons/platforms/mastodon-custom.png | Bin 0 -> 15209 bytes .../public/icons/platforms/mastodon.png | Bin 0 -> 15209 bytes .../launches/add.provider.component.tsx | 101 ++++++++- .../providers/mastodon/mastodon.provider.tsx | 8 + .../launches/providers/show.all.providers.tsx | 2 + .../integrations/integration.repository.ts | 4 +- .../integrations/integration.service.ts | 6 +- .../src/database/prisma/schema.prisma | 43 ++-- .../src/integrations/integration.manager.ts | 7 +- .../social/mastodon.custom.provider.ts | 81 ++++++++ .../integrations/social/mastodon.provider.ts | 193 ++++++++++++++++++ .../social/social.integrations.interface.ts | 26 ++- 13 files changed, 480 insertions(+), 50 deletions(-) create mode 100644 apps/frontend/public/icons/platforms/mastodon-custom.png create mode 100644 apps/frontend/public/icons/platforms/mastodon.png create mode 100644 apps/frontend/src/components/launches/providers/mastodon/mastodon.provider.tsx create mode 100644 libraries/nestjs-libraries/src/integrations/social/mastodon.custom.provider.ts create mode 100644 libraries/nestjs-libraries/src/integrations/social/mastodon.provider.ts diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index b0e01043..bfdada6e 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -27,6 +27,7 @@ import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.req import { NotEnoughScopesFilter } from '@gitroom/nestjs-libraries/integrations/integration.missing.scopes'; import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service'; import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto'; +import { AuthService } from '@gitroom/helpers/auth/auth.service'; @ApiTags('Integrations') @Controller('/integrations') @@ -127,7 +128,8 @@ export class IntegrationsController { @CheckPolicies([AuthorizationActions.Create, Sections.CHANNEL]) async getIntegrationUrl( @Param('integration') integration: string, - @Query('refresh') refresh: string + @Query('refresh') refresh: string, + @Query('externalUrl') externalUrl: string ) { if ( !this._integrationManager @@ -139,11 +141,33 @@ export class IntegrationsController { const integrationProvider = this._integrationManager.getSocialIntegration(integration); - const { codeVerifier, state, url } = - await integrationProvider.generateAuthUrl(refresh); - await ioRedis.set(`login:${state}`, codeVerifier, 'EX', 300); - return { url }; + if (integrationProvider.externalUrl && !externalUrl) { + throw new Error('Missing external url'); + } + + try { + const getExternalUrl = integrationProvider.externalUrl + ? { + ...(await integrationProvider.externalUrl(externalUrl)), + instanceUrl: externalUrl, + } + : undefined; + + const { codeVerifier, state, url } = + await integrationProvider.generateAuthUrl(refresh, getExternalUrl); + await ioRedis.set(`login:${state}`, codeVerifier, 'EX', 300); + await ioRedis.set( + `external:${state}`, + JSON.stringify(getExternalUrl), + 'EX', + 300 + ); + + return { url }; + } catch (err) { + return { err: true }; + } } @Post('/:id/time') @@ -273,6 +297,15 @@ export class IntegrationsController { const integrationProvider = this._integrationManager.getSocialIntegration(integration); + + const details = integrationProvider.externalUrl + ? await ioRedis.get(`external:${body.state}`) + : undefined; + + if (details) { + await ioRedis.del(`external:${body.state}`); + } + const { accessToken, expiresIn, @@ -281,11 +314,14 @@ export class IntegrationsController { name, picture, username, - } = await integrationProvider.authenticate({ - code: body.code, - codeVerifier: getCodeVerifier, - refresh: body.refresh, - }); + } = await integrationProvider.authenticate( + { + code: body.code, + codeVerifier: getCodeVerifier, + refresh: body.refresh, + }, + details ? JSON.parse(details) : undefined + ); if (!id) { throw new Error('Invalid api key'); @@ -304,7 +340,8 @@ export class IntegrationsController { username, integrationProvider.isBetweenSteps, body.refresh, - +body.timezone + +body.timezone, + AuthService.fixedEncryption(details) ); } diff --git a/apps/frontend/public/icons/platforms/mastodon-custom.png b/apps/frontend/public/icons/platforms/mastodon-custom.png new file mode 100644 index 0000000000000000000000000000000000000000..79670abcf0f80a8eaff7bcc590a72f51e7c61dd4 GIT binary patch literal 15209 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 CR@RgN literal 0 HcmV?d00001 diff --git a/apps/frontend/public/icons/platforms/mastodon.png b/apps/frontend/public/icons/platforms/mastodon.png new file mode 100644 index 0000000000000000000000000000000000000000..79670abcf0f80a8eaff7bcc590a72f51e7c61dd4 GIT binary patch literal 15209 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 CR@RgN literal 0 HcmV?d00001 diff --git a/apps/frontend/src/components/launches/add.provider.component.tsx b/apps/frontend/src/components/launches/add.provider.component.tsx index 7d0f6a8b..9f354049 100644 --- a/apps/frontend/src/components/launches/add.provider.component.tsx +++ b/apps/frontend/src/components/launches/add.provider.component.tsx @@ -11,6 +11,7 @@ import { ApiKeyDto } from '@gitroom/nestjs-libraries/dtos/integrations/api.key.d import { useRouter } from 'next/navigation'; import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; import { useVariables } from '@gitroom/react/helpers/variable.context'; +import { useToaster } from '@gitroom/react/toaster/toaster'; const resolver = classValidatorResolver(ApiKeyDto); @@ -127,23 +128,107 @@ export const ApiModal: FC<{ ); }; + +export const UrlModal: FC<{ + gotoUrl(url: string): void; +}> = (props) => { + const { gotoUrl } = props; + const methods = useForm({ + mode: 'onChange', + }); + + const submit = useCallback(async (data: FieldValues) => { + gotoUrl(data.url); + }, []); + + return ( +
+ + + +
+
+ +
+
+ +
+
+
+
+ ); +}; + export const AddProviderComponent: FC<{ - social: Array<{ identifier: string; name: string }>; + social: Array<{ identifier: string; name: string; isExternal: boolean }>; article: Array<{ identifier: string; name: string }>; update?: () => void; }> = (props) => { const { update } = props; - const {isGeneral} = useVariables(); + const { isGeneral } = useVariables(); + const toaster = useToaster(); const fetch = useFetch(); const modal = useModals(); const { social, article } = props; const getSocialLink = useCallback( - (identifier: string) => async () => { - const { url } = await ( - await fetch('/integrations/social/' + identifier) - ).json(); - window.location.href = url; + (identifier: string, isExternal: boolean) => async () => { + const gotoIntegration = async (externalUrl?: string) => { + const { url, err } = await ( + await fetch( + `/integrations/social/${identifier}${ + externalUrl ? `?externalUrl=${externalUrl}` : `` + }` + ) + ).json(); + + if (err) { + toaster.show('Could not connect to the platform', 'warning'); + return ; + } + window.location.href = url; + }; + + if (isExternal) { + modal.closeAll(); + + modal.openModal({ + title: '', + withCloseButton: false, + classNames: { + modal: 'bg-transparent text-textColor', + }, + children: ( + + ), + }); + + return; + } + + await gotoIntegration(); }, [] ); @@ -196,7 +281,7 @@ export const AddProviderComponent: FC<{ {social.map((item) => (
{ + return null; +}; + +export default withProvider(null, Empty, undefined, undefined); diff --git a/apps/frontend/src/components/launches/providers/show.all.providers.tsx b/apps/frontend/src/components/launches/providers/show.all.providers.tsx index 50d9392a..def7733c 100644 --- a/apps/frontend/src/components/launches/providers/show.all.providers.tsx +++ b/apps/frontend/src/components/launches/providers/show.all.providers.tsx @@ -15,6 +15,7 @@ import DribbbleProvider from '@gitroom/frontend/components/launches/providers/dr import ThreadsProvider from '@gitroom/frontend/components/launches/providers/threads/threads.provider'; import DiscordProvider from '@gitroom/frontend/components/launches/providers/discord/discord.provider'; import SlackProvider from '@gitroom/frontend/components/launches/providers/slack/slack.provider'; +import MastodonProvider from '@gitroom/frontend/components/launches/providers/mastodon/mastodon.provider'; export const Providers = [ {identifier: 'devto', component: DevtoProvider}, @@ -33,6 +34,7 @@ export const Providers = [ {identifier: 'threads', component: ThreadsProvider}, {identifier: 'discord', component: DiscordProvider}, {identifier: 'slack', component: SlackProvider}, + {identifier: 'mastodon', component: MastodonProvider}, ]; diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts index f618cdfc..145c5ce3 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts @@ -79,7 +79,8 @@ export class IntegrationRepository { username?: string, isBetweenSteps = false, refresh?: string, - timezone?: number + timezone?: number, + customInstanceDetails?: string ) { const postTimes = timezone ? { @@ -113,6 +114,7 @@ export class IntegrationRepository { ...postTimes, organizationId: org, refreshNeeded: false, + ...(customInstanceDetails ? { customInstanceDetails } : {}), }, update: { type: type as any, diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts index 53f2cad6..0de38c66 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts @@ -47,7 +47,8 @@ export class IntegrationService { username?: string, isBetweenSteps = false, refresh?: string, - timezone?: number + timezone?: number, + customInstanceDetails?: string ) { const loadImage = await axios.get(picture, { responseType: 'arraybuffer' }); const uploadedPicture = await simpleUpload( @@ -69,7 +70,8 @@ export class IntegrationService { username, isBetweenSteps, refresh, - timezone + timezone, + customInstanceDetails ); } diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index 3944ec4d..a2858330 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -242,27 +242,28 @@ model Subscription { } model Integration { - id String @id @default(cuid()) - internalId String - organizationId String - name String - organization Organization @relation(fields: [organizationId], references: [id]) - picture String? - providerIdentifier String - type String - token String - disabled Boolean @default(false) - tokenExpiration DateTime? - refreshToken String? - posts Post[] - profile String? - deletedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime? @updatedAt - orderItems OrderItems[] - inBetweenSteps Boolean @default(false) - refreshNeeded Boolean @default(false) - postingTimes String @default("[{\"time\":120}, {\"time\":400}, {\"time\":700}]") + id String @id @default(cuid()) + internalId String + organizationId String + name String + organization Organization @relation(fields: [organizationId], references: [id]) + picture String? + providerIdentifier String + type String + token String + disabled Boolean @default(false) + tokenExpiration DateTime? + refreshToken String? + posts Post[] + profile String? + deletedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime? @updatedAt + orderItems OrderItems[] + inBetweenSteps Boolean @default(false) + refreshNeeded Boolean @default(false) + postingTimes String @default("[{\"time\":120}, {\"time\":400}, {\"time\":700}]") + customInstanceDetails String? @@index([updatedAt]) @@index([deletedAt]) diff --git a/libraries/nestjs-libraries/src/integrations/integration.manager.ts b/libraries/nestjs-libraries/src/integrations/integration.manager.ts index 00ef821d..2133c05e 100644 --- a/libraries/nestjs-libraries/src/integrations/integration.manager.ts +++ b/libraries/nestjs-libraries/src/integrations/integration.manager.ts @@ -17,8 +17,10 @@ import { LinkedinPageProvider } from '@gitroom/nestjs-libraries/integrations/soc import { ThreadsProvider } from '@gitroom/nestjs-libraries/integrations/social/threads.provider'; import { DiscordProvider } from '@gitroom/nestjs-libraries/integrations/social/discord.provider'; import { SlackProvider } from '@gitroom/nestjs-libraries/integrations/social/slack.provider'; +import { MastodonProvider } from '@gitroom/nestjs-libraries/integrations/social/mastodon.provider'; +import { MastodonCustomProvider } from '@gitroom/nestjs-libraries/integrations/social/mastodon.custom.provider'; -const socialIntegrationList = [ +const socialIntegrationList: SocialProvider[] = [ new XProvider(), new LinkedinProvider(), new LinkedinPageProvider(), @@ -32,6 +34,8 @@ const socialIntegrationList = [ new DribbbleProvider(), new DiscordProvider(), new SlackProvider(), + new MastodonProvider(), + new MastodonCustomProvider(), ]; const articleIntegrationList = [ @@ -47,6 +51,7 @@ export class IntegrationManager { social: socialIntegrationList.map((p) => ({ name: p.name, identifier: p.identifier, + isExternal: !!p.externalUrl })), article: articleIntegrationList.map((p) => ({ name: p.name, diff --git a/libraries/nestjs-libraries/src/integrations/social/mastodon.custom.provider.ts b/libraries/nestjs-libraries/src/integrations/social/mastodon.custom.provider.ts new file mode 100644 index 00000000..d3b11e44 --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/social/mastodon.custom.provider.ts @@ -0,0 +1,81 @@ +import { + ClientInformation, + PostDetails, + PostResponse, +} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; +import { MastodonProvider } from '@gitroom/nestjs-libraries/integrations/social/mastodon.provider'; +import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; + +export class MastodonCustomProvider extends MastodonProvider { + override identifier = 'mastodon-custom'; + override name = 'M. Instance'; + async externalUrl(url: string) { + const form = new FormData(); + form.append('client_name', 'Postiz'); + form.append( + 'redirect_uris', + `${process.env.FRONTEND_URL}/integrations/social/mastodon` + ); + form.append('scopes', this.scopes.join(' ')); + form.append('website', process.env.FRONTEND_URL!); + const { client_id, client_secret } = await ( + await fetch(url + '/api/v1/apps', { + method: 'POST', + body: form, + }) + ).json(); + + return { + client_id, + client_secret, + }; + } + override async generateAuthUrl( + refresh?: string, + external?: ClientInformation + ) { + const state = makeId(6); + const url = this.generateUrlDynamic( + external?.instanceUrl!, + state, + external?.client_id!, + process.env.FRONTEND_URL!, + refresh + ); + + return { + url, + codeVerifier: makeId(10), + state, + }; + } + + override async authenticate( + params: { + code: string; + codeVerifier: string; + refresh?: string; + }, + clientInformation?: ClientInformation + ) { + return this.dynamicAuthenticate( + clientInformation?.client_id!, + clientInformation?.client_secret!, + clientInformation?.instanceUrl!, + params.code + ); + } + + override async post( + id: string, + accessToken: string, + postDetails: PostDetails[] + ): Promise { + return this.dynamicPost( + id, + accessToken, + 'https://mastodon.social', + postDetails + ); + } +} diff --git a/libraries/nestjs-libraries/src/integrations/social/mastodon.provider.ts b/libraries/nestjs-libraries/src/integrations/social/mastodon.provider.ts new file mode 100644 index 00000000..74198db5 --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/social/mastodon.provider.ts @@ -0,0 +1,193 @@ +import { + AuthTokenDetails, + ClientInformation, + PostDetails, + PostResponse, + SocialProvider, +} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; +import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; +import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; +import dayjs from 'dayjs'; + +export class MastodonProvider extends SocialAbstract implements SocialProvider { + identifier = 'mastodon'; + name = 'Mastodon'; + isBetweenSteps = false; + scopes = ['write:statuses', 'profile', 'write:media']; + + async refreshToken(refreshToken: string): Promise { + return { + refreshToken: '', + expiresIn: 0, + accessToken: '', + id: '', + name: '', + picture: '', + username: '', + }; + } + protected generateUrlDynamic( + customUrl: string, + state: string, + clientId: string, + url: string, + refresh?: string + ) { + return `${customUrl}/oauth/authorize?client_id=${clientId}&response_type=code&redirect_uri=${encodeURIComponent( + `${url}/integrations/social/mastodon${ + refresh ? `?refresh=${refresh}` : '' + }` + )}&scope=${this.scopes.join('+')}&state=${state}`; + } + + async generateAuthUrl(refresh?: string) { + const state = makeId(6); + const url = this.generateUrlDynamic( + 'https://mastodon.social', + state, + process.env.MASTODON_CLIENT_ID!, + process.env.FRONTEND_URL!, + refresh + ); + return { + url, + codeVerifier: makeId(10), + state, + }; + } + + protected async dynamicAuthenticate( + clientId: string, + clientSecret: string, + url: string, + code: string + ) { + const form = new FormData(); + form.append('client_id', clientId); + form.append('client_secret', clientSecret); + form.append('code', code); + form.append('grant_type', 'authorization_code'); + form.append( + 'redirect_uri', + `${process.env.FRONTEND_URL}/integrations/social/mastodon` + ); + form.append('scope', this.scopes.join(' ')); + + const tokenInformation = await ( + await this.fetch(`${url}/oauth/token`, { + method: 'POST', + body: form, + }) + ).json(); + + const personalInformation = await ( + await this.fetch(`${url}/api/v1/accounts/verify_credentials`, { + headers: { + Authorization: `Bearer ${tokenInformation.access_token}`, + }, + }) + ).json(); + + return { + id: personalInformation.id, + name: personalInformation.display_name || personalInformation.acct, + accessToken: tokenInformation.access_token, + refreshToken: 'null', + expiresIn: dayjs().add(100, 'years').unix() - dayjs().unix(), + picture: personalInformation.avatar, + username: personalInformation.username, + }; + } + + async authenticate( + params: { + code: string; + codeVerifier: string; + refresh?: string; + } + ) { + return this.dynamicAuthenticate( + process.env.MASTODON_CLIENT_ID!, + process.env.MASTODON_CLIENT_SECRET!, + 'https://mastodon.social', + params.code + ); + } + + async uploadFile(instanceUrl: string, fileUrl: string, accessToken: string) { + const form = new FormData(); + form.append('file', await fetch(fileUrl).then((r) => r.blob())); + const media = await ( + await this.fetch(`${instanceUrl}/api/v1/media`, { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + body: form, + }) + ).json(); + return media.id; + } + + async dynamicPost( + id: string, + accessToken: string, + url: string, + postDetails: PostDetails[] + ): Promise { + let loadId = ''; + const ids = [] as string[]; + for (const getPost of postDetails) { + const uploadFiles = await Promise.all( + getPost?.media?.map((media) => + this.uploadFile(url, media.url, accessToken) + ) || [] + ); + + const form = new FormData(); + form.append('status', getPost.message); + form.append('visibility', 'public'); + if (loadId) { + form.append('in_reply_to_id', loadId); + } + if (uploadFiles.length) { + for (const file of uploadFiles) { + form.append('media_ids[]', file); + } + } + + const post = await ( + await this.fetch(`${url}/api/v1/statuses`, { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + body: form, + }) + ).json(); + + ids.push(post.id); + loadId = loadId || post.id; + } + + return postDetails.map((p, i) => ({ + id: p.id, + postId: ids[i], + releaseURL: `${url}/statuses/${ids[i]}`, + status: 'completed', + })); + } + + async post( + id: string, + accessToken: string, + postDetails: PostDetails[] + ): Promise { + return this.dynamicPost( + id, + accessToken, + 'https://mastodon.social', + postDetails + ); + } +} diff --git a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts index e3b232bb..b36ac6cb 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -1,13 +1,24 @@ import { Integration } from '@prisma/client'; +export interface ClientInformation { + client_id: string; + client_secret: string; + instanceUrl: string; +} export interface IAuthenticator { - authenticate(params: { - code: string; - codeVerifier: string; - refresh?: string; - }): Promise; + authenticate( + params: { + code: string; + codeVerifier: string; + refresh?: string; + }, + clientInformation?: ClientInformation + ): Promise; refreshToken(refreshToken: string): Promise; - generateAuthUrl(refresh?: string): Promise; + generateAuthUrl( + refresh?: string, + clientInformation?: ClientInformation + ): Promise; analytics?( id: string, accessToken: string, @@ -90,4 +101,7 @@ export interface SocialProvider name: string; isBetweenSteps: boolean; scopes: string[]; + externalUrl?: ( + url: string + ) => Promise<{ client_id: string; client_secret: string }>; } From 022af75434cd47bcdb07c584fffaa4ba228b5bcb Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 5 Oct 2024 12:11:06 +0700 Subject: [PATCH 2/2] feat: mastodon --- .../nestjs-libraries/src/integrations/integration.manager.ts | 4 ++-- .../src/integrations/social/mastodon.custom.provider.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/integration.manager.ts b/libraries/nestjs-libraries/src/integrations/integration.manager.ts index 2133c05e..520804fe 100644 --- a/libraries/nestjs-libraries/src/integrations/integration.manager.ts +++ b/libraries/nestjs-libraries/src/integrations/integration.manager.ts @@ -18,7 +18,7 @@ import { ThreadsProvider } from '@gitroom/nestjs-libraries/integrations/social/t import { DiscordProvider } from '@gitroom/nestjs-libraries/integrations/social/discord.provider'; import { SlackProvider } from '@gitroom/nestjs-libraries/integrations/social/slack.provider'; import { MastodonProvider } from '@gitroom/nestjs-libraries/integrations/social/mastodon.provider'; -import { MastodonCustomProvider } from '@gitroom/nestjs-libraries/integrations/social/mastodon.custom.provider'; +// import { MastodonCustomProvider } from '@gitroom/nestjs-libraries/integrations/social/mastodon.custom.provider'; const socialIntegrationList: SocialProvider[] = [ new XProvider(), @@ -35,7 +35,7 @@ const socialIntegrationList: SocialProvider[] = [ new DiscordProvider(), new SlackProvider(), new MastodonProvider(), - new MastodonCustomProvider(), + // new MastodonCustomProvider(), ]; const articleIntegrationList = [ diff --git a/libraries/nestjs-libraries/src/integrations/social/mastodon.custom.provider.ts b/libraries/nestjs-libraries/src/integrations/social/mastodon.custom.provider.ts index d3b11e44..1e48f2da 100644 --- a/libraries/nestjs-libraries/src/integrations/social/mastodon.custom.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/mastodon.custom.provider.ts @@ -18,7 +18,7 @@ export class MastodonCustomProvider extends MastodonProvider { ); form.append('scopes', this.scopes.join(' ')); form.append('website', process.env.FRONTEND_URL!); - const { client_id, client_secret } = await ( + const { client_id, client_secret, ...all } = await ( await fetch(url + '/api/v1/apps', { method: 'POST', body: form,