Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
11cf396b4013d580c8e4eadb06c0a5b540bdbcc2
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ If it doesn't deploy correctly (e.g. `kubectl get pods` shows a status other tha

I actually couldn't get a local registry working so I fell back on using ghcr.io, GitHub container registry.

[Create a Personal Access Token (Classic)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) and log in to ghcr.io. Use the PAT(C) as your password.

```sh
docker login ghcr.io
```

Create a file, `k8s-context`, in the project root, alongside the Dockerfile, with an IMAGE variable for kubectl to use.

```sh
Expand Down
23 changes: 23 additions & 0 deletions cluster/ingress.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: reactibot-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
cert-manager.io/cluster-issuer: letsencrypt-prod # Optional, for TLS
spec:
rules:
- host: api.reactiflux.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: reactibot
port:
number: 80
tls:
- hosts:
- api.reactiflux.com
secretName: my-tls-secret # Used for HTTPS
13 changes: 13 additions & 0 deletions cluster/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: reactibot-service
labels:
app: reactibot
spec:
type: ClusterIP
ports:
- port: 80 # External port
targetPort: 3000 # Port the pod exposes
selector:
app: reactibot
2 changes: 2 additions & 0 deletions kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ namespace: default
commonLabels:
app: reactibot
resources:
- cluster/service.yaml
- cluster/deployment.yaml
- cluster/ingress.yaml

configMapGenerator:
- name: k8s-context # this is an internal name
Expand Down
38 changes: 26 additions & 12 deletions src/features/jobs-moderation/job-mod-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,22 @@ interface StoredMessage {
message: Message;
authorId: Snowflake;
createdAt: Date;
description: string;
tags: string[];
type: PostType;
}
let jobBoardMessageCache: {
forHire: StoredMessage[];
hiring: StoredMessage[];
} = { forHire: [], hiring: [] };

export const getJobPosts = () => {
return {
hiring: jobBoardMessageCache.hiring,
forHire: jobBoardMessageCache.forHire,
};
};

const DAYS_OF_POSTS = 30;

export const loadJobs = async (bot: Client, channel: TextChannel) => {
Expand All @@ -96,18 +105,21 @@ export const loadJobs = async (bot: Client, channel: TextChannel) => {
})
)
// Convert fetched messages to be stored in the cache
.map((message) => ({
message,
authorId: message.author.id,
createdAt: message.createdAt,
// By searching for "hiring", we treat posts without tags as "forhire",
// which makes the subject to deletion after aging out. This will only be
// relevant when this change is first shipped, because afterwards all
// un-tagged posts will be removed.
type: parseContent(message.content)[0].tags.includes("hiring")
? PostType.hiring
: PostType.forHire,
}));
.map((message) => {
const { tags, description } = parseContent(message.content)[0];
return {
message,
authorId: message.author.id,
createdAt: message.createdAt,
description,
tags,
// By searching for "hiring", we treat posts without tags as "forhire",
// which makes the subject to deletion after aging out. This will only be
// relevant when this change is first shipped, because afterwards all
// un-tagged posts will be removed.
type: tags.includes("hiring") ? PostType.hiring : PostType.forHire,
};
});
if (newMessages.length === 0) {
break;
}
Expand Down Expand Up @@ -226,6 +238,8 @@ export const updateJobs = (message: Message) => {
message,
authorId: message.author.id,
createdAt: message.createdAt,
description: parsed.description,
tags: parsed.tags,
type,
});

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
} from "./features/debug-events.js";
import { recommendBookCommand } from "./features/book-list.js";
import { mdnSearch } from "./features/mdn.js";
import "./server.js";

export const bot = new Client({
intents: [
Expand Down
106 changes: 106 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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";

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

const openApiConfig = {
openapi: "3.0.0",
info: {
title: "Job Board API",
version: "1.0.0",
},
components: {
schemas: {
JobPost: {
type: "object",
required: ["tags", "description", "authorId", "message", "createdAt"],
properties: {
tags: {
type: "array",
items: { type: "string" },
},
description: { type: "string" },
authorId: {
type: "string",
format: "snowflake",
},
message: {
type: "object",
description: "Discord Message object",
},
createdAt: {
type: "string",
format: "date-time",
},
},
},
JobBoardCache: {
type: "object",
required: ["forHire", "hiring"],
properties: {
forHire: {
type: "array",
items: { $ref: "JobPost" },
},
hiring: {
type: "array",
items: { $ref: "JobPost" },
},
},
},
},
},
paths: {
"/jobs": {
get: {
tags: ["jobs"],
summary: "Get all job posts",
responses: {
"200": {
description: "Successful response",
content: {
"application/json": {
schema: {
$ref: "JobBoardCache",
},
},
},
},
},
},
},
},
};

try {
Object.entries(openApiConfig.components.schemas).forEach(([k, schema]) => {
fastify.addSchema({ ...schema, $id: k });
});
// @ts-expect-error something's busted but it works
await fastify.register(swagger, openApiConfig);
await fastify.register(cors);
await fastify.register(helmet);
} catch (e) {
console.log(e);
}

fastify.get(
"/jobs",
{
schema: {
response: {
200: {
$ref: "JobBoardCache",
},
},
},
},
async () => {
return getJobPosts();
},
);

await fastify.listen({ port: 3000 });
Loading