Skip to content

Commit 8d8b299

Browse files
committed
all claiming of time-limited flags
1 parent cb904b6 commit 8d8b299

7 files changed

Lines changed: 139 additions & 16 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
--------------------------------------------------------------------------------
2+
-- Up
3+
--------------------------------------------------------------------------------
4+
ALTER TABLE Link ADD flags INT NOT NULL DEFAULT 0;
5+
6+
--------------------------------------------------------------------------------
7+
-- Down
8+
--------------------------------------------------------------------------------
9+
ALTER TABLE Link
10+
DROP COLUMN flags;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "herald",
2+
"name": "@teamgalena/bot",
33
"module": "src/app.ts",
44
"type": "module",
55
"scripts": {

src/commands/claimFlag.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
2+
import { addFlag } from "../database";
3+
import { UserError } from "../error";
4+
import { type Flag } from "../flags";
5+
6+
type DatePredicate = (date: Date) => boolean;
7+
8+
function inMonth(month: number): DatePredicate {
9+
if (month < 1 || month > 12) throw new Error("months are between 1-12");
10+
const utcMonth = month - 1;
11+
return (date) => {
12+
return date.getUTCMonth() === utcMonth;
13+
};
14+
}
15+
16+
const timeRanges: Partial<Record<Flag, DatePredicate>> = {
17+
pride: inMonth(6),
18+
};
19+
20+
export const command = new SlashCommandBuilder()
21+
.setName("claim")
22+
.setDescription("claim a special time-limited tophat");
23+
24+
function availableFlag(date: Date) {
25+
return Object.entries(timeRanges).find(([, test]) => test(date))?.[0] as
26+
| Flag
27+
| undefined;
28+
}
29+
30+
export async function execute(interaction: ChatInputCommandInteraction) {
31+
await interaction.deferReply();
32+
33+
const now = new Date();
34+
const flag = availableFlag(now);
35+
36+
if (!flag) {
37+
throw new UserError("there is no special tophat available right now");
38+
}
39+
40+
await addFlag(interaction.user.id, flag);
41+
42+
if (interaction.isRepliable()) {
43+
await interaction.editReply(`You claimed the ${flag} tophat!`);
44+
}
45+
}

src/commands/register.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
import { config } from "../config";
1212
import { UserError } from "../error";
1313
import logger from "../logger";
14-
import * as makeAnnouncement from "./linkMinecraft";
14+
import * as claimFlag from "./claimFlag";
15+
import * as linkMinecraft from "./linkMinecraft";
1516

1617
type CommandHandler = (
1718
interaction: ChatInputCommandInteraction
@@ -31,7 +32,8 @@ function addCommand({
3132
handlers.set(command.name, execute);
3233
}
3334

34-
addCommand(makeAnnouncement);
35+
addCommand(linkMinecraft);
36+
addCommand(claimFlag);
3537

3638
const rest = new REST().setToken(config.botToken);
3739

src/database.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { open } from "sqlite";
22
import sqlite3 from "sqlite3";
33
import { UserError } from "./error";
4+
import { flagQuery, withFlag, type Flag } from "./flags";
45
import logger from "./logger";
56

67
const db = await open({
@@ -15,8 +16,11 @@ export type LinkEntry = {
1516
discordId: string;
1617
uuid: string;
1718
rank: number;
19+
flags: number;
1820
};
1921

22+
type InputLinkEntry = Omit<LinkEntry, "flags"> & Partial<LinkEntry>;
23+
2024
export type RoleEntry = {
2125
id: string;
2226
rank: number;
@@ -37,24 +41,29 @@ async function getLinkByDiscordId(discordId: string) {
3741
]);
3842
}
3943

40-
async function updateLink(existing: LinkEntry, values: LinkEntry) {
41-
if (values.rank === existing.rank && values.uuid === existing.uuid) {
44+
async function updateLink(existing: LinkEntry, values: InputLinkEntry) {
45+
const next = { ...existing, ...values };
46+
if (
47+
next.rank === existing.rank &&
48+
next.uuid === existing.uuid &&
49+
next.flags === existing.flags
50+
) {
4251
logger.debug("skipped linking, all values are the same");
4352
return;
4453
}
4554

46-
await db.run("UPDATE Link SET uuid = ?, rank = ? WHERE discordId = ?", [
47-
values.uuid,
48-
values.rank,
49-
values.discordId,
50-
]);
55+
await db.run(
56+
"UPDATE Link SET uuid = ?, rank = ?, flags = ? WHERE discordId = ?",
57+
[next.uuid, next.rank, next.flags, next.discordId]
58+
);
5159

60+
const prettyFlags = next.flags.toString(2).padStart(8, "0");
5261
logger.debug(
53-
`updated ${values.discordId} <-> ${values.uuid} (${values.rank})`
62+
`updated ${next.discordId} <-> ${next.uuid} (${next.rank}/${prettyFlags})`
5463
);
5564
}
5665

57-
export async function persistLink(link: LinkEntry) {
66+
export async function persistLink(link: InputLinkEntry) {
5867
const existing = await getLinkByDiscordId(link.discordId);
5968

6069
if (existing) {
@@ -73,11 +82,19 @@ export async function persistLink(link: LinkEntry) {
7382
}
7483
}
7584

76-
export async function updateRank(discordId: string, rank: number) {
85+
async function requireLink(discordId: string) {
7786
const link = await getLinkByDiscordId(discordId);
78-
if (!link)
79-
throw new UserError("you have not linked your minecraft account yet");
87+
if (link) return link;
88+
throw new UserError("you have not linked your minecraft account yet");
89+
}
90+
91+
export async function addFlag(discordId: string, flag: Flag) {
92+
const link = await requireLink(discordId);
93+
await updateLink(link, withFlag(link, flag));
94+
}
8095

96+
export async function updateRank(discordId: string, rank: number) {
97+
const link = await requireLink(discordId);
8198
await updateLink(link, { ...link, rank });
8299
}
83100

@@ -89,6 +106,13 @@ export async function loadSupporterUuids(aboveRank: number) {
89106
return entries.map((it) => it.uuid);
90107
}
91108

109+
export async function loadFlaggedUuids(flag: Flag) {
110+
const entries = await db.all<LinkEntry[]>(
111+
`SELECT uuid FROM Link WHERE ${flagQuery(flag)}`
112+
);
113+
return entries.map((it) => it.uuid);
114+
}
115+
92116
export async function loadSupporterRoles() {
93117
return await db.all<RoleEntry[]>("SELECT * FROM Role ORDER BY rank DESC");
94118
}

src/flags.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { LinkEntry } from "./database";
2+
3+
const Flags = {
4+
pride: 0,
5+
anniversary: 1,
6+
};
7+
8+
export type Flag = keyof typeof Flags;
9+
10+
export function withFlag(link: LinkEntry, flag: Flag): LinkEntry {
11+
const mask = 1 << Flags[flag];
12+
const flags = link.flags | mask;
13+
return { ...link, flags };
14+
}
15+
16+
export function isFlag(flag: string): flag is Flag {
17+
return flag in Flags;
18+
}
19+
20+
export function flagQuery(flag: Flag) {
21+
if (!isFlag(flag)) throw new Error(`unknown flag ${flag}`);
22+
const mask = 1 << Flags[flag];
23+
return `(flags & ${mask} != 0)`;
24+
}

src/server.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { server as createServer } from "@hapi/hapi";
2-
import { loadSupporterUuids } from "./database";
2+
import { loadFlaggedUuids, loadSupporterUuids } from "./database";
3+
import { isFlag } from "./flags";
34
import logger from "./logger";
45

56
const server = createServer({ port: 3000 });
@@ -21,5 +22,22 @@ server.route({
2122
},
2223
});
2324

25+
server.route({
26+
method: "GET",
27+
path: "/api/flagged/{flag}",
28+
handler: async (req, tools) => {
29+
const { flag } = req.params;
30+
if (!isFlag(flag)) {
31+
return tools
32+
.response({
33+
message: `unknown flag '${flag}'`,
34+
})
35+
.code(400);
36+
}
37+
38+
return await loadFlaggedUuids(flag);
39+
},
40+
});
41+
2442
await server.start();
2543
logger.debug(`server accepting responses at ${server.info.uri}`);

0 commit comments

Comments
 (0)