From 3bfac7a38d616e05d31a1196a1448bd1ad1ffe20 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 5 Oct 2024 21:54:52 +0700 Subject: [PATCH] feat: bluesky --- .../src/api/routes/integrations.controller.ts | 22 +- .../public/icons/platforms/bluesky.png | Bin 0 -> 14926 bytes .../social/[provider]/continue/page.tsx | 17 ++ .../launches/add.provider.component.tsx | 218 +++++++++++++++--- .../providers/bluesky/bluesky.provider.tsx | 14 ++ .../launches/providers/show.all.providers.tsx | 2 + .../src/components/layout/redirect.tsx | 15 ++ .../src/integrations/integration.manager.ts | 17 +- .../integrations/social/bluesky.provider.ts | 169 ++++++++++++++ .../integrations/social/mastodon.provider.ts | 1 - .../social/social.integrations.interface.ts | 9 + package-lock.json | 84 +++++++ package.json | 1 + 13 files changed, 519 insertions(+), 50 deletions(-) create mode 100644 apps/frontend/public/icons/platforms/bluesky.png create mode 100644 apps/frontend/src/components/launches/providers/bluesky/bluesky.provider.tsx create mode 100644 apps/frontend/src/components/layout/redirect.tsx create mode 100644 libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index f0665778..6d89b46e 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -288,15 +288,19 @@ export class IntegrationsController { throw new Error('Integration not allowed'); } - const getCodeVerifier = await ioRedis.get(`login:${body.state}`); + const integrationProvider = + this._integrationManager.getSocialIntegration(integration); + + const getCodeVerifier = integrationProvider.customFields + ? 'none' + : await ioRedis.get(`login:${body.state}`); if (!getCodeVerifier) { throw new Error('Invalid state'); } - await ioRedis.del(`login:${body.state}`); - - const integrationProvider = - this._integrationManager.getSocialIntegration(integration); + if (!integrationProvider.customFields) { + await ioRedis.del(`login:${body.state}`); + } const details = integrationProvider.externalUrl ? await ioRedis.get(`external:${body.state}`) @@ -341,7 +345,13 @@ export class IntegrationsController { integrationProvider.isBetweenSteps, body.refresh, +body.timezone, - details ? AuthService.fixedEncryption(details) : undefined + details + ? AuthService.fixedEncryption(details) + : integrationProvider.customFields + ? AuthService.fixedEncryption( + Buffer.from(body.code, 'base64').toString() + ) + : undefined ); } diff --git a/apps/frontend/public/icons/platforms/bluesky.png b/apps/frontend/public/icons/platforms/bluesky.png new file mode 100644 index 0000000000000000000000000000000000000000..492b23a6336cb1ea68621f795ae75ff1a96cf103 GIT binary patch literal 14926 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 T4&Ksap{AqlCYy6sfyw^`6q_Fl literal 0 HcmV?d00001 diff --git a/apps/frontend/src/app/(site)/integrations/social/[provider]/continue/page.tsx b/apps/frontend/src/app/(site)/integrations/social/[provider]/continue/page.tsx index bead77f8..8a06e542 100644 --- a/apps/frontend/src/app/(site)/integrations/social/[provider]/continue/page.tsx +++ b/apps/frontend/src/app/(site)/integrations/social/[provider]/continue/page.tsx @@ -4,6 +4,7 @@ export const dynamic = 'force-dynamic'; import { internalFetch } from '@gitroom/helpers/utils/internal.fetch'; import { redirect } from 'next/navigation'; +import { Redirect } from '@gitroom/frontend/components/layout/redirect'; export default async function Page({ params: { provider }, @@ -30,6 +31,22 @@ export default async function Page({ return redirect(`/launches?scope=missing`); } + if ( + data.status !== HttpStatusCode.Ok && + data.status !== HttpStatusCode.Created + ) { + return ( + <> +
+ Could not add provider. +
+ You are being redirected back +
+ + + ); + } + const { inBetweenSteps, id } = await data.json(); if (inBetweenSteps && !searchParams.refresh) { diff --git a/apps/frontend/src/components/launches/add.provider.component.tsx b/apps/frontend/src/components/launches/add.provider.component.tsx index 9f354049..dc5e9049 100644 --- a/apps/frontend/src/components/launches/add.provider.component.tsx +++ b/apps/frontend/src/components/launches/add.provider.component.tsx @@ -1,7 +1,7 @@ 'use client'; import { useModals } from '@mantine/modals'; -import React, { FC, useCallback } from 'react'; +import React, { FC, useCallback, useMemo } from 'react'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import { Input } from '@gitroom/react/form/input'; import { FieldValues, FormProvider, useForm } from 'react-hook-form'; @@ -12,6 +12,8 @@ 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'; +import { object, string } from 'yup'; +import { yupResolver } from '@hookform/resolvers/yup'; const resolver = classValidatorResolver(ApiKeyDto); @@ -181,55 +183,193 @@ export const UrlModal: FC<{ ); }; +export const CustomVariables: FC<{ + variables: Array<{ + key: string; + label: string; + defaultValue?: string; + validation: string; + type: 'text' | 'password'; + }>; + identifier: string; + gotoUrl(url: string): void; +}> = (props) => { + const { gotoUrl, identifier, variables } = props; + const modals = useModals(); + const schema = useMemo(() => { + return object({ + ...variables.reduce((aIcc, item) => { + const splitter = item.validation.split('/'); + const regex = new RegExp( + splitter.slice(1, -1).join('/'), + splitter.pop() + ); + return { + ...aIcc, + [item.key]: string() + .matches(regex, `${item.label} is invalid`) + .required(), + }; + }, {}), + }); + }, [variables]); + + const methods = useForm({ + mode: 'onChange', + resolver: yupResolver(schema), + values: variables.reduce( + (acc, item) => ({ + ...acc, + ...(item.defaultValue ? { [item.key]: item.defaultValue } : {}), + }), + {} + ), + }); + + const submit = useCallback( + async (data: FieldValues) => { + gotoUrl( + `/integrations/social/${identifier}?state=nostate&code=${Buffer.from( + JSON.stringify(data) + ).toString('base64')}` + ); + }, + [variables] + ); + + return ( +
+ + + +
+ {variables.map((variable) => ( +
+ +
+ ))} +
+ +
+
+
+
+ ); +}; + export const AddProviderComponent: FC<{ - social: Array<{ identifier: string; name: string; isExternal: boolean }>; + social: Array<{ + identifier: string; + name: string; + isExternal: boolean; + customFields?: Array<{ + key: string; + label: string; + validation: string; + type: 'text' | 'password'; + }>; + }>; article: Array<{ identifier: string; name: string }>; update?: () => void; }> = (props) => { - const { update } = props; + const { update, social, article } = props; const { isGeneral } = useVariables(); const toaster = useToaster(); - + const router = useRouter(); const fetch = useFetch(); const modal = useModals(); - const { social, article } = props; const getSocialLink = useCallback( - (identifier: string, isExternal: boolean) => async () => { - const gotoIntegration = async (externalUrl?: string) => { - const { url, err } = await ( - await fetch( - `/integrations/social/${identifier}${ - externalUrl ? `?externalUrl=${externalUrl}` : `` - }` - ) - ).json(); + ( + identifier: string, + isExternal: boolean, + customFields?: Array<{ + key: string; + label: string; + validation: string; + defaultValue?: string; + type: 'text' | 'password'; + }> + ) => + 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 ; + 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; } - window.location.href = url; - }; - if (isExternal) { - modal.closeAll(); + if (customFields) { + modal.closeAll(); - modal.openModal({ - title: '', - withCloseButton: false, - classNames: { - modal: 'bg-transparent text-textColor', - }, - children: ( - - ), - }); + modal.openModal({ + title: '', + withCloseButton: false, + classNames: { + modal: 'bg-transparent text-textColor', + }, + children: ( + router.push(url)} + variables={customFields} + /> + ), + }); + return; + } - return; - } - - await gotoIntegration(); - }, + await gotoIntegration(); + }, [] ); @@ -281,7 +421,11 @@ export const AddProviderComponent: FC<{ {social.map((item) => (
{ + return null; +}; + +export default withProvider(null, Empty, undefined, async (posts) => { + if (posts.some((p) => p.length > 4)) { + return 'There can be maximum 4 pictures in a post.'; + } + + return true; +}); 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 def7733c..0fece0eb 100644 --- a/apps/frontend/src/components/launches/providers/show.all.providers.tsx +++ b/apps/frontend/src/components/launches/providers/show.all.providers.tsx @@ -16,6 +16,7 @@ import ThreadsProvider from '@gitroom/frontend/components/launches/providers/thr 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'; +import BlueskyProvider from '@gitroom/frontend/components/launches/providers/bluesky/bluesky.provider'; export const Providers = [ {identifier: 'devto', component: DevtoProvider}, @@ -35,6 +36,7 @@ export const Providers = [ {identifier: 'discord', component: DiscordProvider}, {identifier: 'slack', component: SlackProvider}, {identifier: 'mastodon', component: MastodonProvider}, + {identifier: 'bluesky', component: BlueskyProvider}, ]; diff --git a/apps/frontend/src/components/layout/redirect.tsx b/apps/frontend/src/components/layout/redirect.tsx new file mode 100644 index 00000000..6a7dca4a --- /dev/null +++ b/apps/frontend/src/components/layout/redirect.tsx @@ -0,0 +1,15 @@ +'use client'; + +import { FC, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; + +export const Redirect: FC<{url: string, delay: number}> = (props) => { + const { url, delay } = props; + const router = useRouter(); + useEffect(() => { + setTimeout(() => { + router.push(url); + }, delay); + }, []); + return null; +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/integrations/integration.manager.ts b/libraries/nestjs-libraries/src/integrations/integration.manager.ts index 520804fe..e4e951d8 100644 --- a/libraries/nestjs-libraries/src/integrations/integration.manager.ts +++ b/libraries/nestjs-libraries/src/integrations/integration.manager.ts @@ -18,6 +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 { BlueskyProvider } from '@gitroom/nestjs-libraries/integrations/social/bluesky.provider'; // import { MastodonCustomProvider } from '@gitroom/nestjs-libraries/integrations/social/mastodon.custom.provider'; const socialIntegrationList: SocialProvider[] = [ @@ -35,6 +36,7 @@ const socialIntegrationList: SocialProvider[] = [ new DiscordProvider(), new SlackProvider(), new MastodonProvider(), + new BlueskyProvider(), // new MastodonCustomProvider(), ]; @@ -46,13 +48,16 @@ const articleIntegrationList = [ @Injectable() export class IntegrationManager { - getAllIntegrations() { + async getAllIntegrations() { return { - social: socialIntegrationList.map((p) => ({ - name: p.name, - identifier: p.identifier, - isExternal: !!p.externalUrl - })), + social: await Promise.all( + socialIntegrationList.map(async (p) => ({ + name: p.name, + identifier: p.identifier, + isExternal: !!p.externalUrl, + ...(p.customFields ? { customFields: await p.customFields() } : {}), + })) + ), article: articleIntegrationList.map((p) => ({ name: p.name, identifier: p.identifier, diff --git a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts new file mode 100644 index 00000000..f6556b05 --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts @@ -0,0 +1,169 @@ +import { + AuthTokenDetails, + 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 { BskyAgent } from '@atproto/api'; +import dayjs from 'dayjs'; +import { Integration } from '@prisma/client'; +import { AuthService } from '@gitroom/helpers/auth/auth.service'; +import sharp from 'sharp'; + +export class BlueskyProvider extends SocialAbstract implements SocialProvider { + identifier = 'bluesky'; + name = 'Bluesky'; + isBetweenSteps = false; + scopes = ['write:statuses', 'profile', 'write:media']; + + async customFields() { + return [ + { + key: 'service', + label: 'Service', + defaultValue: 'https://bsky.social', + validation: `/^(https?:\\/\\/)?((([a-zA-Z0-9\\-_]{1,256}\\.[a-zA-Z]{2,6})|(([0-9]{1,3}\\.){3}[0-9]{1,3}))(:[0-9]{1,5})?)(\\/[^\\s]*)?$/`, + type: 'text' as const, + }, + { + key: 'identifier', + label: 'Identifier', + validation: `/^.{3,}$/`, + 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(refresh?: string) { + const state = makeId(6); + return { + url: '', + codeVerifier: makeId(10), + state, + }; + } + + async authenticate(params: { + code: string; + codeVerifier: string; + refresh?: string; + }) { + const body = JSON.parse(Buffer.from(params.code, 'base64').toString()); + + const agent = new BskyAgent({ + service: body.service, + }); + + const { + data: { accessJwt, refreshJwt, handle, did }, + } = await agent.login({ + identifier: body.identifier, + password: body.password, + }); + + const profile = await agent.getProfile({ + actor: did, + }); + + return { + refreshToken: refreshJwt, + expiresIn: dayjs().add(100, 'years').unix() - dayjs().unix(), + accessToken: accessJwt, + id: did, + name: profile.data.displayName!, + picture: profile.data.avatar!, + username: profile.data.handle!, + }; + } + + async post( + id: string, + accessToken: string, + postDetails: PostDetails[], + integration: Integration + ): Promise { + const body = JSON.parse( + AuthService.fixedDecryption(integration.customInstanceDetails!) + ); + const agent = new BskyAgent({ + service: body.service, + }); + + await agent.login({ + identifier: body.identifier, + password: body.password, + }); + + let loadCid = ''; + let loadUri = ''; + for (const post of postDetails) { + const images = await Promise.all( + post.media?.map(async (p) => { + return await agent.uploadBlob( + new Blob([ + await sharp(await (await fetch(p.url)).arrayBuffer()) + .resize({ width: 400 }) + .toBuffer(), + ]) + ); + }) || [] + ); + + const { cid, uri } = await agent.post({ + text: post.message, + createdAt: new Date().toISOString(), + ...(images.length + ? { + embed: { + $type: 'app.bsky.embed.images', + images: images.map((p) => ({ + // can be an array up to 4 values + alt: 'image', // the alt text + image: p.data.blob, + })), + }, + } + : {}), + ...(loadCid + ? { + reply: { + root: { + uri: loadUri, + cid: loadCid, + }, + parent: { + uri: loadUri, + cid: loadCid, + }, + }, + } + : {}), + }); + + loadCid = loadCid || cid; + loadUri = loadUri || uri; + } + + return []; + } +} diff --git a/libraries/nestjs-libraries/src/integrations/social/mastodon.provider.ts b/libraries/nestjs-libraries/src/integrations/social/mastodon.provider.ts index 74198db5..1587dcec 100644 --- a/libraries/nestjs-libraries/src/integrations/social/mastodon.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/mastodon.provider.ts @@ -1,6 +1,5 @@ import { AuthTokenDetails, - ClientInformation, PostDetails, PostResponse, SocialProvider, 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 b36ac6cb..0b4fa59e 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -98,6 +98,15 @@ export interface SocialProvider ISocialMediaIntegration { identifier: string; refreshWait?: boolean; + customFields?: () => Promise< + { + key: string; + label: string; + defaultValue?: string; + validation: string; + type: 'text' | 'password'; + }[] + >; name: string; isBetweenSteps: boolean; scopes: string[]; diff --git a/package-lock.json b/package-lock.json index d4a91654..0b166b2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "@atproto/api": "^0.13.11", "@aws-sdk/client-s3": "^3.410.0", "@aws-sdk/s3-request-presigner": "^3.410.0", "@casl/ability": "^6.5.0", @@ -463,6 +464,58 @@ "web-streams-polyfill": "^3.2.1" } }, + "node_modules/@atproto/api": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.13.11.tgz", + "integrity": "sha512-YW+4WzZEGGj/SDYo9w+S2PkSaeSS+8Dosk21GFm4EFYq1eq7G0cxuMgvdcq6fov7f9zqsaTFQL2fA6cAgMA0ow==", + "dependencies": { + "@atproto/common-web": "^0.3.1", + "@atproto/lexicon": "^0.4.2", + "@atproto/syntax": "^0.3.0", + "@atproto/xrpc": "^0.6.3", + "await-lock": "^2.2.2", + "multiformats": "^9.9.0", + "tlds": "^1.234.0", + "zod": "^3.23.8" + } + }, + "node_modules/@atproto/common-web": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.3.1.tgz", + "integrity": "sha512-N7wiTnus5vAr+lT//0y8m/FaHHLJ9LpGuEwkwDAeV3LCiPif4m/FS8x/QOYrx1PdZQwKso95RAPzCGWQBH5j6Q==", + "dependencies": { + "graphemer": "^1.4.0", + "multiformats": "^9.9.0", + "uint8arrays": "3.0.0", + "zod": "^3.23.8" + } + }, + "node_modules/@atproto/lexicon": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.2.tgz", + "integrity": "sha512-CXoOkhcdF3XVUnR2oNgCs2ljWfo/8zUjxL5RIhJW/UNLp/FSl+KpF8Jm5fbk8Y/XXVPGRAsv9OYfxyU/14N/pw==", + "dependencies": { + "@atproto/common-web": "^0.3.1", + "@atproto/syntax": "^0.3.0", + "iso-datestring-validator": "^2.2.2", + "multiformats": "^9.9.0", + "zod": "^3.23.8" + } + }, + "node_modules/@atproto/syntax": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.0.tgz", + "integrity": "sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA==" + }, + "node_modules/@atproto/xrpc": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.6.3.tgz", + "integrity": "sha512-S3tRvOdA9amPkKLll3rc4vphlDitLrkN5TwWh5Tu/jzk7mnobVVE3akYgICV9XCNHKjWM+IAPxFFI2qi+VW6nQ==", + "dependencies": { + "@atproto/lexicon": "^0.4.2", + "zod": "^3.23.8" + } + }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", @@ -15780,6 +15833,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/await-lock": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz", + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==" + }, "node_modules/axe-core": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.0.tgz", @@ -24646,6 +24704,11 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/iso-datestring-validator": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz", + "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==" + }, "node_modules/isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", @@ -28889,6 +28952,11 @@ "multicast-dns": "cli.js" } }, + "node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" + }, "node_modules/mustache": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", @@ -36374,6 +36442,14 @@ "node": ">=14.0.0" } }, + "node_modules/tlds": { + "version": "1.255.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.255.0.tgz", + "integrity": "sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw==", + "bin": { + "tlds": "bin.js" + } + }, "node_modules/tldts": { "version": "6.1.47", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.47.tgz", @@ -37068,6 +37144,14 @@ "node": ">=8" } }, + "node_modules/uint8arrays": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz", + "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", + "dependencies": { + "multiformats": "^9.4.2" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index 0019d3f5..744288a1 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ }, "private": true, "dependencies": { + "@atproto/api": "^0.13.11", "@aws-sdk/client-s3": "^3.410.0", "@aws-sdk/s3-request-presigner": "^3.410.0", "@casl/ability": "^6.5.0",