import dotenv from 'dotenv' dotenv.config() import fs from 'fs' import path from 'path' import { Client, GatewayIntentBits } from 'discord.js' import OpenAI from 'openai' import { logger, loadJSONFile } from './utils.js' const openai = new OpenAI() const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, ], }) const botToken = process.env.DISCORD_BOT_TOKEN const channelId = process.env.DISCORD_DELIVERY_CHANNEL_ID // File paths const distributorsPath = path.resolve('distributors.json') const storagePath = path.resolve('storage.json') function resolveDistributorID(distributorParam) { const distributors = loadJSONFile(distributorsPath, []) if (!distributorParam) return null const normalizedParam = distributorParam.trim().toLowerCase() const distributorData = distributors.find( (dist) => dist.id.toString().toLowerCase() === normalizedParam || dist.Име.toLowerCase() === normalizedParam ) if (distributorData) { return distributorData.id.toString() } else { return null } } // Function to save data to the storage file function saveStorage(data) { try { fs.writeFileSync(storagePath, JSON.stringify(data, null, 2)) } catch (error) { console.error('Error saving storage:', error) } } // Fetch the latest state dynamically function getLatestState() { return loadJSONFile(storagePath, { missingItems: [], orderedItems: [] }) } let botMessageId = null // Function to send the list of messages to ChatGPT async function sendToChatGPT(messages) { const combinedInput = messages.map((msg) => msg.content).join('\n') const distributors = loadJSONFile(distributorsPath, []) const storage = getLatestState() // Map distributors by ID for easy lookup const distributorMap = new Map( distributors.map((dist) => [dist.id.toString(), dist]) ) const distributorData = distributors .map( (dist) => `Distributor ID: "${dist.id}", Name: "${dist.Име}", Products: "${dist['Продукт']}"` ) .join('\n\n') const missingItemsData = storage.missingItems .map((group) => { const distributor = distributorMap.get(group.distributor) || {} return `Distributor: "${ distributor.Име || 'Неизвестен дистрибутор' }", Items: "${group.items.join(', ')}"` }) .join('\n') const orderedItemsData = storage.orderedItems.join(', ') const functions = [ { name: 'AddMissingItems', description: 'Добавяне на артикули към списъка с липсващи артикули, групирани по дистрибутор', parameters: { type: 'object', properties: { distributor: { type: 'string', description: 'ID или име на дистрибутор', }, items: { type: 'array', items: { type: 'string' }, description: 'Списък с артикули за добавяне', }, }, required: ['distributor', 'items'], }, }, { name: 'RemoveMissingItems', description: 'Премахване на артикули от списъка с липсващи артикули', parameters: { type: 'object', properties: { distributor: { type: 'string', description: 'ID или име на дистрибутор', }, items: { type: 'array', items: { type: 'string' }, description: 'Списък с артикули за премахване', }, }, required: ['distributor', 'items'], }, }, { name: 'MarkItemsAsOrdered', description: 'Преместване на артикули от липсващи в поръчани', parameters: { type: 'object', properties: { distributor: { type: 'string', description: 'ID или име на дистрибутор', }, items: { type: 'array', items: { type: 'string' }, description: 'Списък с артикули за отбелязване като поръчани', }, }, required: ['distributor', 'items'], }, }, { name: 'MarkItemsAsDelivered', description: 'Премахване на артикули от поръчани артикули и липсващи артикули', parameters: { type: 'object', properties: { items: { type: 'array', items: { type: 'string' }, description: 'Списък с артикули, които са били доставени', }, }, required: ['items'], }, }, { name: 'ShowMissingItems', description: 'Показване на текущите липсващи и поръчани артикули', parameters: { type: 'object', properties: {}, }, }, ] const messagesToSend = [ { role: 'system', content: ` Вие сте асистент, който помага за управление на липсващи и поръчани артикули в магазин. Всички данни и съобщения са на български език. Когато ви се даде съобщение, идентифицирайте предвиденото действие и извлечете параметрите, необходими за изпълнение на действието. **Важно:** - Артикулите са групирани по дистрибутори. - **Когато добавяте артикули, трябва да ги асоциирате с правилния дистрибутор въз основа на продуктите, които те доставят.** - **Използвайте точните имена на продуктите от данните за дистрибуторите, включително всички емоджита.** - Когато премахвате артикули, уверете се, че те съществуват в списъка с липсващи артикули или поръчани артикули. - **Използвайте текущите списъци с липсващи и поръчани артикули, за да съпоставите входовете на потребителя с артикулите, дори ако потребителят използва синоними или частични имена.** - **Когато потребителят споменава, че артикул е доставен или пристигнал, премахнете го от списъците с липсващи и поръчани артикули, като извикате функцията MarkItemsAsDelivered с артикула(ите).** **Дистрибутори:** ${distributorData} **Текущи липсващи артикули:** ${missingItemsData} **Текущи поръчани артикули:** ${orderedItemsData} Отговорете, като извикате една от функциите с извлечените параметри. `, }, { role: 'user', content: combinedInput }, ] try { const response = await openai.chat.completions.create({ model: 'gpt-4o', messages: messagesToSend, functions: functions, function_call: 'auto', }) const message = response.choices[0].message if (message && message.function_call) { const action = message.function_call.name; let parameters; try { parameters = JSON.parse(message.function_call.arguments || '{}'); } catch (parseErr) { console.error('Error parsing function arguments:', parseErr); throw new Error('Invalid function call arguments.'); } logger('Extracted action and parameters:', { action, parameters }); return { action, parameters }; } else { console.error('Function call missing or malformed:', message); throw new Error('No valid function call found in the response.'); } } catch (error) { console.error('Error processing response:', error) } } async function addMissingItems({ distributor, items }, storage) { if (!items || items.length === 0) return const distributorID = resolveDistributorID(distributor) if (!distributorID) { console.error(`Distributor "${distributor}" not found.`) return } // Find or create the group for the distributor let group = storage.missingItems.find((g) => g.distributor === distributorID) if (!group) { group = { distributor: distributorID, items: [] } storage.missingItems.push(group) } // Add the items, avoiding duplicates items.forEach((item) => { if (!group.items.includes(item)) { group.items.push(item) } }) } async function removeMissingItems({ distributor, items }, storage) { if (!items || items.length === 0) return let distributorID if (distributor) { distributorID = resolveDistributorID(distributor) if (!distributorID) { console.error(`Distributor "${distributor}" not found.`) return } } if (distributorID) { // Remove items from the specified distributor const group = storage.missingItems.find( (g) => g.distributor === distributorID ) if (group) { group.items = group.items.filter((item) => !items.includes(item)) // Remove the group if it's empty if (group.items.length === 0) { storage.missingItems = storage.missingItems.filter((g) => g !== group) } } } else { // Remove items from all distributors storage.missingItems.forEach((group) => { group.items = group.items.filter((item) => !items.includes(item)) }) // Remove empty groups storage.missingItems = storage.missingItems.filter( (group) => group.items.length > 0 ) } } async function markItemsAsOrdered({ distributor, items }, storage) { if (!items || items.length === 0) return let distributorID if (distributor) { distributorID = resolveDistributorID(distributor) if (!distributorID) { console.error(`Distributor "${distributor}" not found.`) return } } if (distributorID) { // Remove items from the specified distributor const group = storage.missingItems.find( (g) => g.distributor === distributorID ) if (group) { group.items = group.items.filter((item) => { if (items.includes(item)) { // Add to orderedItems if not already present if (!storage.orderedItems.includes(item)) { storage.orderedItems.push(item) } return false // Remove from missingItems } return true }) // Remove the group if it's empty if (group.items.length === 0) { storage.missingItems = storage.missingItems.filter((g) => g !== group) } } } else { // Remove items from all distributors storage.missingItems.forEach((group) => { group.items = group.items.filter((item) => { if (items.includes(item)) { // Add to orderedItems if not already present if (!storage.orderedItems.includes(item)) { storage.orderedItems.push(item) } return false // Remove from missingItems } return true }) }) // Remove empty groups storage.missingItems = storage.missingItems.filter( (group) => group.items.length > 0 ) } } async function markItemsAsDelivered({ items }, storage) { if (!items || items.length === 0) return // Remove items from orderedItems storage.orderedItems = storage.orderedItems.filter( (item) => !items.includes(item) ) // Remove items from missingItems storage.missingItems.forEach((group) => { group.items = group.items.filter((item) => !items.includes(item)) }) // Remove empty groups storage.missingItems = storage.missingItems.filter( (group) => group.items.length > 0 ) } function displayItems() { const { missingItems, orderedItems } = getLatestState() const distributors = loadJSONFile(distributorsPath, []) // Map distributors by ID for easy lookup const distributorMap = new Map( distributors.map((dist) => { const id = dist.id.toString() return [id, dist] }) ) let result = '📦 **Липсващи продукти:**\n' if (missingItems.length > 0) { missingItems.forEach((group) => { const distributor = distributorMap.get(group.distributor) || {} result += `**${distributor.Име || 'Неизвестен дистрибутор'}** (${ distributor.Номер || 'Няма номер' })\n` result += `${group.items.join(', ')}\n\n` }) } else { result += 'Всички продукти са налични. 🎉\n\n' } result += '🛒 **Поръчани продукти:**\n' if (orderedItems.length > 0) { result += `${orderedItems.join(', ')}\n\n` } else { result += 'Няма поръчани продукти.\n\n' } return result } // Function to update the bot's message in the channel or send a new one async function updateBotMessage() { const content = displayItems() const channel = await client.channels.fetch(channelId) if (botMessageId) { try { const previousMessage = await channel.messages.fetch(botMessageId) await previousMessage.edit(content) } catch (error) { console.error('Error editing previous message:', error) try { await channel.messages.fetch(botMessageId).then((msg) => msg.delete()) } catch (deleteError) { console.error('Error deleting previous message:', deleteError) } } } else { try { const sentMessage = await channel.send(content) botMessageId = sentMessage.id } catch (sendError) { console.error('Error sending new message:', sendError) } } } export const handleDeliveryMessage = async (message) => { const user = message.author try { const response = await sendToChatGPT([message]) if (!response) { throw new Error('Invalid response from ChatGPT') } const { action, parameters } = response let feedbackMessage = '' const storage = getLatestState() switch (action) { case 'AddMissingItems': await addMissingItems(parameters, storage) feedbackMessage = `Артикулите ${parameters.items.join( ', ' )} бяха добавени към списъка с липсващи артикули за дистрибутора ${ parameters.distributor }.` break case 'RemoveMissingItems': await removeMissingItems(parameters, storage) feedbackMessage = `Артикулите ${parameters.items.join( ', ' )} бяха премахнати от списъка с липсващи артикули.` break case 'MarkItemsAsOrdered': await markItemsAsOrdered(parameters, storage) feedbackMessage = `Артикулите ${parameters.items.join( ', ' )} бяха преместени в списъка с поръчани артикули.` break case 'MarkItemsAsDelivered': await markItemsAsDelivered(parameters, storage) feedbackMessage = `Артикулите ${parameters.items.join( ', ' )} бяха отбелязани като доставени и премахнати от всички списъци.` break case 'ShowMissingItems': feedbackMessage = 'Списъците с липсващи и поръчани артикули бяха актуализирани.' break default: feedbackMessage = 'Не разпознавам командата.' } // Save the updated storage and update the channel message saveStorage(storage) await updateBotMessage() // Send a DM to the user try { await user.send(feedbackMessage) logger(`DM sent to ${user.tag}: ${feedbackMessage}`) } catch (dmError) { console.error(`Error sending DM to ${user.tag}:`, dmError) message.channel.send( `Не успях да изпратя лично съобщение на ${user.tag}.` ) } } catch (error) { console.error('Error processing message:', error) message.channel.send( error.message || 'Съжалявам, възникна грешка при обработката на вашата заявка.' ) } } client.login(botToken)