From 04d565745e028bdfa8a3e2b6253df08ceb6d3f24 Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Tue, 7 Nov 2023 19:10:21 -0500 Subject: [PATCH] removed outdated firebase functions --- .../firebase/functions/src/algolia/algolia.ts | 98 -- .../firebase/functions/src/calendly/pubsub.ts | 108 -- .../functions/src/calendly/webhook.ts | 44 - .../src/cloudinary/cloudinaryCookieToken.ts | 57 - .../src/cloudinary/cloudinarysignature.ts | 52 - .../src/cloudinary/scheduledNotionCheck.ts | 303 ---- apps/firebase/functions/src/config/config.ts | 45 - .../src/devto/scheduledNotionToDevto.ts | 308 ++-- .../firebase/functions/src/firebase/notion.ts | 67 - apps/firebase/functions/src/github/index.ts | 522 ------- .../src/hashnode/scheduledNotionToHashNode.ts | 394 +++-- apps/firebase/functions/src/index.ts | 37 - .../firebase/functions/src/models/calendly.ts | 94 -- .../functions/src/stripe/subscriptions.ts | 75 - .../functions/src/utilities/calendly.ts | 129 -- .../functions/src/utilities/cloudinary.ts | 17 - .../src/utilities/cloudinaryUtils.ts | 147 -- apps/firebase/functions/src/utilities/date.ts | 22 - .../firebase/functions/src/utilities/devto.ts | 3 +- .../functions/src/utilities/notion.server.ts | 1348 ----------------- .../functions/src/utilities/twitter.ts | 13 - 21 files changed, 346 insertions(+), 3537 deletions(-) delete mode 100644 apps/firebase/functions/src/algolia/algolia.ts delete mode 100644 apps/firebase/functions/src/calendly/pubsub.ts delete mode 100644 apps/firebase/functions/src/calendly/webhook.ts delete mode 100644 apps/firebase/functions/src/cloudinary/cloudinaryCookieToken.ts delete mode 100644 apps/firebase/functions/src/cloudinary/cloudinarysignature.ts delete mode 100644 apps/firebase/functions/src/cloudinary/scheduledNotionCheck.ts delete mode 100644 apps/firebase/functions/src/firebase/notion.ts delete mode 100644 apps/firebase/functions/src/github/index.ts delete mode 100644 apps/firebase/functions/src/models/calendly.ts delete mode 100644 apps/firebase/functions/src/stripe/subscriptions.ts delete mode 100644 apps/firebase/functions/src/utilities/calendly.ts delete mode 100644 apps/firebase/functions/src/utilities/cloudinary.ts delete mode 100644 apps/firebase/functions/src/utilities/cloudinaryUtils.ts delete mode 100644 apps/firebase/functions/src/utilities/date.ts delete mode 100644 apps/firebase/functions/src/utilities/notion.server.ts delete mode 100644 apps/firebase/functions/src/utilities/twitter.ts diff --git a/apps/firebase/functions/src/algolia/algolia.ts b/apps/firebase/functions/src/algolia/algolia.ts deleted file mode 100644 index c942d3935..000000000 --- a/apps/firebase/functions/src/algolia/algolia.ts +++ /dev/null @@ -1,98 +0,0 @@ -import * as functions from 'firebase-functions'; -import algoliasearch from 'algoliasearch'; -import { algoliaAppId, algoliaApiKey, algoliaIndex } from '../config/config'; -import { - getNotionPageMarkdown, - getPurrfectStreamPageMarkdown, - queryAll, -} from '../utilities/notion.server'; - -const algolia = algoliasearch(algoliaAppId, algoliaApiKey); -const ai = algolia.initIndex(algoliaIndex); - -function sleep(ms: number) { - return new Promise((resolve) => { - console.log('Pausing 3sec'); - setTimeout(resolve, ms); - }); -} - -const paginate = async ( - _type: string, - raw: any, - preview?: boolean -): Promise => { - // Call to get full content and add to algolia - for (const p of raw.results) { - await sleep(1000); - const now = new Date(); - if ( - (p?.properties?.published?.select?.name === 'published' || - p?.properties?.Status?.select?.name === 'Released') && - new Date(p?.properties?.start?.date?.start) <= now && - (new Date(p?.properties?.end?.date?.start) >= now || - !p?.properties?.end?.date?.start) - ) { - console.log(`finding: ${p?.id}`); - if (p?.properties?.slug?.url) { - let post; - if (_type === 'podcast') { - post = await getPurrfectStreamPageMarkdown(p?.properties?.slug?.url); - } else { - post = await getNotionPageMarkdown({ - _type, - slug: p?.properties?.slug?.url, - preview, - }); - } - const result = await ai.saveObject({ - ...post, - objectID: post._id, - type: post._type, - }); - console.log(`Algolia add ${_type}:${p?._id}`, JSON.stringify(result)); - } - } else { - const result = await ai.deleteObject(p?._id); - console.log(`Algolia delete ${_type}:${p?._id}`, JSON.stringify(result)); - } - } - - if (raw.next_cursor) { - const newRaw = await queryAll(_type, 3, raw.next_cursor); - await paginate(_type, newRaw); - } else { - console.log('finished pagination'); - } -}; - -export const scheduledNotionToAlgolia = functions - .runWith({ - timeoutSeconds: 540, - }) - .pubsub.schedule('every 1 hours') - .onRun(async () => { - // .https.onRequest(async (req, res) => { - // Check to see if ther are scheduled pods - console.log('Update Algolia from Notion'); - - //Get initial cursors - const [posts, tutorials, courses, pages, podcasts] = await Promise.all([ - queryAll('post', 3), - queryAll('tutorial', 3), - queryAll('course', 3), - queryAll('page', 3), - queryAll('podcast', 3), - ]); - await paginate('post', posts); - await sleep(3000); - await paginate('tutorial', tutorials); - await sleep(3000); - await paginate('course', courses); - await sleep(3000); - await paginate('page', pages); - await sleep(3000); - await paginate('podcast', podcasts); - return null; - // res.send(200); - }); diff --git a/apps/firebase/functions/src/calendly/pubsub.ts b/apps/firebase/functions/src/calendly/pubsub.ts deleted file mode 100644 index 6a30ef81c..000000000 --- a/apps/firebase/functions/src/calendly/pubsub.ts +++ /dev/null @@ -1,108 +0,0 @@ -import * as functions from 'firebase-functions'; -import { - queryPurrfectGuest, - createPurrfectGuest, - createPurrfectPage, - queryPurrfectPageByCalendarId, -} from './../utilities/notion.server'; - -import { getEvent, listEventInvitees, ccd } from './../utilities/calendly'; -import { utcOffset } from '../utilities/date'; -import { log, LogSeverity } from '../utilities/logging'; -import { WebhookPayload } from '../models/calendly'; - -export const topicId = 'calendlyCreateNotionCard'; - -export const calendlyCreateNotionCardPubSub = functions.pubsub - .topic(topicId) - .onPublish(async (message, context) => { - log(LogSeverity.DEBUG, 'The function was triggered at', context.timestamp); - log(LogSeverity.DEBUG, 'The unique ID for the event is', context.eventId); - log(LogSeverity.DEBUG, message); - const webhookPayload: WebhookPayload = JSON.parse( - JSON.stringify(message.json) - ); - const eventUuid = webhookPayload?.payload?.event?.uuid; - const eventType = webhookPayload?.payload?.event_type?.uuid; - - try { - const calendarIds = await queryPurrfectPageByCalendarId(eventUuid); - if (calendarIds?.results && calendarIds?.results?.length > 0) { - log(LogSeverity.WARNING, `Skipping, already found ${eventUuid}`); - return; - } - - const calendlyEvent = await getEvent(eventUuid); - log(LogSeverity.DEBUG, 'calendlyEvent', calendlyEvent); - const calendlyInvitees = await listEventInvitees(eventUuid); - log( - LogSeverity.DEBUG, - 'calendlyInvitees', - JSON.stringify(calendlyInvitees) - ); - log(LogSeverity.DEBUG, 'calendlyInvitees', calendlyInvitees); - - const email = calendlyInvitees?.collection?.[0]?.email || ''; - const guest = calendlyInvitees?.collection?.[0]?.name || ''; - let twitterHandle = - calendlyInvitees?.collection?.[0]?.questions_and_answers?.find( - (q) => q.question === 'Twitter Handle' - )?.answer || ''; - - if (twitterHandle) { - twitterHandle = twitterHandle.replace('@', ''); - const included = twitterHandle.includes('twitter.com'); - if (!included) { - twitterHandle = `https://twitter.com/${twitterHandle}`; - } - } - // Check if Notion Guest exists, if not create - log(LogSeverity.DEBUG, 'Searching for guest by email: ', email); - const guestRes = await queryPurrfectGuest(email); - let guestId = ''; - if (guestRes?.results.length > 0) { - guestId = guestRes.results?.[0].id; - log(LogSeverity.DEBUG, 'Guest Found: ', guestId); - } else { - const newGuest = { - name: guest, - email, - twitterHandle, - }; - log(LogSeverity.DEBUG, 'Creating guest: ', JSON.stringify(newGuest)); - const guestCreateRes = await createPurrfectGuest(newGuest); - guestId = guestCreateRes.id; - log(LogSeverity.DEBUG, 'Guest Created:', guestId); - } - - // Create new Podcast Page - const newPodcast = { - guestIds: [guestId], - recordingDate: utcOffset(calendlyEvent.resource.start_time), - calendarid: webhookPayload?.payload?.event?.uuid, - Stream: { - select: { - id: - eventType === ccd - ? 'd73f1782-28b1-4b49-9ff8-517e27dabd7a' - : '15d98593-82e7-48a7-85a0-a753918d92d5', - name: eventType === ccd ? 'CodingCat.dev' : 'Purrfect.dev', - color: eventType === ccd ? 'purple' : 'pink', - }, - }, - }; - log( - LogSeverity.DEBUG, - 'Creating Podcast with values: ', - JSON.stringify(newPodcast) - ); - const podcastCreateRes = await createPurrfectPage(newPodcast); - log( - LogSeverity.DEBUG, - 'Created Podcast', - JSON.stringify(podcastCreateRes) - ); - } catch (e) { - return log(LogSeverity.ERROR, 'Error', e); - } - }); diff --git a/apps/firebase/functions/src/calendly/webhook.ts b/apps/firebase/functions/src/calendly/webhook.ts deleted file mode 100644 index f25989a2d..000000000 --- a/apps/firebase/functions/src/calendly/webhook.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { good, log, LogSeverity } from './../utilities/logging'; -import * as functions from 'firebase-functions'; -import { WebhookPayload } from '../models/calendly'; -import { sendTopic } from '../utilities/googleapis'; -import { queryPurrfectPageByCalendarId } from '../utilities/notion.server'; -import { ccd, purrfect } from '../utilities/calendly'; - -export const topicId = 'calendlyCreateNotionCard'; - -export const calendlyWebook = functions.https.onRequest(async (req, res) => { - log(LogSeverity.DEBUG, 'Headers', req.headers); - log(LogSeverity.DEBUG, req.body); - const webhookPayload: WebhookPayload = req.body; - - // TODO: Implement https://calendly.stoplight.io/docs/api-docs/ZG9jOjIxNzQ1ODY-webhook-signatures - if (!webhookPayload) { - good(res, 'No payload'); - return; - } - - // Get Calendly Event Details - const eventUuid = webhookPayload?.payload?.event?.uuid; - if (!eventUuid) { - good(res, `Missing event uuid`); - return; - } - if (webhookPayload.event !== 'invitee.created') { - good(res, 'Not Create Event'); - return; - } - const eventType = webhookPayload?.payload?.event_type?.uuid; - if (eventType !== ccd && eventType !== purrfect) { - good(res, 'Not a Code with Coding Cat or Purrfect.dev Event'); - return; - } - - const calendarIds = await queryPurrfectPageByCalendarId(eventUuid); - if (calendarIds?.results && calendarIds?.results?.length > 0) { - good(res, `Skipping, already found ${eventUuid}`); - return; - } - await sendTopic(topicId, webhookPayload); - good(res, `Sent Topic: ${topicId}`); -}); diff --git a/apps/firebase/functions/src/cloudinary/cloudinaryCookieToken.ts b/apps/firebase/functions/src/cloudinary/cloudinaryCookieToken.ts deleted file mode 100644 index cd91dcadb..000000000 --- a/apps/firebase/functions/src/cloudinary/cloudinaryCookieToken.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as functions from 'firebase-functions'; -// import * as admin from 'firebase-admin'; - -import { v2 as cloudinary } from 'cloudinary'; - -import { - cloudinaryName, - cloudinaryApiKey, - cloudinaryApiSecret, - cloudinaryTokenKey, - cloudinaryVideo, -} from './../config/config'; - -const config = { - cloud_name: cloudinaryName, - api_key: cloudinaryApiKey, - api_secret: cloudinaryApiSecret, -}; -cloudinary.config(config); - -// const validRoles = ['admin', 'editor', 'author']; - -export const cloudinaryCookieToken = functions.https.onCall( - async (data, context) => { - // Verify User is allowed to get a cookie. - // This should verify the token from stripe on the ACL - // if (context.auth && context.auth.uid) { - // const userRef = admin - // .firestore() - // .collection('users') - // .doc(context.auth.uid) - // .get(); - // const user: any = (await userRef).data(); - // const roles: string[] = user.roles; - // console.log('Checking User for roles', JSON.stringify(user)); - // if (!roles.some((a) => validRoles.includes(a))) { - // throw new functions.https.HttpsError( - // 'permission-denied', - // `User missing ${validRoles}` - // ); - // } - // } - // if (!cloudinaryApiSecret || !cloudinaryApiKey || !cloudinaryName) { - // throw new functions.https.HttpsError( - // 'invalid-argument', - // 'The function must be called with data.upload_preset' - // ); - // } - - const cookieToken = await cloudinary.utils.generate_auth_token({ - key: cloudinaryTokenKey, - duration: 600, - acl: `*/${cloudinaryVideo}/*`, - }); - return cookieToken; - } -); diff --git a/apps/firebase/functions/src/cloudinary/cloudinarysignature.ts b/apps/firebase/functions/src/cloudinary/cloudinarysignature.ts deleted file mode 100644 index e40932fa8..000000000 --- a/apps/firebase/functions/src/cloudinary/cloudinarysignature.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as functions from 'firebase-functions'; -import * as admin from 'firebase-admin'; - -import { v2 as cloudinary } from 'cloudinary'; - -import { - cloudinaryName, - cloudinaryApiKey, - cloudinaryApiSecret, -} from './../config/config'; - -cloudinary.config({ - cloud_name: cloudinaryName, - api_key: cloudinaryApiKey, - api_secret: cloudinaryApiSecret, -}); - -const validRoles = ['admin', 'editor', 'author']; - -export const cloudinarysignature = functions.https.onCall( - async (data, context) => { - //Verify User is allowed to upload - if (context.auth && context.auth.uid) { - const userRef = admin - .firestore() - .collection('users') - .doc(context.auth.uid) - .get(); - const user: any = (await userRef).data(); - const roles: string[] = user.roles; - console.log('Checking User for roles', JSON.stringify(user)); - if (!roles.some((a) => validRoles.includes(a))) { - throw new functions.https.HttpsError( - 'permission-denied', - `User missing ${validRoles}` - ); - } - } - if (!cloudinaryApiSecret || !cloudinaryApiKey || !cloudinaryName) { - throw new functions.https.HttpsError( - 'invalid-argument', - 'The function must be called with data.upload_preset' - ); - } - - const signature = await cloudinary.utils.api_sign_request( - data, - cloudinaryApiSecret - ); - return signature; - } -); diff --git a/apps/firebase/functions/src/cloudinary/scheduledNotionCheck.ts b/apps/firebase/functions/src/cloudinary/scheduledNotionCheck.ts deleted file mode 100644 index 9ea9322f2..000000000 --- a/apps/firebase/functions/src/cloudinary/scheduledNotionCheck.ts +++ /dev/null @@ -1,303 +0,0 @@ -import * as functions from 'firebase-functions'; -import { - generateCodeWithCodingCatCoverURL, - generatePurrfectDevCoverURL, - uploadCloudinaryFromUrl, -} from '../utilities/cloudinaryUtils'; -import { projectId } from '../config/config'; - -import { sendTopic } from '../utilities/googleapis'; -import { - getNotionPageBlocks, - getPage, - patchPurrfectPage, - queryNotionDbForCloudinaryConvert, - queryPurrfectPageScheduled, - updateBlock, -} from '../utilities/notion.server'; -import { getUserByUsername } from '../utilities/twitter'; -const cloudinaryFolder = - projectId === 'codingcat-dev' - ? 'main-codingcatdev-photo' - : `dev-codingcatdev-photo`; - -import slugify from 'slugify'; - -const topicId = 'cloudinaryCreateFromNotion'; -const topicNotionPicsToCloudinary = 'notionPicsToCloudinary'; -const topicNotionImageBlockConvert = 'topicNotionImageBlockConvert'; - -export const scheduledNotionToCloudinary = functions.pubsub - .schedule('every 5 minutes') - .onRun(async () => { - // Check to see if ther are scheduled pods - console.log('Checking for scheduled pods'); - const scheduledRes = await queryPurrfectPageScheduled('Scheduled'); - console.log('Scheduled Result:', JSON.stringify(scheduledRes)); - - if (scheduledRes?.results) { - const needCloudinaryPods = scheduledRes?.results.filter( - (p: any) => p.cover === null - ); - console.log('Pods to add to pub/sub', JSON.stringify(needCloudinaryPods)); - - for (const pod of needCloudinaryPods) { - await sendTopic(topicId, pod); - } - } - return true; - }); - -export const cloudinaryToNotionPubSub = functions.pubsub - .topic(topicId) - .onPublish(async (message, context) => { - console.log('The function was triggered at ', context.timestamp); - console.log('The unique ID for the event is', context.eventId); - const page = JSON.parse(JSON.stringify(message.json)); - - if (page?.properties?.Name?.title?.[0]?.plain_text === 'NEW PODCAST') { - console.log('Page Title has not been updated, skipping.'); - return false; - } - - // For each guest update the twitter profile. - for (const guest of page?.properties?.Guest?.relation as { id: string }[]) { - console.log('Getting Guest Details', guest.id); - const guestRes: any = await getPage(guest.id); - console.log('Guest Result: ', JSON.stringify(guestRes)); - const twitter = guestRes.properties.Twitter as { url: string }; - console.log('Guest twitter: ', twitter); - if (twitter) { - let twitterUsername = twitter.url.replace('https://twitter.com/', ''); - twitterUsername = twitterUsername.replace('@', ''); - console.log('fetching twitter user', twitterUsername); - const twitterGuest = await getUserByUsername(twitterUsername); - console.log('twitter user: ', JSON.stringify(twitterGuest)); - if (!twitterGuest?.data?.profile_image_url) { - console.log('Twitter user profile image not found, skipping.'); - continue; - } - const res = await uploadCloudinaryFromUrl( - `${cloudinaryFolder}/podcast-guest/${twitterUsername}`, - twitterGuest.data.profile_image_url.replace('_normal', '') - ); - - const slug = slugify(page.properties.Name.title[0].plain_text, '-'); - const param = { - title: page.properties.Name.title[0].plain_text, - slug: `${cloudinaryFolder}/${slug}`, - guestName: guestRes.properties.Name.title[0].plain_text, - guestImagePublicId: res.public_id, - folder: `${cloudinaryFolder}`, - }; - console.log('generating cloudinary url with: ', JSON.stringify(param)); - - const coverUrl = ( - page?.properties?.Stream?.select?.id === - 'd73f1782-28b1-4b49-9ff8-517e27dabd7a' - ? await generateCodeWithCodingCatCoverURL(param) - : await generatePurrfectDevCoverURL(param) - ).replace('http://', 'https://'); - console.log('coverURL', coverUrl); - const update = { - page_id: page.id, - cover: { - external: { - url: coverUrl, - }, - }, - properties: { - slug: { - id: 'wDeB', - type: 'url', - url: slug, - }, - cover: { - id: coverUrl, - type: 'url', - url: coverUrl, - }, - }, - }; - console.log('Updating page with: ', JSON.stringify(update)); - const purrfectPagePatchRes = await patchPurrfectPage(update); - console.log( - 'Page update result:', - JSON.stringify(purrfectPagePatchRes) - ); - } - } - return true; - }); - -export const scheduledNotionCloudinaryConvert = functions - .runWith({ - timeoutSeconds: 540, - }) - .pubsub.schedule('every 5 minutes') - .onRun(async () => { - // Check to see if ther are database items needing added to cloudinary - console.log('Checking for cloudinary convert'); - // const [post, tutorial, course, podcast, lesson, framework, language, author] = await Promise.all([ - const posts = await Promise.all([ - queryNotionDbForCloudinaryConvert('post'), - queryNotionDbForCloudinaryConvert('tutorial'), - queryNotionDbForCloudinaryConvert('course'), - queryNotionDbForCloudinaryConvert('podcast'), - queryNotionDbForCloudinaryConvert('lesson'), - queryNotionDbForCloudinaryConvert('framework'), - queryNotionDbForCloudinaryConvert('language'), - queryNotionDbForCloudinaryConvert('author'), - ]); - - //Loop through all types - posts.map(async (p) => { - //Loop through all items found in type - p?.results?.map(async (r) => { - console.log(`Sending topic ${topicNotionPicsToCloudinary}:`, r?.id); - // Need to slowly do this as to not overwhelm the API. - await sendTopic(topicNotionPicsToCloudinary, r); - }); - }); - return true; - }); - -export const notionPageFindFileBlocksPublish = functions - .runWith({ - timeoutSeconds: 540, - }) - .pubsub.topic(topicNotionPicsToCloudinary) - .onPublish(async (message, context) => { - console.log('The function was triggered at ', context.timestamp); - console.log('The unique ID for the event is', context.eventId); - const page = JSON.parse(JSON.stringify(message.json)); - - // Get blocks - const blocks = await getNotionPageBlocks(page.id); - const convertBlocks = blocks.filter((b) => b?.image?.file?.url); - - // If no blocks are found mark completed - if (!convertBlocks || convertBlocks.length === 0) { - //If not cloudinary image upload it. - const newExternal = page?.cover?.external?.url?.includes( - 'media.codingcat.dev' - ); - const newFile = page?.cover?.file?.url; - if (!newExternal || newFile) { - const title = - page?.properties?.Name?.title?.[0]?.plain_text || - page?.properties?.title?.title?.[0]?.plain_text; - - try { - console.log('Uploading', newExternal ? newExternal : newFile); - const res = await uploadCloudinaryFromUrl( - `${cloudinaryFolder}/${title}`, - newExternal ? newExternal : newFile - ); - - await patchPurrfectPage({ - page_id: page.id, - cover: { - external: { - url: res.secure_url, - }, - }, - properties: { - cover: { - url: res.secure_url, - }, - }, - }); - } catch (e) { - console.error(e); - } - } - - await patchPurrfectPage({ - page_id: page.id, - properties: { - cloudinary_convert: { - type: 'checkbox', - checkbox: false, - }, - }, - }); - return; - } - - convertBlocks.map(async (b) => { - await sendTopic(topicNotionImageBlockConvert, b); - }); - return true; - }); - -export const cloudinaryConvertBlockPubSub = functions - .runWith({ - timeoutSeconds: 540, - }) - .pubsub.topic(topicNotionImageBlockConvert) - .onPublish(async (message, context) => { - console.log('The function was triggered at ', context.timestamp); - console.log('The unique ID for the event is', context.eventId); - interface CreatedBy { - object: string; - id: string; - } - interface LastEditedBy { - object: string; - id: string; - } - interface File { - url: string; - expiry_time: Date; - } - interface External { - url: string; - } - interface Image { - caption: any[]; - type: string; - file?: File; - external?: External; - } - interface Block { - object: string; - id: string; - created_time: Date; - last_edited_time: Date; - created_by: CreatedBy; - last_edited_by: LastEditedBy; - has_children: boolean; - archived: boolean; - type: string; - image: Image; - } - - const block = JSON.parse(JSON.stringify(message.json)) as Block; - const fileUrl = block?.image?.file?.url; - - if (!fileUrl) { - console.error('missing fileUrl'); - return; - } - - const res = await uploadCloudinaryFromUrl( - `${cloudinaryFolder}/${block.id}`, - fileUrl - ); - - if (!res?.secure_url) { - console.error('Cloudinary missing secure_url'); - return; - } - - const update = await updateBlock(block.id, { - image: { - external: { - url: res.secure_url, - }, - }, - }); - console.log('Successfully updated', JSON.stringify(update)); - return true; - }); diff --git a/apps/firebase/functions/src/config/config.ts b/apps/firebase/functions/src/config/config.ts index 125fbce70..37bf0e4b7 100644 --- a/apps/firebase/functions/src/config/config.ts +++ b/apps/firebase/functions/src/config/config.ts @@ -16,23 +16,6 @@ const checkMain = (objectName: string, paramName: string) => { : ''; }; -// Algolia -export const algoliaAppId = checkMain('algolia', 'app_id'); -export const algoliaApiKey = checkMain('algolia', 'api_key'); -export const algoliaSearchKey = checkMain('algolia', 'search_key'); -export const algoliaIndex = checkMain('algolia', 'index'); - -// Cloudinary -export const cloudinaryName = checkMain('cloudinary', 'name'); -export const cloudinaryApiKey = checkMain('cloudinary', 'api_key'); -export const cloudinaryApiSecret = checkMain('cloudinary', 'api_secret'); -export const cloudinaryTokenKey = checkMain('cloudinary', 'token_key'); -export const cloudinaryVideo = checkMain('cloudinary', 'video'); - -// Calendly -export const calendlyAccessToken = checkMain('calendly', 'pat'); -export const calendlySigningKey = checkMain('calendly', 'signing_key'); - // Google export const calendarChannelId = checkMain('calendar', 'channel_id'); export const calendarRefreshToken = checkMain('calendar', 'refresh_token'); @@ -40,34 +23,6 @@ export const clientId = checkMain('google', 'client_id'); export const clientSecret = checkMain('google', 'client_secret'); export const redirectUri = checkMain('google', 'redirect_uri'); -// Twitter -export const twitterBearerToken = checkMain('twitter', 'bearer_token'); - -// Notion -export const notionToken = checkMain('notion', 'token'); -export const notionPurrfectDatabaseId = checkMain( - 'notion', - 'purrfect_database_id' -); -export const notionPurrfectGuestDatabaseId = checkMain( - 'notion', - 'purrfect_guest_database_id' -); -export const notionPurrfectCompanyDatabaseId = checkMain( - 'notion', - 'purrfect_company_database_id' -); -export const notionPurrfectPicks = checkMain('notion', 'purrfect_picks'); -export const notionAuthors = checkMain('notion', 'authors'); -export const notionLessons = checkMain('notion', 'lessons'); -export const notionCourses = checkMain('notion', 'courses'); -export const notionFrameworks = checkMain('notion', 'frameworks'); -export const notionPosts = checkMain('notion', 'posts'); -export const notionTutorials = checkMain('notion', 'tutorials'); -export const notionPages = checkMain('notion', 'pages'); -export const notionSections = checkMain('notion', 'sections'); -export const notionLanguages = checkMain('notion', 'languages'); - // Dev.to export const devto = checkMain('devto', 'key'); diff --git a/apps/firebase/functions/src/devto/scheduledNotionToDevto.ts b/apps/firebase/functions/src/devto/scheduledNotionToDevto.ts index 87eea7fc8..4844679b1 100644 --- a/apps/firebase/functions/src/devto/scheduledNotionToDevto.ts +++ b/apps/firebase/functions/src/devto/scheduledNotionToDevto.ts @@ -1,157 +1,151 @@ -import * as functions from 'firebase-functions'; -import { sendTopic } from '../utilities/googleapis'; -import { - queryPurrfectStreamDevTo, - patchPurrfectPage, - queryByDevto, - getNotionPageMarkdown, -} from '../utilities/notion.server'; -import { addArticle } from '../utilities/devto'; - -const topicId = 'devtoCreateFromNotion'; - -const scheduleCheck = async () => { - // Check to see if ther are scheduled pods - console.log('Checking for scheduled pods'); - const scheduledRes = await queryPurrfectStreamDevTo(1); - console.log('Scheduled Result:', JSON.stringify(scheduledRes)); - - if (scheduledRes?.results) { - const needCloudinaryPods = scheduledRes?.results; - console.log('Pods to add to pub/sub', JSON.stringify(needCloudinaryPods)); - - for (const pod of needCloudinaryPods) { - await sendTopic(topicId, pod); - } - } - - for (const _type of ['post', 'tutorial']) { - console.log('Checking for devto missing'); - const posts = await queryByDevto(_type, 1); - console.log('Posts:', JSON.stringify(posts)); - - if (posts?.results) { - const needposts = posts?.results; - console.log('Posts to add to pub/sub', JSON.stringify(needposts)); - - for (const p of needposts) { - await sendTopic(topicId, p); - } - } - } -}; - -export const scheduledNotionToDevto = functions.pubsub - .schedule('every 5 minutes') - .onRun(async () => { - await scheduleCheck(); - return true; - }); - -export const devtoToNotionPubSub = functions.pubsub - .topic(topicId) - .onPublish(async (message, context) => { - console.log('The function was triggered at ', context.timestamp); - console.log('The unique ID for the event is', context.eventId); - const page = JSON.parse(JSON.stringify(message.json)); - console.log('page', page); - - let data; - if (page._type === 'podcast') { - data = { - article: { - title: page.title, - published: true, - tags: ['podcast', 'webdev', 'javascript', 'beginners'], - series: `codingcatdev_podcast_${page.properties.Season.number}`, - main_image: `https://media.codingcat.dev/image/upload/b_rgb:5e1186,c_pad,w_1000,h_420/${page?.coverPhoto?.public_id}`, - canonical_url: `https://codingcat.dev/${page._type}/${page.slug}`, - description: page.excerpt, - organization_id: '1009', - body_markdown: `Original: https://codingcat.dev/${page._type}/${ - page.slug - } - {% youtube ${page.properties.youtube.url} %} - {% spotify spotify:episode:${page.properties.spotify.url - .split('/') - .at(-1) - .split('?') - .at(0)} %} - - `, - }, - }; - } else { - console.log( - `Getting ${page._type}: ${page.id} markdown, with slug ${page?.properties?.slug?.url}` - ); - const post = await getNotionPageMarkdown({ - _type: page._type, - slug: page?.properties?.slug?.url, - preview: false, - }); - - console.log('Block Result', post); - - if (post && post?.content) { - data = { - article: { - title: page.title, - published: true, - tags: ['podcast', 'webdev', 'javascript', 'beginners'], - main_image: `https://media.codingcat.dev/image/upload/b_rgb:5e1186,c_pad,w_1000,h_420/${page?.coverPhoto?.public_id}`, - canonical_url: `https://codingcat.dev/${page._type}/${page.slug}`, - description: page.excerpt, - organization_id: '1009', - body_markdown: post.content, - }, - }; - } - } - - if (data) { - try { - console.log('addArticle to devto'); - const response = await addArticle(data); - console.log('addArticle result:', response); - - const devto = response?.data?.url; - - if (!devto) { - console.log('devto url missing'); - return; - } - - const update = { - page_id: page.id, - properties: { - devto: { - id: 'remote', - type: 'url', - url: devto, - }, - }, - }; - console.log('Updating page with: ', JSON.stringify(update)); - const purrfectPagePatchRes = await patchPurrfectPage(update); - console.log( - 'Page update result:', - JSON.stringify(purrfectPagePatchRes) - ); - - return purrfectPagePatchRes; - } catch (error) { - console.error(error); - } - } else { - console.error('No Data matched for article'); - } - return; - }); - -// Used for testing don't forget to remove for production -export const httpNotionToDevto = functions.https.onRequest(async (req, res) => { - await scheduleCheck(); - - res.send({ msg: 'started' }); -}); +// import * as functions from 'firebase-functions'; +// import { sendTopic } from '../utilities/googleapis'; +// import { addArticle } from '../utilities/devto'; + +// const topicId = 'devtoCreateFromNotion'; + +// const scheduleCheck = async () => { +// // Check to see if ther are scheduled pods +// console.log('Checking for scheduled pods'); +// const scheduledRes = await queryPurrfectStreamDevTo(1); +// console.log('Scheduled Result:', JSON.stringify(scheduledRes)); + +// if (scheduledRes?.results) { +// const needCloudinaryPods = scheduledRes?.results; +// console.log('Pods to add to pub/sub', JSON.stringify(needCloudinaryPods)); + +// for (const pod of needCloudinaryPods) { +// await sendTopic(topicId, pod); +// } +// } + +// for (const _type of ['post', 'tutorial']) { +// console.log('Checking for devto missing'); +// const posts = await queryByDevto(_type, 1); +// console.log('Posts:', JSON.stringify(posts)); + +// if (posts?.results) { +// const needposts = posts?.results; +// console.log('Posts to add to pub/sub', JSON.stringify(needposts)); + +// for (const p of needposts) { +// await sendTopic(topicId, p); +// } +// } +// } +// }; + +// export const scheduledNotionToDevto = functions.pubsub +// .schedule('every 5 minutes') +// .onRun(async () => { +// await scheduleCheck(); +// return true; +// }); + +// export const devtoToNotionPubSub = functions.pubsub +// .topic(topicId) +// .onPublish(async (message, context) => { +// console.log('The function was triggered at ', context.timestamp); +// console.log('The unique ID for the event is', context.eventId); +// const page = JSON.parse(JSON.stringify(message.json)); +// console.log('page', page); + +// let data; +// if (page._type === 'podcast') { +// data = { +// article: { +// title: page.title, +// published: true, +// tags: ['podcast', 'webdev', 'javascript', 'beginners'], +// series: `codingcatdev_podcast_${page.properties.Season.number}`, +// main_image: `https://media.codingcat.dev/image/upload/b_rgb:5e1186,c_pad,w_1000,h_420/${page?.coverPhoto?.public_id}`, +// canonical_url: `https://codingcat.dev/${page._type}/${page.slug}`, +// description: page.excerpt, +// organization_id: '1009', +// body_markdown: `Original: https://codingcat.dev/${page._type}/${ +// page.slug +// } +// {% youtube ${page.properties.youtube.url} %} +// {% spotify spotify:episode:${page.properties.spotify.url +// .split('/') +// .at(-1) +// .split('?') +// .at(0)} %} + +// `, +// }, +// }; +// } else { +// console.log( +// `Getting ${page._type}: ${page.id} markdown, with slug ${page?.properties?.slug?.url}` +// ); +// const post = await getNotionPageMarkdown({ +// _type: page._type, +// slug: page?.properties?.slug?.url, +// preview: false, +// }); + +// console.log('Block Result', post); + +// if (post && post?.content) { +// data = { +// article: { +// title: page.title, +// published: true, +// tags: ['podcast', 'webdev', 'javascript', 'beginners'], +// main_image: `https://media.codingcat.dev/image/upload/b_rgb:5e1186,c_pad,w_1000,h_420/${page?.coverPhoto?.public_id}`, +// canonical_url: `https://codingcat.dev/${page._type}/${page.slug}`, +// description: page.excerpt, +// organization_id: '1009', +// body_markdown: post.content, +// }, +// }; +// } +// } + +// if (data) { +// try { +// console.log('addArticle to devto'); +// const response = await addArticle(data); +// console.log('addArticle result:', response); + +// const devto = response?.data?.url; + +// if (!devto) { +// console.log('devto url missing'); +// return; +// } + +// const update = { +// page_id: page.id, +// properties: { +// devto: { +// id: 'remote', +// type: 'url', +// url: devto, +// }, +// }, +// }; +// console.log('Updating page with: ', JSON.stringify(update)); +// const purrfectPagePatchRes = await patchPurrfectPage(update); +// console.log( +// 'Page update result:', +// JSON.stringify(purrfectPagePatchRes) +// ); + +// return purrfectPagePatchRes; +// } catch (error) { +// console.error(error); +// } +// } else { +// console.error('No Data matched for article'); +// } +// return; +// }); + +// // Used for testing don't forget to remove for production +// export const httpNotionToDevto = functions.https.onRequest(async (req, res) => { +// await scheduleCheck(); + +// res.send({ msg: 'started' }); +// }); diff --git a/apps/firebase/functions/src/firebase/notion.ts b/apps/firebase/functions/src/firebase/notion.ts deleted file mode 100644 index 65287e19c..000000000 --- a/apps/firebase/functions/src/firebase/notion.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { firestore } from './../config/config'; -import * as functions from 'firebase-functions'; - -import { queryAll, querySectionsByCourseId } from '../utilities/notion.server'; - -export const scheduledNotionToFirestore = functions - .runWith({ - timeoutSeconds: 540, - }) - .pubsub.schedule('every 5 minutes') - .onRun(async () => { - const courses = await queryAll('course', 10000); - - await courses.results.map(async (q: any) => { - const courseRef = firestore.collection('courses').doc(q.id); - await courseRef.set(q, { merge: true }); - const sectionsRaw = await querySectionsByCourseId(q.id, false); - const sections: any = []; - for (const s of sectionsRaw.results as any) { - const lesson = { - title: `${s?.properties?.lesson_title?.rollup?.array - ?.at(0) - ?.title.map((t: any) => t.plain_text) - .join('')}`, - _id: s?.properties?.lesson_id?.rollup?.array?.at(0)?.formula?.string, - id: s?.properties?.lesson_id?.rollup?.array?.at(0)?.formula?.string, - slug: s?.properties?.lesson_slug?.rollup?.array?.at(0)?.url - ? s?.properties?.lesson_slug.rollup?.array?.at(0)?.url - : null, - }; - const exists = sections.find( - (e: any) => - e.title === - `${s?.properties?.title?.title - .map((t: any) => t.plain_text) - .join('')}` - ); - - if (exists) { - exists.lessons.push(lesson); - } else { - sections.push({ - ...s, - title: `${s?.properties?.title?.title - .map((t: any) => t.plain_text) - .join('')}`, - _key: s.id, - lessons: [lesson], - }); - } - } - - for (const section of sections) { - const sectionRef = courseRef.collection('sections').doc(section.id); - await sectionRef.set(section, { merge: true }); - - for (const lesson of section.lessons) { - await sectionRef - .collection('lessons') - .doc(lesson.id) - .set(lesson, { merge: true }); - } - } - }); - - return true; - }); diff --git a/apps/firebase/functions/src/github/index.ts b/apps/firebase/functions/src/github/index.ts deleted file mode 100644 index af64f8cae..000000000 --- a/apps/firebase/functions/src/github/index.ts +++ /dev/null @@ -1,522 +0,0 @@ -import * as functions from 'firebase-functions'; -import * as admin from 'firebase-admin'; -import { Octokit } from 'octokit'; -import * as crypto from 'crypto'; -import { sendTopic } from '../utilities/pubsub'; -import slugify from 'slugify'; -// @ts-ignore -import * as matter from 'gray-matter'; - -const updatetopic = 'GitHubUpdateFirestore'; -const webhookupdatetopic = 'GitHubUpdateFirestoreFromWebhook'; -const webhookdeletetopic = 'GitHubRemoveFirestoreFromWebhook'; -const OWNER = 'codingcatdev'; -const REPO = 'v2-codingcat.dev'; - -// Initialize Firebase admin -admin.initializeApp(); - -/** - * Uncomment to test locally - */ -// export const test = functions -// .runWith({ secrets: ["GH_TOKEN"] }) -// .https.onRequest(async (request, response) => { -// if (!process.env.GH_TOKEN) { -// throw new functions.https.HttpsError( -// "failed-precondition", -// "Missing GitHub Personal Token" -// ); -// } -// try { -// await updateContentFromGitHub(); -// } catch (error) { -// throw new functions.https.HttpsError("unknown"); -// } -// response.status(200).send(); -// }); - -/** - * GitHub Webhook for push events - */ -export const webhook = functions - .runWith({ secrets: ['GH_WEBHOOK_SECRET'] }) - .https.onRequest(async (request, response) => { - if (!process.env.GH_WEBHOOK_SECRET) { - response.status(401).send('Missing GitHub Webhook Secret'); - return; - } - const method = request.method; - - if (method !== 'POST') { - throw new functions.https.HttpsError('unimplemented', 'Wrong method'); - } - - const payload = request.body; - const xHubSignature256 = request.get('X-Hub-Signature-256'); - - if (!xHubSignature256) { - response.status(401).send('Missing xHubSignature256'); - return; - } - - const sig = Buffer.from(xHubSignature256); - const hmac = crypto.createHmac('sha256', process.env.GH_WEBHOOK_SECRET); - - const digest = Buffer.from( - 'sha256=' + hmac.update(request.rawBody).digest('hex'), - 'utf8' - ); - if (sig.length !== digest.length || !crypto.timingSafeEqual(digest, sig)) { - throw new functions.https.HttpsError( - 'permission-denied', - 'Signature of digest did not match' - ); - } - - functions.logger.debug('gh:payload', payload); - - const owner = payload.repository.owner.name; - const repo = payload.repository.name; - - //TODO: maybe have more included in secrets array? - if (owner !== 'CodingCatDev' || repo !== 'v2-codingcat.dev') { - throw new functions.https.HttpsError( - 'permission-denied', - 'Incorrect Owner/Repo' - ); - } - - /** - * Loop through changed files and collect from all commits - */ - let changed; - let removed; - for (const commit of payload.commits) { - changed = [...commit.added, ...commit.modified]; - removed = [...commit.removed]; - } - //Remove dups - changed = [...new Set(changed)]; - removed = [...new Set(removed)]; - - /** - * Send Topic to lookup each files data and update Firestore - */ - for (const path of changed) { - const splitPath = path.split('/'); - const content = splitPath.at(0); - // Skip if this file is not in the content directory - if (content !== 'content') { - continue; - } - - await sendTopic(webhookupdatetopic, { path, owner, repo }); - } - /** - * Send Topic to lookup each files data and remove from Firestore - */ - for (const path of removed) { - const splitPath = path.split('/'); - const content = splitPath.at(0); - // Skip if this file is not in the content directory - if (content !== 'content') { - continue; - } - - await sendTopic(webhookdeletetopic, { path }); - } - response.status(200).send(); - }); - -/* - * Adds file data to firestore from GitHub content - */ -export const addItemToFirestoreFromWebhook = functions - .runWith({ secrets: ['GH_TOKEN'] }) - .pubsub.topic(webhookupdatetopic) - .onPublish(async (message) => { - if (!process.env.GH_TOKEN) { - functions.logger.error('Missing GitHub Personal Token'); - return; - } - /** @type {{type: string, content: Object}} payload */ - const payload = JSON.parse(JSON.stringify(message.json)); - functions.logger.debug('payload', payload); - - const { path, owner, repo } = payload; - - const splitPath = path.split('/'); - const type = splitPath.at(1); - - if (!path || !type || !owner || !repo) { - functions.logger.debug(`Missing Data in Payload`); - return; - } - - /** - * Send Topic for single path object - */ - const octokit = createOctokit(); - const { data, headers } = await octokit.rest.repos.getContent({ - owner, - repo, - path, - }); - functions.logger.debug(headers['x-ratelimit-remaining']); - await sendTopic(updatetopic, { type, content: data }); - }); - -/* - * Adds file data to firestore from GitHub content - */ -export const removeItemFromFirestoreFromWebhook = functions - .runWith({ secrets: ['GH_TOKEN'] }) - .pubsub.topic(webhookdeletetopic) - .onPublish(async (message) => { - if (!process.env.GH_TOKEN) { - functions.logger.error('Missing GitHub Personal Token'); - return; - } - /** @type {{type: string, content: Object}} payload */ - const payload = JSON.parse(JSON.stringify(message.json)); - functions.logger.debug('payload', payload); - - const { path } = payload; - const splitPath = path.split('/'); - const type = splitPath.at(1); - const name = splitPath.at(2); - const lesson = splitPath.at(3); - const lessonName = splitPath.at(4); - - if (lesson && lessonName) { - const ref = admin - .firestore() - .collection(type) - .doc(slugify(name)) - .collection(lesson) - .doc(slugify(lessonName)); - await ref.delete(); - functions.logger.debug(`removed: ${lessonName}`); - } else { - const ref = admin.firestore().collection(type).doc(slugify(name)); - await ref.delete(); - functions.logger.debug(`removed: ${name}`); - } - }); - -/** - * Allows for authenticated user to run update - * TODO: only allow admins - */ -export const addContent = functions - .runWith({ secrets: ['GH_TOKEN'] }) - .https.onCall(async (data, context) => { - if (!process.env.GH_TOKEN) { - throw new functions.https.HttpsError( - 'failed-precondition', - 'Missing GitHub Personal Token' - ); - } - // Checking that the user is authenticated. - if (!context.auth) { - // Throwing an HttpsError so that the client gets the error details. - throw new functions.https.HttpsError( - 'failed-precondition', - 'The function must be called while authenticated.' - ); - } - try { - await updateContentFromGitHub(); - functions.logger.info(`Successfully added content for pubsub`); - } catch (error) { - functions.logger.error(`Failed to load cron data`); - } - }); - -/** - * Runs nightly update for GitHub content - * This checks all content so we might not want this after initial load. - * TODO: Should this run more/less often? - */ -export const addContentNightly = functions - .runWith({ secrets: ['GH_TOKEN'] }) - .pubsub.schedule('0 0 * * *') - .timeZone('America/New_York') - .onRun(async (context) => { - if (!process.env.GH_TOKEN) { - throw new functions.https.HttpsError( - 'failed-precondition', - 'Missing GitHub Personal Token' - ); - } - try { - await updateContentFromGitHub(); - } catch (error) { - throw new functions.https.HttpsError('unknown', 'Unknown Error'); - } - }); - -/* - * Adds file data to firestore from GitHub content - */ -export const addItemToFirestore = functions - .runWith({ secrets: ['GH_TOKEN'] }) - .pubsub.topic(updatetopic) - .onPublish(async (message) => { - if (!process.env.GH_TOKEN) { - functions.logger.error('Missing GitHub Personal Token'); - return; - } - /** @type {{type: string, content: Object}} payload */ - const payload = JSON.parse(JSON.stringify(message.json)); - functions.logger.debug('payload', payload); - - const { content } = payload; - const { path } = content; - - const splitPath = path.split('/'); - const type = splitPath.at(1); - const name = splitPath.at(2); - const lesson = splitPath.at(3); - const lessonName = splitPath.at(4); - if (!type) { - functions.logger.error(`Missing Type for content update.`); - } - - let ref; - if (lesson && lessonName) { - ref = admin - .firestore() - .collection(type) - .doc(slugify(name)) - .collection(lesson) - .doc(slugify(lessonName)); - await updateDocumentFromGitHub(ref, payload); - } else { - ref = admin.firestore().collection(type).doc(createSlug(type, content)); - await updateDocumentFromGitHub(ref, payload); - } - /** - * If this is a course there should be lesson data as well. - * Only should run when full run, not webhook - * */ - if (type === 'course' && content.lesson) { - functions.logger.debug('lesson', content.lesson); - const lessonSlug = slugify(content.lesson.name); - const lessonRef = ref.collection('lesson').doc(lessonSlug); - await updateDocumentFromGitHub(lessonRef, { - content: content.lesson, - }); - } - }); - -/** - * Set Octokit instance and return */ -const createOctokit = () => { - // Create a personal access token at https://github.com/settings/tokens/new?scopes=repo - return new Octokit({ - auth: process.env.GH_TOKEN, - }); -}; - -const createSlug = ( - type: string, - content: { name: string; path: string } -): string => { - let fileSlug = slugify(content.name); - if (type === 'course') { - // Get the Folder name of course for uniqueness - fileSlug = slugify(content.path.split('/').at(-2) || ''); - } - return fileSlug; -}; - -/** - * Returns GitHub's version of content, including encoded file - * @param {string} path */ -const getGitHubContent = async (path: string) => { - const octokit = createOctokit(); - const { data, headers } = await octokit.rest.repos.getContent({ - owner: OWNER, - repo: REPO, - path, - }); - functions.logger.debug(data); - functions.logger.debug( - 'x-ratelimit-remaining', - headers['x-ratelimit-remaining'] - ); - return data; -}; - -/** - * Returns GitHub's version of content, including encoded file - * @param {string} path */ -const getGitHubCommit = async (path: string) => { - const octokit = createOctokit(); - const { data, headers } = await octokit.rest.repos.listCommits({ - owner: OWNER, - repo: REPO, - path, - }); - functions.logger.debug(data); - functions.logger.debug( - 'x-ratelimit-remaining', - headers['x-ratelimit-remaining'] - ); - return data; -}; - -/** - * Triggers the lookup of content from GitHub - * and updates Firestore based on latest content - */ -const updateContentFromGitHub = async () => { - const octokit = createOctokit(); - - // Find all the content to send to firestore - for (const type of [ - 'framework', - 'language', - 'page', - 'podcast', - 'post', - 'tutorial', - ]) { - const { data, headers }: { data: any; headers: any } = - await octokit.rest.repos.getContent({ - owner: OWNER, - repo: REPO, - path: `content/${type}`, - }); - functions.logger.info(`Found ${data?.length} ${type} to check.`); - functions.logger.debug(headers['x-ratelimit-remaining']); - - // trigger pubsub to scale this update all at once - // TODO: recursive for directories - for (const d of data) { - await sendTopic(updatetopic, { type, content: d }); - } - } - - /** - * Find all Course data - */ - const { - data: courseDirs, - headers: courseDirHeaders, - }: { data: any; headers: any } = await octokit.rest.repos.getContent({ - owner: OWNER, - repo: REPO, - path: `content/course`, - }); - functions.logger.info(`Found ${courseDirs?.length} courses to check.`); - functions.logger.debug(courseDirHeaders['x-ratelimit-remaining']); - - // Loop each Course - for (const course of courseDirs) { - // Get Course detail from index - const { - data: indexFile, - headers: indexFileHeaders, - }: { data: any; headers: any } = await octokit.rest.repos.getContent({ - owner: OWNER, - repo: REPO, - path: `${course.path}/index.md`, - }); - functions.logger.debug(`Found index for course to check.`); - functions.logger.debug(indexFileHeaders['x-ratelimit-remaining']); - - // If there is no course, just bail - if (!indexFile?.name) { - continue; - } - - // Get Course lessons - const { - data: lessonFiles, - headers: lessonFilesHeaders, - }: { data: any; headers: any } = await octokit.rest.repos.getContent({ - owner: OWNER, - repo: REPO, - path: `${course.path}/lesson`, - }); - functions.logger.debug( - `Found ${lessonFiles?.length} lessons for ${indexFile?.name} to check.` - ); - functions.logger.debug(lessonFilesHeaders['x-ratelimit-remaining']); - - for (const lesson of lessonFiles) { - // Send the details for course with each lesson - await sendTopic(updatetopic, { - type: 'course', - content: { - ...indexFile, - lesson, - }, - }); - } - } -}; - -/** - * Gets raw markdown file and updates Firestore - * @param {admin.firestore.DocumentReference} ref - * @param {any} payload - * @returns Promise - */ -const updateDocumentFromGitHub = async ( - ref: admin.firestore.DocumentReference, - payload: any -) => { - // Check if this file already exists - const doc = await ref.get(); - - if (doc.exists) { - functions.logger.info(`Document already exists, checking sha...`); - if (doc.data()?.sha == payload?.content?.sha) { - functions.logger.info(`sha matches no need to continue`); - return; - } - } - - // Get raw file - const gitHubContent: any = await getGitHubContent(payload.content.path); - functions.logger.info( - `${payload.content.path} github file content`, - gitHubContent - ); - - // Get commit - const gitHubCommit = await getGitHubCommit(payload.content.path); - - // Convert encoded file - const bufferObj = Buffer.from(gitHubContent.content, gitHubContent.encoding); - const decodedFile = bufferObj.toString(); - // Decode markdown and get frontmatter - const mdObject = matter(decodedFile); - - /** - * Update Firestore with the github data, frontmatter, and markdown. - * Flatten the Author data for easier searches - */ - await ref.set( - { - github: { - content: gitHubContent, - commit: gitHubCommit, - }, - content: mdObject.content, - ...mdObject.data, - weight: mdObject?.data?.weight ? mdObject?.data?.weight : 0, - published: mdObject?.data?.published - ? mdObject?.data?.published - : 'draft', - start: mdObject?.data?.start - ? admin.firestore.Timestamp.fromDate(new Date(mdObject?.data?.start)) - : admin.firestore.Timestamp.fromDate(new Date('Jan 01, 1900')), - }, - { merge: true } - ); -}; diff --git a/apps/firebase/functions/src/hashnode/scheduledNotionToHashNode.ts b/apps/firebase/functions/src/hashnode/scheduledNotionToHashNode.ts index 27f8b89b3..e5fde022b 100644 --- a/apps/firebase/functions/src/hashnode/scheduledNotionToHashNode.ts +++ b/apps/firebase/functions/src/hashnode/scheduledNotionToHashNode.ts @@ -1,200 +1,194 @@ -import * as functions from 'firebase-functions'; -import { sendTopic } from '../utilities/googleapis'; -import { - queryPurrfectStreamHashnode, - patchPurrfectPage, - queryByHashnode, - getNotionPageMarkdown, -} from '../utilities/notion.server'; -import { createPublicationStory } from '../utilities/hashnode'; - -const topicId = 'hashnodeCreateFromNotion'; - -const scheduleCheck = async () => { - // Check to see if ther are scheduled pods - console.log('Checking for scheduled pods'); - const scheduledRes = await queryPurrfectStreamHashnode(1); - console.log('Scheduled Result:', JSON.stringify(scheduledRes)); - - if (scheduledRes?.results) { - const needCloudinaryPods = scheduledRes?.results; - console.log('Pods to add to pub/sub', JSON.stringify(needCloudinaryPods)); - - for (const pod of needCloudinaryPods) { - await sendTopic(topicId, pod); - } - } - - for (const _type of ['post', 'tutorial']) { - console.log('Checking for hashnode missing'); - const posts = await queryByHashnode(_type, 1); - console.log('Posts:', JSON.stringify(posts)); - - if (posts?.results) { - const needposts = posts?.results; - console.log('Posts to add to pub/sub', JSON.stringify(needposts)); - - for (const p of needposts) { - await sendTopic(topicId, p); - } - } - } -}; - -export const scheduledNotionToHashnode = functions.pubsub - .schedule('every 5 minutes') - .onRun(async () => { - await scheduleCheck(); - return true; - }); - -export const hashnodeToNotionPubSub = functions.pubsub - .topic(topicId) - .onPublish(async (message, context) => { - console.log('The function was triggered at ', context.timestamp); - console.log('The unique ID for the event is', context.eventId); - const page = JSON.parse(JSON.stringify(message.json)); - console.log('page', page); - - let input; - if (page._type === 'podcast') { - input = { - title: page.title, - subtitle: page.excerpt, - slug: `${page._type}-${page.slug}`, - contentMarkdown: `Original: https://codingcat.dev/${page._type}/${page.slug} - -%[${page.properties.youtube.url}] - -%[${page.properties.spotify.url}] - `, - coverImageURL: `https://media.codingcat.dev/image/upload/f_auto,c_limit,w_1920,q_auto/${page?.coverPhoto?.public_id}`, - isRepublished: { - originalArticleURL: `https://codingcat.dev/${page._type}/${page.slug}`, - }, - tags: [ - { - _id: '56744722958ef13879b950d3', - name: 'podcast', - slug: 'podcast', - }, - { - _id: '56744721958ef13879b94cad', - name: 'JavaScript', - slug: 'javascript', - }, - { - _id: '56744722958ef13879b94f1b', - name: 'Web Development', - slug: 'web-development', - }, - { - _id: '56744723958ef13879b955a9', - name: 'Beginner Developers', - slug: 'beginners', - }, - ], - }; - } else { - console.log( - `Getting ${page._type}: ${page.id} markdown, with slug ${page?.properties?.slug?.url}` - ); - const post = await getNotionPageMarkdown({ - _type: page._type, - slug: page?.properties?.slug?.url, - preview: false, - }); - - console.log('Block Result', post); - if (post && post?.content) { - if (page?.properties?.spotify?.url) { - post.content = ` -%[${page.properties.spotify.url}] - -${post.content}`; - } - if (page?.properties?.youtube?.url) { - post.content = ` -%[${page.properties.youtube.url}] - -${post.content}`; - } - post.content = ` -Original: https://codingcat.dev/${page._type}/${page.slug} - -${post.content} - `; - input = { - title: page.title, - subtitle: page.excerpt, - slug: `${page._type}-${page.slug}`, - contentMarkdown: post.content, - coverImageURL: `https://media.codingcat.dev/image/upload/f_auto,c_limit,w_1920,q_auto/${page?.coverPhoto?.public_id}`, - isRepublished: { - originalArticleURL: `https://codingcat.dev/${page._type}/${page.slug}`, - }, - tags: [ - { - _id: '56744721958ef13879b94cad', - name: 'JavaScript', - slug: 'javascript', - }, - { - _id: '56744722958ef13879b94f1b', - name: 'Web Development', - slug: 'web-development', - }, - { - _id: '56744723958ef13879b955a9', - name: 'Beginner Developers', - slug: 'beginners', - }, - ], - }; - } - } - - if (input) { - const response = await createPublicationStory(input); - console.log( - 'createPublicationStory result:', - JSON.stringify(response) - ); - - const hashnodeSlug = - response?.data?.data?.createPublicationStory?.post?.slug; - - if (!hashnodeSlug) { - console.log('hasnode url missing'); - return; - } - - const update = { - page_id: page.id, - properties: { - hashnode: { - id: 'remote', - type: 'url', - url: `https://hashnode.codingcat.dev/${hashnodeSlug}`, - }, - }, - }; - console.log('Updating page with: ', JSON.stringify(update)); - const purrfectPagePatchRes = await patchPurrfectPage(update); - console.log('Page update result:', JSON.stringify(purrfectPagePatchRes)); - - return purrfectPagePatchRes; - } else { - console.log('No Data matched for article'); - } - return; - }); - -// Used for testing don't forget to remove for production -export const httpNotionToHashnode = functions.https.onRequest( - async (req, res) => { - await scheduleCheck(); - - res.send({ msg: 'started' }); - } -); +// import * as functions from 'firebase-functions'; +// import { sendTopic } from '../utilities/googleapis'; +// import { createPublicationStory } from '../utilities/hashnode'; + +// const topicId = 'hashnodeCreateFromNotion'; + +// const scheduleCheck = async () => { +// // Check to see if ther are scheduled pods +// console.log('Checking for scheduled pods'); +// const scheduledRes = await queryPurrfectStreamHashnode(1); +// console.log('Scheduled Result:', JSON.stringify(scheduledRes)); + +// if (scheduledRes?.results) { +// const needCloudinaryPods = scheduledRes?.results; +// console.log('Pods to add to pub/sub', JSON.stringify(needCloudinaryPods)); + +// for (const pod of needCloudinaryPods) { +// await sendTopic(topicId, pod); +// } +// } + +// for (const _type of ['post', 'tutorial']) { +// console.log('Checking for hashnode missing'); +// const posts = await queryByHashnode(_type, 1); +// console.log('Posts:', JSON.stringify(posts)); + +// if (posts?.results) { +// const needposts = posts?.results; +// console.log('Posts to add to pub/sub', JSON.stringify(needposts)); + +// for (const p of needposts) { +// await sendTopic(topicId, p); +// } +// } +// } +// }; + +// export const scheduledNotionToHashnode = functions.pubsub +// .schedule('every 5 minutes') +// .onRun(async () => { +// await scheduleCheck(); +// return true; +// }); + +// export const hashnodeToNotionPubSub = functions.pubsub +// .topic(topicId) +// .onPublish(async (message, context) => { +// console.log('The function was triggered at ', context.timestamp); +// console.log('The unique ID for the event is', context.eventId); +// const page = JSON.parse(JSON.stringify(message.json)); +// console.log('page', page); + +// let input; +// if (page._type === 'podcast') { +// input = { +// title: page.title, +// subtitle: page.excerpt, +// slug: `${page._type}-${page.slug}`, +// contentMarkdown: `Original: https://codingcat.dev/${page._type}/${page.slug} + +// %[${page.properties.youtube.url}] + +// %[${page.properties.spotify.url}] +// `, +// coverImageURL: `https://media.codingcat.dev/image/upload/f_auto,c_limit,w_1920,q_auto/${page?.coverPhoto?.public_id}`, +// isRepublished: { +// originalArticleURL: `https://codingcat.dev/${page._type}/${page.slug}`, +// }, +// tags: [ +// { +// _id: '56744722958ef13879b950d3', +// name: 'podcast', +// slug: 'podcast', +// }, +// { +// _id: '56744721958ef13879b94cad', +// name: 'JavaScript', +// slug: 'javascript', +// }, +// { +// _id: '56744722958ef13879b94f1b', +// name: 'Web Development', +// slug: 'web-development', +// }, +// { +// _id: '56744723958ef13879b955a9', +// name: 'Beginner Developers', +// slug: 'beginners', +// }, +// ], +// }; +// } else { +// console.log( +// `Getting ${page._type}: ${page.id} markdown, with slug ${page?.properties?.slug?.url}` +// ); +// const post = await getNotionPageMarkdown({ +// _type: page._type, +// slug: page?.properties?.slug?.url, +// preview: false, +// }); + +// console.log('Block Result', post); +// if (post && post?.content) { +// if (page?.properties?.spotify?.url) { +// post.content = ` +// %[${page.properties.spotify.url}] + +// ${post.content}`; +// } +// if (page?.properties?.youtube?.url) { +// post.content = ` +// %[${page.properties.youtube.url}] + +// ${post.content}`; +// } +// post.content = ` +// Original: https://codingcat.dev/${page._type}/${page.slug} + +// ${post.content} +// `; +// input = { +// title: page.title, +// subtitle: page.excerpt, +// slug: `${page._type}-${page.slug}`, +// contentMarkdown: post.content, +// coverImageURL: `https://media.codingcat.dev/image/upload/f_auto,c_limit,w_1920,q_auto/${page?.coverPhoto?.public_id}`, +// isRepublished: { +// originalArticleURL: `https://codingcat.dev/${page._type}/${page.slug}`, +// }, +// tags: [ +// { +// _id: '56744721958ef13879b94cad', +// name: 'JavaScript', +// slug: 'javascript', +// }, +// { +// _id: '56744722958ef13879b94f1b', +// name: 'Web Development', +// slug: 'web-development', +// }, +// { +// _id: '56744723958ef13879b955a9', +// name: 'Beginner Developers', +// slug: 'beginners', +// }, +// ], +// }; +// } +// } + +// if (input) { +// const response = await createPublicationStory(input); +// console.log( +// 'createPublicationStory result:', +// JSON.stringify(response) +// ); + +// const hashnodeSlug = +// response?.data?.data?.createPublicationStory?.post?.slug; + +// if (!hashnodeSlug) { +// console.log('hasnode url missing'); +// return; +// } + +// const update = { +// page_id: page.id, +// properties: { +// hashnode: { +// id: 'remote', +// type: 'url', +// url: `https://hashnode.codingcat.dev/${hashnodeSlug}`, +// }, +// }, +// }; +// console.log('Updating page with: ', JSON.stringify(update)); +// const purrfectPagePatchRes = await patchPurrfectPage(update); +// console.log('Page update result:', JSON.stringify(purrfectPagePatchRes)); + +// return purrfectPagePatchRes; +// } else { +// console.log('No Data matched for article'); +// } +// return; +// }); + +// // Used for testing don't forget to remove for production +// export const httpNotionToHashnode = functions.https.onRequest( +// async (req, res) => { +// await scheduleCheck(); + +// res.send({ msg: 'started' }); +// } +// ); diff --git a/apps/firebase/functions/src/index.ts b/apps/firebase/functions/src/index.ts index ce4a7f35a..819dface4 100644 --- a/apps/firebase/functions/src/index.ts +++ b/apps/firebase/functions/src/index.ts @@ -2,40 +2,3 @@ export const projectId = process.env.GCP_PROJECT || process.env.GCLOUD_PROJECT; export { scheduledFirestoreExport } from './backups/firestore'; export { newUserSetup } from './user/user'; -export { cloudinarysignature } from './cloudinary/cloudinarysignature'; -export { cloudinaryCookieToken } from './cloudinary/cloudinaryCookieToken'; -export { - onSubscriptionCreate, - onSubscriptionCancel, -} from './stripe/subscriptions'; - -export { getCode, getToken } from './google/auth'; -export { - scheduledNotionToCloudinary, - cloudinaryToNotionPubSub, - scheduledNotionCloudinaryConvert, - notionPageFindFileBlocksPublish, - cloudinaryConvertBlockPubSub, -} from './cloudinary/scheduledNotionCheck'; - -// Algolia scheduled -export { scheduledNotionToAlgolia } from './algolia/algolia'; - -// Calendly webhook pushes to pubsub -export { calendlyWebook } from './calendly/webhook'; -export { calendlyCreateNotionCardPubSub } from './calendly/pubsub'; - -// Notion to Firestore -export { scheduledNotionToFirestore } from './firebase/notion'; - -// Notion to Devto -export { - scheduledNotionToDevto, - devtoToNotionPubSub, -} from './devto/scheduledNotionToDevto'; - -// Notion to Hashnode -export { - scheduledNotionToHashnode, - hashnodeToNotionPubSub, -} from './hashnode/scheduledNotionToHashNode'; diff --git a/apps/firebase/functions/src/models/calendly.ts b/apps/firebase/functions/src/models/calendly.ts deleted file mode 100644 index 3a02258fa..000000000 --- a/apps/firebase/functions/src/models/calendly.ts +++ /dev/null @@ -1,94 +0,0 @@ -export interface WebhookPayload { - event: string; - payload: Payload; - time: string; -} - -export interface Payload { - event_type: EventType; - event: Event; - invitee: Invitee; - questions_and_answers: QuestionsAndAnswer[]; - questions_and_responses: QuestionsAndResponses; - tracking: Tracking; - old_event: any; - old_invitee: any; - new_event: any; - new_invitee: any; -} - -export interface EventType { - uuid: string; - kind: string; - slug: string; - name: string; - duration: number; - owner: Owner; -} - -export interface Owner { - type: string; - uuid: string; -} - -export interface Event { - uuid: string; - assigned_to: string[]; - extended_assigned_to: ExtendedAssignedTo[]; - start_time: string; - start_time_pretty: string; - invitee_start_time: string; - invitee_start_time_pretty: string; - end_time: string; - end_time_pretty: string; - invitee_end_time: string; - invitee_end_time_pretty: string; - created_at: string; - location: string; - canceled: boolean; - canceler_name: any; - cancel_reason: any; - canceled_at: any; -} - -export interface ExtendedAssignedTo { - name: string; - email: string; - primary: boolean; -} - -export interface Invitee { - uuid: string; - first_name: any; - last_name: any; - name: string; - email: string; - text_reminder_number: any; - timezone: string; - created_at: string; - is_reschedule: boolean; - payments: any[]; - canceled: boolean; - canceler_name: any; - cancel_reason: any; - canceled_at: any; -} - -export interface QuestionsAndAnswer { - question: string; - answer: string; -} - -export interface QuestionsAndResponses { - '1_question': string; - '1_response': string; -} - -export interface Tracking { - utm_campaign: any; - utm_source: any; - utm_medium: any; - utm_content: any; - utm_term: any; - salesforce_uuid: any; -} diff --git a/apps/firebase/functions/src/stripe/subscriptions.ts b/apps/firebase/functions/src/stripe/subscriptions.ts deleted file mode 100644 index 866266176..000000000 --- a/apps/firebase/functions/src/stripe/subscriptions.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { firestore } from '../config/config'; -import * as functions from 'firebase-functions'; -import * as admin from 'firebase-admin'; - -export const onSubscriptionCreate = functions.firestore - .document('customers/{customerId}/subscriptions/{subscriptionId}') - .onCreate(async (snap, context) => { - const subscription = snap.data(); - const customerId = context.params.customerId; - const subscriptionId = context.params.subscriptionId; - - console.log(`Subscription ${subscriptionId} was added for ${customerId}`); - console.log(`Subscription:`, JSON.stringify(subscription)); - - if (!subscription) { - console.log('Subscription missing data'); - return; - } - - return firestore.doc(`users/${customerId}`).set( - { - memberships: admin.firestore.FieldValue.arrayUnion({ - membership: subscription.role === 'supporter' ? false : true, - membershipType: subscription.role, - subscriptionId, - }), - }, - { merge: true } - ); - }); - -export const onSubscriptionCancel = functions.firestore - .document('customers/{customerId}/subscriptions/{subscriptionId}') - .onUpdate(async (change, context) => { - const subscriptionOld = change.before.data(); - const subscription = change.after.data(); - const customerId = context.params.customerId; - const subscriptionId = context.params.subscriptionId; - - console.log(`Subscription ${subscriptionId} was deleted for ${customerId}`); - console.log(`Subscription:`, JSON.stringify(subscription)); - - if (!subscription) { - console.log('Subscription missing data'); - return; - } - - if ( - subscription.canceled_at === subscriptionOld.canceled_at && - subscription.canceled_at !== null - ) { - console.log('Already updated, ejecting'); - } - - const userDoc = await firestore.doc(`users/${customerId}`).get(); - const user = userDoc.data(); - - let removeMembership; - if (user && user.memberships) { - console.log(`Found user ${user}`); - removeMembership = user.memberships.find( - (m: any) => m.subscriptionId === subscriptionId - ); - } else { - console.log('User with memberships not found.'); - return false; - } - console.log(`Removing membership`, JSON.stringify(removeMembership)); - return firestore.doc(`users/${customerId}`).set( - { - memberships: admin.firestore.FieldValue.arrayRemove(removeMembership), - }, - { merge: true } - ); - }); diff --git a/apps/firebase/functions/src/utilities/calendly.ts b/apps/firebase/functions/src/utilities/calendly.ts deleted file mode 100644 index 56a23afd9..000000000 --- a/apps/firebase/functions/src/utilities/calendly.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { calendlyAccessToken } from '../config/config'; -const calendlyApi = `https://api.calendly.com`; - -export const ccd = `e3f45196-30ad-4597-9e7d-6b4ae147ca00`; -export const purrfect = `c4e6fdd3-768a-4157-846e-41b4ef5fae9d`; -// const sirens = `1d4ddcce-8fac-4dfa-805b-88f3be093990`; - -export const call = async (path: string) => { - const url = `${calendlyApi}${path}`; - console.log('Calling', url); - - const response = await fetch(url, { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${calendlyAccessToken}`, - }, - }); - if (response.status !== 200) { - console.log('Error', response.status); - throw response.status; - } - const json = await response.json(); - console.log('RESPONSE:', JSON.stringify(json)); - return json; -}; - -export const getEvent = async (uuid: string) => { - return (await call(`/scheduled_events/${uuid}`)) as CalendlyEventResponse; -}; - -export const listEventInvitees = async (uuid: string) => { - return (await call( - `/scheduled_events/${uuid}/invitees` - )) as CalendlyInviteeResponse; -}; - -export interface Location { - type: string; - location: string; -} - -export interface InviteesCounter { - total: number; - active: number; - limit: number; -} - -export interface EventMembership { - user: string; -} - -export interface EventGuest { - email: string; - created_at: string; - updated_at: string; -} - -export interface Resource { - uri: string; - name: string; - status: string; - start_time: string; - end_time: string; - event_type: string; - location: Location; - invitees_counter: InviteesCounter; - created_at: string; - updated_at: string; - event_memberships: EventMembership[]; - event_guests: EventGuest[]; -} - -export interface CalendlyEventResponse { - resource: Resource; -} -export interface QuestionsAndAnswer { - answer: string; - position: number; - question: string; -} - -export interface Tracking { - utm_campaign?: any; - utm_source?: any; - utm_medium?: any; - utm_content?: any; - utm_term?: any; - salesforce_uuid?: any; -} - -export interface Payment { - external_id: string; - provider: string; - amount: number; - currency: string; - terms: string; - successful: boolean; -} - -export interface Collection { - cancel_url: string; - created_at: string; - email: string; - event: string; - name: string; - new_invitee?: any; - old_invitee?: any; - questions_and_answers: QuestionsAndAnswer[]; - reschedule_url: string; - rescheduled: boolean; - status: string; - text_reminder_number?: any; - timezone: string; - tracking: Tracking; - updated_at: string; - uri: string; - canceled: boolean; - payment: Payment; -} - -export interface Pagination { - count: number; - next_page: string; -} - -export interface CalendlyInviteeResponse { - collection: Collection[]; - pagination: Pagination; -} diff --git a/apps/firebase/functions/src/utilities/cloudinary.ts b/apps/firebase/functions/src/utilities/cloudinary.ts deleted file mode 100644 index 03c097663..000000000 --- a/apps/firebase/functions/src/utilities/cloudinary.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { v2 as cloudinary } from 'cloudinary'; - -import { - cloudinaryName, - cloudinaryApiKey, - cloudinaryApiSecret, -} from './../config/config'; - -const config = { - cloud_name: cloudinaryName, - api_key: cloudinaryApiKey, - api_secret: cloudinaryApiSecret, -}; -console.log('Cloudinary Config cloud_name', config.cloud_name); -cloudinary.config(config); - -export default cloudinary; diff --git a/apps/firebase/functions/src/utilities/cloudinaryUtils.ts b/apps/firebase/functions/src/utilities/cloudinaryUtils.ts deleted file mode 100644 index e1b36b881..000000000 --- a/apps/firebase/functions/src/utilities/cloudinaryUtils.ts +++ /dev/null @@ -1,147 +0,0 @@ -import cloudinary from './cloudinary'; -export const generateCodeWithCodingCatCoverURL = async ({ - title, - slug, - guestName, - guestImagePublicId, - folder, -}: { - title: string; - slug: string; - guestName: string; - guestImagePublicId: string; - folder: string; -}) => { - const url = cloudinary.url(`${folder}/Code_with_Coding_Cat_Empty_Title`, { - quality: 'auto', - fetch_format: 'jpg', - transformation: [ - { - width: 1090, - color: '#FBFBFB', - y: '0', - x: '100', - crop: 'fit', - gravity: 'west', - overlay: { - text: title, - font_family: 'Nunito', - font_size: 120, - font_weight: 'black', - line_spacing: '-10', - }, - }, - ], - }); - - //Write real image with new name - const res = await cloudinary.uploader.upload(url, { - public_id: slug, - }); - return res.secure_url; -}; - -export const generatePurrfectDevCoverURL = async ({ - title, - slug, - guestName, - guestImagePublicId, - folder, -}: { - title: string; - slug: string; - guestName: string; - guestImagePublicId: string; - folder: string; -}) => { - const url = cloudinary.url(`${folder}/codingcatdev-podcast-season2`, { - quality: 'auto', - fetch_format: 'jpg', - transformation: [ - { - width: 1200, - color: '#FBFBFB', - y: '59', - x: '87', - crop: 'fit', - gravity: 'north_west', - overlay: { - text: title, - font_family: 'Nunito', - font_size: 96, - font_weight: 'black', - line_spacing: '-10', - text_align: 'left', - }, - }, - { - color: '#D11663', - y: '410', - x: '0', - crop: 'fit', - gravity: 'south_west', - background: '#D11663', - radius: '8:50', - border: '30px_solid_rgb:D11663', - overlay: { - font_family: 'Nunito', - font_size: 65, - text: guestName, - font_weight: 'bold', - text_align: 'left', - }, - }, - { - color: '#FBFBFB', - y: '417', - x: '40', - crop: 'fit', - gravity: 'south_west', - background: '#4B0A75', - radius: '8:50', - border: '30px_solid_rgb:4B0A75', - overlay: { - font_family: 'Nunito', - font_size: 65, - text: guestName, - font_weight: 'bold', - text_align: 'center', - }, - }, - { - overlay: guestImagePublicId.split('/').join(':'), - height: '400', - width: '400', - y: '0', - x: '0', - gravity: 'south_west', - crop: 'fill', - border: '5px_solid_rgb:FBFBFB', - }, - ], - }); - - //Write real image with new name - const res = await cloudinary.uploader.upload(url, { - public_id: slug, - }); - return res.secure_url; -}; - -export const uploadCloudinaryFromUrl = async ( - publicId: string, - url: string -) => { - console.log(`Uploading image for: ${publicId}`); - console.log(`Upload image from: ${url}`); - try { - const res = await cloudinary.uploader.upload(url, { - public_id: publicId, - }); - console.log('cloudinary payload response: ', JSON.stringify(res)); - return res; - } catch (error) { - console.error(error); - throw error; - } -}; diff --git a/apps/firebase/functions/src/utilities/date.ts b/apps/firebase/functions/src/utilities/date.ts deleted file mode 100644 index a1989c141..000000000 --- a/apps/firebase/functions/src/utilities/date.ts +++ /dev/null @@ -1,22 +0,0 @@ -export function utcOffset(isoString: string, timeZone = 'America/new_york') { - console.log('conversion:input', isoString); - const isoStringDate = new Date(isoString); - const offset = getOffset(timeZone, isoStringDate); - console.log('conversion:offsethours', offset / 60 / 60 / 1000); - const output = new Date(Date.parse(isoStringDate.toISOString()) + offset) - .toISOString() - .replace('Z', ''); - console.log('conversion:output', output); - return output; -} - -const getOffset = (timeZone = 'UTC', date = new Date()) => { - const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' })); - const tzDate = new Date(date.toLocaleString('en-US', { timeZone })); - return tzDate.getTime() - utcDate.getTime(); -}; - -const calendlyDate = '2022-01-26T14:00:00.000000Z'; -console.log('hardcoded:calendlyDate', calendlyDate); -console.log('hardcoded:fromFunc', utcOffset(calendlyDate)); -console.log('hardcoded:shouldMatch', '2022-01-26T09:00:00.000'); diff --git a/apps/firebase/functions/src/utilities/devto.ts b/apps/firebase/functions/src/utilities/devto.ts index 1b23b67fe..922a5e699 100644 --- a/apps/firebase/functions/src/utilities/devto.ts +++ b/apps/firebase/functions/src/utilities/devto.ts @@ -1,12 +1,11 @@ import axios, { AxiosRequestConfig } from 'axios'; -import { devto } from '../config/config'; export const addArticle = async (data: any) => { const URL = 'https://dev.to/api/articles/'; const options: AxiosRequestConfig = { headers: { - 'api-key': devto, + 'api-key': 'none', 'Content-Type': 'application/json', }, }; diff --git a/apps/firebase/functions/src/utilities/notion.server.ts b/apps/firebase/functions/src/utilities/notion.server.ts deleted file mode 100644 index 0c9da391a..000000000 --- a/apps/firebase/functions/src/utilities/notion.server.ts +++ /dev/null @@ -1,1348 +0,0 @@ -import { Client } from '@notionhq/client'; -import { - QueryDatabaseResponse, - UpdatePageParameters, -} from '@notionhq/client/build/src/api-endpoints'; -import { NotionToMarkdown } from 'notion-to-md'; -import { - notionToken, - notionPosts, - notionTutorials, - notionCourses, - notionPurrfectDatabaseId, - notionLessons, - notionPages, - notionFrameworks, - notionLanguages, - notionAuthors, - notionPurrfectPicks, - notionPurrfectGuestDatabaseId, - notionSections, - notionPurrfectCompanyDatabaseId, -} from '../config/config'; - -const notionConfig = { - postsDb: notionPosts, - tutorialsDb: notionTutorials, - coursesDb: notionCourses, - purrfectStreamsDb: notionPurrfectDatabaseId, - lessonsDb: notionLessons, - pagesDb: notionPages, - frameworksDb: notionFrameworks, - languagesDb: notionLanguages, - authorsDb: notionAuthors, - purrfectPicksDb: notionPurrfectPicks, - purrfectGuestDb: notionPurrfectGuestDatabaseId, - sectionsDb: notionSections, -}; - -// Initializing a client -const notionClient = new Client({ - auth: notionToken, -}); - -const n2m = new NotionToMarkdown({ notionClient }); - -interface NotionPosts extends Omit { - results: any[]; -} - -export const getNotionDbByType = (_type: string) => { - switch (_type) { - case 'post': - return notionConfig.postsDb; - case 'tutorial': - return notionConfig.tutorialsDb; - case 'course': - return notionConfig.coursesDb; - case 'podcast': - return notionConfig.purrfectStreamsDb; - case 'lesson': - return notionConfig.lessonsDb; - case 'page': - return notionConfig.pagesDb; - case 'framework': - return notionConfig.frameworksDb; - case 'language': - return notionConfig.languagesDb; - default: - return notionConfig.authorsDb; - } -}; - -// CodingCat.dev - -//TODO: Finish theses -export const getPageById = async ({ - _id, - _type, -}: { - _id: string; - _type: string; -}) => { - const raw = await notionClient.pages.retrieve({ - page_id: _id, - }); - return formatPost(raw, _type); -}; - -export const getAuthorBySlugService = async ({ - preview, - slug, -}: { - preview?: boolean; - slug: string; -}) => { - const raw = await notionClient.databases.query({ - database_id: notionConfig.authorsDb, - filter: { - and: [ - { - property: 'slug', - url: { - contains: slug, - }, - }, - { - property: 'published', - select: { - equals: 'published', - }, - }, - { - property: 'start', - date: { - on_or_before: new Date().toISOString(), - }, - }, - ], - }, - }); - - return formatPosts(raw, 'author'); -}; - -const formatPost = async ( - q: any, - _type: string, - preview?: boolean, - list?: boolean -) => { - //Flat authors - const authors = []; - let post = q; - - if (q?.properties?.author_title?.rollup?.array) { - for (const [ - i, - a, - ] of q?.properties?.author_title?.rollup?.array?.entries()) { - const cover = q?.properties?.author_cover?.rollup?.array?.at(i)?.url; - - let photoURL = null; - if (cover) { - photoURL = { - public_id: cover.split('upload/')?.at(1) - ? cover.split('upload/').at(1) - : cover, - }; - } - const slug = q?.properties?.author_slug?.rollup?.array?.at(i)?.url; - - const author = { - displayName: `${a?.title.map((t: any) => t.plain_text).join('')}`, - photoURL, - slug, - }; - authors.push(author); - } - } - post = { - ...post, - _id: q?.id ? q.id : null, - title: - _type === 'podcast' - ? `${q.properties.Season.number}.${ - q.properties.Episode.number - } - ${q?.properties?.Name?.title - .map((t: any) => t.plain_text) - .join('')}` - : `${q?.properties?.title?.title - .map((t: any) => t.plain_text) - .join('')}`, - coverPhoto: - _type === 'podcast' - ? { - public_id: q?.cover?.external?.url - ? q?.cover?.external?.url.split('upload/').at(1) - : null, - } - : { - public_id: q?.properties?.cover?.url - ? q?.properties?.cover.url.split('upload/')?.at(1) || - q?.properties?.cover?.url - : null, - }, - coverVideo: q?.properties?.youtube?.url - ? { url: q.properties.youtube.url } - : null, - _type, - slug: q?.properties?.slug?.url ? q?.properties?.slug.url : null, - excerpt: q?.properties?.excerpt?.rich_text - .map((t: any) => t.plain_text) - .join(''), - _createdAt: q?.properties?.start?.date?.start || q?.created_time, - _updatedAt: q?.last_edited_time, - authors, - access_mode: q?.properties?.access_mode?.select?.name - ? q?.properties?.access_mode?.select?.name - : 'closed', - }; - - if (_type === 'framework' || _type === 'language') { - post = { - ...post, - courses_count: q?.properties?.courses_count?.rollup?.number || 0, - tutorials_count: q?.properties?.tutorials_count?.rollup?.number || 0, - podcasts_count: q?.properties?.podcasts_count?.rollup?.number || 0, - posts_count: q?.properties?.posts_count?.rollup?.number || 0, - }; - } - - if (_type === 'podcast') { - const sponsors: any = []; - for (const [i] of q?.properties?.sponsors?.relation.entries()) { - sponsors.push({ - url: q?.properties?.sponsors_url?.rollup?.array?.at(i)?.url || null, - coverPhoto: { - public_id: q?.properties?.sponsors_cover?.rollup?.array?.at(i)?.url - ? q?.properties?.sponsors_cover?.rollup?.array - ?.at(i) - ?.url?.split('upload/') - ?.at(1) || - q?.properties?.sponsors_cover?.rollup?.array?.at(i)?.url - : null, - }, - description: - q?.properties?.sponsors_description?.rollup?.array - ?.at(i) - ?.rich_text?.map((t: any) => t.plain_text) - .join('') || null, - company: - q?.properties?.sponsors_name?.rollup?.array - ?.at(i) - ?.title?.map((t: any) => t.plain_text) - .join('') || null, - }); - } - - post = { - ...post, - sponsors, - }; - } - - // Get sections and lessons for course - if (_type === 'course' && !list) { - console.log('got list'); - const sectionsRaw = await querySectionsByCourseId(q.id, preview); - const sections: any = []; - for (const s of sectionsRaw.results as any) { - const lesson = { - title: `${s?.properties?.lesson_title?.rollup?.array - ?.at(0) - ?.title.map((t: any) => t.plain_text) - .join('')}`, - _id: s.id, - slug: s?.properties?.lesson_slug?.rollup?.array?.at(0)?.url - ? s?.properties?.lesson_slug.rollup?.array?.at(0)?.url - : null, - access_mode: s?.properties?.access_mode?.select?.name - ? s?.properties?.access_mode?.select?.name - : post.access_mode, - }; - const exists = sections.find( - (e: any) => - e.title === - `${s?.properties?.title?.title - .map((t: any) => t.plain_text) - .join('')}` - ); - - if (exists) { - exists.lessons.push(lesson); - } else { - sections.push({ - ...s, - title: `${s?.properties?.title?.title - .map((t: any) => t.plain_text) - .join('')}`, - _key: s.id, - lessons: [lesson], - }); - } - } - post = { - ...post, - sections, - }; - } - if (_type === 'author') { - post = { - ...post, - _id: q?.id ? q.id : null, - displayName: `${q?.properties?.title?.title - .map((t: any) => t.plain_text) - .join('')}`, - photoURL: { - public_id: q?.properties?.cover?.url - ? q?.properties?.cover.url.split('upload/')?.at(1) || - q?.properties?.cover?.url - : null, - }, - slug: q?.properties?.slug?.url ? q?.properties?.slug.url : null, - }; - } - return post; -}; - -const formatPosts = async ( - raw: QueryDatabaseResponse, - _type: string, - preview?: boolean, - list?: boolean -) => { - const results = await Promise.all( - raw.results.map((q: any) => formatPost(q, _type, preview, list)) - ); - - const post = { - ...raw, - results, - } as unknown as NotionPosts; - return post; -}; - -export const queryByPublished = async ( - _type: string, - page_size?: number, - start_cursor?: string | null -) => { - let sorts: any = [ - { - property: 'start', - direction: 'descending', - }, - ]; - let filter: any = { - and: [ - { - property: 'slug', - url: { - is_not_empty: true, - }, - }, - { - property: 'published', - select: { - equals: 'published', - }, - }, - { - property: 'start', - date: { - on_or_before: new Date().toISOString(), - }, - }, - ], - }; - - if (_type === 'author') { - sorts = [ - { - property: 'title', - direction: 'ascending', - }, - ]; - } - - if (_type === 'framework' || _type === 'language') { - filter = { - and: [ - { - property: 'slug', - url: { - is_not_empty: true, - }, - }, - { - or: [ - { - property: 'courses_count', - rollup: { - number: { greater_than: 0 }, - }, - }, - { - property: 'tutorials_count', - rollup: { - number: { greater_than: 0 }, - }, - }, - { - property: 'podcasts_count', - rollup: { - number: { greater_than: 0 }, - }, - }, - { - property: 'posts_count', - rollup: { - number: { greater_than: 0 }, - }, - }, - ], - }, - ], - }; - sorts = [ - { - property: 'title', - direction: 'ascending', - }, - ]; - } - - const raw = await notionClient.databases.query({ - database_id: getNotionDbByType(_type), - start_cursor: start_cursor ? start_cursor : undefined, - page_size, - filter, - sorts, - }); - return await formatPosts(raw, _type, false, true); -}; - -export const queryByDevto = async ( - _type: string, - page_size?: number, - start_cursor?: string | null -) => { - const sorts: any = [ - { - property: 'start', - direction: 'ascending', - }, - ]; - const filter: any = { - and: [ - { - property: 'slug', - url: { - is_not_empty: true, - }, - }, - { - property: 'published', - select: { - equals: 'published', - }, - }, - { - property: 'devto', - url: { - is_empty: true, - }, - }, - { - property: 'start', - date: { - is_not_empty: true, - }, - }, - ], - }; - - const raw = await notionClient.databases.query({ - database_id: getNotionDbByType(_type), - start_cursor: start_cursor ? start_cursor : undefined, - page_size, - filter, - sorts, - }); - return await formatPosts(raw, _type, false, true); -}; - -export const queryByHashnode = async ( - _type: string, - page_size?: number, - start_cursor?: string | null -) => { - const sorts: any = [ - { - property: 'start', - direction: 'ascending', - }, - ]; - const filter: any = { - and: [ - { - property: 'slug', - url: { - is_not_empty: true, - }, - }, - { - property: 'published', - select: { - equals: 'published', - }, - }, - { - property: 'hashnode', - url: { - is_empty: true, - }, - }, - { - property: 'start', - date: { - is_not_empty: true, - }, - }, - ], - }; - - const raw = await notionClient.databases.query({ - database_id: getNotionDbByType(_type), - start_cursor: start_cursor ? start_cursor : undefined, - page_size, - filter, - sorts, - }); - return await formatPosts(raw, _type, false, true); -}; - -export const queryAll = async ( - _type: string, - page_size?: number, - start_cursor?: string | null -) => { - const raw = await notionClient.databases.query({ - database_id: getNotionDbByType(_type), - start_cursor: start_cursor ? start_cursor : undefined, - page_size, - }); - return await formatPosts(raw, _type, false, true); -}; - -export const getNotionPageMarkdown = async ({ - _type, - slug, - preview, -}: { - _type: string; - slug?: string; - preview: boolean | undefined; -}) => { - let pageId; - let page; - - if (slug) { - const raw = await queryNotionDbBySlug(_type, slug, preview); - if (!raw.results.length) { - return null; - } - page = raw.results.at(0); - pageId = page?.id; - } - if (!page) { - return null; - } - if (!pageId) { - return null; - } - let content = ''; - const blocks = await n2m.pageToMarkdown(pageId); - content += n2m.toMarkdownString(blocks); - - return { - ...page, - content, - }; -}; - -export const queryNotionDbBySlug = async ( - _type: string, - slug: string, - preview?: boolean -) => { - let filter: any; - let sorts: any; - filter = { - and: [ - { - property: 'slug', - url: { - contains: slug, - }, - }, - ], - }; - - if (!preview) { - filter = { - ...filter, - and: [ - ...filter.and, - ...[ - { - property: 'published', - select: { - equals: 'published', - }, - }, - { - property: 'start', - date: { - on_or_before: new Date().toISOString(), - }, - }, - ], - ], - }; - } - - if (_type === 'framework' || _type === 'language') { - filter = { - and: [ - { - property: 'slug', - url: { - contains: slug, - }, - }, - ], - }; - sorts = [ - { - property: 'title', - direction: 'ascending', - }, - ]; - } - - const raw = await notionClient.databases.query({ - database_id: getNotionDbByType(_type), - filter, - sorts, - }); - return await formatPosts(raw, _type, preview); -}; - -export const querySectionsByCourseId = async ( - id: string, - preview: boolean | undefined -) => { - let filter; - filter = { - property: 'courses', - relation: { - contains: id, - }, - }; - if (!preview) { - filter = { - and: [ - { - property: 'courses', - relation: { - contains: id, - }, - }, - { - property: 'lesson_published', - rollup: { - every: { - select: { - equals: 'published', - }, - }, - }, - }, - ], - }; - } - - const raw = await notionClient.databases.query({ - database_id: notionConfig.sectionsDb, - filter, - sorts: [ - { - property: 'section_order', - direction: 'ascending', - }, - { - property: 'lesson_order', - direction: 'ascending', - }, - ], - }); - return raw; -}; - -export const queryRelationById = async ( - id: string, - relation: string, - _type: string -) => { - let sorts: any = [ - { - property: 'start', - direction: 'descending', - }, - ]; - - if (_type === 'podcast') { - sorts = [ - { - property: 'Season', - direction: 'descending', - }, - { - property: 'Episode', - direction: 'descending', - }, - ]; - } - const raw = await notionClient.databases.query({ - database_id: getNotionDbByType(_type), - filter: { - property: relation, - relation: { - contains: id, - }, - }, - sorts, - }); - return await formatPosts(raw, _type); -}; - -// Purrfect.dev - -export const queryPurrfectStreamAll = async ( - page_size?: number, - start_cursor?: string | null -) => { - const raw = await notionClient.databases.query({ - database_id: notionConfig.purrfectStreamsDb, - start_cursor: start_cursor ? start_cursor : undefined, - page_size, - }); - return await formatPosts(raw, 'podcast'); -}; - -export const queryPurrfectStreamByReleased = async ( - page_size?: number, - start_cursor?: string | null -) => { - const raw = await notionClient.databases.query({ - database_id: notionConfig.purrfectStreamsDb, - start_cursor: start_cursor ? start_cursor : undefined, - page_size, - filter: { - and: [ - { - property: 'slug', - url: { - is_not_empty: true, - }, - }, - { - property: 'Status', - select: { - equals: 'Released', - }, - }, - { - property: 'Episode', - number: { - is_not_empty: true, - }, - }, - { - property: 'start', - date: { - on_or_before: new Date().toISOString(), - }, - }, - ], - }, - sorts: [ - { - property: 'Season', - direction: 'descending', - }, - { - property: 'Episode', - direction: 'descending', - }, - ], - }); - return await formatPosts(raw, 'podcast'); -}; - -export const queryPurrfectStreamDevTo = async ( - page_size?: number, - start_cursor?: string | null -) => { - const raw = await notionClient.databases.query({ - database_id: notionConfig.purrfectStreamsDb, - start_cursor: start_cursor ? start_cursor : undefined, - page_size, - filter: { - and: [ - { - property: 'slug', - url: { - is_not_empty: true, - }, - }, - { - property: 'Status', - select: { - equals: 'Released', - }, - }, - { - property: 'Episode', - number: { - is_not_empty: true, - }, - }, - { - property: 'start', - date: { - on_or_before: new Date().toISOString(), - }, - }, - { - property: 'devto', - url: { - is_empty: true, - }, - }, - { - property: 'youtube', - url: { - is_not_empty: true, - }, - }, - { - property: 'spotify', - url: { - is_not_empty: true, - }, - }, - ], - }, - sorts: [ - { - property: 'Season', - direction: 'ascending', - }, - { - property: 'Episode', - direction: 'ascending', - }, - ], - }); - return await formatPosts(raw, 'podcast'); -}; - -export const queryPurrfectStreamHashnode = async ( - page_size?: number, - start_cursor?: string | null -) => { - const raw = await notionClient.databases.query({ - database_id: notionConfig.purrfectStreamsDb, - start_cursor: start_cursor ? start_cursor : undefined, - page_size, - filter: { - and: [ - { - property: 'slug', - url: { - is_not_empty: true, - }, - }, - { - property: 'Status', - select: { - equals: 'Released', - }, - }, - { - property: 'Episode', - number: { - is_not_empty: true, - }, - }, - { - property: 'start', - date: { - on_or_before: new Date().toISOString(), - }, - }, - { - property: 'hashnode', - url: { - is_empty: true, - }, - }, - { - property: 'youtube', - url: { - is_not_empty: true, - }, - }, - { - property: 'spotify', - url: { - is_not_empty: true, - }, - }, - ], - }, - sorts: [ - { - property: 'Season', - direction: 'ascending', - }, - { - property: 'Episode', - direction: 'ascending', - }, - ], - }); - return await formatPosts(raw, 'podcast'); -}; - -export const queryPurrfectStreamBySlug = async (slug: string) => { - const raw = await notionClient.databases.query({ - database_id: notionConfig.purrfectStreamsDb, - filter: { - and: [ - { - property: 'slug', - rich_text: { - contains: slug, - }, - }, - { - property: 'Status', - select: { - equals: 'Released', - }, - }, - { - property: 'Episode', - number: { - is_not_empty: true, - }, - }, - { - property: 'start', - date: { - on_or_before: new Date().toISOString(), - }, - }, - ], - }, - sorts: [ - { - property: 'Season', - direction: 'descending', - }, - { - property: 'Episode', - direction: 'descending', - }, - ], - }); - return await formatPosts(raw, 'podcast'); -}; - -export const getPurrfectStreamPageMarkdown = async (slug: string) => { - const raw = await queryPurrfectStreamBySlug(slug); - if (!raw.results.length) { - return null; - } - - //Get purrfect picks - const page = raw.results.at(0); - if (!page) { - return null; - } - const id = page.id; - const [purrfectPicks, purrfectGuests] = - await Promise.all([ - queryPurrfectPicksByStreamId(id), - queryPurrfectGuestsByStreamId(id), - ]); - - let content = ''; - // Build the markdown for page - for (const guest of purrfectGuests.results) { - const blocks = await n2m.pageToMarkdown(guest.id); - content += n2m.toMarkdownString(blocks); - } - for (const p of raw.results) { - const blocks = await n2m.pageToMarkdown(p.id); - content += n2m.toMarkdownString(blocks); - } - - // Create picks blocks - let picks: - | [ - { - name: string; - picks: [{ name: string; url: string }]; - } - ] - | [] = []; - for (const pick of purrfectPicks.results as any) { - const guestId = pick.properties?.Guest?.relation?.at(0)?.id; - const guest = { - name: '', - picks: [] as [{ name: string; url: string }] | [], - }; - // Find name - if (guestId) { - const g: any = purrfectGuests.results.find((d: any) => d.id === guestId); - guest.name = g?.properties?.Name?.title - .map((t: any) => t.plain_text) - .join(''); - } else { - guest.name = pick.properties?.Us?.people?.at(0)?.name; - } - const link = { - name: pick?.properties?.Name?.title - .map((t: any) => t.plain_text) - .join('') as string, - url: pick?.properties?.Site?.url as string, - }; - const alreadyUsed = picks.find((p: any) => p.name === guest.name); - if (alreadyUsed) { - alreadyUsed.picks = [...alreadyUsed.picks, link] as any; - } else { - guest.picks = [link]; - picks = [...picks, guest] as any; - } - } - let pickBlocks: any; - if (picks.length > 0) { - pickBlocks = [{ parent: '## Purrfect Picks', children: [] }]; - - picks.map((p) => { - pickBlocks.push({ - parent: `### ${p.name}`, - children: [], - }); - p.picks.map((pick) => { - pickBlocks.push({ - parent: `- [${pick.name}](${pick.url})`, - children: [], - }); - }); - }); - } - - content += n2m.toMarkdownString(pickBlocks); - - return { - ...raw.results[0], - content, - }; -}; - -export const queryPurrfectPicksByStreamId = async (streamId: string) => { - const raw = await notionClient.databases.query({ - database_id: notionConfig.purrfectPicksDb, - filter: { - property: 'streams', - relation: { - contains: streamId, - }, - }, - sorts: [ - { - property: 'Guest', - direction: 'ascending', - }, - { - property: 'Us', - direction: 'descending', - }, - ], - }); - return raw; -}; -export const queryPurrfectGuestsByStreamId = async (streamId: string) => { - const raw = await notionClient.databases.query({ - database_id: notionConfig.purrfectGuestDb, - filter: { - property: 'streams', - relation: { - contains: streamId, - }, - }, - }); - return raw; -}; - -export const queryNotionDbForCloudinaryConvert = async (_type: string) => { - return await notionClient.databases.query({ - database_id: getNotionDbByType(_type), - filter: { - and: [ - { - property: 'cloudinary_convert', - checkbox: { - equals: true, - }, - }, - ], - }, - }); -}; - -export const getGuestRelations = (guestIds?: string[]) => { - const guestRelation = []; - if (guestIds) { - for (const guestId of guestIds) { - guestRelation.push({ - database_id: notionPurrfectGuestDatabaseId, - id: guestId, - }); - } - } - return guestRelation; -}; -export const getCompanytRelations = (companyIds?: string[]) => { - const companyRelation = []; - if (companyIds) { - for (const companyId of companyIds) { - companyRelation.push({ - database_id: notionPurrfectCompanyDatabaseId, - id: companyId, - }); - } - } - return companyRelation; -}; - -export const queryPurrfectPageByCalendarId = (calendarid: string) => { - return notionClient.databases.query({ - database_id: notionPurrfectDatabaseId, - filter: { - property: 'calendarid', - rich_text: { - contains: calendarid, - }, - }, - }); -}; - -export const queryPurrfectPageScheduled = (status: string) => { - return notionClient.databases.query({ - database_id: notionPurrfectDatabaseId, - filter: { - property: 'Status', - select: { - equals: status, - }, - }, - }); -}; - -export const getPage = (page_id: string) => { - return notionClient.pages.retrieve({ - page_id, - }); -}; - -export const createPurrfectPage = ({ - calendarid, - guestIds, - recordingDate, - companyIds, -}: { - calendarid: string; - recordingDate: string; - guestIds?: string[]; - companyIds?: string[]; -}) => { - const notionCreatePayload = { - parent: { - database_id: notionPurrfectDatabaseId, - }, - properties: { - calendarid: { - id: 'RZ { - return notionClient.pages.update(pageParams); -}; - -export const queryPurrfectGuest = (email: string) => { - return notionClient.databases.query({ - database_id: notionPurrfectGuestDatabaseId, - filter: { - property: 'email', - email: { - equals: email, - }, - }, - }); -}; - -export const createPurrfectGuest = ({ - name, - email, - twitterHandle, - companyIds, -}: { - name: string; - email: string; - twitterHandle?: string; - companyIds?: string[]; -}) => { - const page = { - parent: { - database_id: notionPurrfectGuestDatabaseId, - }, - properties: { - email: { - type: 'email', - email: email, - }, - Name: { - id: 'title', - type: 'title', - title: [ - { - type: 'text', - text: { - content: name, - }, - }, - ], - }, - }, - } as any; - - if (twitterHandle) { - page.properties.Twitter = { - type: 'url', - url: `${twitterHandle}`, - }; - } - - return notionClient.pages.create(page); -}; - -export const getBlocks = async (blockId: string) => { - const response = await notionClient.blocks.children.list({ - block_id: blockId, - }); - - return response.results as any[]; -}; - -export const getChildBlocks = async (blocks: any[]) => { - // Retrieve block children for nested blocks (one level deep), for example toggle blocks - // https://developers.notion.com/docs/working-with-page-content#reading-nested-blocks - - const blocksWithChildren: any[] = []; - for (const block of blocks as any) { - blocksWithChildren.push(block); - if (block.has_children) { - const childrenBlocks = await getBlocks(block.id); - for (const b of childrenBlocks as any) { - blocksWithChildren.push(b); - } - } - } - return blocksWithChildren; -}; - -export const getNotionPageBlocks = async (id: string) => { - return await getChildBlocks(await getBlocks(id)); -}; - -export const updateBlock = async (blockId: string, block: any) => { - const response = await notionClient.blocks.update({ - block_id: blockId, - ...block, - }); - - return response; -}; diff --git a/apps/firebase/functions/src/utilities/twitter.ts b/apps/firebase/functions/src/utilities/twitter.ts deleted file mode 100644 index 568b8ad9a..000000000 --- a/apps/firebase/functions/src/utilities/twitter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import TwitterApi from 'twitter-api-v2'; - -import { twitterBearerToken } from '../config/config'; - -const twitterClient = new TwitterApi(twitterBearerToken); -// Tell typescript it's a readonly app -const roClient = twitterClient.readOnly; - -export const getUserByUsername = (username: string) => { - return roClient.v2.userByUsername(username, { - 'user.fields': 'profile_image_url', - }); -};