Skip to content

Commit 2f8c57e

Browse files
committed
feat: create api route
1 parent 58a4fb7 commit 2f8c57e

8 files changed

Lines changed: 165 additions & 23 deletions

File tree

packages/private/core/src/types/entities.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
44
: ColumnType<T, T | undefined, T>;
55
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
66

7-
export type AmaQuestion = {
7+
export type AMAQuestion = {
88
id: Generated<number>;
99
amaId: number;
1010
authorId: string;
@@ -15,11 +15,11 @@ export type AmaQuestion = {
1515
export type AMASession = {
1616
id: Generated<number>;
1717
guildId: string;
18-
modQueue: string | null;
19-
flaggedQueue: string | null;
20-
guestQueue: string | null;
18+
modQueueId: string | null;
19+
flaggedQueueId: string | null;
20+
guestQueueId: string | null;
2121
title: string;
22-
answersChannel: string;
22+
answersChannelId: string;
2323
promptChannelId: string;
2424
promptMessageId: string;
2525
ended: Generated<boolean>;
@@ -38,7 +38,7 @@ export type ExperimentOverride = {
3838
experimentName: string;
3939
};
4040
export type DB = {
41-
AmaQuestion: AmaQuestion;
41+
AMAQuestion: AMAQuestion;
4242
AMASession: AMASession;
4343
Experiment: Experiment;
4444
ExperimentOverride: ExperimentOverride;

prisma/schema.prisma

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,22 @@ model ExperimentOverride {
3232
// SECTION ama bot
3333

3434
model AMASession {
35-
id Int @id @default(autoincrement())
36-
guildId String
37-
modQueue String?
38-
flaggedQueue String?
39-
guestQueue String?
40-
title String
41-
answersChannel String
42-
promptChannelId String
43-
promptMessageId String @unique
44-
ended Boolean @default(false)
45-
createdAt DateTime @default(now())
46-
47-
questions AmaQuestion[]
35+
id Int @id @default(autoincrement())
36+
guildId String
37+
modQueueId String?
38+
flaggedQueueId String?
39+
guestQueueId String?
40+
title String
41+
answersChannelId String
42+
promptChannelId String
43+
promptMessageId String @unique
44+
ended Boolean @default(false)
45+
createdAt DateTime @default(now())
46+
47+
questions AMAQuestion[]
4848
}
4949

50-
model AmaQuestion {
50+
model AMAQuestion {
5151
id Int @id @default(autoincrement())
5252
amaId Int
5353
ama AMASession @relation(fields: [amaId], references: [id], onDelete: Cascade)

services/api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@discordjs/core": "^3.0.0-dev.1759363313-f510b5ffa",
3232
"@discordjs/rest": "^3.0.0-dev.1759363313-f510b5ffa",
3333
"@hapi/boom": "^10.0.1",
34+
"@sapphire/discord-utilities": "^3.5.0",
3435
"bcrypt": "^6.0.0",
3536
"busboy": "^1.6.0",
3637
"cookie": "^1.0.2",
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import type { AMASession } from '@chatsift/core';
2+
import type { RESTPostAPIChannelMessageJSONBody } from '@discordjs/core';
3+
import { ButtonStyle, ComponentType } from '@discordjs/core';
4+
import { DiscordAPIError } from '@discordjs/rest';
5+
import { badRequest } from '@hapi/boom';
6+
import type { Selectable } from 'kysely';
7+
import type { NextHandler, Response } from 'polka';
8+
import { z } from 'zod';
9+
import { context } from '../../context.js';
10+
import { isAuthed } from '../../middleware/isAuthed.js';
11+
import { discordAPIAma } from '../../util/discordAPI.js';
12+
import { snowflakeSchema } from '../../util/schemas.js';
13+
import type { TRequest } from '../route.js';
14+
import { Route, RouteMethod } from '../route.js';
15+
16+
const promptSchema = z.union([
17+
z.strictObject({
18+
prompt: z
19+
.object({
20+
description: z.string().max(4_000).optional(),
21+
plainText: z.string().max(100).optional(),
22+
imageURL: z.url().optional(),
23+
thumbnailURL: z.url().optional(),
24+
})
25+
.strict(),
26+
}),
27+
z.strictObject({
28+
prompt_raw: z.strictObject({
29+
content: z.string().optional(),
30+
embeds: z.array(z.any()).optional(),
31+
}),
32+
}),
33+
]);
34+
35+
const bodySchema = z.intersection(
36+
z.strictObject({
37+
modQueueId: snowflakeSchema.nullable(),
38+
flaggedQueueId: snowflakeSchema.nullable(),
39+
guestQueueId: snowflakeSchema.nullable(),
40+
title: z.string().min(1).max(255),
41+
answersChannelId: snowflakeSchema,
42+
promptChannelId: snowflakeSchema,
43+
}),
44+
promptSchema,
45+
);
46+
47+
export type CreateAMABody = z.infer<typeof bodySchema>;
48+
49+
export type CreateAMAResult = Selectable<AMASession>;
50+
51+
export default class CreateAMA extends Route<CreateAMAResult, CreateAMABody> {
52+
public readonly info = {
53+
method: RouteMethod.post,
54+
path: '/v3/guilds/:guildId/ama/amas',
55+
} as const;
56+
57+
public override readonly bodyValidationSchema = bodySchema;
58+
59+
public override readonly middleware = [
60+
...isAuthed({ fallthrough: false, isGlobalAdmin: false, isGuildManager: true }),
61+
];
62+
63+
public override async handle(req: TRequest<CreateAMABody>, res: Response, next: NextHandler) {
64+
const data = req.body as CreateAMABody;
65+
const { guildId } = req.params as { guildId: string };
66+
67+
// TODO(DD): Reconsider?
68+
const messageBodyBase: RESTPostAPIChannelMessageJSONBody =
69+
'prompt_raw' in data
70+
? data.prompt_raw
71+
: {
72+
content: data.prompt.plainText,
73+
embeds: [
74+
{
75+
color: 0x7289da, // blurple
76+
title: data.title,
77+
description: data.prompt.description,
78+
image: data.prompt.imageURL ? { url: data.prompt.imageURL } : undefined,
79+
thumbnail: data.prompt.thumbnailURL ? { url: data.prompt.thumbnailURL } : undefined,
80+
timestamp: new Date().toISOString(),
81+
},
82+
],
83+
};
84+
85+
let promptMessage;
86+
try {
87+
promptMessage = await discordAPIAma.channels.createMessage(data.promptChannelId, {
88+
...messageBodyBase,
89+
components: [
90+
{
91+
type: ComponentType.ActionRow,
92+
components: [
93+
{
94+
type: ComponentType.Button,
95+
style: ButtonStyle.Primary,
96+
label: 'Submit a question',
97+
custom_id: 'submit-question',
98+
},
99+
],
100+
},
101+
],
102+
});
103+
} catch (error) {
104+
if (error instanceof DiscordAPIError && error.status === 400 && 'prompt_raw' in data) {
105+
return next(badRequest('invalid prompt_raw data'));
106+
}
107+
108+
throw error;
109+
}
110+
111+
const created: CreateAMAResult = await context.db
112+
.insertInto('AMASession')
113+
.values({
114+
guildId,
115+
title: data.title,
116+
answersChannelId: data.answersChannelId,
117+
promptChannelId: data.promptChannelId,
118+
promptMessageId: promptMessage.id,
119+
modQueueId: data.modQueueId,
120+
flaggedQueueId: data.flaggedQueueId,
121+
guestQueueId: data.guestQueueId,
122+
ended: false,
123+
createdAt: new Date(),
124+
})
125+
.returningAll()
126+
.executeTakeFirstOrThrow();
127+
128+
res.statusCode = 200;
129+
res.setHeader('Content-Type', 'application/json');
130+
return res.end(JSON.stringify(created));
131+
}
132+
}

services/api/src/routes/ama/getAMAs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export default class GetAMAs extends Route<AMASessionWithCount[], GetAMAsQuery>
4747
const sessionIds = sessions.map((s) => s.id);
4848
const questionCounts = sessionIds.length
4949
? await context.db
50-
.selectFrom('AmaQuestion')
50+
.selectFrom('AMAQuestion')
5151
.select(['amaId'])
5252
.select((eb) => eb.fn.count<string>('id').as('count'))
5353
.where('amaId', 'in', sessionIds)
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { API } from '@discordjs/core';
22
import { REST } from '@discordjs/rest';
3+
import { context } from '../context.js';
34

4-
const rest = new REST({ version: '10' });
5-
export const discordAPIOAuth = new API(rest);
5+
const oauthREST = new REST({ version: '10' });
6+
export const discordAPIOAuth = new API(oauthREST);
7+
8+
const amaREST = new REST({ version: '10' }).setToken(context.env.AMA_BOT_TOKEN);
9+
export const discordAPIAma = new API(amaREST);

services/api/src/util/schemas.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { SnowflakeRegex } from '@sapphire/discord-utilities';
2+
import z from 'zod';
3+
4+
export const snowflakeSchema = z.string().regex(SnowflakeRegex);

yarn.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,7 @@ __metadata:
565565
"@discordjs/core": "npm:^3.0.0-dev.1759363313-f510b5ffa"
566566
"@discordjs/rest": "npm:^3.0.0-dev.1759363313-f510b5ffa"
567567
"@hapi/boom": "npm:^10.0.1"
568+
"@sapphire/discord-utilities": "npm:^3.5.0"
568569
"@types/bcrypt": "npm:^6.0.0"
569570
"@types/busboy": "npm:^1.5.4"
570571
"@types/cors": "npm:^2.8.19"

0 commit comments

Comments
 (0)