Это основной класс моего бота:

@file:Suppress("UNUSED_VARIABLE")

package ru.deflecta.auroomy

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.json.Json
import net.dv8tion.jda.api.JDABuilder
import net.dv8tion.jda.api.entities.channel.concrete.NewsChannel
import net.dv8tion.jda.api.entities.MessageEmbed
import net.dv8tion.jda.api.events.session.ReadyEvent
import net.dv8tion.jda.api.hooks.ListenerAdapter
import net.dv8tion.jda.api.utils.FileUpload
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.encodeToString
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.interactions.commands.build.Commands
import net.dv8tion.jda.api.requests.GatewayIntent
import java.io.*
import java.util.zip.ZipInputStream
import kotlin.system.exitProcess
import java.util.Timer
import java.util.TimerTask
import java.io.File

// Глобальный CoroutineScope
val globalScope = CoroutineScope(Dispatchers.IO + SupervisorJob())

fun main() {
    val configContent = File("config.json").readText()
    val config = Json { ignoreUnknownKeys = true }.decodeFromString<Config>(configContent)

    val jda = JDABuilder.createDefault(config.discordToken, GatewayIntent.GUILD_MESSAGES, GatewayIntent.MESSAGE_CONTENT)
        .addEventListeners(BotListener(config))
        .build()

    // startApiServer(config)
}

class BotListener(private val config: Config) : ListenerAdapter() {
    private val googlePlayTimer = Timer()
    private val modsTimer = Timer()

    private val publisher = GooglePlayPublisher()
    private val langFiles = mutableMapOf<String, ModLanguageData>()
    private val mutex = Mutex()

    private val messageCache = MessageCache()

    private lateinit var googlePlayChannel: NewsChannel
    private lateinit var modsChannel: NewsChannel

    private lateinit var russianFeedChannel: TextChannel
    private lateinit var englishFeedChannel: TextChannel

    private val e621Timer = Timer()

    // Создаем сет для хранения ID отправленных постов
    private val sentE621Posts = mutableSetOf<Long>()
    private val e621PostsFile = "e621posts.json"

