diff --git a/.changeset/ten-buses-attend.md b/.changeset/ten-buses-attend.md new file mode 100644 index 0000000..58695bc --- /dev/null +++ b/.changeset/ten-buses-attend.md @@ -0,0 +1,5 @@ +--- +"@everipedia/iq-utils": minor +--- + +Add sendTwitterApiRequest lib function diff --git a/.gitignore b/.gitignore index cfbb41a..b40100d 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ coverage # API keys and secrets .env +.env.local # Dependency directory node_modules diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..e02c24e --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm lint-staged \ No newline at end of file diff --git a/package.json b/package.json index 1eadc03..d53e367 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,12 @@ "lint:fix": "pnpm lint --apply", "watch:build": "tsc -p tsconfig.json -w", "test": "vitest", - "coverage": "vitest run --coverage" + "coverage": "vitest run --coverage", + "prepare": "husky" }, "dependencies": { "axios": "^1.7.7", + "oauth-1.0a": "^2.2.6", "quick-lru": "^7.0.0", "zod": "^3.23.8" }, @@ -29,6 +31,7 @@ "@changesets/cli": "^2.24.0", "@types/node": "^22.7.4", "changeset": "^0.2.6", + "husky": "^9.1.7", "lint-staged": "^15.2.10", "tsup": "^8.3.0", "typescript": "^4.7.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4bc210..23dbb58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: axios: specifier: ^1.7.7 version: 1.7.7 + oauth-1.0a: + specifier: ^2.2.6 + version: 2.2.6 quick-lru: specifier: ^7.0.0 version: 7.0.0 @@ -30,6 +33,9 @@ importers: changeset: specifier: ^0.2.6 version: 0.2.6 + husky: + specifier: ^9.1.7 + version: 9.1.7 lint-staged: specifier: ^15.2.10 version: 15.2.10 @@ -923,6 +929,11 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -1104,6 +1115,9 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + oauth-1.0a@2.2.6: + resolution: {integrity: sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2411,6 +2425,8 @@ snapshots: human-signals@5.0.0: {} + husky@9.1.7: {} + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -2574,6 +2590,8 @@ snapshots: dependencies: path-key: 4.0.0 + oauth-1.0a@2.2.6: {} + object-assign@4.1.1: {} onetime@5.1.2: diff --git a/src/index.ts b/src/index.ts index dc3c643..0756a9a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ export * from "./lib/wiki-score"; export * from "./lib/check-deep-equal"; export * from "./lib/check-wiki-validity"; +export * from "./lib/twitter-api-client"; export * from "./data/constants"; export * from "./schema"; diff --git a/src/lib/twitter-api-client.ts b/src/lib/twitter-api-client.ts new file mode 100644 index 0000000..9365f6d --- /dev/null +++ b/src/lib/twitter-api-client.ts @@ -0,0 +1,72 @@ +import crypto from "node:crypto"; +import OAuth from "oauth-1.0a"; + +type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; + +interface IDiscordLogger { + log(data: DiscordLogData): Promise; +} + +type DiscordLogData = { + message: string; + title?: string; + color?: number; +}; + +export const oauth = new OAuth({ + consumer: { + key: process.env.TWITTER_CONSUMER_KEY ?? "", + secret: process.env.TWITTER_CONSUMER_SECRET ?? "", + }, + signature_method: "HMAC-SHA1", + hash_function: (base_string, key) => { + return crypto.createHmac("sha1", key).update(base_string).digest("base64"); + }, +}); + +const twitterAuthConfig = { + key: process.env.TWITTER_ACCESS_TOKEN ?? "", + secret: process.env.TWITTER_ACCESS_TOKEN_SECRET ?? "", +}; + +export async function sendTwitterApiRequest( + url: string, + method: HttpMethod, + logger?: IDiscordLogger, + body?: string, +): Promise { + if (!url.startsWith("https://api.twitter.com/")) { + throw new Error("Invalid Twitter API URL"); + } + const oauth_headers = oauth.toHeader( + oauth.authorize( + { + url, + method, + }, + twitterAuthConfig, + ), + ); + + const response = await fetch(url, { + method, + headers: { + ...oauth_headers, + "Content-Type": "application/json", + }, + body, + }); + + if (!response.ok) { + const errorBody = await response.text(); + logger?.log({ + message: `🚨 HTTP error! status: ${response.status} ${response.statusText}. Body: ${errorBody}`, + title: "Twitter Authentication: makeAuthenticatedRequest", + }); + throw new Error( + `HTTP error! status: ${response.status} ${response.statusText}. Body: ${errorBody}`, + ); + } + + return response; +} diff --git a/src/lib/wiki-helpers.ts b/src/lib/wiki-helpers.ts index fe4794f..769fd65 100644 --- a/src/lib/wiki-helpers.ts +++ b/src/lib/wiki-helpers.ts @@ -15,6 +15,7 @@ import { MediaType, type MetaData, Tag, + Wiki, } from "../schema"; // =============================== @@ -103,12 +104,15 @@ export function isMediaContentAndCountValid(media: Media[]): boolean { // =============================== // Wiki-specific validation helpers // =============================== -export function isEventWikiValid(wiki: any): boolean { - if (wiki.tags.some((tag: any) => tag.id === "Events")) { +export function isEventWikiValid(wiki: { + tags: { id: string }[]; + metadata: { id: string; value: string }[]; + events: unknown[]; +}): boolean { + if (wiki.tags.some((tag) => tag.id === "Events")) { const referencesData = - wiki.metadata.find( - (meta: any) => meta.id === CommonMetaIds.Enum.references, - )?.value || "[]"; + wiki.metadata.find((meta) => meta.id === CommonMetaIds.Enum.references) + ?.value || "[]"; const references: { description: string }[] = JSON.parse( referencesData, ) as { description: string }[];