Initial commit

This commit is contained in:
NekoMonci12
2025-04-04 13:46:37 +07:00
commit a6a6710d95
14 changed files with 2033 additions and 0 deletions

15
.env.example Normal file
View File

@@ -0,0 +1,15 @@
DISCORD_TOKEN=DISCORD_BOT_TOKEN
CLIENT_ID=DISCORD_BOT_CLIENT_ID
OPENAI_API_KEY=sk-***********************************
OPENAI_BASE_URL=https://api.deepseek.com/chat/completions
MONGO_URL=mongodb://username:password@localhost:27000
MONGO_DB_NAME=RakunNakun
REDIS_URL=redis://localhost:6424
PASTEBIN_DEV_KEY=PASTEBIN_DEVELOPER_KEY
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=
MYSQL_DATABASE=database

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

130
.gitignore vendored Normal file
View File

@@ -0,0 +1,130 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

2
README.md Normal file
View File

@@ -0,0 +1,2 @@
# RakunNakun-AI

74
cacheManagerRedis.js Normal file
View File

@@ -0,0 +1,74 @@
const { createClient } = require('redis');
class CacheManagerRedis {
/**
* @param {object} options - Options for the Redis client.
* Example: { url: 'redis://localhost:6379' }
*/
constructor(options) {
this.client = createClient(options);
this.client.on('error', (err) => console.error('Redis Client Error', err));
this.client.connect();
}
// Normalize input for consistent comparison
normalize(input) {
return input.trim().toLowerCase();
}
// Compute the Levenshtein distance between two strings
levenshtein(a, b) {
const matrix = [];
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j] + 1
);
}
}
}
return matrix[b.length][a.length];
}
// Calculate similarity between two strings (1 means identical, 0 means completely different)
similarity(a, b) {
const distance = this.levenshtein(a, b);
const maxLen = Math.max(a.length, b.length);
if (maxLen === 0) return 1;
return 1 - distance / maxLen;
}
// Check the cache for a result that is at least 80% similar to the new input.
async getCachedResult(input) {
const normalizedInput = this.normalize(input);
const keys = await this.client.keys('cache:*');
for (const key of keys) {
const storedNormalizedInput = key.slice(6); // remove "cache:" prefix
const sim = this.similarity(normalizedInput, storedNormalizedInput);
if (sim >= 0.8) {
const cachedOutput = await this.client.get(key);
return cachedOutput;
}
}
return null;
}
// Store the result in cache with key as normalized input
async setCache(input, output) {
const normalizedInput = this.normalize(input);
await this.client.set(`cache:${normalizedInput}`, output, { EX: 3600 });
}
}
module.exports = CacheManagerRedis;

76
database.js Normal file
View File

