레디스 제거 버전 작업
This commit is contained in:
parent
019cb314be
commit
e43af4f944
|
|
@ -6,6 +6,5 @@ DISCORD_CLIENT_ID=your_client_id_here
|
||||||
# User/pass from docker-compose.yml
|
# User/pass from docker-compose.yml
|
||||||
DATABASE_URL="postgresql://kord:password@localhost:5432/kord_db?schema=public"
|
DATABASE_URL="postgresql://kord:password@localhost:5432/kord_db?schema=public"
|
||||||
|
|
||||||
# Redis Configuration
|
## NOTE
|
||||||
REDIS_HOST="localhost"
|
## This project does not use Redis.
|
||||||
REDIS_PORT=6379
|
|
||||||
|
|
@ -2,7 +2,7 @@ FROM node:20-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json yarn.lock .yarnrc.yml ./
|
COPY package.json yarn.lock .yarnrc.yml ./
|
||||||
COPY .yarn ./.yarn
|
COPY .yarn ./.yarn
|
||||||
RUN corepack enable && yarn install
|
RUN corepack enable && yarn install --immutable
|
||||||
|
|
||||||
# Generate Prisma Client
|
# Generate Prisma Client
|
||||||
COPY prisma ./prisma/
|
COPY prisma ./prisma/
|
||||||
|
|
@ -17,9 +17,8 @@ FROM node:20-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json yarn.lock .yarnrc.yml ./
|
COPY package.json yarn.lock .yarnrc.yml ./
|
||||||
COPY .yarn ./.yarn
|
COPY .yarn ./.yarn
|
||||||
RUN corepack enable && yarn install
|
RUN corepack enable && yarn install --immutable --production
|
||||||
|
|
||||||
COPY --from=builder /app/node_modules/@prisma/client ./node_modules/@prisma/client
|
|
||||||
COPY --from=builder /app/dist ./dist
|
COPY --from=builder /app/dist ./dist
|
||||||
|
|
||||||
CMD ["yarn", "node", "dist/index.js"]
|
CMD ["node", "dist/index.js"]
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,13 @@ Kord는 Discord 서버 관리를 돕는 강력하고 유연한 다기능 봇입
|
||||||
|
|
||||||
## 1. 개요 (Overview)
|
## 1. 개요 (Overview)
|
||||||
|
|
||||||
**Kord**는 효율적인 서버 운영을 위해 설계된 Discord 봇입니다. TypeScript와 Discord.js를 기반으로 구축되었으며, Prisma(PostgreSQL)와 Redis를 활용하여 안정적이고 확장 가능한 아키텍처를 제공합니다. 임시 음성 채널 관리, 상세 감사 로그, 권한 진단 등의 핵심 기능을 통해 서버 관리자의 부담을 줄여줍니다.
|
**Kord**는 효율적인 서버 운영을 위해 설계된 Discord 봇입니다. TypeScript와 Discord.js를 기반으로 구축되었으며, Prisma(PostgreSQL)를 활용하여 안정적이고 확장 가능한 아키텍처를 제공합니다. 임시 음성 채널 관리, 상세 감사 로그, 권한 진단 등의 핵심 기능을 통해 서버 관리자의 부담을 줄여줍니다.
|
||||||
|
|
||||||
## 2. 요구사항 (Requirements)
|
## 2. 요구사항 (Requirements)
|
||||||
|
|
||||||
- **Runtime**: Node.js v20 이상
|
- **Runtime**: Node.js v20 이상
|
||||||
- **Package Manager**: Yarn v4 (Berry)
|
- **Package Manager**: Yarn v4 (Berry)
|
||||||
- **Database**: PostgreSQL (Prisma 사용)
|
- **Database**: PostgreSQL (Prisma 사용)
|
||||||
- **Cache**: Redis (다중 인스턴스 동기화 및 캐싱)
|
|
||||||
- **Discord**: Bot Token 및 Client ID (Slash Command 등록용)
|
- **Discord**: Bot Token 및 Client ID (Slash Command 등록용)
|
||||||
|
|
||||||
## 3. 테스트 방법 (Test Methods)
|
## 3. 테스트 방법 (Test Methods)
|
||||||
|
|
@ -59,7 +58,7 @@ Kord는 Discord 서버 관리를 돕는 강력하고 유연한 다기능 봇입
|
||||||
|
|
||||||
1. **빌드**: `yarn build`
|
1. **빌드**: `yarn build`
|
||||||
2. **실행**: `yarn start`
|
2. **실행**: `yarn start`
|
||||||
3. **Docker**: `docker-compose up -d`를 통해 전체 스택(Bot, DB, Redis)을 실행할 수 있습니다.
|
3. **Docker**: `docker-compose up -d`를 통해 전체 스택(Bot, DB)을 실행할 수 있습니다.
|
||||||
|
|
||||||
## 5. 기능 목록 (Feature List)
|
## 5. 기능 목록 (Feature List)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,15 @@
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
kord:
|
||||||
|
build: .
|
||||||
|
container_name: kord-bot
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
container_name: kord-postgres
|
container_name: kord-postgres
|
||||||
|
|
@ -14,15 +23,5 @@ services:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: kord-redis
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
redis_data:
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "6.4.1",
|
"@prisma/client": "6.4.1",
|
||||||
"discord.js": "^14.25.1",
|
"discord.js": "^14.25.1",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1"
|
||||||
"ioredis": "^5.10.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,52 @@
|
||||||
import Redis from 'ioredis';
|
type CacheEntry = { value: string; expiresAtMs?: number };
|
||||||
import { env } from '../config/env';
|
|
||||||
import { logger } from '../utils/logger';
|
|
||||||
|
|
||||||
export const redis = new Redis({
|
const nowMs = () => Date.now();
|
||||||
host: env.REDIS_HOST,
|
|
||||||
port: env.REDIS_PORT,
|
|
||||||
lazyConnect: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
redis.on('error', (err) => {
|
class LocalCache {
|
||||||
logger.error('Redis Error:', err);
|
private store = new Map<string, CacheEntry>();
|
||||||
});
|
|
||||||
|
|
||||||
export const connectRedis = async () => {
|
private isExpired(entry: CacheEntry | undefined) {
|
||||||
try {
|
return entry?.expiresAtMs !== undefined && entry.expiresAtMs <= nowMs();
|
||||||
await redis.connect();
|
|
||||||
logger.info('Connected to Redis successfully.');
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to connect to Redis:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getEntry(key: string): CacheEntry | undefined {
|
||||||
|
const entry = this.store.get(key);
|
||||||
|
if (!entry) return undefined;
|
||||||
|
if (this.isExpired(entry)) {
|
||||||
|
this.store.delete(key);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(key: string): Promise<string | null> {
|
||||||
|
const entry = this.getEntry(key);
|
||||||
|
return entry ? entry.value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(key: string, value: string, opts?: { exSeconds?: number; nx?: boolean }): Promise<boolean> {
|
||||||
|
const existing = this.getEntry(key);
|
||||||
|
if (opts?.nx && existing) return false;
|
||||||
|
|
||||||
|
const expiresAtMs = opts?.exSeconds !== undefined ? nowMs() + opts.exSeconds * 1000 : undefined;
|
||||||
|
this.store.set(key, { value, expiresAtMs });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async del(key: string): Promise<boolean> {
|
||||||
|
const existed = this.getEntry(key) !== undefined;
|
||||||
|
this.store.delete(key);
|
||||||
|
return existed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cache = new LocalCache();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Best-effort local process lock.
|
||||||
|
* - Returns true only once per key during ttl window.
|
||||||
|
* - Single process only (no cross-instance coordination).
|
||||||
|
*/
|
||||||
|
export const tryAcquireLock = async (key: string, ttlSeconds: number): Promise<boolean> => {
|
||||||
|
return cache.set(`lock:${key}`, '1', { exSeconds: ttlSeconds, nx: true });
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import { loadCommands } from '../handlers/CommandLoader';
|
||||||
import { loadEvents } from '../handlers/EventLoader';
|
import { loadEvents } from '../handlers/EventLoader';
|
||||||
import { handleGlobalExceptions } from '../utils/errorHandler';
|
import { handleGlobalExceptions } from '../utils/errorHandler';
|
||||||
import { connectDB } from '../database';
|
import { connectDB } from '../database';
|
||||||
import { connectRedis } from '../cache';
|
|
||||||
|
|
||||||
export class KordClient extends Client {
|
export class KordClient extends Client {
|
||||||
public commands: Collection<string, any> = new Collection();
|
public commands: Collection<string, any> = new Collection();
|
||||||
|
|
@ -28,7 +27,6 @@ export class KordClient extends Client {
|
||||||
|
|
||||||
// Connect to external services
|
// Connect to external services
|
||||||
await connectDB();
|
await connectDB();
|
||||||
await connectRedis();
|
|
||||||
|
|
||||||
// Load Handlers
|
// Load Handlers
|
||||||
await loadCommands(this);
|
await loadCommands(this);
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@ export const env = {
|
||||||
DISCORD_TOKEN: process.env.DISCORD_TOKEN || '',
|
DISCORD_TOKEN: process.env.DISCORD_TOKEN || '',
|
||||||
DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID || '',
|
DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID || '',
|
||||||
DATABASE_URL: process.env.DATABASE_URL || '',
|
DATABASE_URL: process.env.DATABASE_URL || '',
|
||||||
REDIS_HOST: process.env.REDIS_HOST || 'localhost',
|
|
||||||
REDIS_PORT: parseInt(process.env.REDIS_PORT || '6379', 10),
|
|
||||||
VOICE_WAITING_ROOM_ID: process.env.VOICE_WAITING_ROOM_ID || '',
|
VOICE_WAITING_ROOM_ID: process.env.VOICE_WAITING_ROOM_ID || '',
|
||||||
VOICE_CATEGORY_ID: process.env.VOICE_CATEGORY_ID || '',
|
VOICE_CATEGORY_ID: process.env.VOICE_CATEGORY_ID || '',
|
||||||
INSTANCE_ID: generateInstanceId(),
|
INSTANCE_ID: generateInstanceId(),
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { VoiceService } from '../services/VoiceService';
|
||||||
import { PresenceService } from '../services/PresenceService';
|
import { PresenceService } from '../services/PresenceService';
|
||||||
import { EventService } from '../services/EventService';
|
import { EventService } from '../services/EventService';
|
||||||
import { auditLogService } from '../services/AuditLogService';
|
import { auditLogService } from '../services/AuditLogService';
|
||||||
import { redis } from '../cache';
|
import { tryAcquireLock } from '../cache';
|
||||||
import { env } from '../config/env';
|
import { env } from '../config/env';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -22,7 +22,7 @@ export default {
|
||||||
try {
|
try {
|
||||||
const lockKey = 'commands:sync:lock';
|
const lockKey = 'commands:sync:lock';
|
||||||
// EX 300 = 5 minutes lock. Only one instance needs to do this per boot cycle.
|
// EX 300 = 5 minutes lock. Only one instance needs to do this per boot cycle.
|
||||||
const acquired = await redis.set(lockKey, '1', 'EX', 300, 'NX');
|
const acquired = await tryAcquireLock(lockKey, 300);
|
||||||
|
|
||||||
if (acquired) {
|
if (acquired) {
|
||||||
const commandsData = Array.from(client.commands.values()).map(c => c.data.toJSON());
|
const commandsData = Array.from(client.commands.values()).map(c => c.data.toJSON());
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Client, Guild, Invite, GuildMember } from 'discord.js';
|
import { Client, Guild, Invite, GuildMember } from 'discord.js';
|
||||||
import { redis } from '../cache';
|
import { cache } from '../cache';
|
||||||
import { prisma } from '../database';
|
import { prisma } from '../database';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@ export class InviteService {
|
||||||
code: inv.code,
|
code: inv.code,
|
||||||
uses: inv.uses || 0
|
uses: inv.uses || 0
|
||||||
}));
|
}));
|
||||||
await redis.set(`invites:${guild.id}`, JSON.stringify(inviteData));
|
await cache.set(`invites:${guild.id}`, JSON.stringify(inviteData));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`InviteMaster: Failed to cache invites for guild ${guild.id}:`, error);
|
logger.error(`InviteMaster: Failed to cache invites for guild ${guild.id}:`, error);
|
||||||
}
|
}
|
||||||
|
|
@ -41,7 +41,7 @@ export class InviteService {
|
||||||
try {
|
try {
|
||||||
// Fetch current active invites
|
// Fetch current active invites
|
||||||
const newInvites = await guild.invites.fetch();
|
const newInvites = await guild.invites.fetch();
|
||||||
const cachedData = await redis.get(`invites:${guild.id}`);
|
const cachedData = await cache.get(`invites:${guild.id}`);
|
||||||
|
|
||||||
let usedInvite: Invite | undefined;
|
let usedInvite: Invite | undefined;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { VoiceState, ChannelType, PermissionFlagsBits, VoiceChannel, ActionRowBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, Client, GuildMember } from 'discord.js';
|
import { VoiceState, ChannelType, PermissionFlagsBits, VoiceChannel, ActionRowBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, Client, GuildMember } from 'discord.js';
|
||||||
import { prisma } from '../database';
|
import { prisma } from '../database';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { redis } from '../cache';
|
import { tryAcquireLock } from '../cache';
|
||||||
import { ErrorDefs } from '../errors/ErrorCodes';
|
import { ErrorDefs } from '../errors/ErrorCodes';
|
||||||
import { t, SupportedLocale, DEFAULT_LOCALE } from '../i18n';
|
import { t, SupportedLocale, DEFAULT_LOCALE } from '../i18n';
|
||||||
import { getContextLocale } from '../i18n/localeHelper';
|
import { getContextLocale } from '../i18n/localeHelper';
|
||||||
|
|
@ -10,8 +10,7 @@ import { auditLogService } from './AuditLogService';
|
||||||
export class VoiceService {
|
export class VoiceService {
|
||||||
public static async syncChannels(client: Client) {
|
public static async syncChannels(client: Client) {
|
||||||
const lockKey = 'voice:sync:lock';
|
const lockKey = 'voice:sync:lock';
|
||||||
// NX = only set if not exists, EX = expire in 60s
|
const acquired = await tryAcquireLock(lockKey, 60);
|
||||||
const acquired = await redis.set(lockKey, '1', 'EX', 60, 'NX');
|
|
||||||
if (!acquired) {
|
if (!acquired) {
|
||||||
logger.info('VoiceService: Another instance is already syncing channels. Skipping.');
|
logger.info('VoiceService: Another instance is already syncing channels. Skipping.');
|
||||||
return;
|
return;
|
||||||
|
|
@ -55,9 +54,8 @@ export class VoiceService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.info('VoiceService: Channel synchronization complete.');
|
logger.info('VoiceService: Channel synchronization complete.');
|
||||||
} finally {
|
} catch (error) {
|
||||||
// Free the lock just in case, though EX ensures it doesn't hang forever
|
logger.error('VoiceService: Failed during channel synchronization', error);
|
||||||
await redis.del(lockKey).catch(() => {});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static async handleVoiceStateUpdate(oldState: VoiceState, newState: VoiceState) {
|
public static async handleVoiceStateUpdate(oldState: VoiceState, newState: VoiceState) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { TextChannel, WebhookClient } from 'discord.js';
|
import { TextChannel, WebhookClient } from 'discord.js';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { redis } from '../cache';
|
import { cache } from '../cache';
|
||||||
|
|
||||||
export class WebhookService {
|
export class WebhookService {
|
||||||
private static readonly MAX_WEBHOOKS = 10;
|
private static readonly MAX_WEBHOOKS = 10;
|
||||||
|
|
@ -9,7 +9,7 @@ export class WebhookService {
|
||||||
public static async getWebhookClient(channel: TextChannel): Promise<WebhookClient | null> {
|
public static async getWebhookClient(channel: TextChannel): Promise<WebhookClient | null> {
|
||||||
try {
|
try {
|
||||||
// 1. Check cache
|
// 1. Check cache
|
||||||
const cachedData = await redis.get(`webhook:${channel.id}`);
|
const cachedData = await cache.get(`webhook:${channel.id}`);
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
const { id, token } = JSON.parse(cachedData);
|
const { id, token } = JSON.parse(cachedData);
|
||||||
return new WebhookClient({ id, token });
|
return new WebhookClient({ id, token });
|
||||||
|
|
@ -40,14 +40,11 @@ export class WebhookService {
|
||||||
logger.info(`Created new webhook for channel ${channel.id}`);
|
logger.info(`Created new webhook for channel ${channel.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Save to Redis Cache (expire in 1 day to ensure token freshness)
|
// 3. Save to cache (expire in 1 day to ensure token freshness)
|
||||||
if (kordWebhook.token) {
|
if (kordWebhook.token) {
|
||||||
await redis.set(
|
await cache.set(`webhook:${channel.id}`, JSON.stringify({ id: kordWebhook.id, token: kordWebhook.token }), {
|
||||||
`webhook:${channel.id}`,
|
exSeconds: 86400,
|
||||||
JSON.stringify({ id: kordWebhook.id, token: kordWebhook.token }),
|
});
|
||||||
'EX',
|
|
||||||
86400
|
|
||||||
);
|
|
||||||
return new WebhookClient({ id: kordWebhook.id, token: kordWebhook.token });
|
return new WebhookClient({ id: kordWebhook.id, token: kordWebhook.token });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
76
yarn.lock
76
yarn.lock
|
|
@ -774,13 +774,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@ioredis/commands@npm:1.5.1":
|
|
||||||
version: 1.5.1
|
|
||||||
resolution: "@ioredis/commands@npm:1.5.1"
|
|
||||||
checksum: 10c0/cb8f6d13cff0753e3e7ef001fb895491985d9a623248192538f13bc2fd9bfdfde3c18cf2ba6f20ec8ceaa681b0771070d3a09b82eed044c798bcfef5e3ae54b3
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@isaacs/cliui@npm:^8.0.2":
|
"@isaacs/cliui@npm:^8.0.2":
|
||||||
version: 8.0.2
|
version: 8.0.2
|
||||||
resolution: "@isaacs/cliui@npm:8.0.2"
|
resolution: "@isaacs/cliui@npm:8.0.2"
|
||||||
|
|
@ -2105,13 +2098,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"cluster-key-slot@npm:^1.1.0":
|
|
||||||
version: 1.1.2
|
|
||||||
resolution: "cluster-key-slot@npm:1.1.2"
|
|
||||||
checksum: 10c0/d7d39ca28a8786e9e801eeb8c770e3c3236a566625d7299a47bb71113fb2298ce1039596acb82590e598c52dbc9b1f088c8f587803e697cb58e1867a95ff94d3
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"co@npm:^4.6.0":
|
"co@npm:^4.6.0":
|
||||||
version: 4.6.0
|
version: 4.6.0
|
||||||
resolution: "co@npm:4.6.0"
|
resolution: "co@npm:4.6.0"
|
||||||
|
|
@ -2205,13 +2191,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"denque@npm:^2.1.0":
|
|
||||||
version: 2.1.0
|
|
||||||
resolution: "denque@npm:2.1.0"
|
|
||||||
checksum: 10c0/f9ef81aa0af9c6c614a727cb3bd13c5d7db2af1abf9e6352045b86e85873e629690f6222f4edd49d10e4ccf8f078bbeec0794fafaf61b659c0589d0c511ec363
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"detect-newline@npm:^3.1.0":
|
"detect-newline@npm:^3.1.0":
|
||||||
version: 3.1.0
|
version: 3.1.0
|
||||||
resolution: "detect-newline@npm:3.1.0"
|
resolution: "detect-newline@npm:3.1.0"
|
||||||
|
|
@ -2947,23 +2926,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"ioredis@npm:^5.10.1":
|
|
||||||
version: 5.10.1
|
|
||||||
resolution: "ioredis@npm:5.10.1"
|
|
||||||
dependencies:
|
|
||||||
"@ioredis/commands": "npm:1.5.1"
|
|
||||||
cluster-key-slot: "npm:^1.1.0"
|
|
||||||
debug: "npm:^4.3.4"
|
|
||||||
denque: "npm:^2.1.0"
|
|
||||||
lodash.defaults: "npm:^4.2.0"
|
|
||||||
lodash.isarguments: "npm:^3.1.0"
|
|
||||||
redis-errors: "npm:^1.2.0"
|
|
||||||
redis-parser: "npm:^3.0.0"
|
|
||||||
standard-as-callback: "npm:^2.1.0"
|
|
||||||
checksum: 10c0/d0507b52520d3bdd5dacaa33aed9dd3133794d8633b43a6b7fc3199a5e73f92cb77409f6904abe68e3221a95a630d97073b8c1c9e2c0c7613124db67e97c0eb0
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"ip-address@npm:^10.0.1":
|
"ip-address@npm:^10.0.1":
|
||||||
version: 10.1.0
|
version: 10.1.0
|
||||||
resolution: "ip-address@npm:10.1.0"
|
resolution: "ip-address@npm:10.1.0"
|
||||||
|
|
@ -3617,7 +3579,6 @@ __metadata:
|
||||||
discord.js: "npm:^14.25.1"
|
discord.js: "npm:^14.25.1"
|
||||||
dotenv: "npm:^17.3.1"
|
dotenv: "npm:^17.3.1"
|
||||||
eslint: "npm:^10.1.0"
|
eslint: "npm:^10.1.0"
|
||||||
ioredis: "npm:^5.10.1"
|
|
||||||
jest: "npm:^30.3.0"
|
jest: "npm:^30.3.0"
|
||||||
prettier: "npm:^3.8.1"
|
prettier: "npm:^3.8.1"
|
||||||
prisma: "npm:6.4.1"
|
prisma: "npm:6.4.1"
|
||||||
|
|
@ -3669,20 +3630,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"lodash.defaults@npm:^4.2.0":
|
|
||||||
version: 4.2.0
|
|
||||||
resolution: "lodash.defaults@npm:4.2.0"
|
|
||||||
checksum: 10c0/d5b77aeb702caa69b17be1358faece33a84497bcca814897383c58b28a2f8dfc381b1d9edbec239f8b425126a3bbe4916223da2a576bb0411c2cefd67df80707
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"lodash.isarguments@npm:^3.1.0":
|
|
||||||
version: 3.1.0
|
|
||||||
resolution: "lodash.isarguments@npm:3.1.0"
|
|
||||||
checksum: 10c0/5e8f95ba10975900a3920fb039a3f89a5a79359a1b5565e4e5b4310ed6ebe64011e31d402e34f577eca983a1fc01ff86c926e3cbe602e1ddfc858fdd353e62d8
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"lodash.memoize@npm:^4.1.2":
|
"lodash.memoize@npm:^4.1.2":
|
||||||
version: 4.1.2
|
version: 4.1.2
|
||||||
resolution: "lodash.memoize@npm:4.1.2"
|
resolution: "lodash.memoize@npm:4.1.2"
|
||||||
|
|
@ -4257,22 +4204,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"redis-errors@npm:^1.0.0, redis-errors@npm:^1.2.0":
|
|
||||||
version: 1.2.0
|
|
||||||
resolution: "redis-errors@npm:1.2.0"
|
|
||||||
checksum: 10c0/5b316736e9f532d91a35bff631335137a4f974927bb2fb42bf8c2f18879173a211787db8ac4c3fde8f75ed6233eb0888e55d52510b5620e30d69d7d719c8b8a7
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"redis-parser@npm:^3.0.0":
|
|
||||||
version: 3.0.0
|
|
||||||
resolution: "redis-parser@npm:3.0.0"
|
|
||||||
dependencies:
|
|
||||||
redis-errors: "npm:^1.0.0"
|
|
||||||
checksum: 10c0/ee16ac4c7b2a60b1f42a2cdaee22b005bd4453eb2d0588b8a4939718997ae269da717434da5d570fe0b05030466eeb3f902a58cf2e8e1ca058bf6c9c596f632f
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"require-directory@npm:^2.1.1":
|
"require-directory@npm:^2.1.1":
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
resolution: "require-directory@npm:2.1.1"
|
resolution: "require-directory@npm:2.1.1"
|
||||||
|
|
@ -4435,13 +4366,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"standard-as-callback@npm:^2.1.0":
|
|
||||||
version: 2.1.0
|
|
||||||
resolution: "standard-as-callback@npm:2.1.0"
|
|
||||||
checksum: 10c0/012677236e3d3fdc5689d29e64ea8a599331c4babe86956bf92fc5e127d53f85411c5536ee0079c52c43beb0026b5ce7aa1d834dd35dd026e82a15d1bcaead1f
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"string-length@npm:^4.0.2":
|
"string-length@npm:^4.0.2":
|
||||||
version: 4.0.2
|
version: 4.0.2
|
||||||
resolution: "string-length@npm:4.0.2"
|
resolution: "string-length@npm:4.0.2"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue