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)