diff --git a/.github/workflows/codesee-arch-diagram.yml b/.github/workflows/codesee-arch-diagram.yml deleted file mode 100644 index 01d7f7e6..00000000 --- a/.github/workflows/codesee-arch-diagram.yml +++ /dev/null @@ -1,22 +0,0 @@ -# This workflow was added by CodeSee. Learn more at https://codesee.io/ -# This is v2.0 of this workflow file -on: - push: - branches: - - main - pull_request_target: - types: [opened, synchronize, reopened] - -name: CodeSee - -permissions: read-all - -jobs: - codesee: - runs-on: ubuntu-latest - continue-on-error: true - name: Analyze the repo with CodeSee - steps: - - uses: Codesee-io/codesee-action@v2 - with: - codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} diff --git a/package-lock.json b/package-lock.json index c7fbf613..0229d1f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "fastify": "^5.2.0", "gists": "2.0.0", "lru-cache": "10.2.2", + "marked": "^15.0.6", "node-cron": "3.0.3", "node-fetch": "2.6.12", "open-graph-scraper": "6.5.2", @@ -28,7 +29,8 @@ "pino": "^9.6.0", "pino-pretty": "^13.0.0", "query-string": "7.1.3", - "uuid": "9.0.1" + "uuid": "9.0.1", + "xss": "^1.0.15" }, "devDependencies": { "@types/node": "20.14.2", @@ -2014,6 +2016,12 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2081,6 +2089,12 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", + "license": "MIT" + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -4212,6 +4226,18 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/marked": { + "version": "15.0.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.6.tgz", + "integrity": "sha512-Y07CUOE+HQXbVDCGl3LXggqJDbXDP2pArc2C1N1RRMN0ONiShoSsIInMd5Gsxupe7fKLpgimTV+HOJ9r7bA+pg==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -7180,6 +7206,22 @@ } } }, + "node_modules/xss": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", + "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", + "license": "MIT", + "dependencies": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "bin": { + "xss": "bin/xss" + }, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 71e73ee6..237385be 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "fastify": "^5.2.0", "gists": "2.0.0", "lru-cache": "10.2.2", + "marked": "^15.0.6", "node-cron": "3.0.3", "node-fetch": "2.6.12", "open-graph-scraper": "6.5.2", @@ -40,7 +41,8 @@ "pino": "^9.6.0", "pino-pretty": "^13.0.0", "query-string": "7.1.3", - "uuid": "9.0.1" + "uuid": "9.0.1", + "xss": "^1.0.15" }, "devDependencies": { "@types/node": "20.14.2", diff --git a/src/features/jobs-moderation/job-mod-helpers.ts b/src/features/jobs-moderation/job-mod-helpers.ts index 75e10755..62ded1fa 100644 --- a/src/features/jobs-moderation/job-mod-helpers.ts +++ b/src/features/jobs-moderation/job-mod-helpers.ts @@ -65,7 +65,7 @@ export const failedTooManyEmojis = ( ): e is PostFailureTooManyEmojis => e.type === POST_FAILURE_REASONS.tooManyEmojis; -interface StoredMessage { +export interface StoredMessage { message: Message; authorId: Snowflake; createdAt: Date; diff --git a/src/features/jobs-moderation/parse-content.test.ts b/src/features/jobs-moderation/parse-content.test.ts index 36baa527..09e71aaa 100644 --- a/src/features/jobs-moderation/parse-content.test.ts +++ b/src/features/jobs-moderation/parse-content.test.ts @@ -10,7 +10,7 @@ describe("parseContent", () => { ); expectTypeOf(parsed).toBeArray(); expect(parsed[0]).toMatchObject({ - tags: ["company", "jobtitle", "location", "compensation", "jobtype"], + tags: ["company", "job title", "location", "compensation", "job type"], }); const emptyTags = ["|", "|||", "[]", " [ ] "]; @@ -27,7 +27,6 @@ describe("parseContent", () => { "[forhire]", "for hire|", "|for hire|", - "[f o r h i r e]", "|FoRhIrE|", ]; validForHireTags.forEach((tag) => { @@ -42,7 +41,6 @@ describe("parseContent", () => { "[hire]", "[HIRE]", "hiring|", - "|h i r i n g|", "|HiRiNg|", ]; validHiringTags.forEach((tag) => { @@ -50,6 +48,23 @@ describe("parseContent", () => { expect(parsed).toMatchObject({ tags: [PostType.hiring] }); }); }); + it("fancy", () => { + const parsed = parseContent( + "Company | Job Title | Location | Compensation | Job Type | Part time / full time", + ); + expectTypeOf(parsed).toBeArray(); + expect(parsed[0]).toMatchObject({ + tags: [ + "company", + "job title", + "location", + "compensation", + "job type", + "part time", + "full time", + ], + }); + }); }); it("parses description", () => { let parsed = parseContent(`[hiring] diff --git a/src/features/jobs-moderation/parse-content.ts b/src/features/jobs-moderation/parse-content.ts index 81a7cbd8..ff2f78b5 100644 --- a/src/features/jobs-moderation/parse-content.ts +++ b/src/features/jobs-moderation/parse-content.ts @@ -8,12 +8,17 @@ type StandardTag = string; // interpreting compensation, all sorts of fun follow ons. const tagMap = new Map StandardTag>([ ["forhire", () => PostType.forHire], + ["for hire", () => PostType.forHire], ["hiring", () => PostType.hiring], ["hire", () => PostType.hiring], ]); -const standardizeTag = (tag: string) => { - const simpleTag = simplifyString(tag).replace(/\W/g, ""); +const standardizeTag = (tag: string): string | string[] => { + if (tag.includes("/")) { + return tag.split("/").flatMap(standardizeTag); + } + + const simpleTag = simplifyString(tag).replace(/\W+/g, " ").trim(); const standardTagBuilder = tagMap.get(simpleTag); return standardTagBuilder?.(simpleTag) ?? simpleTag; }; @@ -21,7 +26,7 @@ const standardizeTag = (tag: string) => { export const parseTags = (tags: string) => { return tags .split(/[|[\]]/g) - .map((tag) => standardizeTag(tag.trim())) + .flatMap((tag) => standardizeTag(tag.trim())) .filter((tag) => tag !== ""); }; diff --git a/src/helpers/string.ts b/src/helpers/string.ts index d4f8a9de..2a0dbca2 100644 --- a/src/helpers/string.ts +++ b/src/helpers/string.ts @@ -16,3 +16,11 @@ export const extractEmoji = (s: string) => s.match(EMOJI_RANGE) || []; const NEWLINE = /\n/g; export const countLines = (s: string) => s.match(NEWLINE)?.length || 0; + +const DOUBLE_NEWLINE = /\n\n/g; +export const compressLineBreaks = (s: string) => { + while (DOUBLE_NEWLINE.test(s)) { + s = s.replaceAll(DOUBLE_NEWLINE, "\n"); + } + return s; +}; diff --git a/src/server.ts b/src/server.ts index 032efa6c..b31ef9fd 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,7 +2,13 @@ import Fastify from "fastify"; import cors from "@fastify/cors"; import helmet from "@fastify/helmet"; import swagger from "@fastify/swagger"; -import { getJobPosts } from "./features/jobs-moderation/job-mod-helpers.js"; +import { marked } from "marked"; +import xss from "xss"; +import { + StoredMessage, + getJobPosts, +} from "./features/jobs-moderation/job-mod-helpers.js"; +import { compressLineBreaks } from "./helpers/string.js"; const fastify = Fastify({ logger: true }); @@ -23,13 +29,18 @@ const openApiConfig = { items: { type: "string" }, }, description: { type: "string" }, - authorId: { - type: "string", - format: "snowflake", - }, - message: { + author: { type: "object", - description: "Discord Message object", + requried: ["username", "displayName", "avatar"], + properties: { + username: { type: "string" }, + displayName: { type: "string" }, + avatar: { type: "string" }, + }, + }, + reactions: { + type: "array", + items: { type: "string" }, }, createdAt: { type: "string", @@ -99,8 +110,42 @@ fastify.get( }, }, async () => { - return getJobPosts(); + const { hiring, forHire } = getJobPosts(); + + return { hiring: hiring.map(renderPost), forHire: forHire.map(renderPost) }; }, ); +interface RenderedPost extends Omit { + reactions: string[]; + author: { + username: string; + displayName: string; + avatar: string; + }; +} + +const renderPost = (post: StoredMessage): RenderedPost => { + console.log({ + reactions: post.message.reactions.cache.map((r) => r.emoji.name), + }); + return { + ...post, + description: renderMdToHtml(compressLineBreaks(post.description)), + author: { + username: post.message.author.username, + displayName: post.message.author.displayName, + avatar: post.message.author.displayAvatarURL({ + size: 128, + extension: "jpg", + forceStatic: true, + }), + }, + reactions: post.message.reactions.cache.map((r) => r.emoji.name ?? "☐"), + }; +}; + await fastify.listen({ port: 3000, host: "0.0.0.0" }); + +const renderMdToHtml = (md: string) => + xss(marked(md, { async: false, gfm: true })); diff --git a/tsconfig.json b/tsconfig.json index 17a1277c..6fe18bf7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,14 @@ { "compilerOptions": { "target": "ES2022", - "module": "Node16", + "module": "NodeNext", "sourceMap": true, "outDir": "dist", "skipLibCheck": true, "strict": true, "allowJs": false, - "moduleResolution": "node16", + "moduleResolution": "nodenext", + "resolveJsonModule": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true },