Skip to content

Commit 07e3e17

Browse files
committed
feat: bedrock accounts and sibling linking
1 parent 8b81472 commit 07e3e17

13 files changed

Lines changed: 274 additions & 48 deletions

File tree

worker/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"scripts": {
2020
"lint": "eslint src && prettier --check .",
2121
"build": "tsc",
22-
"publish": "wrangler deploy",
22+
"publish": "yarn build && wrangler deploy",
2323
"dev": "wrangler dev",
2424
"schema:dev": "wrangler d1 execute db --local --file=./schema.sql",
2525
"schema": "wrangler d1 execute db --remote --file=./schema.sql",

worker/schema.sql

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
DROP TABLE IF EXISTS linked_accounts;
2-
CREATE TABLE IF NOT EXISTS linked_accounts (
2+
CREATE TABLE linked_accounts (
33
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
44
discord_id TEXT NOT NULL UNIQUE,
5-
minecraft_username TEXT NOT NULL UNIQUE,
6-
confirmed BOOLEAN NOT NULL DEFAULT 0
5+
java_username TEXT UNIQUE,
6+
java_confirmed BOOLEAN NOT NULL DEFAULT 0,
7+
bedrock_username TEXT UNIQUE,
8+
bedrock_confirmed BOOLEAN NOT NULL DEFAULT 0
9+
);
10+
11+
DROP TABLE IF EXISTS linked_siblings;
12+
CREATE TABLE linked_siblings (
13+
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
14+
discord_id TEXT NOT NULL,
15+
sibling_username TEXT NOT NULL UNIQUE
716
);

worker/src/commands/manage.ts

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
} from '@discordjs/core/http-only';
99
import type { API } from '@discordjs/core/http-only';
1010
import { InteractionOptionResolver } from '@sapphire/discord-utilities';
11-
import type { Env, LinkedAccount } from '../util.js';
11+
import type { LinkedSibling } from '../util.js';
12+
import { type Env, type LinkedAccount } from '../util.js';
1213

1314
export const interaction: RESTPostAPIApplicationCommandsJSONBody = {
1415
name: 'manage',
@@ -57,26 +58,53 @@ export async function handle(interaction: APIChatInputApplicationCommandGuildInt
5758
}
5859

5960
await env.DB.prepare('DELETE FROM linked_accounts WHERE discord_id = ?').bind(user.id).run();
61+
await env.DB.prepare('DELETE FROM linked_siblings WHERE discord_id = ?').bind(user.id).run();
6062

6163
return api.interactions.editReply(env.CLIENT_ID, interaction.token, {
6264
content: 'Successfully revoked the connection',
6365
});
6466
}
6567

6668
case 'view': {
67-
const connections = await env.DB.prepare('SELECT * FROM linked_accounts').all<LinkedAccount>();
69+
const { results: connectionsRaw } = await env.DB.prepare('SELECT * FROM linked_accounts').all<LinkedAccount>();
70+
const { results: siblingConnections } = await env.DB.prepare(
71+
'SELECT * FROM linked_siblings',
72+
).all<LinkedSibling>();
6873

69-
if (!connections.results.length) {
74+
const connections = connectionsRaw.map((connection) => {
75+
const siblings = siblingConnections
76+
.filter((sibling) => sibling.discord_id === connection.discord_id)
77+
.map((sibling) => sibling.sibling_username);
78+
79+
return {
80+
...connection,
81+
siblings,
82+
};
83+
});
84+
85+
if (!connections.length) {
7086
return api.interactions.editReply(env.CLIENT_ID, interaction.token, {
7187
content: 'No connections found',
7288
});
7389
}
7490

75-
const users = await Promise.all(connections.results.map(async (conn) => api.users.get(conn.discord_id)));
76-
const lines = connections.results.map(
77-
(conn, index) =>
78-
`${users[index]!.username} (${conn.discord_id}) - ${conn.minecraft_username} (confirmed: ${conn.confirmed ? 'Yes' : 'No'})`,
79-
);
91+
const users = await Promise.all(connections.map(async (conn) => api.users.get(conn.discord_id)));
92+
93+
const lines: string[] = [];
94+
for (const [index, conn] of connections.entries()) {
95+
const user = users[index]!;
96+
lines.push(`${user.username} (${user.id}):`);
97+
lines.push(` Java: ${conn.java_username} (confirmed: ${conn.java_confirmed ? 'Yes' : 'No'})`);
98+
lines.push(` Bedrock: ${conn.bedrock_username} (confirmed: ${conn.bedrock_confirmed ? 'Yes' : 'No'})`);
99+
100+
lines.push(' Added siblings:');
101+
for (const sibling of conn.siblings) {
102+
lines.push(` - ${sibling}`);
103+
}
104+
105+
// for extra newline on join
106+
lines.push('');
107+
}
80108

81109
return api.interactions.editReply(env.CLIENT_ID, interaction.token, {
82110
content: "Here's the full list",

worker/src/commands/setup.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const interaction: RESTPostAPIApplicationCommandsJSONBody = {
2626
},
2727
{
2828
name: 'prompt',
29-
description: 'Message URL to the prompt (message must belong to bot webhook)',
29+
description: 'Sets up the main bot prompt used for linking accounts',
3030
type: ApplicationCommandOptionType.Subcommand,
3131
options: [
3232
{
@@ -105,9 +105,15 @@ export async function handle(interaction: APIChatInputApplicationCommandGuildInt
105105
{
106106
type: ComponentType.Button,
107107
style: ButtonStyle.Primary,
108-
label: 'Link my account',
108+
label: 'Link my Minecraft account',
109109
custom_id: 'link',
110110
},
111+
{
112+
type: ComponentType.Button,
113+
style: ButtonStyle.Secondary,
114+
label: "Add a sibling's Minecraft account",
115+
custom_id: 'link-sibling',
116+
},
111117
],
112118
},
113119
],

worker/src/components/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import type { IRequest } from 'itty-router';
22
import type { JsonResponse } from '../response.js';
3+
import type { Env } from '../util.js';
4+
import * as linkSibling from './link-sibling.js';
35
import * as link from './link.js';
46

57
interface Component {
6-
handle(req: IRequest, ctx: ExecutionContext, interaction: any): Promise<JsonResponse>;
8+
handle(req: IRequest, ctx: ExecutionContext, env: Env, interaction: any): Promise<JsonResponse>;
79
}
810

911
export const components: Record<string, Component> = {
1012
link,
13+
'link-sibling': linkSibling,
1114
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { ActionRowBuilder, ModalBuilder, TextInputBuilder } from '@discordjs/builders';
2+
import {
3+
InteractionResponseType,
4+
MessageFlags,
5+
TextInputStyle,
6+
type APIModalSubmitGuildInteraction,
7+
} from '@discordjs/core/http-only';
8+
import type { IRequest } from 'itty-router';
9+
import { JsonResponse } from '../response.js';
10+
import type { Env, LinkedAccount } from '../util.js';
11+
12+
export async function handle(
13+
_: IRequest,
14+
__: ExecutionContext,
15+
env: Env,
16+
interaction: APIModalSubmitGuildInteraction,
17+
): Promise<JsonResponse> {
18+
const existing = await env.DB.prepare('SELECT * FROM linked_accounts WHERE discord_id = ?')
19+
.bind(interaction.member.user.id)
20+
.first<LinkedAccount>();
21+
22+
if (existing?.bedrock_confirmed || existing?.java_confirmed) {
23+
return new JsonResponse({
24+
type: InteractionResponseType.Modal,
25+
data: new ModalBuilder()
26+
.setCustomId('submit-sibling')
27+
.setTitle("Add a sibling's Minecraft account(s)")
28+
.addComponents(
29+
new ActionRowBuilder<TextInputBuilder>().addComponents(
30+
new TextInputBuilder()
31+
.setCustomId('username')
32+
.setLabel('Java/Bedrock Username')
33+
.setPlaceholder('Joe')
34+
.setStyle(TextInputStyle.Short)
35+
.setRequired(true),
36+
),
37+
)
38+
.toJSON(),
39+
});
40+
}
41+
42+
return new JsonResponse({
43+
type: InteractionResponseType.ChannelMessageWithSource,
44+
data: {
45+
content: 'You must link & confirm your own account before you can link a sibling account.',
46+
flags: MessageFlags.Ephemeral,
47+
},
48+
});
49+
}

worker/src/components/link.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,23 @@ export async function handle(): Promise<JsonResponse> {
77
type: InteractionResponseType.Modal,
88
data: new ModalBuilder()
99
.setCustomId('submit')
10-
.setTitle('Link your Minecraft account')
10+
.setTitle('Link your Minecraft account(s)')
1111
.addComponents(
1212
new ActionRowBuilder<TextInputBuilder>().addComponents(
1313
new TextInputBuilder()
14-
.setCustomId('minecraft-username')
15-
.setLabel('Minecraft Username')
14+
.setCustomId('java-username')
15+
.setLabel('Java Username')
1616
.setPlaceholder('Joe')
1717
.setStyle(TextInputStyle.Short)
18-
.setRequired(true),
18+
.setRequired(false),
19+
),
20+
new ActionRowBuilder<TextInputBuilder>().addComponents(
21+
new TextInputBuilder()
22+
.setCustomId('bedrock-username')
23+
.setLabel('Bedrock Username')
24+
.setPlaceholder('JoeBedrock')
25+
.setStyle(TextInputStyle.Short)
26+
.setRequired(false),
1927
),
2028
)
2129
.toJSON(),

worker/src/index.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { commands } from './commands/index.js';
88
import { components } from './components/index.js';
99
import { modals } from './modals/index.js';
1010
import { JsonResponse } from './response.js';
11-
import { logger, type Env, type LinkedAccount } from './util.js';
11+
import { logger, type Env, type LinkedAccount, type LinkedSibling } from './util.js';
1212

1313
const router = Router<IRequest, [Env, ExecutionContext, API]>();
1414

@@ -90,7 +90,7 @@ router.post('/api/interactions/handle', async (req, env, ctx, api) => {
9090
});
9191
}
9292

93-
return component.handle(req, ctx, message);
93+
return component.handle(req, ctx, env, message);
9494
}
9595

9696
case InteractionType.ApplicationCommandAutocomplete: {
@@ -133,21 +133,56 @@ router.get('/api/whitelist', async (req, env) => {
133133
return new Response('Unauthorized', { status: 401 });
134134
}
135135

136-
const { results: connections } = await env.DB.prepare('SELECT * FROM linked_accounts').all<LinkedAccount>();
136+
const { results: connectionsRaw } = await env.DB.prepare('SELECT * FROM linked_accounts').all<LinkedAccount>();
137+
const { results: siblingConnections } = await env.DB.prepare('SELECT * FROM linked_siblings').all<LinkedSibling>();
138+
139+
const connections = connectionsRaw.map((connection) => {
140+
const siblings = siblingConnections
141+
.filter((sibling) => sibling.discord_id === connection.discord_id)
142+
.map((sibling) => sibling.sibling_username);
143+
144+
return {
145+
...connection,
146+
siblings,
147+
};
148+
});
149+
137150
return new JsonResponse(
138-
connections.map((conn) => ({ discord_id: conn.discord_id, minecraft_username: conn.minecraft_username })),
151+
connections.map((conn) => ({
152+
discord_id: conn.discord_id,
153+
java_username: conn.java_username,
154+
bedrock_username: conn.bedrock_username,
155+
sibling_usernames: conn.siblings,
156+
})),
139157
{ status: 200 },
140158
);
141159
});
142160

143-
router.put('/api/whitelist/verify/:discord_id', async (req, env) => {
161+
router.put('/api/whitelist/java/verify/:discord_id', async (req, env) => {
162+
const headers = req.headers as Headers;
163+
if (headers.get('authorization') !== env.AUTH_PASS) {
164+
return new Response('Unauthorized', { status: 401 });
165+
}
166+
167+
const { discord_id } = req.params;
168+
const updated = await env.DB.prepare(
169+
'UPDATE linked_accounts SET java_confirmed = true WHERE discord_id = ? RETURNING *',
170+
)
171+
.bind(discord_id)
172+
.first<LinkedAccount>();
173+
return new JsonResponse(updated!, { status: 200 });
174+
});
175+
176+
router.put('/api/whitelist/bedrock/verify/:discord_id', async (req, env) => {
144177
const headers = req.headers as Headers;
145178
if (headers.get('authorization') !== env.AUTH_PASS) {
146179
return new Response('Unauthorized', { status: 401 });
147180
}
148181

149182
const { discord_id } = req.params;
150-
const updated = await env.DB.prepare('UPDATE linked_accounts SET confirmed = true WHERE discord_id = ? RETURNING *')
183+
const updated = await env.DB.prepare(
184+
'UPDATE linked_accounts SET bedrock_confirmed = true WHERE discord_id = ? RETURNING *',
185+
)
151186
.bind(discord_id)
152187
.first<LinkedAccount>();
153188
return new JsonResponse(updated!, { status: 200 });

worker/src/modals/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { API } from '@discordjs/core/http-only';
22
import type { IRequest } from 'itty-router';
33
import type { JsonResponse } from '../response.js';
44
import type { Env } from '../util.js';
5+
import * as submitSibling from './submit-sibling.js';
56
import * as submit from './submit.js';
67

78
interface Modal {
@@ -10,4 +11,5 @@ interface Modal {
1011

1112
export const modals: Record<string, Modal> = {
1213
submit,
14+
'submit-sibling': submitSibling,
1315
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { API, APIModalSubmitGuildInteraction } from '@discordjs/core/http-only';
2+
import { InteractionResponseType, MessageFlags } from '@discordjs/core/http-only';
3+
import type { IRequest } from 'itty-router';
4+
import { JsonResponse } from '../response.js';
5+
import type { Env } from '../util.js';
6+
7+
export async function handle(
8+
_: IRequest,
9+
ctx: ExecutionContext,
10+
env: Env,
11+
interaction: APIModalSubmitGuildInteraction,
12+
api: API,
13+
): Promise<JsonResponse> {
14+
const response = new JsonResponse({
15+
type: InteractionResponseType.DeferredChannelMessageWithSource,
16+
data: {
17+
flags: MessageFlags.Ephemeral,
18+
},
19+
});
20+
21+
ctx.waitUntil(handleSubmit(interaction, env, api));
22+
return response;
23+
}
24+
25+
async function handleSubmit(interaction: APIModalSubmitGuildInteraction, env: Env, api: API) {
26+
const siblingUsername = interaction.data.components[0]!.components[0]!.value;
27+
28+
await env.DB.prepare(
29+
'INSERT INTO linked_siblings (discord_id, sibling_username) VALUES ($1, $2) ON CONFLICT DO NOTHING',
30+
)
31+
.bind(interaction.member.user.id, siblingUsername)
32+
.all();
33+
34+
return api.interactions.editReply(env.CLIENT_ID, interaction.token, {
35+
content: 'Successfully added sibling account!',
36+
flags: MessageFlags.Ephemeral,
37+
});
38+
}

0 commit comments

Comments
 (0)