@@ -0,0 +1,76 @@
// database.js
const mysql = require('mysql2/promise');
require('dotenv').config();
class Database {
constructor() {
this.pool = mysql.createPool({
host: process.env.MYSQL_HOST || 'localhost',
port: process.env.MYSQL_PORT || 3306,
user: process.env.MYSQL_USER || 'root',
password: process.env.MYSQL_PASSWORD || '',
database: process.env.MYSQL_DATABASE || 'discord_bot',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
}
// Initializes the database by creating the Models and Guilds tables
async init() {
// SQL query to create the "Models" table with columns MODEL_ID, TOKEN_INPUT, TOKEN_OUTPUT, and API_KEY
const createModelsQuery = `
CREATE TABLE IF NOT EXISTS Models (
MODEL_ID VARCHAR(255) NOT NULL PRIMARY KEY,
TOKEN_INPUT DECIMAL(10,2),
TOKEN_OUTPUT DECIMAL(10,2),
TOKEN_CACHED_INPUT DECIMAL(10,2),
TOKEN_CACHED_OUPUT DECIMAL(10,2),
API_KEY VARCHAR(255),
API_URL VARCHAR(255)
);
`;
// SQL query to create the "Guilds" table with a foreign key on CHAT_MODELS referencing Models.MODEL_ID
const createGuildsQuery = `
CREATE TABLE IF NOT EXISTS Guilds (
GUILD_ID VARCHAR(255) NOT NULL PRIMARY KEY,
GUILD_OWNER_ID VARCHAR(255) NOT NULL,
GUILD_USERS_ID TEXT,
CHAT_TOKENS TEXT,
CHAT_MODELS VARCHAR(255),
CHAT_PERSONA TEXT NOT NULL DEFAULT 'You are a helpful assistant.',
STATUS TINYINT(1) DEFAULT 1,
DEBUG TINYINT(1) DEFAULT 0,
CONSTRAINT fk_chat_models FOREIGN KEY (CHAT_MODELS) REFERENCES Models(MODEL_ID)
ON UPDATE CASCADE
ON DELETE SET NULL
);
`;
// Create ChatLogs table
const createChatLogsQuery = `
CREATE TABLE IF NOT EXISTS ChatLogs (
GUILD_ID VARCHAR(255) NOT NULL,
GUILD_USERS_ID TEXT,
MESSAGE_INPUT TEXT NOT NULL,
MESSAGE_OUTPUT TEXT NOT NULL,
CACHED TINYINT(1) NOT NULL DEFAULT 0,
TIMESTAMP TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`;
try {
await this.pool.query(createModelsQuery);
console.log('Models table is ready or already exists.');
await this.pool.query(createGuildsQuery);
console.log('Guilds table is ready or already exists.');
await this.pool.query(createChatLogsQuery);
console.log('ChatLogs table is ready or already exists.');
} catch (error) {
console.error('Error creating tables:', error);
}
}
}
module.exports = Database;

33
deploy-commands.js Normal file
View File

@@ -0,0 +1,33 @@
// deployCommands.js
const { REST, Routes } = require('discord.js');
require('dotenv').config();
class CommandDeployer {
/**
* @param {Array} commands - Array of command definitions (JSON).
* @param {string} clientId - Your Discord application client ID.
* @param {string} token - Your Discord bot token.
*/
constructor(commands, clientId, token) {
this.commands = commands;
this.clientId = clientId;
this.token = token;
this.rest = new REST({ version: '10' }).setToken(token);
}
async deploy() {
try {
console.log('Started refreshing application (/) commands.');
await this.rest.put(
Routes.applicationCommands(this.clientId),
{ body: this.commands }
);
console.log('Successfully reloaded application (/) commands.');
} catch (error) {
console.error('Error deploying commands:', error);
}
}
}
module.exports = CommandDeployer;

42
hybridCacheManager.js Normal file
View File

@@ -0,0 +1,42 @@
const CacheManagerRedis = require('./cacheManagerRedis');
const MongoCacheManager = require('./mongoCacheManager');
class HybridCacheManager {
/**
* @param {object} redisOptions - Options for Redis.
* @param {string} mongoUrl - Connection URL for MongoDB.
* @param {string} dbName - MongoDB database name.
* @param {string} collectionName - MongoDB collection name for cache.
*/
constructor(redisOptions = {}, mongoUrl, dbName, collectionName = 'cache') {
this.redisCache = new CacheManagerRedis(redisOptions);
this.mongoCache = new MongoCacheManager(mongoUrl, dbName, collectionName);
}
async getCachedResult(input) {
// Try Redis first.
let result = await this.redisCache.getCachedResult(input);
if (result) {
console.log("Hybrid Cache: Found result in Redis.");
return result;
}
// If not in Redis, try MongoDB.
result = await this.mongoCache.getCachedResult(input);
if (result) {
console.log("Hybrid Cache: Found result in MongoDB.");
// Optionally, refresh Redis cache.
await this.redisCache.setCache(input, result);
return result;
}
console.log("Hybrid Cache: No cached result found.");
return null;
}
async setCache(input, value) {
await this.redisCache.setCache(input, value);
await this.mongoCache.setCache(input, value);
console.log("Hybrid Cache: Stored value in both caches for key:", input);
}
}
module.exports = HybridCacheManager;

319
index.js Normal file
View File

@@ -0,0 +1,319 @@
const { Client, GatewayIntentBits, SlashCommandBuilder } = require('discord.js');
const Database = require('./database');
const CommandDeployer = require('./deploy-commands');
const axios = require('axios');
const HybridCacheManager = require('./hybridCacheManager');
const MessageSplitter = require('./messageSplitter');
const PastebinClient = require('./pastebinClient');
require('dotenv').config();
// Simple logging helpers
const logInfo = (msg, ...args) => console.log(`[INFO] ${msg}`, ...args);
const logDebug = (msg, ...args) => console.log(`[DEBUG] ${msg}`, ...args);
const logError = (msg, ...args) => console.error(`[ERROR] ${msg}`, ...args);
// Initialize PasteBin (pastebin method is left unchanged)
const pastebinClient = new PastebinClient(process.env.PASTEBIN_DEV_KEY);
async function safeDeferReply(interaction) {
try {
if (!interaction.deferred && !interaction.replied) {
await interaction.deferReply();
}
} catch (error) {
logError("Error deferring reply:", error);
}
}
(async () => {
// Initialize database (create tables if needed)
const db = new Database();
await db.init();
logInfo("Database initialization complete.");
// Instantiate Hybrid Cache Manager and message splitter
const redisOptions = { url: process.env.REDIS_URL || 'redis://localhost:6379' };
const mongoUrl = process.env.MONGO_URL || 'mongodb://localhost:27017';
const dbName = process.env.MONGO_DB_NAME || 'discordCache';
const cache = new HybridCacheManager(redisOptions, mongoUrl, dbName);
const splitter = new MessageSplitter(2000);
// Specify the default model ID and persona to use if none is set for a guild
const defaultModelId = 'deepseek-chat';
const defaultPersona = 'You are a helpful assistant.';
const defaultApiUrl = process.env.OPENAI_BASE_URL;
// --- Define Slash Command ---
const commands = [
new SlashCommandBuilder()
.setName('chat')
.setDescription('Chat with the RakunNakun')
.addStringOption(option =>
option.setName('message')
.setDescription('Your question to RakunNakun')
.setRequired(true)
)
].map(command => command.toJSON());
// --- Deploy Commands using CommandDeployer class ---
const deployer = new CommandDeployer(commands, process.env.CLIENT_ID, process.env.DISCORD_TOKEN);
await deployer.deploy();
logInfo("Slash commands deployed.");
// Create a new Discord client with the required intents
const client = new Client({
intents: [GatewayIntentBits.Guilds]
});
client.once('ready', () => {
logInfo(`Logged in as ${client.user.tag}`);
});
// Listen for slash command interactions
client.on('interactionCreate', async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName !== 'chat') return;
const userMessage = interaction.options.getString('message');
logInfo(`Received message: "${userMessage}" from ${interaction.user.tag}`);
// Set default factors and values
let tokenInputFactor = 0.6;
let tokenOutputFactor = 0.9;
let tokenCachedInputFactor = 0.6;
let tokenCachedOutputFactor = 0.9;
let modelApiKey = process.env.OPENAI_API_KEY;
let modelBaseUrl = process.env.OPENAI_BASE_URL;
let modelIdToUse = defaultModelId;
let personaToUse = defaultPersona;
let requiredRoleId = null;
let debugMode = false;
let currentTokens = 0;
let pasteUrl;
try {
// Fetch guild settings including model, token balance, persona, etc.
if (interaction.guild && interaction.guild.id) {
const [guildRows] = await db.pool.query(
'SELECT CHAT_MODELS, GUILD_USERS_ID, GUILD_OWNER_ID, DEBUG, CHAT_TOKENS, CHAT_PERSONA FROM Guilds WHERE GUILD_ID = ?',
[interaction.guild.id]
);
if (guildRows.length === 0) {
logError("Guild not registered:", interaction.guild.id);
await interaction.reply(`Your guild is not registered in our system. Please contact the guild owner <@${interaction.guild.ownerId}>.`);
return;
}
currentTokens = Number(guildRows[0].CHAT_TOKENS) || 0;
logDebug(`Current token balance: ${currentTokens}`);
if (currentTokens < 2000) {
logInfo(`Token balance (${currentTokens}) is below threshold for guild ${interaction.guild.id}.`);
await interaction.reply("Insufficient tokens in your guild. Please recharge tokens.");
return;
}
if (guildRows[0].CHAT_MODELS) {
modelIdToUse = guildRows[0].CHAT_MODELS;
logInfo(`Guild ${interaction.guild.id} is using model ${modelIdToUse}`);
} else {
logInfo(`Guild ${interaction.guild.id} has no custom model. Using default model.`);
}
if (guildRows[0].GUILD_USERS_ID) {
requiredRoleId = guildRows[0].GUILD_USERS_ID;
}
if (guildRows[0].CHAT_PERSONA) {
personaToUse = guildRows[0].CHAT_PERSONA;
}
if (guildRows[0].DEBUG && Number(guildRows[0].DEBUG) === 1) {
debugMode = true;
logDebug("Debug mode enabled for this guild.");
}
}
// Check required role if defined.
if (requiredRoleId && !interaction.member.roles.cache.has(requiredRoleId)) {
logInfo(`User ${interaction.user.tag} lacks the required role (${requiredRoleId}).`);
await interaction.reply("You don't have the required role to use this command.");
return;
}
// Query the Models table for the model parameters using modelIdToUse
const [modelRows] = await db.pool.query(
'SELECT TOKEN_INPUT, TOKEN_OUTPUT, TOKEN_CACHED_INPUT, TOKEN_CACHED_OUTPUT, API_KEY, API_URL FROM Models WHERE MODEL_ID = ?',
[modelIdToUse]
);
if (modelRows.length > 0) {
tokenInputFactor = modelRows[0].TOKEN_INPUT || tokenInputFactor;
tokenOutputFactor = modelRows[0].TOKEN_OUTPUT || tokenOutputFactor;
tokenCachedInputFactor = modelRows[0].TOKEN_CACHED_INPUT || tokenInputFactor;
tokenCachedOutputFactor = modelRows[0].TOKEN_CACHED_OUTPUT || tokenOutputFactor;
if (modelRows[0].API_KEY) {
modelApiKey = modelRows[0].API_KEY;
}
if (modelRows[0].API_URL) {
modelBaseUrl = modelRows[0].API_URL;
}
if (debugMode) {
logDebug("Model row:", modelRows[0]);
}
} else {
logError(`No record found in Models for ${modelIdToUse}. Using default parameters.`);
}
} catch (error) {
logError('Error fetching model parameters:', error);
}
// --- Helper Functions to Count Tokens using dynamic factors ---
const calculateInputTokens = (text, factor = tokenInputFactor) => Math.ceil(text.length * factor);
// Check cache before making an API call
const cachedOutput = await cache.getCachedResult(userMessage);
if (cachedOutput) {
logDebug("Cache hit: Using cached result for input:", userMessage);
const cachedInputTokens = Math.ceil(userMessage.length * tokenCachedInputFactor);
const cachedOutputTokens = Math.ceil(cachedOutput.length * tokenCachedOutputFactor);
let finalReply = cachedOutput;
if (debugMode) {
finalReply += `\n\n**Token Usage (Cached):**\n- Input Tokens: ${cachedInputTokens}\n- Output Tokens: ${cachedOutputTokens}`;
}
try {
pasteUrl = await pastebinClient.createPaste(finalReply, '1M', 'Chat Log (Cached)');
if (debugMode) {
logInfo(`Chat logged on Pastebin: ${pasteUrl}`);
}
} catch (pasteError) {
if (debugMode) {
logError('Failed to create Pastebin paste:', pasteError);
}
pasteUrl = "FAILED ERROR CODE";
}
await db.pool.query(
'INSERT INTO ChatLogs (GUILD_ID, GUILD_USERS_ID, MESSAGE_INPUT, MESSAGE_OUTPUT, CACHED) VALUES (?, ?, ?, ?, 1)',
[interaction.guild.id, interaction.user.id, userMessage, pasteUrl]
);
const parts = splitter.split(finalReply);
if (parts.length === 1) {
await interaction.reply(parts[0]);
} else {
await interaction.reply(parts[0]);
for (let i = 1; i < parts.length; i++) {
await interaction.followUp(parts[i]);
}
}
const tokensUsed = cachedInputTokens + cachedOutputTokens;
await db.pool.query(
'UPDATE Guilds SET CHAT_TOKENS = CHAT_TOKENS - ? WHERE GUILD_ID = ?',
[tokensUsed, interaction.guild.id]
);
logInfo(`Deducted ${tokensUsed} tokens (cached). New balance: ${currentTokens - tokensUsed}`);
return;
} else {
logDebug("Cache miss: No cached result for input:", userMessage);
}
// Defer reply to allow time for API processing
await safeDeferReply(interaction);
try {
logDebug("Sending API request with model:", modelIdToUse);
let axiosResponse;
try {
axiosResponse = await axios.post(
modelBaseUrl,
{
model: modelIdToUse,
messages: [
{ role: 'system', content: personaToUse },
{ role: 'user', content: userMessage }
],
stream: false
},
{
headers: {
'Authorization': `Bearer ${modelApiKey}`,
'Content-Type': 'application/json'
}
}
);
} catch (error) {
if (
error.response &&
error.response.data &&
error.response.data.error &&
error.response.data.error.message &&
error.response.data.error.message.includes('Service is too busy')
) {
logInfo("Primary API is too busy. Falling back to default model and default API.");
axiosResponse = await axios.post(
defaultApiUrl,
{
model: defaultModelId,
messages: [
{ role: 'system', content: personaToUse },
{ role: 'user', content: userMessage }
],
stream: false
},
{
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json'
}
}
);
} else {
throw error;
}
}
logDebug("API response received.");
const reply = axiosResponse.data.choices[0].message.content.trim();
const outputTokens = Math.ceil(reply.length * tokenOutputFactor);
const apiInputTokens = calculateInputTokens(userMessage);
await cache.setCache(userMessage, reply);
logDebug("Caching API response for input:", userMessage);
let finalReply = reply;
if (debugMode) {
finalReply += `\n\n**Token Usage:**\n- Model-ID: \`${modelIdToUse}\`\n- Input Tokens: ${apiInputTokens}\n- Output Tokens: ${outputTokens}`;
}
try {
pasteUrl = await pastebinClient.createPaste(finalReply, '1M', 'Chat Log');
if (debugMode) {
logInfo(`Chat logged on Pastebin: ${pasteUrl}`);
}
} catch (pasteError) {
if (debugMode) {
logError('Failed to create Pastebin paste:', pasteError);
}
pasteUrl = "FAILED ERROR CODE";
}
await db.pool.query(
'INSERT INTO ChatLogs (GUILD_ID, GUILD_USERS_ID, MESSAGE_INPUT, MESSAGE_OUTPUT, CACHED) VALUES (?, ?, ?, ?, 0)',
[interaction.guild.id, interaction.user.id, userMessage, pasteUrl]
);
const parts = splitter.split(finalReply);
if (parts.length === 1) {
await interaction.editReply(parts[0]);
} else {
await interaction.editReply(parts[0]);
for (let i = 1; i < parts.length; i++) {
await interaction.followUp(parts[i]);
}
}
const tokensUsed = apiInputTokens + outputTokens;
await db.pool.query(
'UPDATE Guilds SET CHAT_TOKENS = CHAT_TOKENS - ? WHERE GUILD_ID = ?',
[tokensUsed, interaction.guild.id]
);
logInfo(`Deducted ${tokensUsed} tokens (API). New balance: ${currentTokens - tokensUsed}`);
} catch (error) {
logError('Error with API:', error.response ? error.response.data : error.message);
await interaction.editReply('There was an error processing your request.');
}
});
client.login(process.env.DISCORD_TOKEN);
})();

