refactor: Update time planning feature

- Update checksums for browser compatibility
- Add script to kill running server process
- Refactor time planning structure and handling
- Adjust database schema and type definitions accordingly
This commit is contained in:
Aron Malcher 2024-03-15 10:59:41 +01:00
parent faa42f0899
commit 3c404ab5fa
Signed by: aronmal
GPG key ID: 816B7707426FC612
8 changed files with 130 additions and 146 deletions

View file

@ -197,9 +197,9 @@ test.describe.serial("User auth process", () => {
if (getConfigResponse.error) throw new Error(getConfigResponse.error.error); if (getConfigResponse.error) throw new Error(getConfigResponse.error.error);
switch (getConfigResponse.data?.checksum) { switch (getConfigResponse.data?.checksum) {
case "209a644c31a5ef123c432c2885d231a2e0efc4de": // chromium case "9d9ba8fa6405653cb98a961c533ac7e92cbc3af6": // webkit
case "aead21e132a94ab897ec28e0f0c337a66207bad3": // webkit case "cf6316140d481bd5c1728828b065efbe8f7bb537": // firefox
case "c3e2ff2ce5a8936234552125a54c2fe1ce1a35da": // firefox case "9e608cb56e0818c83334389ab3913eade9c011f7": // chromium
break; break;
default: default:
@ -211,11 +211,15 @@ test.describe.serial("User auth process", () => {
const putTimePlanningResponse = await PUT("/api/{guildId}/timePlanning", { const putTimePlanningResponse = await PUT("/api/{guildId}/timePlanning", {
body: { body: {
enabled: true,
channelId: "1234567890123456789", channelId: "1234567890123456789",
rolesEnabled: true, targetMinute: 1,
isAvailableRoleId: "1234567890123456789", targetHour: 2,
wantsToBeNotifieRoledId: "1234567890123456789", targetDay: 3,
roles: {
enabled: true,
isAvailableRoleId: "1234567890123456789",
wantsToBeNotifieRoledId: "1234567890123456789",
},
messageIds: { messageIds: {
"0": "1234567890123456789", "0": "1234567890123456789",
"1": "1234567890123456789", "1": "1234567890123456789",
@ -255,9 +259,9 @@ test.describe.serial("User auth process", () => {
if (getConfigResponse.error) throw new Error(getConfigResponse.error.error); if (getConfigResponse.error) throw new Error(getConfigResponse.error.error);
switch (getConfigResponse.data?.checksum) { switch (getConfigResponse.data?.checksum) {
case "681c8324b21096255d942bb78bd6655da90d352e": // chromium case "843ea341487f777b614f4c1a07b19730a7fd12e3": // webkit
case "a2fb3601b7d0949b1ceada3b3ac0ba408c6159bb": // webkit case "8c4909abb19f7ca520840c54697f78ca4d0b5089": // firefox
case "bf20daba95e8f3ddd17cc64e8a7ba184b68ad37b": // firefox case "17701fc9adcffc4df764f35774a752d3a9b43017": // chromium
break; break;
default: default:

5
kill-running-server.sh Executable file
View file

@ -0,0 +1,5 @@
#!/bin/bash
# This script checks if a process is running on port 3000 and kills it if it's found
lsof -i TCP:3000 | grep LISTEN | awk '{print $2}' | xargs kill -9

View file

@ -569,54 +569,7 @@
"required": ["timePlanning"], "required": ["timePlanning"],
"properties": { "properties": {
"timePlanning": { "timePlanning": {
"type": "object", "$ref": "#/components/schemas/timePlanning"
"required": [
"enabled",
"channelId",
"targetMinute",
"targetHour",
"targetDay",
"roles"
],
"properties": {
"enabled": {
"type": "boolean"
},
"channelId": {
"$ref": "#/components/schemas/idOrNull"
},
"targetMinute": {
"type": "number",
"example": 0
},
"targetHour": {
"type": "number",
"example": 1
},
"targetDay": {
"type": "number",
"example": 1
},
"roles": {
"type": "object",
"required": [
"enabled",
"isAvailableRoleId",
"wantsToBeNotifieRoledId"
],
"properties": {
"enabled": {
"type": "boolean"
},
"isAvailableRoleId": {
"$ref": "#/components/schemas/idOrNull"
},
"wantsToBeNotifieRoledId": {
"$ref": "#/components/schemas/idOrNull"
}
}
}
}
} }
} }
}, },
@ -674,28 +627,47 @@
"timePlanning": { "timePlanning": {
"type": "object", "type": "object",
"required": [ "required": [
"enabled",
"channelId", "channelId",
"rolesEnabled", "targetMinute",
"isAvailableRoleId", "targetHour",
"wantsToBeNotifieRoledId", "targetDay",
"roles",
"messageIds" "messageIds"
], ],
"properties": { "properties": {
"enabled": {
"type": "boolean"
},
"channelId": { "channelId": {
"$ref": "#/components/schemas/idOrNull" "$ref": "#/components/schemas/idOrNull"
}, },
"rolesEnabled": { "targetMinute": {
"type": "boolean" "type": "number",
"example": 0
}, },
"isAvailableRoleId": { "targetHour": {
"$ref": "#/components/schemas/idOrNull" "type": "number",
"example": 1
}, },
"wantsToBeNotifieRoledId": { "targetDay": {
"$ref": "#/components/schemas/idOrNull" "type": "number",
"example": 1
},
"roles": {
"type": "object",
"required": [
"enabled",
"isAvailableRoleId",
"wantsToBeNotifieRoledId"
],
"properties": {
"enabled": {
"type": "boolean"
},
"isAvailableRoleId": {
"$ref": "#/components/schemas/idOrNull"
},
"wantsToBeNotifieRoledId": {
"$ref": "#/components/schemas/idOrNull"
}
}
}, },
"messageIds": { "messageIds": {
"type": "object", "type": "object",

View file

@ -41,7 +41,6 @@ export const discordTokens = pgTable("tokens", {
export const guilds = pgTable("guilds", { export const guilds = pgTable("guilds", {
id: bigint("id", { mode: "bigint" }).primaryKey(), id: bigint("id", { mode: "bigint" }).primaryKey(),
timezone: text("timezone").notNull().default("Etc/UTC"), timezone: text("timezone").notNull().default("Etc/UTC"),
tpEnabled: boolean("tp_enabled").notNull().default(false),
tpChannelId: bigint("tp_channel_id", { mode: "bigint" }), tpChannelId: bigint("tp_channel_id", { mode: "bigint" }),
tpInterval: smallint("target_interval").notNull().default(64), tpInterval: smallint("target_interval").notNull().default(64),
tpRolesEnabled: boolean("tp_roles_enabled").notNull().default(false), tpRolesEnabled: boolean("tp_roles_enabled").notNull().default(false),

View file

@ -27,6 +27,39 @@ export const buildMatches = (
}), }),
); );
export const splitInterval = (tpInterval: number) => {
const targetMinute = tpInterval & 63;
const targetHour = (tpInterval >> 6) & 31;
const targetDay = (tpInterval >> 11) & 7;
return { targetMinute, targetHour, targetDay };
};
export const DayKeys = ["0", "1", "2", "3", "4", "5", "6"] as const;
export type DayKeys = (typeof DayKeys)[number];
export type Messages = Record<DayKeys, string | null>;
export const buildTpMessages = (
messages: ExtractDataTypes<GetColumns<typeof tpMessages>>[],
) =>
messages.reduce(
(acc, message) => {
const day = message.day.toString() as DayKeys;
if (!/^[0-6]$/.test(day)) return acc;
acc[day] = message.messageId?.toString() ?? null;
return acc;
},
{
"0": null,
"1": null,
"2": null,
"3": null,
"4": null,
"5": null,
"6": null,
} as Messages,
);
export function buildConfig( export function buildConfig(
guildQuery: ExtractDataTypes<GetColumns<typeof guilds>> & { guildQuery: ExtractDataTypes<GetColumns<typeof guilds>> & {
tpMessages: ExtractDataTypes<GetColumns<typeof tpMessages>>[]; tpMessages: ExtractDataTypes<GetColumns<typeof tpMessages>>[];
@ -36,33 +69,27 @@ export function buildConfig(
const { const {
id, id,
timezone, timezone,
tpEnabled,
tpChannelId, tpChannelId,
tpInterval, tpInterval,
tpRolesEnabled: tpRoles, tpRolesEnabled,
isAvailableRoleId, isAvailableRoleId,
wantsToBeNotifieRoledId, wantsToBeNotifieRoledId,
tpMessages,
} = guildQuery; } = guildQuery;
const targetMinute = tpInterval & 63;
const targetHour = (tpInterval >> 6) & 31;
const targetDay = (tpInterval >> 11) & 7;
const payload = { const payload = {
guildId: id.toString(), guildId: id.toString(),
timezone, timezone,
features: { features: {
timePlanning: { timePlanning: {
enabled: tpEnabled,
channelId: tpChannelId?.toString() ?? null, channelId: tpChannelId?.toString() ?? null,
targetMinute, ...splitInterval(tpInterval),
targetHour,
targetDay,
roles: { roles: {
enabled: tpRoles, enabled: tpRolesEnabled,
isAvailableRoleId: isAvailableRoleId?.toString() ?? null, isAvailableRoleId: isAvailableRoleId?.toString() ?? null,
wantsToBeNotifieRoledId: wantsToBeNotifieRoledId?.toString() ?? null, wantsToBeNotifieRoledId: wantsToBeNotifieRoledId?.toString() ?? null,
}, },
messageIds: buildTpMessages(tpMessages),
}, },
}, },
matches: buildMatches(guildQuery.matches), matches: buildMatches(guildQuery.matches),

View file

@ -7,11 +7,15 @@ const zodId = z
export const zodBigIntId = zodId.transform((value) => BigInt(value)); export const zodBigIntId = zodId.transform((value) => BigInt(value));
export const zodTpMessages = z.object({ export const zodTpMessages = z.object({
enabled: z.boolean(),
channelId: zodId.nullable(), channelId: zodId.nullable(),
rolesEnabled: z.boolean(), targetMinute: z.number().nonnegative().max(59),
isAvailableRoleId: zodId.nullable(), targetHour: z.number().nonnegative().max(23),
wantsToBeNotifieRoledId: zodId.nullable(), targetDay: z.number().nonnegative().max(6),
roles: z.object({
enabled: z.boolean(),
isAvailableRoleId: zodId.nullable(),
wantsToBeNotifieRoledId: zodId.nullable(),
}),
messageIds: z.object({ messageIds: z.object({
"0": zodId.nullable(), "0": zodId.nullable(),
"1": zodId.nullable(), "1": zodId.nullable(),

View file

@ -3,16 +3,17 @@ import { and, eq } from "drizzle-orm";
import db from "~/drizzle"; import db from "~/drizzle";
import { guilds, tpMessages } from "~/drizzle/schema"; import { guilds, tpMessages } from "~/drizzle/schema";
import { BasicAuth } from "~/lib/auth"; import { BasicAuth } from "~/lib/auth";
import {
DayKeys,
buildTpMessages,
splitInterval,
} from "~/lib/responseBuilders";
import { ErrorResponse, Res } from "~/lib/responses"; import { ErrorResponse, Res } from "~/lib/responses";
import { zodBigIntId, zodTpMessages } from "~/lib/zod"; import { zodBigIntId, zodTpMessages } from "~/lib/zod";
import { APIResponse, RequestBody } from "~/types/backend"; import { APIResponse, RequestBody } from "~/types/backend";
type Path = "/api/{guildId}/timePlanning"; type Path = "/api/{guildId}/timePlanning";
const DayKeys = ["0", "1", "2", "3", "4", "5", "6"] as const;
type DayKeys = (typeof DayKeys)[number];
type Messages = Record<DayKeys, string | null>;
export const GET = async ( export const GET = async (
event: APIEvent, event: APIEvent,
): Promise<APIResponse<Path, "get">> => { ): Promise<APIResponse<Path, "get">> => {
@ -41,31 +42,16 @@ export const GET = async (
if (!guild) return ErrorResponse("NOT_FOUND"); if (!guild) return ErrorResponse("NOT_FOUND");
const tpMessages = guild.tpMessages.reduce(
(acc, message) => {
const day = message.day.toString() as DayKeys;
if (!/^[0-6]$/.test(day)) return acc;
acc[day] = message.messageId?.toString() ?? null;
return acc;
},
{
"0": null,
"1": null,
"2": null,
"3": null,
"4": null,
"5": null,
"6": null,
} as Messages,
);
return Res("OK", { return Res("OK", {
enabled: guild.tpEnabled,
channelId: guild.tpChannelId?.toString() ?? null, channelId: guild.tpChannelId?.toString() ?? null,
rolesEnabled: guild.tpRolesEnabled, ...splitInterval(guild.tpInterval),
isAvailableRoleId: guild.isAvailableRoleId?.toString() ?? null, roles: {
wantsToBeNotifieRoledId: guild.wantsToBeNotifieRoledId?.toString() ?? null, enabled: guild.tpRolesEnabled,
messageIds: tpMessages, isAvailableRoleId: guild.isAvailableRoleId?.toString() ?? null,
wantsToBeNotifieRoledId:
guild.wantsToBeNotifieRoledId?.toString() ?? null,
},
messageIds: buildTpMessages(guild.tpMessages),
}); });
}; };
@ -106,24 +92,18 @@ export const PUT = async (
return ErrorResponse("BAD_REQUEST", JSON.stringify(e)); return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
} }
const { const { channelId, roles, messageIds } = body;
enabled,
channelId,
rolesEnabled,
isAvailableRoleId,
wantsToBeNotifieRoledId,
messageIds,
} = body;
if (guild.tpChannelId !== channelId) if (guild.tpChannelId !== channelId)
await db await db
.update(guilds) .update(guilds)
.set({ .set({
tpEnabled: enabled,
tpChannelId: channelId ? BigInt(channelId) : null, tpChannelId: channelId ? BigInt(channelId) : null,
tpRolesEnabled: rolesEnabled, tpRolesEnabled: roles.enabled,
isAvailableRoleId: isAvailableRoleId ? BigInt(isAvailableRoleId) : null, isAvailableRoleId: roles.isAvailableRoleId
wantsToBeNotifieRoledId: wantsToBeNotifieRoledId ? BigInt(roles.isAvailableRoleId)
? BigInt(wantsToBeNotifieRoledId) : null,
wantsToBeNotifieRoledId: roles.wantsToBeNotifieRoledId
? BigInt(roles.wantsToBeNotifieRoledId)
: null, : null,
}) })
.where(eq(guilds.id, guild.id)) .where(eq(guilds.id, guild.id))

View file

@ -67,21 +67,7 @@ export interface components {
*/ */
timezone: string; timezone: string;
features: { features: {
timePlanning: { timePlanning: components["schemas"]["timePlanning"];
enabled: boolean;
channelId: components["schemas"]["idOrNull"];
/** @example 0 */
targetMinute: number;
/** @example 1 */
targetHour: number;
/** @example 1 */
targetDay: number;
roles: {
enabled: boolean;
isAvailableRoleId: components["schemas"]["idOrNull"];
wantsToBeNotifieRoledId: components["schemas"]["idOrNull"];
};
};
}; };
matches: components["schemas"]["match"][]; matches: components["schemas"]["match"][];
checksum: string; checksum: string;
@ -105,11 +91,18 @@ export interface components {
utc_ts: string; utc_ts: string;
}; };
timePlanning: { timePlanning: {
enabled: boolean;
channelId: components["schemas"]["idOrNull"]; channelId: components["schemas"]["idOrNull"];
rolesEnabled: boolean; /** @example 0 */
isAvailableRoleId: components["schemas"]["idOrNull"]; targetMinute: number;
wantsToBeNotifieRoledId: components["schemas"]["idOrNull"]; /** @example 1 */
targetHour: number;
/** @example 1 */
targetDay: number;
roles: {
enabled: boolean;
isAvailableRoleId: components["schemas"]["idOrNull"];
wantsToBeNotifieRoledId: components["schemas"]["idOrNull"];
};
messageIds: { messageIds: {
0: components["schemas"]["idOrNull"]; 0: components["schemas"]["idOrNull"];
1: components["schemas"]["idOrNull"]; 1: components["schemas"]["idOrNull"];