Skip to content

Commit babcc41

Browse files
authored
Render jobs in API (#437)
* Render job posts to markdown in the API * Codesee died * Support JSON imports * Update how tags get parsed No more arbitrary spaces (hasn't been necessary, from what I've seen), automatically split any tags where a `/` is found (because I've seen "part time / full time" for instance) * Render message to html, include reactions + author info
1 parent 73a4aa2 commit babcc41

File tree

9 files changed

+137
-41
lines changed

9 files changed

+137
-41
lines changed

.github/workflows/codesee-arch-diagram.yml

Lines changed: 0 additions & 22 deletions
This file was deleted.

package-lock.json

Lines changed: 43 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"fastify": "^5.2.0",
3333
"gists": "2.0.0",
3434
"lru-cache": "10.2.2",
35+
"marked": "^15.0.6",
3536
"node-cron": "3.0.3",
3637
"node-fetch": "2.6.12",
3738
"open-graph-scraper": "6.5.2",
@@ -40,7 +41,8 @@
4041
"pino": "^9.6.0",
4142
"pino-pretty": "^13.0.0",
4243
"query-string": "7.1.3",
43-
"uuid": "9.0.1"
44+
"uuid": "9.0.1",
45+
"xss": "^1.0.15"
4446
},
4547
"devDependencies": {
4648
"@types/node": "20.14.2",

src/features/jobs-moderation/job-mod-helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export const failedTooManyEmojis = (
6565
): e is PostFailureTooManyEmojis =>
6666
e.type === POST_FAILURE_REASONS.tooManyEmojis;
6767

68-
interface StoredMessage {
68+
export interface StoredMessage {
6969
message: Message;
7070
authorId: Snowflake;
7171
createdAt: Date;

src/features/jobs-moderation/parse-content.test.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe("parseContent", () => {
1010
);
1111
expectTypeOf(parsed).toBeArray();
1212
expect(parsed[0]).toMatchObject({
13-
tags: ["company", "jobtitle", "location", "compensation", "jobtype"],
13+
tags: ["company", "job title", "location", "compensation", "job type"],
1414
});
1515

1616
const emptyTags = ["|", "|||", "[]", " [ ] "];
@@ -27,7 +27,6 @@ describe("parseContent", () => {
2727
"[forhire]",
2828
"for hire|",
2929
"|for hire|",
30-
"[f o r h i r e]",
3130
"|FoRhIrE|",
3231
];
3332
validForHireTags.forEach((tag) => {
@@ -42,14 +41,30 @@ describe("parseContent", () => {
4241
"[hire]",
4342
"[HIRE]",
4443
"hiring|",
45-
"|h i r i n g|",
4644
"|HiRiNg|",
4745
];
4846
validHiringTags.forEach((tag) => {
4947
const [parsed] = parseContent(tag);
5048
expect(parsed).toMatchObject({ tags: [PostType.hiring] });
5149
});
5250
});
51+
it("fancy", () => {
52+
const parsed = parseContent(
53+
"Company | Job Title | Location | Compensation | Job Type | Part time / full time",
54+
);
55+
expectTypeOf(parsed).toBeArray();
56+
expect(parsed[0]).toMatchObject({
57+
tags: [
58+
"company",
59+
"job title",
60+
"location",
61+
"compensation",
62+
"job type",
63+
"part time",
64+
"full time",
65+
],
66+
});
67+
});
5368
});
5469
it("parses description", () => {
5570
let parsed = parseContent(`[hiring]

src/features/jobs-moderation/parse-content.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,25 @@ type StandardTag = string;
88
// interpreting compensation, all sorts of fun follow ons.
99
const tagMap = new Map<string, (s: SimplifiedTag) => StandardTag>([
1010
["forhire", () => PostType.forHire],
11+
["for hire", () => PostType.forHire],
1112
["hiring", () => PostType.hiring],
1213
["hire", () => PostType.hiring],
1314
]);
1415

15-
const standardizeTag = (tag: string) => {
16-
const simpleTag = simplifyString(tag).replace(/\W/g, "");
16+
const standardizeTag = (tag: string): string | string[] => {
17+
if (tag.includes("/")) {
18+
return tag.split("/").flatMap(standardizeTag);
19+
}
20+
21+
const simpleTag = simplifyString(tag).replace(/\W+/g, " ").trim();
1722
const standardTagBuilder = tagMap.get(simpleTag);
1823
return standardTagBuilder?.(simpleTag) ?? simpleTag;
1924
};
2025

2126
export const parseTags = (tags: string) => {
2227
return tags
2328
.split(/[|[\]]/g)
24-
.map((tag) => standardizeTag(tag.trim()))
29+
.flatMap((tag) => standardizeTag(tag.trim()))
2530
.filter((tag) => tag !== "");
2631
};
2732

src/helpers/string.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,11 @@ export const extractEmoji = (s: string) => s.match(EMOJI_RANGE) || [];
1616

1717
const NEWLINE = /\n/g;
1818
export const countLines = (s: string) => s.match(NEWLINE)?.length || 0;
19+
20+
const DOUBLE_NEWLINE = /\n\n/g;
21+
export const compressLineBreaks = (s: string) => {
22+
while (DOUBLE_NEWLINE.test(s)) {
23+
s = s.replaceAll(DOUBLE_NEWLINE, "\n");
24+
}
25+
return s;
26+
};

src/server.ts

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import Fastify from "fastify";
22
import cors from "@fastify/cors";
33
import helmet from "@fastify/helmet";
44
import swagger from "@fastify/swagger";
5-
import { getJobPosts } from "./features/jobs-moderation/job-mod-helpers.js";
5+
import { marked } from "marked";
6+
import xss from "xss";
7+
import {
8+
StoredMessage,
9+
getJobPosts,
10+
} from "./features/jobs-moderation/job-mod-helpers.js";
11+
import { compressLineBreaks } from "./helpers/string.js";
612

713
const fastify = Fastify({ logger: true });
814

@@ -23,13 +29,18 @@ const openApiConfig = {
2329
items: { type: "string" },
2430
},
2531
description: { type: "string" },
26-
authorId: {
27-
type: "string",
28-
format: "snowflake",
29-
},
30-
message: {
32+
author: {
3133
type: "object",
32-
description: "Discord Message object",
34+
requried: ["username", "displayName", "avatar"],
35+
properties: {
36+
username: { type: "string" },
37+
displayName: { type: "string" },
38+
avatar: { type: "string" },
39+
},
40+
},
41+
reactions: {
42+
type: "array",
43+
items: { type: "string" },
3344
},
3445
createdAt: {
3546
type: "string",
@@ -99,8 +110,42 @@ fastify.get(
99110
},
100111
},
101112
async () => {
102-
return getJobPosts();
113+
const { hiring, forHire } = getJobPosts();
114+
115+
return { hiring: hiring.map(renderPost), forHire: forHire.map(renderPost) };
103116
},
104117
);
105118

119+
interface RenderedPost extends Omit<StoredMessage, "message" | "authorId"> {
120+
reactions: string[];
121+
author: {
122+
username: string;
123+
displayName: string;
124+
avatar: string;
125+
};
126+
}
127+
128+
const renderPost = (post: StoredMessage): RenderedPost => {
129+
console.log({
130+
reactions: post.message.reactions.cache.map((r) => r.emoji.name),
131+
});
132+
return {
133+
...post,
134+
description: renderMdToHtml(compressLineBreaks(post.description)),
135+
author: {
136+
username: post.message.author.username,
137+
displayName: post.message.author.displayName,
138+
avatar: post.message.author.displayAvatarURL({
139+
size: 128,
140+
extension: "jpg",
141+
forceStatic: true,
142+
}),
143+
},
144+
reactions: post.message.reactions.cache.map((r) => r.emoji.name ?? "☐"),
145+
};
146+
};
147+
106148
await fastify.listen({ port: 3000, host: "0.0.0.0" });
149+
150+
const renderMdToHtml = (md: string) =>
151+
xss(marked(md, { async: false, gfm: true }));

tsconfig.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
{
22
"compilerOptions": {
33
"target": "ES2022",
4-
"module": "Node16",
4+
"module": "NodeNext",
55
"sourceMap": true,
66
"outDir": "dist",
77
"skipLibCheck": true,
88
"strict": true,
99
"allowJs": false,
10-
"moduleResolution": "node16",
10+
"moduleResolution": "nodenext",
11+
"resolveJsonModule": true,
1112
"esModuleInterop": true,
1213
"forceConsistentCasingInFileNames": true
1314
},

0 commit comments

Comments
 (0)