vkashti-bots / delivery.js
delivery.js
Raw
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)