43
messageSplitter.js Normal file
View File

@@ -0,0 +1,43 @@
// messageSplitter.js
class MessageSplitter {
/**
* Create a new MessageSplitter instance.
* @param {number} maxLength - Maximum allowed length per message. Defaults to 2000.
*/
constructor(maxLength = 2000) {
this.maxLength = maxLength;
}
/**
* Splits the given message into an array of parts that do not exceed maxLength,
* without cutting off words.
* @param {string} message - The message to split.
* @returns {string[]} - An array of message parts.
*/
split(message) {
if (message.length <= this.maxLength) return [message];
const parts = [];
// Split message by spaces.
const words = message.split(' ');
let currentPart = '';
for (const word of words) {
// If adding the next word would exceed maxLength, push the current part.
if (currentPart.length + word.length + 1 > this.maxLength) {
parts.push(currentPart.trim());
currentPart = word + ' ';
} else {
currentPart += word + ' ';
}
}
// Push any remaining text.
if (currentPart.length > 0) {
parts.push(currentPart.trim());
}
return parts;
}
}
module.exports = MessageSplitter;

74
mongoCacheManager.js Normal file
View File

@@ -0,0 +1,74 @@
const { MongoClient } = require('mongodb');
class MongoCacheManager {
/**
* @param {string} mongoUrl - Connection URL for MongoDB.
* @param {string} dbName - Database name.
* @param {string} collectionName - Collection name for caching.
*/
constructor(mongoUrl, dbName, collectionName = 'cache') {
this.mongoUrl = mongoUrl;
this.dbName = dbName;
this.collectionName = collectionName;
this.client = new MongoClient(mongoUrl, { useUnifiedTopology: true });
this.connected = false;
}
async connect() {
if (!this.connected) {
try {
await this.client.connect();
this.collection = this.client.db(this.dbName).collection(this.collectionName);
this.connected = true;
console.log("[MongoCache] Connected to MongoDB for caching.");
} catch (error) {
console.error("[MongoCache] MongoDB connection error:", error);
this.connected = false;
}
}
}
normalize(input) {
return input.trim().toLowerCase();
}
async getCachedResult(input) {
try {
await this.connect();
if (!this.connected || !this.collection) {
console.error("[MongoCache] Not connected to MongoDB, skipping getCachedResult.");
return null;
}
const key = this.normalize(input);
const doc = await this.collection.findOne({ key });
if (doc) {
console.log("[MongoCache] Found cached result for key:", key);
return doc.value;
}
} catch (error) {
console.error("[MongoCache] Error retrieving cache for key:", this.normalize(input), error);
}
return null;
}
async setCache(input, value) {
try {
await this.connect();
if (!this.connected || !this.collection) {
console.error("[MongoCache] Not connected to MongoDB, skipping setCache.");
return;
}
const key = this.normalize(input);
const result = await this.collection.updateOne(
{ key },
{ $set: { value, updatedAt: new Date() } },
{ upsert: true }
);
console.log("[MongoCache] Stored value for key:", key, "Update result:", result.result);
} catch (error) {
console.error("[MongoCache] Error storing cache for key:", this.normalize(input), error);
}
}
}
module.exports = MongoCacheManager;

1157
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

12
package.json Normal file
View File

@@ -0,0 +1,12 @@
{
"dependencies": {
"discord.js": "^14.11.0",
"dotenv": "^16.0.0",
"mysql2": "^3.2.0",
"openai": "^3.2.1",
"axios": "^1.4.0",
"qs": "^6.11.2",
"redis": "^4.6.7",
"mongodb": "^5.7.0"
}
}

54
pastebinClient.js Normal file
View File

@@ -0,0 +1,54 @@
const axios = require('axios');
const qs = require('qs'); // for form URL-encoded data
class PastebinClient {
/**
* Create a new PastebinClient instance.
* @param {string} devKey - Your Pastebin developer key.
*/
constructor(devKey) {
this.devKey = devKey;
this.apiUrl = 'https://pastebin.com/api/api_post.php';
}
/**
* Creates a new paste on Pastebin.
* @param {string} text - The text to paste.
* @param {string} expireDate - Expiration for the paste (e.g. '1M' for 1 month).
* @param {string} pasteName - Optional paste title.
* @param {string} pasteFormat - Optional paste format.
* @param {number} pastePrivate - 0=public, 1=unlisted, 2=private.
* @returns {Promise<string>} - The URL of the created paste.
*/
async createPaste(text, expireDate = '1M', pasteName = 'Chat Log', pasteFormat = '', pastePrivate = 1) {
// Prepare the payload as form URL encoded data.
const payload = {
api_dev_key: this.devKey,
api_option: 'paste',
api_paste_code: text,
api_paste_expire_date: expireDate, // "1M" for 1 month
api_paste_name: pasteName,
api_paste_format: pasteFormat,
api_paste_private: pastePrivate,
};
try {
const response = await axios.post(this.apiUrl, qs.stringify(payload), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
// If Pastebin returns a URL, then paste creation succeeded.
// If not, it might return an error message.
if (response.data.startsWith('http')) {
return response.data;
} else {
throw new Error(`Pastebin error: ${response.data}`);
}
} catch (error) {
throw new Error(`Pastebin API request failed: ${error.message}`);
}
}
}
module.exports = PastebinClient;