    override fun onReady(event: ReadyEvent) {
        println("Бот запущен!")

        // Получаем каналы
        googlePlayChannel = event.jda.getNewsChannelById(config.googlePlayChannelId)
            ?: run {
                println("Канал Google Play с ID ${config.googlePlayChannelId} не найден!")
                exitProcess(1)
            }

        modsChannel = event.jda.getNewsChannelById(config.modsChannelId)
            ?: run {
                println("Канал модов с ID ${config.modsChannelId} не найден!")
                exitProcess(1)
            }

        // Инициализация: загрузка текущих языковых файлов модов
        globalScope.launch {
            for (mod in config.mods) {
                try {
                    println("Пытаемся получить языковые файлы для мода ${mod.name}")
                    val newLanguageData = getLanguageFiles(mod, client, config.curseForgeApiKey)
                    if (newLanguageData != null) {
                        println("Языковые файлы мода ${mod.name} получены")

                        var shouldNotify = false
                        var ruChanged = false
                        var enChanged = false
                        var ruDiff: String? = null
                        var enDiff: String? = null

                        mutex.withLock {
                            val previousData = langFiles[mod.name]
                            if (previousData == null) {
                                // Первая инициализация: загрузка текущих данных и предыдущей версии
                                println("Первая инициализация для мода ${mod.name}")
                                val previousLanguageData = getPreviousLanguageData(mod, client, config.curseForgeApiKey)
                                if (previousLanguageData != null) {
                                    // Вычисляем различия между предыдущей и новой версиями
                                    ruChanged = previousLanguageData.ruRuMap != null && newLanguageData.ruRuMap != null &&
                                            previousLanguageData.ruRuMap != newLanguageData.ruRuMap
                                    enChanged = previousLanguageData.enUsMap != null && newLanguageData.enUsMap != null &&
                                            previousLanguageData.enUsMap != newLanguageData.enUsMap

                                    if (ruChanged) {
                                        ruDiff = computeDiff(previousLanguageData.ruRuMap, newLanguageData.ruRuMap)
                                    }
                                    if (enChanged) {
                                        enDiff = computeDiff(previousLanguageData.enUsMap, newLanguageData.enUsMap)
                                    }

                                    // Обновляем langFiles с новой версией
                                    langFiles[mod.name] = newLanguageData

                                    shouldNotify = ruChanged || enChanged
                                }
                            } else {
                                // Обновление существующих данных
                                println("Обновление данных для мода ${mod.name}")
                                ruChanged = newLanguageData.ruRuMap != null && previousData.ruRuMap != null &&
                                        newLanguageData.ruRuMap != previousData.ruRuMap
                                enChanged = newLanguageData.enUsMap != null && previousData.enUsMap != null &&
                                        newLanguageData.enUsMap != previousData.enUsMap

                                if (ruChanged) {
                                    ruDiff = computeDiff(previousData.ruRuMap!!, newLanguageData.ruRuMap!!)
                                }
                                if (enChanged) {
                                    enDiff = computeDiff(previousData.enUsMap!!, newLanguageData.enUsMap!!)
                                }

                                if (ruChanged || enChanged) {
                                    // Обновляем langFiles с новой версией
                                    langFiles[mod.name] = newLanguageData
                                    shouldNotify = true
                                }
                            }
                        }

                        if (shouldNotify && (ruDiff != null || enDiff != null)) {
                            println("Обнаружены изменения в моде ${mod.name}. Отправляем уведомление.")
                            println("ruDiff: $ruDiff")
                            println("enDiff: $enDiff")

                            val cacheKey = "mod_${mod.name}_${newLanguageData.modVersion}"
                            val dataForCache =
                                (newLanguageData.ruRu ?: ByteArray(0)) + (newLanguageData.enUs ?: ByteArray(0))
                            if (!messageCache.isAlreadySent(cacheKey, dataForCache)) {
                                withContext(Dispatchers.IO) {
                                    sendLanguageUpdateEmbed(
                                        modsChannel,
                                        mod.name,
                                        newLanguageData,
                                        ruChanged,
                                        enChanged,
                                        ruDiff,
                                        enDiff,
                                        cacheKey,
                                        dataForCache
                                    )
                                }
                            } else {
                                println("Сообщение для мода ${mod.name} уже отправлено. Пропуск.")
                            }
                        } else {
                            println("Изменений для мода ${mod.name} не обнаружено.")
                        }
                    } else {
                        println("Не удалось получить языковые файлы для мода ${mod.name}")
                    }
                } catch (e: Exception) {
                    println("Ошибка при обращении к Modrinth у мода ${mod.name}: ${e.message}")
                }
            }
        }

        // Остальные таймеры инициализации, если они не зависят от e621, можно оставить вне корутины
        // Периодическая задача проверки обновлений Google Play
        googlePlayTimer.scheduleAtFixedRate(
            object : TimerTask() {
                override fun run() {
                    checkGooglePlayUpdates()
                }
            }, 0, 5 * 60 * 1000 // Каждые 5 минут
        )

        // Периодическая задача проверки обновлений модов
        modsTimer.scheduleAtFixedRate(
            object : TimerTask() {
                override fun run() {
                    checkModsUpdates()
                }
            }, 0, 10 * 60 * 1000 // Каждые 10 минут
        )

        // Инициализируем каналы
        russianFeedChannel = event.jda.getTextChannelById(config.russianFeedChannelId)
            ?: throw IllegalStateException("Канал с ID ${config.russianFeedChannelId} не найден")

        englishFeedChannel = event.jda.getTextChannelById(config.englishFeedChannelId)
            ?: throw IllegalStateException("Канал с ID ${config.englishFeedChannelId} не найден")

        // Запускаем корутину
        globalScope.launch {
            // Загружаем отправленные посты
            loadSentE621Posts()

            // Запускаем таймер для проверки новых артов после завершения загрузки
            e621Timer.scheduleAtFixedRate(
                object : TimerTask() {
                    override fun run() {
                        checkE621Updates()
                    }
                },
                0,
                10 * 60 * 1000 // Каждые 10 минут
            )
        }

        // Регистрация слэш-команды `/tontaro` на уровне гильдии
        event.jda.getGuildById("878019629925158932")?.let { guild ->
            guild.updateCommands().addCommands(
                Commands.slash("tontaro", "Найти случайный рисунок от Tontaro")
                // Здесь можно добавить опции, если необходимо
            ).queue()
        }

        event.jda.updateCommands().queue()

        val guildId = "878019629925158932" // Замените на ID вашей гильдии
        event.jda.getGuildById(guildId)?.let { guild ->
            guild.updateCommands().addCommands(
                Commands.slash("tontaro", "Search random Tontaro art"),
                Commands.slash("тонтаро", "Найти случайный арт Тонтаро")
            ).queue(
                { println("Команды успешно зарегистрированы в гильдии ${guild.name}") },
                { error -> println("Ошибка при регистрации команд: ${error.message}") }
            )
        } ?: println("Гильдия с ID $guildId не найдена")
    }

    private suspend fun loadSentE621Posts() {
        val file = File(e621PostsFile)
        if (file.exists()) {
            val jsonString = file.readText()
            val loadedPosts = Json.decodeFromString<Set<Long>>(jsonString)
            sentE621Posts.addAll(loadedPosts)
            println("Загружено ${sentE621Posts.size} отправленных постов из файла.")
        } else {
            println("Файл с сохраненными постами не найден. Инициализируем список отправленных постов.")
            try {
                val recentPosts = fetchRecentE621Posts()
                for (post in recentPosts) {
                    sentE621Posts.add(post.id)
                }
                // Сохраняем инициализированные посты в файл
                saveSentE621Posts()
                println("Инициализировано ${sentE621Posts.size} постов из e621.")
            } catch (e: Exception) {
                println("Ошибка при инициализации e621 постов: ${e.message}")
            }
        }
    }

    private fun saveSentE621Posts() {
        try {
            val jsonString = Json.encodeToString(sentE621Posts)
            File(e621PostsFile).writeText(jsonString)
            println("Сохранено ${sentE621Posts.size} отправленных постов в файл.")
        } catch (e: Exception) {
            println("Ошибка при сохранении постов e621: ${e.message}")
        }
    }

    private suspend fun fetchRecentE621Posts(): List<E621Post> {
        return retryIO {
            val response: E621Response = client.get("https://e621.net/posts.json") {
                header("User-Agent", config.e621UserAgent)
                parameter("tags", "tontaro -rating:explicit order:id_desc")
                parameter("limit", 50)
            }.body()

            response.posts
        }
    }

    private fun checkE621Updates() {
        globalScope.launch {
            try {
                val newPosts = fetchNewE621Posts()
                for (post in newPosts) {
                    val imageUrl = post.file.url
                    if (imageUrl != null) {
                        val embed = net.dv8tion.jda.api.EmbedBuilder()
                            // .setTitle("Новый арт от Tontaro")
                            .setImage(imageUrl)
                            // .setUrl("https://e621.net/posts/${post.id}")
                            // .setFooter("Автор: Tontaro")
                            .setColor(0x50FA7B)
                            .build()

                        // Отправляем в русскоязычный канал
                        russianFeedChannel.sendMessageEmbeds(embed).queue()

                        // Отправляем в англоязычный канал
                        englishFeedChannel.sendMessageEmbeds(embed).queue()

                        // Добавляем ID поста в набор отправленных
                        sentE621Posts.add(post.id)
                    }
                }

                // После обработки новых постов сохраняем их в файл
                saveSentE621Posts()

            } catch (e: Exception) {
                println("Ошибка при проверке обновлений e621: ${e.message}")
            }
        }
    }

    private suspend fun fetchNewE621Posts(): List<E621Post> {
        val response: E621Response = client.get("https://e621.net/posts.json") {
            header("User-Agent", config.e621UserAgent)
            parameter("tags", "tontaro -rating:explicit order:id_asc")
            parameter("limit", 50)
        }.body()

        // Фильтруем только новые посты
        return response.posts.filter { post ->
            !sentE621Posts.contains(post.id) && (post.rating == "s" || post.rating == "q")
        }
    }

    private fun checkGooglePlayUpdates() {
        val latestRelease = publisher.getLatestRelease()
        latestRelease?.let { release ->
            val cacheKey = "googleplay_${release.versionCode}"
            val releaseData = release.releaseNotes.toByteArray()
            val cachedData = messageCache.getMessageData(cacheKey)
            val messageId = messageCache.getMessageId(cacheKey)

            if (cachedData == null || messageId == null) {
                // Сообщение не было отправлено ранее или нет информации о messageId
                sendGooglePlayUpdate(release, cacheKey, releaseData)
            } else if (!cachedData.contentEquals(releaseData)) {
                // Данные изменились, пытаемся обновить сообщение
                val messageExists = checkMessageExists(googlePlayChannel, messageId)
                if (messageExists) {
                    // Обновляем существующее сообщение
                    googlePlayChannel.editMessageEmbedsById(messageId, buildEmbed(release))
                        .setActionRow(
                            net.dv8tion.jda.api.interactions.components.buttons.Button.link(
                                "https://play.google.com/store/apps/details?id=ru.deflecta.fnwb",
                                "Скачать"
                            )
                        )
                        .queue()
                    // Обновляем кеш
                    messageCache.saveMessage(cacheKey, releaseData, messageId)
                } else {
                    // Сообщение удалено, отправляем заново
                    sendGooglePlayUpdate(release, cacheKey, releaseData)
                }
            } else {
                // Данные не изменились, но проверим, существует ли сообщение
                val messageExists = checkMessageExists(googlePlayChannel, messageId)
                if (!messageExists) {
                    // Сообщение было удалено, отправляем заново
                    sendGooglePlayUpdate(release, cacheKey, releaseData)
                }
            }
        }
    }

    private fun sendGooglePlayUpdate(release: ReleaseInfo, cacheKey: String, releaseData: ByteArray) {
        googlePlayChannel.sendMessageEmbeds(buildEmbed(release))
            .setActionRow(
                net.dv8tion.jda.api.interactions.components.buttons.Button.link(
                    "https://play.google.com/store/apps/details?id=ru.deflecta.fnwb",
                    "Скачать"
                )
            )
            .queue { message ->
                messageCache.saveMessage(cacheKey, releaseData, message.id)
            }
    }

    private fun checkModsUpdates() {
        globalScope.launch {
            for (mod in config.mods) {
                try {
                    println("Пытаемся получить языковые файлы для мода ${mod.name}")
                    val newLanguageData = getLanguageFiles(mod, client, config.curseForgeApiKey)
                    if (newLanguageData != null) {
                        println("Языковые файлы мода ${mod.name} получены")

                        var shouldNotify = false
                        var ruChanged = false
                        var enChanged = false
                        var ruDiff: String? = null
                        var enDiff: String? = null

                        mutex.withLock {
                            val previousData = langFiles[mod.name]
                            if (previousData == null) {
                                // Первая инициализация: загрузка предыдущей версии
                                println("Первая инициализация для мода ${mod.name}")
                                val previousLanguageData = getPreviousLanguageData(mod, client, config.curseForgeApiKey)
                                if (previousLanguageData != null) {
                                    ruChanged = previousLanguageData.ruRuMap != null && newLanguageData.ruRuMap != null &&
                                            previousLanguageData.ruRuMap != newLanguageData.ruRuMap
                                    enChanged = previousLanguageData.enUsMap != null && newLanguageData.enUsMap != null &&
                                            previousLanguageData.enUsMap != newLanguageData.enUsMap

                                    if (ruChanged) {
                                        ruDiff = computeDiff(previousLanguageData.ruRuMap, newLanguageData.ruRuMap)
                                    }
                                    if (enChanged) {
                                        enDiff = computeDiff(previousLanguageData.enUsMap, newLanguageData.enUsMap)
                                    }

                                    langFiles[mod.name] = newLanguageData

                                    shouldNotify = ruChanged || enChanged
                                }
                            } else {
                                // Обновление существующих данных
                                println("Обновление данных для мода ${mod.name}")
                                ruChanged = newLanguageData.ruRuMap != null && previousData.ruRuMap != null &&
                                        newLanguageData.ruRuMap != previousData.ruRuMap
                                enChanged = newLanguageData.enUsMap != null && previousData.enUsMap != null &&
                                        newLanguageData.enUsMap != previousData.enUsMap

                                if (ruChanged) {
                                    ruDiff = computeDiff(previousData.ruRuMap!!, newLanguageData.ruRuMap!!)
                                }
                                if (enChanged) {
                                    enDiff = computeDiff(previousData.enUsMap!!, newLanguageData.enUsMap!!)
                                }

                                if (ruChanged || enChanged) {
                                    langFiles[mod.name] = newLanguageData
                                    shouldNotify = true
                                }
                            }
                        }

                        if (shouldNotify && (ruDiff != null || enDiff != null)) {
                            println("Обнаружены изменения в моде ${mod.name}. Отправляем уведомление.")
                            println("ruDiff: $ruDiff")
                            println("enDiff: $enDiff")

                            val cacheKey = "mod_${mod.name}_${newLanguageData.modVersion}"
                            val dataForCache =
                                (newLanguageData.ruRu ?: ByteArray(0)) + (newLanguageData.enUs ?: ByteArray(0))
                            if (!messageCache.isAlreadySent(cacheKey, dataForCache)) {
                                withContext(Dispatchers.IO) {
                                    sendLanguageUpdateEmbed(
                                        modsChannel,
                                        mod.name,
                                        newLanguageData,
                                        ruChanged,
                                        enChanged,
                                        ruDiff,
                                        enDiff,
                                        cacheKey,
                                        dataForCache
                                    )
                                }
                            } else {
                                println("Сообщение для мода ${mod.name} уже отправлено. Пропуск.")
                            }
                        } else {
                            println("Изменений для мода ${mod.name} не обнаружено.")
                        }
                    } else {
                        println("Не удалось получить языковые файлы для мода ${mod.name}")
                    }
                } catch (e: Exception) {
                    println("Ошибка при проверке мода ${mod.name}: ${e.message}")
                }
            }
        }

    private fun buildLanguageEmbed(
        modName: String,
        languageData: ModLanguageData,
        ruChanged: Boolean,
        enChanged: Boolean
    ): net.dv8tion.jda.api.EmbedBuilder {
        val embedBuilder = net.dv8tion.jda.api.EmbedBuilder()
            .setTitle("Мод $modName обновился!")
            .setDescription("Возможно вы захотите обновить свои переводы.")
            .addField("Версия мода", languageData.modVersion ?: "Неизвестно", true)
            .setColor(0x50FA7B)

        val changesDescription = when {
            ruChanged && enChanged -> "Изменились оба языковых файла: `ru_ru` и `en_us`."
            ruChanged -> "Изменился только `ru_ru`."
            enChanged -> "Изменился только `en_us`."
            else -> "Нет изменений в языковых файлах."
        }

        embedBuilder.addField("Какие из файлов изменились", changesDescription, false)
        return embedBuilder
    }

    private fun buildAttachments(modName: String, languageData: ModLanguageData): List<FileUpload> {
        val attachments = mutableListOf<FileUpload>()
        languageData.ruRu?.let {
            attachments.add(FileUpload.fromData(it, "${modName}_ru_ru.json"))
        }
        languageData.enUs?.let {
            attachments.add(FileUpload.fromData(it, "${modName}_en_us.json"))
        }
        return attachments
    }

    private fun checkMessageExists(channel: NewsChannel, messageId: String): Boolean {
        return try {
            val message = channel.retrieveMessageById(messageId).complete()
            message != null
        } catch (e: Exception) {
            // Сообщение не найдено или нет доступа
            false
        }
    }

    private fun sendLanguageUpdateEmbed(
        channel: NewsChannel,
        modName: String,
        languageData: ModLanguageData,
        ruChanged: Boolean,
        enChanged: Boolean,
        ruDiff: String?,
        enDiff: String?,
        cacheKey: String,
        dataForCache: ByteArray
    ) {
        println("Попытка отправить сообщение об обновлении модa $modName")

        val embedBuilder = net.dv8tion.jda.api.EmbedBuilder()
            .setTitle("Мод $modName обновился!")
            .setDescription("Возможно вы захотите обновить свои переводы.")
            .addField("Версия мода", languageData.modVersion ?: "Неизвестно", true)
            .setColor(0x50FA7B)

        val changesDescription = when {
            ruChanged && enChanged -> "Изменились оба языковых файла: `ru_ru` и `en_us`."
            ruChanged -> "Изменился только `ru_ru`."
            enChanged -> "Изменился только `en_us`."
            else -> "Нет изменений в языковых файлах."
        }

        embedBuilder.addField("Какие из файлов изменились", changesDescription, false)

        if (ruChanged && ruDiff != null) {
            embedBuilder.addField("Различия в `ru_ru`:", "```diff\n$ruDiff\n```", false)
        }

        if (enChanged && enDiff != null) {
            embedBuilder.addField("Различия в `en_us`:", "```diff\n$enDiff\n```", false)
        }

        channel.sendMessageEmbeds(embedBuilder.build())
            .addFiles(buildAttachments(modName, languageData))
            .queue(
                { message ->
                    println("Сообщение об обновлении мода $modName успешно отправлено: ${message.id}")
                    messageCache.saveMessage(cacheKey, dataForCache, message.id)
                },
                { error ->
                    println("Ошибка при отправке сообщения об обновлении мода $modName: ${error.message}")
                }
            )
    }

    private fun buildEmbed(release: ReleaseInfo): MessageEmbed {
        val releaseType = when (release.releaseType) {
            "internal" -> "Внутреннее тестирование"
            "alpha" -> "Альфа"
            "beta" -> "Бета"
            "production" -> "Релиз"
            else -> release.releaseType
        }
        val embedBuilder = net.dv8tion.jda.api.EmbedBuilder()
            .setTitle("Вышла новая версия для тестировщиков")
            .setDescription("Протестируйте её!")
            .addField("Версия", release.versionName, true)
            .addField("Тип выпуска", releaseType, true)
            .addField("Список изменений", release.releaseNotes, false)
            .setThumbnail("https://lh3.googleusercontent.com/g-SDH_bNyQg5IZgsO0Ft1enmz72Cm-e94nOb-wLu3JtSISKmQk-sO_2atrv8K8TAkg")
            .setColor(0x50FA7B)
        return embedBuilder.build()
    }

        private suspend fun getLanguageFiles(mod: ModConfig, client: HttpClient, apiKey: String): ModLanguageData? {
            // Попытка найти мод на Modrinth
            try {
                if (!mod.modrinthId.isNullOrBlank()) {
                    // Получение всех версий Modrinth
                    val versions = client.get("https://api.modrinth.com/v2/project/${mod.modrinthId}/version").body<List<ModrinthVersion>>()
                    if (versions.isNotEmpty()) {
                        // Получаем последнюю и предыдущую версии
                        val latestVersion = versions.first()
                        val previousVersion = if (versions.size >= 2) versions[1] else null

                        // Получаем языковые файлы для последней версии
                        val latestLangData = getLanguageFilesForVersion(mod, latestVersion, client, apiKey)

                        // Получаем языковые файлы для предыдущей версии (если есть)
                        val previousLangData = previousVersion?.let { getLanguageFilesForVersion(mod, it, client, apiKey) }

                        // Если есть обе версии, можно сразу вычислить различия здесь или передать их
                        return latestLangData
                    }
                } else {
                    // Поиск по имени на Modrinth
                    val modrinthDataByName = searchModrinthByName(mod.name, client)
                    if (modrinthDataByName != null) return modrinthDataByName
                }
            } catch (e: Exception) {
                println("Ошибка при обращении к Modrinth для мода ${mod.name}: ${e.message}")
                // Продолжаем к CurseForge
            }

            // Попытка найти мод на CurseForge
            try {
                if (!mod.curseForgeId.isNullOrBlank()) {
                    // Получение списка файлов мода
                    val filesResponse: CurseForgeFileResponse =
                        client.get("https://api.curseforge.com/v1/mods/$curseForgeId/files") {
                            header("x-api-key", apiKey)
                        }.body()

                    if (filesResponse.data.isNotEmpty()) {
                        val latestFile = filesResponse.data.first { it.downloadUrl != null }
                        val previousFile = if (filesResponse.data.size >= 2) filesResponse.data[1] else null

                        val latestLangData = getLanguageFilesForVersion(mod, latestFile, client, apiKey)
                        val previousLangData = previousFile?.let { getLanguageFilesForVersion(mod, it, client, apiKey) }

                        return latestLangData
                    }
                } else {
                    // Поиск по имени на CurseForge
                    val curseForgeDataByName = searchCurseForgeByName(mod.name, client, apiKey)
                    if (curseForgeDataByName != null) return curseForgeDataByName
                }
            } catch (e: Exception) {
                println("Ошибка при обращении к CurseForge для мода ${mod.name}: ${e.message}")
            }

            // Если ничего не найдено
            return null
        }

    private suspend fun getModrinthLanguageFiles(modrinthId: String, client: HttpClient): ModLanguageData? {
        return retryIO(times = 5, initialDelay = 1000) { // Повторять 5 раз с задержкой 1 секунда
        // Получение версий мода
        val versions: List<ModrinthVersion> =
            client.get("https://api.modrinth.com/v2/project/$modrinthId/version").body()
        if (versions.isEmpty()) return@retryIO null

        // Предполагаем, что первая версия самая новая
        val latestVersion = versions.first()
        val fileInfo = latestVersion.files.find { it.primary } ?: latestVersion.files.firstOrNull() ?: return@retryIO null

        // Скачивание JAR-файла
        val jarBytes: ByteArray = client.get(fileInfo.url).body()

        // Извлечение языковых файлов
        val languageData = extractLanguageFiles(jarBytes)

        // Создаём новый экземпляр с обновлёнными данными
        val updatedLanguageData = languageData.copy(
            modVersion = latestVersion.version_number,
            releaseDate = latestVersion.date_published
        )

            return@retryIO updatedLanguageData
        }
    }

    private suspend fun searchModrinthByName(name: String, client: HttpClient): ModLanguageData? {
        val searchResponse: ModrinthSearchResponse = client.get("https://api.modrinth.com/v2/search") {
            parameter("query", name)
            parameter("limit", 1)
        }.body()

        if (searchResponse.hits.isEmpty()) return null

        return getModrinthLanguageFiles(searchResponse.hits[0].id, client)
    }

    private suspend fun getCurseForgeLanguageFiles(
        curseForgeId: String,
        client: HttpClient,
        apiKey: String
    ): ModLanguageData? {
        // Получение списка файлов мода
        val filesResponse: CurseForgeFileResponse =
            client.get("https://api.curseforge.com/v1/mods/$curseForgeId/files") {
                header("x-api-key", apiKey)
            }.body()

        if (filesResponse.data.isEmpty()) return null

        // Предполагаем, что первый файл самый последний
        val fileData = filesResponse.data.firstOrNull { !it.downloadUrl.isNullOrBlank() } ?: return null
        val downloadUrl = fileData.downloadUrl!!

        // Скачивание JAR-файла
        val jarBytes: ByteArray = client.get(downloadUrl).body()

        // Извлечение языковых файлов
        val languageData = extractLanguageFiles(jarBytes)

        // Создаём новый экземпляр с обновлёнными данными
        val updatedLanguageData = languageData.copy(
            modVersion = fileData.displayName,
            releaseDate = fileData.fileDate
        )

        return updatedLanguageData
    }

    private suspend fun searchCurseForgeByName(
        name: String,
        client: HttpClient,
        apiKey: String
    ): ModLanguageData? {
        val searchResponse: CurseForgeSearchResponse = client.get("https://api.curseforge.com/v1/mods/search") {
            header("x-api-key", apiKey)
            parameter("gameId", 432) // ID игры Minecraft
            parameter("searchFilter", name)
            parameter("pageSize", 1)
        }.body()

        if (searchResponse.data.isEmpty()) return null

        val modId = searchResponse.data.first().id
        return getCurseForgeLanguageFiles(modId.toString(), client, apiKey)
    }

    private suspend fun extractLanguageFiles(jarBytes: ByteArray): ModLanguageData {
        val jarInputStream = ZipInputStream(jarBytes.inputStream())
        var ruRuBytes: ByteArray? = null
        var enUsBytes: ByteArray? = null
        var modVersion: String? = null
        var releaseDate: String? = null

        var entry = jarInputStream.nextEntry
        while (entry != null) {
            val name = entry.name.lowercase()
            when {
                name.matches(Regex(""".*/lang/ru_ru\.(json|lang|properties)$""")) -> {
                    val baos = ByteArrayOutputStream()
                    jarInputStream.copyTo(baos)
                    ruRuBytes = baos.toByteArray()
                }

                name.matches(Regex(""".*/lang/en_us\.(json|lang|properties)$""")) -> {
                    val baos = ByteArrayOutputStream()
                    jarInputStream.copyTo(baos)
                    enUsBytes = baos.toByteArray()
                }

                name.matches(Regex(""".*/mods\.toml$""")) -> {
                    val baos = ByteArrayOutputStream()
                    jarInputStream.copyTo(baos)
                    val modsTomlContent = baos.toString("UTF-8")
                    modVersion = parseModVersionFromModsToml(modsTomlContent)
                }
            }
            jarInputStream.closeEntry()
            if (ruRuBytes != null && enUsBytes != null && modVersion != null) {
                break
            }
            entry = jarInputStream.nextEntry
        }
        jarInputStream.close()

        val ruRuMap = ruRuBytes?.let { parseLanguageFile(it) }
        val enUsMap = enUsBytes?.let { parseLanguageFile(it) }

        return ModLanguageData(ruRuBytes, enUsBytes, ruRuMap, enUsMap, modVersion, releaseDate)
    }

    private fun parseModVersionFromModsToml(modsTomlContent: String): String? {
        val versionRegex = Regex("""\s*version\s*=\s*"([^"]+)"""")
        val matchResult = versionRegex.find(modsTomlContent)
        return matchResult?.groups?.get(1)?.value
    }

    override fun onSlashCommandInteraction(event: SlashCommandInteractionEvent) {
        val commandName = event.name
        if (commandName == "tontaro" || commandName == "тонтаро") {
            // Проверяем ID канала
            val channelId = event.channel.id
            val allowedChannels = listOf("878021004838969376", "927956548200841286")
            if (channelId !in allowedChannels) {
                event.reply("Эта команда недоступна в этом канале.").setEphemeral(true).queue()
                return
            }

            // Обработка команды
            globalScope.launch {
                try {
                    val post = fetchRandomE621Post()
                    if (post != null && post.file.url != null) {
                        val imageUrl = post.file.url
                        val embed = net.dv8tion.jda.api.EmbedBuilder()
                            // .setTitle("Случайный арт от Tontaro")
                            .setImage(imageUrl)
                            // .setUrl("https://e621.net/posts/${post.id}")
                            // .setFooter("Автор: Tontaro")
                            .setColor(0x50FA7B)
                            .build()

                        event.replyEmbeds(embed).queue()
                    } else {
                        event.reply("Не удалось получить арт. Попробуйте позже.").setEphemeral(true).queue()
                    }
                } catch (e: Exception) {
                    println("Ошибка при выполнении команды /$commandName: ${e.message}")
                    event.reply("Произошла ошибка. Попробуйте позже.").setEphemeral(true).queue()
                }
            }
        }
    }

    private suspend fun fetchRandomE621Post(): E621Post? {
        val response: E621Response = client.get("https://e621.net/posts.json") {
            header("User-Agent", config.e621UserAgent)
            parameter("tags", "tontaro -rating:explicit order:random")
            parameter("limit", 1)
        }.body()

        return response.posts.firstOrNull()
    }

    private suspend fun <T> retryIO(
        times: Int = 3,
        initialDelay: Long = 100,
        maxDelay: Long = 1000,
        factor: Double = 2.0,
        block: suspend () -> T
    ): T {
        var currentDelay = initialDelay
        repeat(times - 1) {
            try {
                return block()
            } catch (e: Exception) {
                println("Неудачная попытка: ${e.message}")
                delay(currentDelay)
                currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
            }
        }
        return block()
    }

    private val client = HttpClient(CIO) {
        install(ContentNegotiation) {
            json(Json { ignoreUnknownKeys = true })
        }
        engine {
            requestTimeout = 20000 // 20 секунд
        }
    }
}

// Общие функции

private fun ensureMessage(
    channel: NewsChannel,
    cacheKey: String,
    dataForCache: ByteArray,
    messageCache: MessageCache,
    buildEmbed: () -> MessageEmbed,
    buildAttachments: () -> List<FileUpload> = { emptyList() }
) {
    val cachedData = messageCache.getMessageData(cacheKey)
    val messageId = messageCache.getMessageId(cacheKey)

    if (cachedData == null || messageId == null || !cachedData.contentEquals(dataForCache)) {
        sendOrUpdateMessage(channel, cacheKey, dataForCache, messageCache, buildEmbed, buildAttachments)
    } else {
        if (!doesMessageExist(channel, messageId)) {
            sendOrUpdateMessage(channel, cacheKey, dataForCache, messageCache, buildEmbed, buildAttachments)
        }
    }
}

private fun sendOrUpdateMessage(
    channel: NewsChannel,
    cacheKey: String,
    dataForCache: ByteArray,
    messageCache: MessageCache,
    buildEmbed: () -> MessageEmbed,
    buildAttachments: () -> List<FileUpload>
) {
    channel.sendMessageEmbeds(buildEmbed())
        .addFiles(buildAttachments())
        .queue { message ->
            messageCache.saveMessage(cacheKey, dataForCache, message.id)
        }
}

private fun doesMessageExist(channel: NewsChannel, messageId: String): Boolean {
    return try {
        val message = channel.retrieveMessageById(messageId).complete()
        message != null
    } catch (e: Exception) {
        // Сообщение не найдено или ошибка доступа
        false
    }
}

private fun parseLanguageFile(bytes: ByteArray): Map<String, String> {
    val jsonString = bytes.toString(Charsets.UTF_8)
    return try {
        Json.decodeFromString(jsonString)
    } catch (e: Exception) {
        println("Ошибка при парсинге языкового файла: ${e.message}")
        emptyMap()
    }
}

private fun computeDiff(oldMap: Map<String, String>, newMap: Map<String, String>): String {
    val added = newMap.keys - oldMap.keys
    val removed = oldMap.keys - newMap.keys
    val changed = newMap.keys.intersect(oldMap.keys).filter { oldMap[it] != newMap[it] }

    val sb = StringBuilder()
    if (added.isNotEmpty()) {
        sb.append("**Добавленные ключи:**\n")
        added.forEach { key ->
            sb.append("- `$key`: `${newMap[key]}`\n")
        }
    }
    if (removed.isNotEmpty()) {
        sb.append("**Удалённые ключи:**\n")
        removed.forEach { key ->
            sb.append("- `$key`: `${oldMap[key]}`\n")
        }
    }
    if (changed.isNotEmpty()) {
        sb.append("**Изменённые ключи:**\n")
        changed.forEach { key ->
            sb.append("- `$key`: `${oldMap[key]}` → `${newMap[key]}`\n")
        }
    }

    if (sb.isEmpty()) {
        sb.append("Нет различий.")
    }

    println("Вычисленные различия: $sb")
    return sb.toString()
}

private suspend fun getPreviousLanguageData(mod: ModConfig, client: HttpClient, apiKey: String): ModLanguageData? {
    return try {
        val versions = getAllModVersions(mod, client, apiKey)
        if (versions.size < 2) return null // Недостаточно версий для сравнения

        val previousVersion = versions[1] // Вторая версия - предыдущая
        getLanguageFilesForVersion(mod, previousVersion, client, apiKey)
    } catch (e: Exception) {
        println("Ошибка при получении предыдущей версии для мода ${mod.name}: ${e.message}")
        null
    }
}

private suspend fun getAllModVersions(mod: ModConfig, client: HttpClient, apiKey: String): List<ModrinthVersion> {
    // Пример для Modrinth
    if (!mod.modrinthId.isNullOrBlank()) {
        return client.get("https://api.modrinth.com/v2/project/${mod.modrinthId}/version").body()
    }
    // Пример для CurseForge
    // Реализуйте аналогично для CurseForge, возвращая список версий
    return emptyList()
}

private suspend fun getLanguageFilesForVersion(
    mod: ModConfig,
    version: Any, // Может быть ModrinthVersion или CurseForgeFileData
    client: HttpClient,
    apiKey: String
): ModLanguageData? {
    return when (version) {
        is ModrinthVersion -> getLanguageFilesForVersion(mod, version, client, apiKey)
        is CurseForgeFileData -> getLanguageFilesForVersion(mod, version, client, apiKey)
        else -> {
            println("Неизвестный тип версии для мода ${mod.name}")
            null
        }
    }
}

Я получаю следующие ошибки:
90
Unresolved reference: getLanguageFiles
105
Unresolved reference: client
114
Type mismatch: inferred type is Map<String, String>? but Map<String, String> was expected
158
Unresolved reference: sendLanguageUpdateEmbed
284
Unresolved reference: retryIO
286
<html>Unresolved reference. None of the following candidates is applicable because of receiver type mismatch:<br/>public fun HttpMessageBuilder.header(key: String, value: Any?): Unit defined in io.ktor.client.request
356
Unresolved reference: checkMessageExists

Разреши эти проблемы, пожалуйста!