- better reading of avalable routes
All checks were successful
Code quality checks / biome (push) Successful in 10s
All checks were successful
Code quality checks / biome (push) Successful in 10s
- add name validation for channel names and catagories - add create, delete, move for channels
This commit is contained in:
parent
0cb7ebb245
commit
93ed37b3e9
10 changed files with 559 additions and 6 deletions
29
config/setup/tables/guilds/categories.ts
Normal file
29
config/setup/tables/guilds/categories.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { cassandra } from "@lib/cassandra";
|
||||||
|
|
||||||
|
async function createTable() {
|
||||||
|
const client = cassandra.getClient();
|
||||||
|
|
||||||
|
await client.execute(`
|
||||||
|
CREATE TABLE IF NOT EXISTS categories (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
guild_id TEXT,
|
||||||
|
name TEXT,
|
||||||
|
position INT,
|
||||||
|
created_at TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await client.execute(`
|
||||||
|
CREATE INDEX IF NOT EXISTS categories_guild_id_idx ON categories (guild_id);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dropTable() {
|
||||||
|
const client = cassandra.getClient();
|
||||||
|
|
||||||
|
await client.execute("DROP TABLE IF EXISTS categories;");
|
||||||
|
await client.execute("DROP INDEX IF EXISTS categories_guild_id_idx;");
|
||||||
|
}
|
||||||
|
|
||||||
|
export { createTable, dropTable };
|
37
config/setup/tables/guilds/channels.ts
Normal file
37
config/setup/tables/guilds/channels.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { cassandra } from "@lib/cassandra";
|
||||||
|
|
||||||
|
async function createTable() {
|
||||||
|
const client = cassandra.getClient();
|
||||||
|
|
||||||
|
await client.execute(`
|
||||||
|
CREATE TABLE IF NOT EXISTS channels (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
guild_id TEXT,
|
||||||
|
name TEXT,
|
||||||
|
type TEXT,
|
||||||
|
topic TEXT,
|
||||||
|
position INT,
|
||||||
|
category_id TEXT,
|
||||||
|
created_at TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await client.execute(`
|
||||||
|
CREATE INDEX IF NOT EXISTS channels_guild_id_idx ON channels (guild_id);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await client.execute(`
|
||||||
|
CREATE INDEX IF NOT EXISTS channels_category_id_idx ON channels (category_id);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dropTable() {
|
||||||
|
const client = cassandra.getClient();
|
||||||
|
|
||||||
|
await client.execute("DROP TABLE IF EXISTS channels;");
|
||||||
|
await client.execute("DROP INDEX IF EXISTS channels_guild_id_idx;");
|
||||||
|
await client.execute("DROP INDEX IF EXISTS channels_category_id_idx;");
|
||||||
|
}
|
||||||
|
|
||||||
|
export { createTable, dropTable };
|
|
@ -17,7 +17,6 @@ async function main(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
await cassandra.connect();
|
await cassandra.connect();
|
||||||
|
|
||||||
serverHandler.initialize();
|
serverHandler.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,11 @@ const pika = new Pika([
|
||||||
prefix: "ch",
|
prefix: "ch",
|
||||||
description: "Channel ID",
|
description: "Channel ID",
|
||||||
},
|
},
|
||||||
|
"category",
|
||||||
|
{
|
||||||
|
prefix: "cat",
|
||||||
|
description: "Category ID",
|
||||||
|
},
|
||||||
"role",
|
"role",
|
||||||
{
|
{
|
||||||
prefix: "role",
|
prefix: "role",
|
||||||
|
|
|
@ -26,4 +26,69 @@ function isValidGuildName(rawName: string): validationResult {
|
||||||
return { valid: true, name };
|
return { valid: true, name };
|
||||||
}
|
}
|
||||||
|
|
||||||
export { guildNameRestrictions, isValidGuildName };
|
const channelNameRestrictions: genericValidation = {
|
||||||
|
length: { min: 1, max: 100 },
|
||||||
|
regex: /^[\p{L}\p{N}\p{Emoji}_\-.]+$/u, // emojis + underscore, dot, dash ?
|
||||||
|
};
|
||||||
|
|
||||||
|
function isValidChannelName(rawName: string): validationResult {
|
||||||
|
const name = rawName.trim().normalize("NFC");
|
||||||
|
|
||||||
|
if (!name) return { valid: false, error: "Channel name is required" };
|
||||||
|
|
||||||
|
if (name.length < channelNameRestrictions.length.min)
|
||||||
|
return { valid: false, error: "Channel name is too short" };
|
||||||
|
|
||||||
|
if (name.length > channelNameRestrictions.length.max)
|
||||||
|
return { valid: false, error: "Channel name is too long" };
|
||||||
|
|
||||||
|
if (/\s/.test(name))
|
||||||
|
return { valid: false, error: "Channel name cannot contain spaces" };
|
||||||
|
|
||||||
|
if (!channelNameRestrictions.regex.test(name))
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: "Channel name contains invalid characters",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (/^[._-]|[._-]$/.test(name))
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: "Channel name can't start or end with special characters",
|
||||||
|
};
|
||||||
|
|
||||||
|
return { valid: true, name };
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryNameRestrictions: genericValidation = {
|
||||||
|
length: { min: 1, max: 100 },
|
||||||
|
regex: /^[\p{L}\p{N} ._-]+$/u,
|
||||||
|
};
|
||||||
|
|
||||||
|
function isValidCategoryName(rawName: string): validationResult {
|
||||||
|
const name = rawName.trim().normalize("NFC");
|
||||||
|
|
||||||
|
if (!name) return { valid: false, error: "Category name is required" };
|
||||||
|
|
||||||
|
if (name.length < categoryNameRestrictions.length.min)
|
||||||
|
return { valid: false, error: "Category name is too short" };
|
||||||
|
|
||||||
|
if (name.length > categoryNameRestrictions.length.max)
|
||||||
|
return { valid: false, error: "Category name is too long" };
|
||||||
|
|
||||||
|
if (!categoryNameRestrictions.regex.test(name))
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: "Category name contains invalid characters",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (/^[._ -]|[._ -]$/.test(name))
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: "Category name can't start or end with special characters",
|
||||||
|
};
|
||||||
|
|
||||||
|
return { valid: true, name };
|
||||||
|
}
|
||||||
|
|
||||||
|
export { isValidGuildName, isValidChannelName, isValidCategoryName };
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "@lib/validators/name";
|
export * from "@lib/validators/name";
|
||||||
export * from "@lib/validators/password";
|
export * from "@lib/validators/password";
|
||||||
export * from "@lib/validators/email";
|
export * from "@lib/validators/email";
|
||||||
|
export * from "@lib/validators/guild";
|
||||||
|
|
187
src/routes/guild/[id]/channel/create.ts
Normal file
187
src/routes/guild/[id]/channel/create.ts
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
import { logger } from "@creations.works/logger";
|
||||||
|
import { cassandra } from "@lib/cassandra";
|
||||||
|
import { jsonResponse } from "@lib/http";
|
||||||
|
import { pika } from "@lib/pika";
|
||||||
|
import { isValidChannelName } from "@lib/validators";
|
||||||
|
import type { Client } from "cassandra-driver";
|
||||||
|
|
||||||
|
const routeDef: RouteDef = {
|
||||||
|
method: "POST",
|
||||||
|
accepts: "application/json",
|
||||||
|
returns: "application/json;charset=utf-8",
|
||||||
|
needsBody: "json",
|
||||||
|
};
|
||||||
|
|
||||||
|
async function handler(
|
||||||
|
request: ExtendedRequest,
|
||||||
|
requestBody: unknown,
|
||||||
|
): Promise<Response> {
|
||||||
|
const user: UserSession | null = request.session;
|
||||||
|
if (!user) {
|
||||||
|
return jsonResponse(401, {
|
||||||
|
message: "Unauthorized",
|
||||||
|
error: "You must be logged in to create a channel",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, type, topic, position, category_id } = requestBody as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
|
||||||
|
const { id: guildId } = request.params;
|
||||||
|
if (!guildId || typeof guildId !== "string") {
|
||||||
|
return jsonResponse(400, {
|
||||||
|
message: "Missing or invalid guild ID",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
if (typeof name !== "string") {
|
||||||
|
errors.push("Channel name is required");
|
||||||
|
} else {
|
||||||
|
const result = isValidChannelName(name);
|
||||||
|
if (!result.valid) errors.push(result.error ?? "Invalid channel name");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type !== "text" && type !== "voice") {
|
||||||
|
errors.push("Invalid or missing type (must be 'text' or 'voice')");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
topic !== undefined &&
|
||||||
|
(typeof topic !== "string" || topic.length > 1024)
|
||||||
|
) {
|
||||||
|
errors.push("Invalid topic");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
position !== undefined &&
|
||||||
|
(typeof position !== "number" || position < 0)
|
||||||
|
) {
|
||||||
|
errors.push("Invalid position");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category_id !== undefined) {
|
||||||
|
if (typeof category_id !== "string") {
|
||||||
|
errors.push("Invalid category_id");
|
||||||
|
} else {
|
||||||
|
const result = pika.validate(category_id, "category");
|
||||||
|
if (!result) errors.push("Invalid category name format");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return jsonResponse(400, {
|
||||||
|
message: "Validation failed",
|
||||||
|
error: errors.join("; "),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const channelId = pika.gen("channel");
|
||||||
|
const client: Client = cassandra.getClient();
|
||||||
|
|
||||||
|
let finalPosition = typeof position === "number" ? position : 0;
|
||||||
|
|
||||||
|
if (position === undefined) {
|
||||||
|
try {
|
||||||
|
const result = await client.execute(
|
||||||
|
"SELECT position FROM channels WHERE guild_id = ?",
|
||||||
|
[guildId],
|
||||||
|
{ prepare: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
let maxPos = -1;
|
||||||
|
for (const row of result.rows) {
|
||||||
|
if (typeof row.position === "number" && row.position > maxPos) {
|
||||||
|
maxPos = row.position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finalPosition = maxPos + 1;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(["Failed to fetch existing channels:", err as Error]);
|
||||||
|
return jsonResponse(500, {
|
||||||
|
message: "Internal server error",
|
||||||
|
error: "Could not determine channel position",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category_id !== undefined) {
|
||||||
|
try {
|
||||||
|
const categoryCheck = await client.execute(
|
||||||
|
"SELECT id FROM categories WHERE id = ? AND guild_id = ?",
|
||||||
|
[category_id, guildId],
|
||||||
|
{ prepare: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (categoryCheck.rowLength === 0) {
|
||||||
|
return jsonResponse(400, {
|
||||||
|
message: "Validation failed",
|
||||||
|
error: "Provided category does not exist in this guild",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(["Failed to verify category:", err as Error]);
|
||||||
|
return jsonResponse(500, {
|
||||||
|
message: "Internal server error",
|
||||||
|
error: "Failed to verify category existence",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Check if the user has permission to create a channel in this guild
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.execute(
|
||||||
|
`INSERT INTO channels (
|
||||||
|
id, guild_id, name, type, topic, position, category_id, created_at, updated_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
[
|
||||||
|
channelId,
|
||||||
|
guildId,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
topic ?? null,
|
||||||
|
finalPosition,
|
||||||
|
category_id ?? null,
|
||||||
|
now,
|
||||||
|
now,
|
||||||
|
],
|
||||||
|
{ prepare: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.custom(
|
||||||
|
"[CHANNEL CREATE]",
|
||||||
|
`(${channelId})`,
|
||||||
|
`${name} in guild ${guildId} by ${user.id}`,
|
||||||
|
"36",
|
||||||
|
);
|
||||||
|
|
||||||
|
return jsonResponse(201, {
|
||||||
|
message: "Channel created successfully",
|
||||||
|
data: {
|
||||||
|
id: channelId,
|
||||||
|
guildId,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
topic: topic ?? null,
|
||||||
|
position: finalPosition,
|
||||||
|
category_id: category_id ?? null,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(["Failed to create channel:", error as Error]);
|
||||||
|
return jsonResponse(500, {
|
||||||
|
message: "Internal server error",
|
||||||
|
error: "Failed to create channel",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { handler, routeDef };
|
90
src/routes/guild/[id]/channel/delete.ts
Normal file
90
src/routes/guild/[id]/channel/delete.ts
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import { logger } from "@creations.works/logger";
|
||||||
|
import { cassandra } from "@lib/cassandra";
|
||||||
|
import { jsonResponse } from "@lib/http";
|
||||||
|
import { pika } from "@lib/pika";
|
||||||
|
import type { Client } from "cassandra-driver";
|
||||||
|
|
||||||
|
const routeDef: RouteDef = {
|
||||||
|
method: "DELETE",
|
||||||
|
accepts: "application/json",
|
||||||
|
returns: "application/json;charset=utf-8",
|
||||||
|
needsBody: "json",
|
||||||
|
};
|
||||||
|
|
||||||
|
async function handler(
|
||||||
|
request: ExtendedRequest,
|
||||||
|
requestBody: unknown,
|
||||||
|
): Promise<Response> {
|
||||||
|
const user: UserSession | null = request.session;
|
||||||
|
if (!user) {
|
||||||
|
return jsonResponse(401, {
|
||||||
|
message: "Unauthorized",
|
||||||
|
error: "You must be logged in to delete a channel",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id: guildId } = request.params;
|
||||||
|
if (!guildId || typeof guildId !== "string") {
|
||||||
|
return jsonResponse(400, {
|
||||||
|
message: "Missing or invalid guild ID",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id: channel_id } = requestBody as Record<string, unknown>;
|
||||||
|
|
||||||
|
if (typeof channel_id !== "string" || !pika.validate(channel_id, "channel")) {
|
||||||
|
return jsonResponse(400, {
|
||||||
|
message: "Validation failed",
|
||||||
|
error: "Invalid or missing channel_id",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const client: Client = cassandra.getClient();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await client.execute(
|
||||||
|
"SELECT id, guild_id FROM channels WHERE id = ?",
|
||||||
|
[channel_id],
|
||||||
|
{ prepare: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rowLength === 0) {
|
||||||
|
return jsonResponse(404, {
|
||||||
|
message: "Channel not found",
|
||||||
|
error: "The specified channel does not exist",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = result.first();
|
||||||
|
|
||||||
|
if (channel.guild_id !== guildId) {
|
||||||
|
return jsonResponse(403, {
|
||||||
|
message: "Forbidden",
|
||||||
|
error: "Channel does not belong to this guild",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.execute("DELETE FROM channels WHERE id = ?", [channel_id], {
|
||||||
|
prepare: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.custom(
|
||||||
|
"[CHANNEL DELETE]",
|
||||||
|
`(${channel_id})`,
|
||||||
|
`from guild ${guildId} by ${user.id}`,
|
||||||
|
"31",
|
||||||
|
);
|
||||||
|
|
||||||
|
return jsonResponse(200, {
|
||||||
|
message: "Channel deleted successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(["Failed to delete channel:", error as Error]);
|
||||||
|
return jsonResponse(500, {
|
||||||
|
message: "Internal server error",
|
||||||
|
error: "Failed to delete channel",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { handler, routeDef };
|
131
src/routes/guild/[id]/channel/move.ts
Normal file
131
src/routes/guild/[id]/channel/move.ts
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
import { logger } from "@creations.works/logger";
|
||||||
|
import { cassandra } from "@lib/cassandra";
|
||||||
|
import { jsonResponse } from "@lib/http";
|
||||||
|
import { pika } from "@lib/pika";
|
||||||
|
import type { Client } from "cassandra-driver";
|
||||||
|
|
||||||
|
const routeDef: RouteDef = {
|
||||||
|
method: "POST",
|
||||||
|
accepts: "application/json",
|
||||||
|
returns: "application/json;charset=utf-8",
|
||||||
|
needsBody: "json",
|
||||||
|
};
|
||||||
|
|
||||||
|
async function handler(
|
||||||
|
request: ExtendedRequest,
|
||||||
|
requestBody: unknown,
|
||||||
|
): Promise<Response> {
|
||||||
|
const user: UserSession | null = request.session;
|
||||||
|
if (!user) {
|
||||||
|
return jsonResponse(401, {
|
||||||
|
message: "Unauthorized",
|
||||||
|
error: "You must be logged in to move a channel",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id: guildId } = request.params;
|
||||||
|
if (!guildId || typeof guildId !== "string") {
|
||||||
|
return jsonResponse(400, {
|
||||||
|
message: "Missing or invalid guild ID",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { channel_id, position, category_id } = requestBody as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
if (typeof channel_id !== "string" || !pika.validate(channel_id, "channel")) {
|
||||||
|
errors.push("Invalid or missing channel_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof position !== "number" || position < 0) {
|
||||||
|
errors.push("Invalid or missing position");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category_id !== undefined) {
|
||||||
|
if (
|
||||||
|
typeof category_id !== "string" ||
|
||||||
|
!pika.validate(category_id, "category")
|
||||||
|
) {
|
||||||
|
errors.push("Invalid category_id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return jsonResponse(400, {
|
||||||
|
message: "Validation failed",
|
||||||
|
error: errors.join("; "),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const client: Client = cassandra.getClient();
|
||||||
|
|
||||||
|
// TODO: check if user has permission to move the channel
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existing = await client.execute(
|
||||||
|
"SELECT id FROM channels WHERE id = ? AND guild_id = ?",
|
||||||
|
[channel_id, guildId],
|
||||||
|
{ prepare: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing.rowLength === 0) {
|
||||||
|
return jsonResponse(404, {
|
||||||
|
message: "Channel not found",
|
||||||
|
error: "The specified channel does not exist in this guild",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category_id !== undefined) {
|
||||||
|
const catCheck = await client.execute(
|
||||||
|
"SELECT id FROM categories WHERE id = ? AND guild_id = ?",
|
||||||
|
[category_id, guildId],
|
||||||
|
{ prepare: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (catCheck.rowLength === 0) {
|
||||||
|
return jsonResponse(400, {
|
||||||
|
message: "Validation failed",
|
||||||
|
error: "Provided category does not exist in this guild",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
await client.execute(
|
||||||
|
"UPDATE channels SET position = ?, category_id = ?, updated_at = ? WHERE id = ? AND guild_id = ?",
|
||||||
|
[position, category_id ?? null, now, channel_id, guildId],
|
||||||
|
{ prepare: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.custom(
|
||||||
|
"[CHANNEL MOVE]",
|
||||||
|
`(${channel_id})`,
|
||||||
|
`moved to pos ${position}, cat ${category_id ?? "null"} in guild ${guildId} by ${user.id}`,
|
||||||
|
"33",
|
||||||
|
);
|
||||||
|
|
||||||
|
return jsonResponse(200, {
|
||||||
|
message: "Channel moved successfully",
|
||||||
|
data: {
|
||||||
|
id: channel_id,
|
||||||
|
guildId,
|
||||||
|
position,
|
||||||
|
category_id: category_id ?? null,
|
||||||
|
updated_at: now,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(["Failed to move channel:", error as Error]);
|
||||||
|
return jsonResponse(500, {
|
||||||
|
message: "Internal server error",
|
||||||
|
error: "Failed to move channel",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { handler, routeDef };
|
|
@ -51,12 +51,21 @@ class ServerHandler {
|
||||||
|
|
||||||
const sortedRoutes: [string, string][] = Object.entries(
|
const sortedRoutes: [string, string][] = Object.entries(
|
||||||
this.router.routes,
|
this.router.routes,
|
||||||
).sort(([pathA]: [string, string], [pathB]: [string, string]) =>
|
).sort(([pathA], [pathB]) => pathA.localeCompare(pathB));
|
||||||
pathA.localeCompare(pathB),
|
|
||||||
);
|
let lastCategory: string | null = null;
|
||||||
|
|
||||||
for (const [path, filePath] of sortedRoutes) {
|
for (const [path, filePath] of sortedRoutes) {
|
||||||
logger.info(`Route: ${path}, File: ${filePath}`);
|
const parts = path.split("/").filter(Boolean);
|
||||||
|
const category = parts.length === 0 ? "" : parts[0];
|
||||||
|
|
||||||
|
if (category !== lastCategory) {
|
||||||
|
if (lastCategory !== null) logger.space();
|
||||||
|
logger.info(`› ${category}/`);
|
||||||
|
lastCategory = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(` Route: ${path}, File: ${filePath}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue