diff --git a/.vscode/settings.json b/.vscode/settings.json index 42e2d695..7c21de88 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,7 @@ { - "cSpell.words": [ - "octokit" - ] -} \ No newline at end of file + "cSpell.words": ["octokit"], + "editor.codeActionsOnSave": { + "source.organizeImports": true, + "source.addMissingImports": true + } +} diff --git a/packages/notion-post-publisher/__mocks__/ToggleBlock.mock.ts b/packages/notion-post-publisher/__mocks__/ToggleBlock.mock.ts new file mode 100644 index 00000000..97b9b9fd --- /dev/null +++ b/packages/notion-post-publisher/__mocks__/ToggleBlock.mock.ts @@ -0,0 +1,23 @@ +import { faker } from "@faker-js/faker"; + +import { NotionToggleBlock } from "../src/types/notion"; + +import { mockUser } from "./User.mock"; +import { mockRichText } from "./RichText.mock"; + +export function mockToggleBlock(): NotionToggleBlock { + const user = mockUser(); + + return { + object: "block", + id: faker.datatype.uuid(), + created_time: "2022-04-04T19:49:00.000Z", + last_edited_time: "2022-04-04T19:50:00.000Z", + created_by: user, + last_edited_by: user, + has_children: false, + archived: false, + type: "toggle", + toggle: { rich_text: [mockRichText()], color: "default" }, + }; +} diff --git a/packages/notion-post-publisher/__tests__/lib/Post.spec.ts b/packages/notion-post-publisher/__tests__/lib/Post.spec.ts index b2feb5e5..af7a5633 100644 --- a/packages/notion-post-publisher/__tests__/lib/Post.spec.ts +++ b/packages/notion-post-publisher/__tests__/lib/Post.spec.ts @@ -1,6 +1,6 @@ +import { format as formatDate } from "date-fns"; import fs from "fs"; import path from "path"; -import { Post } from "../../src/lib/Post"; import { mockBulletedListItemBlock, mockNumberedListItemBlock, @@ -8,11 +8,12 @@ import { mockPagePropertiesResponse, mockParagraphBlock, } from "../../__mocks__"; +import { Block, CreatableBlock } from "../../src/lib/Block"; +import { Post } from "../../src/lib/Post"; import { getAllPageBlocks, getPageProperties, } from "../../src/utils/notion-utils"; -import { Block, CreatableBlock } from "../../src/lib/Block"; jest.mock("../../src/utils/notion-utils", () => { return { getAllPageBlocks: jest.fn(), getPageProperties: jest.fn() }; @@ -44,7 +45,7 @@ describe("Post", () => { title: "Hello World", }); const post = await Post.create("SOME_PAGE_ID"); - const dateStr = new Date().toISOString().split("T")[0]; + const dateStr = formatDate(new Date(), "yyyy-MM-dd"); const slug = "hello-world"; const expFilename = `${dateStr}-${slug}.md`; expect(post.date).toEqual(dateStr); diff --git a/packages/notion-post-publisher/__tests__/lib/blocks/EmbedBlock.spec.ts b/packages/notion-post-publisher/__tests__/lib/blocks/EmbedBlock.spec.ts index 70c42701..5678de9c 100644 --- a/packages/notion-post-publisher/__tests__/lib/blocks/EmbedBlock.spec.ts +++ b/packages/notion-post-publisher/__tests__/lib/blocks/EmbedBlock.spec.ts @@ -14,30 +14,16 @@ describe("EmbedBlock", () => { }); it("Supports twitter embeds", async () => { const data = mockEmbedBlock({ - url: "https://twitter.com/seancdavis29/status/1550468441533870080", + url: "https://twitter.com/seancdavis29/status/1756294848431149188", }); const block = new EmbedBlock(data); - await block.prerender(); const result = block.render(); expect(result).toBe( prettier.format( `
-

- Every time I get close to wrapping up a project working with a new designer, - I’m reminded of the benefit of considering extremes early on. We require so - much flexibility and variability today that it’s impossible to capture a - single, idealistic design. https://t.co/qTphiBEbNf -

- — Sean C Davis (@seancdavis29) - July 22, 2022 -
- `, + + + `, { parser: "html" } ) ); @@ -47,7 +33,6 @@ describe("EmbedBlock", () => { "https://stackblitz.com/edit/nextjs-ehvtnq?ctl=1&embed=1&file=components/Link.jsx"; const data = mockEmbedBlock({ url }); const block = new EmbedBlock(data); - await block.prerender(); const result = block.render(); expect(result).toBe(`{% code_playground url="${url}" %}`); }); diff --git a/packages/notion-post-publisher/__tests__/lib/blocks/ToggleBlock.spec.ts b/packages/notion-post-publisher/__tests__/lib/blocks/ToggleBlock.spec.ts new file mode 100644 index 00000000..8711f534 --- /dev/null +++ b/packages/notion-post-publisher/__tests__/lib/blocks/ToggleBlock.spec.ts @@ -0,0 +1,11 @@ +import { ToggleBlock } from "../../../src/lib/blocks/ToggleBlock"; +import { mockToggleBlock } from "../../../__mocks__/ToggleBlock.mock"; + +describe("ToggleBlock", () => { + it("Renders nothing", () => { + const data = mockToggleBlock(); + const block = new ToggleBlock(data); + const result = block.render(); + expect(result).toBeNull(); + }); +}); diff --git a/packages/notion-post-publisher/dist/lib/Block.js b/packages/notion-post-publisher/dist/lib/Block.js index 6261c6c2..9268e0ca 100644 --- a/packages/notion-post-publisher/dist/lib/Block.js +++ b/packages/notion-post-publisher/dist/lib/Block.js @@ -26,6 +26,7 @@ const BlockMap = { paragraph: blocks_1.ParagraphBlock, quote: blocks_1.QuoteBlock, table_of_contents: blocks_1.TableOfContentsBlock, + toggle: blocks_1.ToggleBlock, video: blocks_1.VideoBlock, }; class Block { diff --git a/packages/notion-post-publisher/dist/lib/blocks/EmbedBlock.js b/packages/notion-post-publisher/dist/lib/blocks/EmbedBlock.js index abbf8894..d3ad0831 100644 --- a/packages/notion-post-publisher/dist/lib/blocks/EmbedBlock.js +++ b/packages/notion-post-publisher/dist/lib/blocks/EmbedBlock.js @@ -13,7 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); exports.EmbedBlock = void 0; -const twitter_api_sdk_1 = require("twitter-api-sdk"); +const axios_1 = __importDefault(require("axios")); const date_fns_1 = require("date-fns"); const prettier_1 = __importDefault(require("prettier")); class EmbedBlock { @@ -51,31 +51,46 @@ class TwitterEmbedBlock { this.id = id; } prerender() { - var _a, _b, _c, _d, _e; return __awaiter(this, void 0, void 0, function* () { - const client = new twitter_api_sdk_1.Client(process.env.TWITTER_BEARER_TOKEN); - const tweet = yield client.tweets.findTweetById(this.id, { - "tweet.fields": ["created_at", "text", "author_id"], - }); - if (!((_a = tweet.data) === null || _a === void 0 ? void 0 : _a.author_id) || - !((_b = tweet.data) === null || _b === void 0 ? void 0 : _b.created_at) || - !((_c = tweet.data) === null || _c === void 0 ? void 0 : _c.text)) { - throw new Error(`Could not find appropriate attributes for tweet: ${this.id}`); - } - const author = yield client.users.findUserById(tweet.data.author_id, { - "user.fields": ["name", "username"], - }); - if (!((_d = author.data) === null || _d === void 0 ? void 0 : _d.name) || !((_e = author.data) === null || _e === void 0 ? void 0 : _e.username)) { - throw new Error(`Could not find appropriate attributes for author: ${tweet.data.author_id}`); - } - this.tweet = { - created_at: new Date(tweet.data.created_at), - text: tweet.data.text, - author: { - name: author.data.name, - username: author.data.username, - }, - }; + // const client = new Client(process.env.TWITTER_BEARER_TOKEN!); + // console.log(process.env.TWITTER_BEARER_TOKEN); + const tweetUrl = `https://twitter.com/_/status/${this.id}`; + const response = yield axios_1.default.get(tweetUrl); + console.log(response.data, response.status); + // let tweet: any; + // try { + // tweet = await client.tweets.findTweetById(this.id, { + // "tweet.fields": ["created_at", "text", "author_id"], + // }); + // } catch (error) { + // console.log(error); + // throw new Error(`Could not find tweet: ${this.id}`); + // } + // if ( + // !tweet.data?.author_id || + // !tweet.data?.created_at || + // !tweet.data?.text + // ) { + // throw new Error( + // `Could not find appropriate attributes for tweet: ${this.id}` + // ); + // } + // const author = await client.users.findUserById(tweet.data.author_id, { + // "user.fields": ["name", "username"], + // }); + // if (!author.data?.name || !author.data?.username) { + // throw new Error( + // `Could not find appropriate attributes for author: ${tweet.data.author_id}` + // ); + // } + // this.tweet = { + // created_at: new Date(tweet.data.created_at), + // text: tweet.data.text, + // author: { + // name: author.data.name, + // username: author.data.username, + // }, + // }; }); } render() { diff --git a/packages/notion-post-publisher/dist/lib/blocks/ToggleBlock.js b/packages/notion-post-publisher/dist/lib/blocks/ToggleBlock.js new file mode 100644 index 00000000..c12d03a1 --- /dev/null +++ b/packages/notion-post-publisher/dist/lib/blocks/ToggleBlock.js @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ToggleBlock = void 0; +class ToggleBlock { + constructor(_) { } + render() { + return null; + } +} +exports.ToggleBlock = ToggleBlock; diff --git a/packages/notion-post-publisher/dist/lib/blocks/index.js b/packages/notion-post-publisher/dist/lib/blocks/index.js index 941e8542..cbfba0cf 100644 --- a/packages/notion-post-publisher/dist/lib/blocks/index.js +++ b/packages/notion-post-publisher/dist/lib/blocks/index.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.VideoBlock = exports.TableOfContentsBlock = exports.QuoteBlock = exports.ParagraphBlock = exports.NumberedListItemBlock = exports.ImageBlock = exports.Heading3Block = exports.Heading2Block = exports.Heading1Block = exports.EmbedBlock = exports.DividerBlock = exports.CodeBlock = exports.ChildPageBlock = exports.CalloutBlock = exports.BulletedListItemBlock = void 0; +exports.VideoBlock = exports.ToggleBlock = exports.TableOfContentsBlock = exports.QuoteBlock = exports.ParagraphBlock = exports.NumberedListItemBlock = exports.ImageBlock = exports.Heading3Block = exports.Heading2Block = exports.Heading1Block = exports.EmbedBlock = exports.DividerBlock = exports.CodeBlock = exports.ChildPageBlock = exports.CalloutBlock = exports.BulletedListItemBlock = void 0; var BulletedListItemBlock_1 = require("./BulletedListItemBlock"); Object.defineProperty(exports, "BulletedListItemBlock", { enumerable: true, get: function () { return BulletedListItemBlock_1.BulletedListItemBlock; } }); var CalloutBlock_1 = require("./CalloutBlock"); @@ -29,5 +29,7 @@ var QuoteBlock_1 = require("./QuoteBlock"); Object.defineProperty(exports, "QuoteBlock", { enumerable: true, get: function () { return QuoteBlock_1.QuoteBlock; } }); var TableOfContentsBlock_1 = require("./TableOfContentsBlock"); Object.defineProperty(exports, "TableOfContentsBlock", { enumerable: true, get: function () { return TableOfContentsBlock_1.TableOfContentsBlock; } }); +var ToggleBlock_1 = require("./ToggleBlock"); +Object.defineProperty(exports, "ToggleBlock", { enumerable: true, get: function () { return ToggleBlock_1.ToggleBlock; } }); var VideoBlock_1 = require("./VideoBlock"); Object.defineProperty(exports, "VideoBlock", { enumerable: true, get: function () { return VideoBlock_1.VideoBlock; } }); diff --git a/packages/notion-post-publisher/dist/utils/render-utils.js b/packages/notion-post-publisher/dist/utils/render-utils.js index c92861cb..0dda56dc 100644 --- a/packages/notion-post-publisher/dist/utils/render-utils.js +++ b/packages/notion-post-publisher/dist/utils/render-utils.js @@ -1,6 +1,10 @@ "use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.renderBlocks = exports.trailingNewlines = exports.renderRichText = void 0; +const chalk_1 = __importDefault(require("chalk")); const blocks_1 = require("../lib/blocks"); /** * Given a an array of rich text objects from Notion, return a markdown string. @@ -30,6 +34,13 @@ function sanitizeText(text) { * @returns {string} Text to render to the markdown file. */ function renderRichTextItem(richText) { + // A mention appears as a link, but the link is to a Notion page, which is + // likely inaccessible to the public. We log and ignore these for now. + if (richText.type === "mention") { + const msg = `Mention found: ${richText.plain_text} (${richText.href})`; + console.log(chalk_1.default.cyan.bold("[info]"), msg); + return ""; + } if (richText.type !== "text") { throw new Error(`Rich text type not supported: ${richText.type}`); } diff --git a/packages/notion-post-publisher/jest.config.js b/packages/notion-post-publisher/jest.config.js index f9e616ef..1f6e666d 100644 --- a/packages/notion-post-publisher/jest.config.js +++ b/packages/notion-post-publisher/jest.config.js @@ -3,4 +3,7 @@ module.exports = { preset: "ts-jest", testEnvironment: "node", setupFiles: ["/__mocks__/s3-utils.ts"], + moduleNameMapper: { + "^axios$": "axios/dist/node/axios.cjs", + }, }; diff --git a/packages/notion-post-publisher/package.json b/packages/notion-post-publisher/package.json index c58c1b6d..c204261d 100644 --- a/packages/notion-post-publisher/package.json +++ b/packages/notion-post-publisher/package.json @@ -17,8 +17,7 @@ "fast-glob": "^3.2.12", "js-yaml": "^4.1.0", "prettier": "^2.8.2", - "slugify": "^1.6.5", - "twitter-api-sdk": "^1.2.1" + "slugify": "^1.6.5" }, "scripts": { "build": "tsc", diff --git a/packages/notion-post-publisher/src/lib/Block.ts b/packages/notion-post-publisher/src/lib/Block.ts index b71d070b..5f4e12cf 100644 --- a/packages/notion-post-publisher/src/lib/Block.ts +++ b/packages/notion-post-publisher/src/lib/Block.ts @@ -14,6 +14,7 @@ import type { NotionParagraphBlock, NotionQuoteBlock, NotionTableOfContentsBlock, + NotionToggleBlock, NotionVideoBlock, } from "../types/notion"; @@ -32,6 +33,7 @@ import { ParagraphBlock, QuoteBlock, TableOfContentsBlock, + ToggleBlock, VideoBlock, } from "./blocks"; @@ -50,6 +52,7 @@ type SupportedNotionBlocks = | NotionParagraphBlock | NotionQuoteBlock | NotionTableOfContentsBlock + | NotionToggleBlock | NotionVideoBlock; const BlockMap = { @@ -67,6 +70,7 @@ const BlockMap = { paragraph: ParagraphBlock, quote: QuoteBlock, table_of_contents: TableOfContentsBlock, + toggle: ToggleBlock, video: VideoBlock, }; @@ -82,6 +86,7 @@ export type CreatableBlock = | Heading2Block | Heading3Block | ImageBlock + | ToggleBlock | NumberedListItemBlock | ParagraphBlock | QuoteBlock diff --git a/packages/notion-post-publisher/src/lib/blocks/EmbedBlock.ts b/packages/notion-post-publisher/src/lib/blocks/EmbedBlock.ts index 1db94c0f..adedd11c 100644 --- a/packages/notion-post-publisher/src/lib/blocks/EmbedBlock.ts +++ b/packages/notion-post-publisher/src/lib/blocks/EmbedBlock.ts @@ -1,5 +1,3 @@ -import { Client } from "twitter-api-sdk"; -import { format as formatDate } from "date-fns"; import prettier from "prettier"; import type { NotionEmbedBlock } from "../../types/notion"; @@ -15,11 +13,11 @@ export class EmbedBlock { } } - async prerender() { - if (this.embedBlock && "prerender" in this.embedBlock) { - await this.embedBlock.prerender(); - } - } + // async prerender() { + // if (this.embedBlock && "prerender" in this.embedBlock) { + // await this.embedBlock.prerender(); + // } + // } render() { if (!this.embedBlock) { @@ -53,54 +51,10 @@ class TwitterEmbedBlock { this.id = id; } - async prerender() { - const client = new Client(process.env.TWITTER_BEARER_TOKEN!); - const tweet = await client.tweets.findTweetById(this.id, { - "tweet.fields": ["created_at", "text", "author_id"], - }); - if ( - !tweet.data?.author_id || - !tweet.data?.created_at || - !tweet.data?.text - ) { - throw new Error( - `Could not find appropriate attributes for tweet: ${this.id}` - ); - } - const author = await client.users.findUserById(tweet.data.author_id, { - "user.fields": ["name", "username"], - }); - if (!author.data?.name || !author.data?.username) { - throw new Error( - `Could not find appropriate attributes for author: ${tweet.data.author_id}` - ); - } - this.tweet = { - created_at: new Date(tweet.data.created_at), - text: tweet.data.text, - author: { - name: author.data.name, - username: author.data.username, - }, - }; - } - render() { - if (!this.tweet) { - throw new Error(`Tweet not properly prerendered: ${this.id}`); - } - const output = `
-

- ${this.tweet.text} -

- — ${this.tweet.author.name} (@${ - this.tweet.author.username - }) ${formatDate( - this.tweet.created_at, - "MMMM d, yyyy" - )} +
`; diff --git a/packages/notion-post-publisher/src/lib/blocks/ToggleBlock.ts b/packages/notion-post-publisher/src/lib/blocks/ToggleBlock.ts new file mode 100644 index 00000000..1a9931f7 --- /dev/null +++ b/packages/notion-post-publisher/src/lib/blocks/ToggleBlock.ts @@ -0,0 +1,9 @@ +import type { NotionToggleBlock } from "../../types/notion"; + +export class ToggleBlock { + constructor(_: NotionToggleBlock) {} + + render() { + return null; + } +} diff --git a/packages/notion-post-publisher/src/lib/blocks/index.ts b/packages/notion-post-publisher/src/lib/blocks/index.ts index 0daa1bfa..c62c5bf4 100644 --- a/packages/notion-post-publisher/src/lib/blocks/index.ts +++ b/packages/notion-post-publisher/src/lib/blocks/index.ts @@ -12,4 +12,5 @@ export { NumberedListItemBlock } from "./NumberedListItemBlock"; export { ParagraphBlock } from "./ParagraphBlock"; export { QuoteBlock } from "./QuoteBlock"; export { TableOfContentsBlock } from "./TableOfContentsBlock"; +export { ToggleBlock } from "./ToggleBlock"; export { VideoBlock } from "./VideoBlock"; diff --git a/packages/notion-post-publisher/src/types/notion.ts b/packages/notion-post-publisher/src/types/notion.ts index ed2b4e20..b3eb1fc9 100644 --- a/packages/notion-post-publisher/src/types/notion.ts +++ b/packages/notion-post-publisher/src/types/notion.ts @@ -43,6 +43,8 @@ export type NotionChildPageBlock = Extract; export type NotionEmbedBlock = Extract; +export type NotionToggleBlock = Extract; + /* ----- Shared Types ----- */ // These are extracted from types above, but the objects are the same regardless diff --git a/packages/notion-post-publisher/src/utils/render-utils.ts b/packages/notion-post-publisher/src/utils/render-utils.ts index ae50bac9..d40e7ca0 100644 --- a/packages/notion-post-publisher/src/utils/render-utils.ts +++ b/packages/notion-post-publisher/src/utils/render-utils.ts @@ -1,3 +1,4 @@ +import chalk from "chalk"; import { CreatableBlock } from "../lib/Block"; import { BulletedListItemBlock, NumberedListItemBlock } from "../lib/blocks"; import { NotionRichText } from "../types/notion"; @@ -31,6 +32,13 @@ function sanitizeText(text: string): string { * @returns {string} Text to render to the markdown file. */ function renderRichTextItem(richText: NotionRichText): string { + // A mention appears as a link, but the link is to a Notion page, which is + // likely inaccessible to the public. We log and ignore these for now. + if (richText.type === "mention") { + const msg = `Mention found: ${richText.plain_text} (${richText.href})`; + console.log(chalk.cyan.bold("[info]"), msg); + return ""; + } if (richText.type !== "text") { throw new Error(`Rich text type not supported: ${richText.type}`); } diff --git a/www/.vscode/settings.json b/www/.vscode/settings.json index 8e8209a1..55d5cc6c 100644 --- a/www/.vscode/settings.json +++ b/www/.vscode/settings.json @@ -16,7 +16,5 @@ "[nunjucks]": { "editor.formatOnSave": false }, - "cSpell.words": [ - "reposts" - ] + "cSpell.words": ["reposts"] } diff --git a/www/netlify.toml b/www/netlify.toml index 32e44dde..c72a0120 100644 --- a/www/netlify.toml +++ b/www/netlify.toml @@ -5,8 +5,12 @@ [[plugins]] package = "@seancdavis/netlify-plugin-jest" -[[plugins]] - package = "@seancdavis/netlify-plugin-github-dispatch" +# Note: This is the portion of the workflow that triggers publishing new tweets +# after a build was completed. The Twitter API no longer works and I can't use +# it programmatically. Tweets will go back to being published manually. +# +# [[plugins]] +# package = "@seancdavis/netlify-plugin-github-dispatch" [[plugins]] package = "netlify-plugin-checklinks" diff --git a/yarn.lock b/yarn.lock index 77539c07..22888f12 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1589,13 +1589,6 @@ abbrev@1: resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" @@ -4087,11 +4080,6 @@ etag@1.8.1, etag@^1.8.1, etag@~1.8.1: resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" @@ -9984,14 +9972,6 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= -twitter-api-sdk@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/twitter-api-sdk/-/twitter-api-sdk-1.2.1.tgz#53ac3f13cdccfdf06ad71fbc7c24a0184cb9bb15" - integrity sha512-tNQ6DGYucFk94JlnUMsHCkHg5o1wnCdHh71Y2ukygNVssOdD1gNVjOpaojJrdwbEAhoZvcWdGHerCa55F8HKxQ== - dependencies: - abort-controller "^3.0.0" - node-fetch "^2.6.1" - twitter-api-v2@^1.14.1: version "1.14.1" resolved "https://registry.yarnpkg.com/twitter-api-v2/-/twitter-api-v2-1.14.1.tgz#fe93b430413706ee102b3b1781cd6c45a313b493"