fix: standardize error classes in shared service modules
Replace raw `Error` with `UserError` for user-facing conditions (invalid trade state, user not found, permission/channel type checks) and `SystemError` for internal failures (DB insert failures, external API errors, missing config). Improves Discord UX by ensuring user-facing errors are surfaced cleanly via withCommandErrorHandling. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -96,7 +96,7 @@ export const economyService = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error("User not found");
|
throw new UserError("User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
let streak = (user.dailyStreak || 0) + 1;
|
let streak = (user.dailyStreak || 0) + 1;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { TimerType, TransactionType } from "@shared/lib/constants";
|
|||||||
import { config } from "@shared/lib/config";
|
import { config } from "@shared/lib/config";
|
||||||
import { withTransaction } from "@/lib/db";
|
import { withTransaction } from "@/lib/db";
|
||||||
import type { Transaction } from "@shared/lib/types";
|
import type { Transaction } from "@shared/lib/types";
|
||||||
import { UserError } from "@shared/lib/errors";
|
import { UserError, SystemError } from "@shared/lib/errors";
|
||||||
|
|
||||||
const EXAM_TIMER_TYPE = TimerType.EXAM_SYSTEM;
|
const EXAM_TIMER_TYPE = TimerType.EXAM_SYSTEM;
|
||||||
const EXAM_TIMER_KEY = 'default';
|
const EXAM_TIMER_KEY = 'default';
|
||||||
@@ -86,7 +86,7 @@ export const examService = {
|
|||||||
// Ensure user exists
|
// Ensure user exists
|
||||||
const { userService } = await import("@shared/modules/user/user.service");
|
const { userService } = await import("@shared/modules/user/user.service");
|
||||||
const user = await userService.getOrCreateUser(userId, username, txFn);
|
const user = await userService.getOrCreateUser(userId, username, txFn);
|
||||||
if (!user) throw new Error("Failed to get or create user.");
|
if (!user) throw new SystemError("Failed to get or create user.");
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const currentDay = now.getDay();
|
const currentDay = now.getDay();
|
||||||
@@ -126,7 +126,7 @@ export const examService = {
|
|||||||
where: eq(users.id, BigInt(userId))
|
where: eq(users.id, BigInt(userId))
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) throw new Error("User not found");
|
if (!user) throw new UserError("User not found");
|
||||||
|
|
||||||
const timer = await txFn.query.userTimers.findFirst({
|
const timer = await txFn.query.userTimers.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { gameSettings } from "@db/schema";
|
import { gameSettings } from "@db/schema";
|
||||||
import { DrizzleClient } from "@shared/db/DrizzleClient";
|
import { DrizzleClient } from "@shared/db/DrizzleClient";
|
||||||
|
import { SystemError } from "@shared/lib/errors";
|
||||||
import type {
|
import type {
|
||||||
LevelingConfig,
|
LevelingConfig,
|
||||||
EconomyConfig,
|
EconomyConfig,
|
||||||
@@ -88,7 +89,7 @@ export const gameSettingsService = {
|
|||||||
const existing = await gameSettingsService.getSettings(false);
|
const existing = await gameSettingsService.getSettings(false);
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
throw new Error("Game settings not found. Initialize settings first.");
|
throw new SystemError("Game settings not found. Initialize settings first.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const updates: Partial<GameSettingsData> = { [section]: value };
|
const updates: Partial<GameSettingsData> = { [section]: value };
|
||||||
@@ -101,7 +102,7 @@ export const gameSettingsService = {
|
|||||||
const settings = await gameSettingsService.getSettings(false);
|
const settings = await gameSettingsService.getSettings(false);
|
||||||
|
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
throw new Error("Game settings not found. Initialize settings first.");
|
throw new SystemError("Game settings not found. Initialize settings first.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const commands = {
|
const commands = {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { withTransaction } from "@/lib/db";
|
|||||||
import { config } from "@shared/lib/config";
|
import { config } from "@shared/lib/config";
|
||||||
import type { Transaction } from "@shared/lib/types";
|
import type { Transaction } from "@shared/lib/types";
|
||||||
import { TimerKey, TimerType } from "@shared/lib/constants";
|
import { TimerKey, TimerType } from "@shared/lib/constants";
|
||||||
|
import { UserError } from "@shared/lib/errors";
|
||||||
|
|
||||||
export const levelingService = {
|
export const levelingService = {
|
||||||
// Calculate total XP required to REACH a specific level (Cumulative)
|
// Calculate total XP required to REACH a specific level (Cumulative)
|
||||||
@@ -49,7 +50,7 @@ export const levelingService = {
|
|||||||
where: eq(users.id, BigInt(id)),
|
where: eq(users.id, BigInt(id)),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) throw new Error("User not found");
|
if (!user) throw new UserError("User not found");
|
||||||
|
|
||||||
const currentXp = user.xp ?? 0n;
|
const currentXp = user.xp ?? 0n;
|
||||||
const newXp = currentXp + amount;
|
const newXp = currentXp + amount;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { DrizzleClient } from "@shared/db/DrizzleClient";
|
|||||||
import type { CreateCaseOptions, ClearCaseOptions, SearchCasesFilter } from "@/modules/moderation/moderation.types";
|
import type { CreateCaseOptions, ClearCaseOptions, SearchCasesFilter } from "@/modules/moderation/moderation.types";
|
||||||
import { getUserWarningEmbed } from "@/modules/moderation/moderation.view";
|
import { getUserWarningEmbed } from "@/modules/moderation/moderation.view";
|
||||||
import { CaseType } from "@shared/lib/constants";
|
import { CaseType } from "@shared/lib/constants";
|
||||||
|
import { SystemError } from "@shared/lib/errors";
|
||||||
|
|
||||||
export interface ModerationCaseConfig {
|
export interface ModerationCaseConfig {
|
||||||
dmOnWarn?: boolean;
|
dmOnWarn?: boolean;
|
||||||
@@ -100,7 +101,7 @@ export const moderationService = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!moderationCase) {
|
if (!moderationCase) {
|
||||||
throw new Error("Failed to create moderation case");
|
throw new SystemError("Failed to create moderation case");
|
||||||
}
|
}
|
||||||
|
|
||||||
const warningCount = await getActiveWarningCount(options.userId);
|
const warningCount = await getActiveWarningCount(options.userId);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Collection, Message, PermissionFlagsBits } from "discord.js";
|
|||||||
import type { TextBasedChannel } from "discord.js";
|
import type { TextBasedChannel } from "discord.js";
|
||||||
import type { PruneOptions, PruneResult, PruneProgress } from "@/modules/moderation/prune.types";
|
import type { PruneOptions, PruneResult, PruneProgress } from "@/modules/moderation/prune.types";
|
||||||
import { config } from "@shared/lib/config";
|
import { config } from "@shared/lib/config";
|
||||||
|
import { UserError, SystemError } from "@shared/lib/errors";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch messages from a channel
|
* Fetch messages from a channel
|
||||||
@@ -30,7 +31,7 @@ async function processBatch(
|
|||||||
userId?: string
|
userId?: string
|
||||||
): Promise<{ deleted: number; skipped: number }> {
|
): Promise<{ deleted: number; skipped: number }> {
|
||||||
if (!('bulkDelete' in channel)) {
|
if (!('bulkDelete' in channel)) {
|
||||||
throw new Error("This channel type does not support bulk deletion");
|
throw new UserError("This channel type does not support bulk deletion");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by user if specified
|
// Filter by user if specified
|
||||||
@@ -54,7 +55,7 @@ async function processBatch(
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error during bulk delete:", error);
|
console.error("Error during bulk delete:", error);
|
||||||
throw new Error("Failed to delete messages");
|
throw new SystemError("Failed to delete messages");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,12 +77,12 @@ export const pruneService = {
|
|||||||
): Promise<PruneResult> {
|
): Promise<PruneResult> {
|
||||||
// Validate channel permissions
|
// Validate channel permissions
|
||||||
if (!('permissionsFor' in channel)) {
|
if (!('permissionsFor' in channel)) {
|
||||||
throw new Error("Cannot check permissions for this channel type");
|
throw new UserError("Cannot check permissions for this channel type");
|
||||||
}
|
}
|
||||||
|
|
||||||
const permissions = channel.permissionsFor(channel.client.user!);
|
const permissions = channel.permissionsFor(channel.client.user!);
|
||||||
if (!permissions?.has(PermissionFlagsBits.ManageMessages)) {
|
if (!permissions?.has(PermissionFlagsBits.ManageMessages)) {
|
||||||
throw new Error("Missing permission to manage messages in this channel");
|
throw new UserError("Missing permission to manage messages in this channel");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { amount, userId, all } = options;
|
const { amount, userId, all } = options;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { itemTransactions } from "@db/schema";
|
|||||||
import { withTransaction } from "@/lib/db";
|
import { withTransaction } from "@/lib/db";
|
||||||
import type { Transaction } from "@shared/lib/types";
|
import type { Transaction } from "@shared/lib/types";
|
||||||
import { TransactionType, ItemTransactionType } from "@shared/lib/constants";
|
import { TransactionType, ItemTransactionType } from "@shared/lib/constants";
|
||||||
|
import { UserError } from "@shared/lib/errors";
|
||||||
|
|
||||||
// Module-level session storage
|
// Module-level session storage
|
||||||
const sessions = new Map<string, TradeSession>();
|
const sessions = new Map<string, TradeSession>();
|
||||||
@@ -114,11 +115,11 @@ export const tradeService = {
|
|||||||
*/
|
*/
|
||||||
updateMoney: (threadId: string, userId: string, amount: bigint) => {
|
updateMoney: (threadId: string, userId: string, amount: bigint) => {
|
||||||
const session = tradeService.getSession(threadId);
|
const session = tradeService.getSession(threadId);
|
||||||
if (!session) throw new Error("Session not found");
|
if (!session) throw new UserError("Session not found");
|
||||||
if (session.state !== 'NEGOTIATING') throw new Error("Trade is not active");
|
if (session.state !== 'NEGOTIATING') throw new UserError("Trade is not active");
|
||||||
|
|
||||||
const participant = session.userA.id === userId ? session.userA : session.userB.id === userId ? session.userB : null;
|
const participant = session.userA.id === userId ? session.userA : session.userB.id === userId ? session.userB : null;
|
||||||
if (!participant) throw new Error("User not in trade");
|
if (!participant) throw new UserError("User not in trade");
|
||||||
|
|
||||||
participant.offer.money = amount;
|
participant.offer.money = amount;
|
||||||
unlockAll(session);
|
unlockAll(session);
|
||||||
@@ -127,11 +128,11 @@ export const tradeService = {
|
|||||||
|
|
||||||
addItem: (threadId: string, userId: string, item: { id: number, name: string }, quantity: bigint) => {
|
addItem: (threadId: string, userId: string, item: { id: number, name: string }, quantity: bigint) => {
|
||||||
const session = tradeService.getSession(threadId);
|
const session = tradeService.getSession(threadId);
|
||||||
if (!session) throw new Error("Session not found");
|
if (!session) throw new UserError("Session not found");
|
||||||
if (session.state !== 'NEGOTIATING') throw new Error("Trade is not active");
|
if (session.state !== 'NEGOTIATING') throw new UserError("Trade is not active");
|
||||||
|
|
||||||
const participant = session.userA.id === userId ? session.userA : session.userB.id === userId ? session.userB : null;
|
const participant = session.userA.id === userId ? session.userA : session.userB.id === userId ? session.userB : null;
|
||||||
if (!participant) throw new Error("User not in trade");
|
if (!participant) throw new UserError("User not in trade");
|
||||||
|
|
||||||
const existing = participant.offer.items.find(i => i.id === item.id);
|
const existing = participant.offer.items.find(i => i.id === item.id);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -146,10 +147,10 @@ export const tradeService = {
|
|||||||
|
|
||||||
removeItem: (threadId: string, userId: string, itemId: number) => {
|
removeItem: (threadId: string, userId: string, itemId: number) => {
|
||||||
const session = tradeService.getSession(threadId);
|
const session = tradeService.getSession(threadId);
|
||||||
if (!session) throw new Error("Session not found");
|
if (!session) throw new UserError("Session not found");
|
||||||
|
|
||||||
const participant = session.userA.id === userId ? session.userA : session.userB.id === userId ? session.userB : null;
|
const participant = session.userA.id === userId ? session.userA : session.userB.id === userId ? session.userB : null;
|
||||||
if (!participant) throw new Error("User not in trade");
|
if (!participant) throw new UserError("User not in trade");
|
||||||
|
|
||||||
participant.offer.items = participant.offer.items.filter(i => i.id !== itemId);
|
participant.offer.items = participant.offer.items.filter(i => i.id !== itemId);
|
||||||
|
|
||||||
@@ -159,10 +160,10 @@ export const tradeService = {
|
|||||||
|
|
||||||
toggleLock: (threadId: string, userId: string): boolean => {
|
toggleLock: (threadId: string, userId: string): boolean => {
|
||||||
const session = tradeService.getSession(threadId);
|
const session = tradeService.getSession(threadId);
|
||||||
if (!session) throw new Error("Session not found");
|
if (!session) throw new UserError("Session not found");
|
||||||
|
|
||||||
const participant = session.userA.id === userId ? session.userA : session.userB.id === userId ? session.userB : null;
|
const participant = session.userA.id === userId ? session.userA : session.userB.id === userId ? session.userB : null;
|
||||||
if (!participant) throw new Error("User not in trade");
|
if (!participant) throw new UserError("User not in trade");
|
||||||
|
|
||||||
participant.locked = !participant.locked;
|
participant.locked = !participant.locked;
|
||||||
session.lastInteraction = Date.now();
|
session.lastInteraction = Date.now();
|
||||||
@@ -179,10 +180,10 @@ export const tradeService = {
|
|||||||
*/
|
*/
|
||||||
executeTrade: async (threadId: string): Promise<void> => {
|
executeTrade: async (threadId: string): Promise<void> => {
|
||||||
const session = tradeService.getSession(threadId);
|
const session = tradeService.getSession(threadId);
|
||||||
if (!session) throw new Error("Session not found");
|
if (!session) throw new UserError("Session not found");
|
||||||
|
|
||||||
if (!session.userA.locked || !session.userB.locked) {
|
if (!session.userA.locked || !session.userB.locked) {
|
||||||
throw new Error("Both players must accept the trade first.");
|
throw new UserError("Both players must accept the trade first.");
|
||||||
}
|
}
|
||||||
|
|
||||||
session.state = 'COMPLETED'; // Prevent double execution
|
session.state = 'COMPLETED'; // Prevent double execution
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { eq, and, sql } from "drizzle-orm";
|
|||||||
import { config } from "@shared/lib/config";
|
import { config } from "@shared/lib/config";
|
||||||
import { withTransaction } from "@/lib/db";
|
import { withTransaction } from "@/lib/db";
|
||||||
import type { Transaction } from "@shared/lib/types";
|
import type { Transaction } from "@shared/lib/types";
|
||||||
import { UserError } from "@shared/lib/errors";
|
import { UserError, SystemError } from "@shared/lib/errors";
|
||||||
import { TimerType, TransactionType } from "@shared/lib/constants";
|
import { TimerType, TransactionType } from "@shared/lib/constants";
|
||||||
import { DrizzleClient } from "@shared/db/DrizzleClient";
|
import { DrizzleClient } from "@shared/db/DrizzleClient";
|
||||||
|
|
||||||
@@ -94,13 +94,13 @@ class TriviaService {
|
|||||||
const data = await response.json() as OpenTDBResponse;
|
const data = await response.json() as OpenTDBResponse;
|
||||||
|
|
||||||
if (data.response_code !== 0 || !data.results || data.results.length === 0) {
|
if (data.response_code !== 0 || !data.results || data.results.length === 0) {
|
||||||
throw new Error('Failed to fetch trivia question');
|
throw new SystemError('Failed to fetch trivia question');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = data.results[0];
|
const result = data.results[0];
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new Error('No trivia question returned');
|
throw new SystemError('No trivia question returned');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode base64
|
// Decode base64
|
||||||
|
|||||||
Reference in New Issue
Block a user