namespace Supercell.Laser.Bot.Settings { using System; using System.IO; using System.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Supercell.Laser.Bot; /// /// Конфигурация Supercell.Laser.Bot (файл Supercell.Laser.Bot/config.json). /// /// ВАЖНО: сервер реально подключается к своей базе через PostgreSQL (Npgsql), а не /// MySQL — см. Supercell.Laser.Server/Database/Accounts.cs. Поэтому BotData тоже /// работает через Npgsql. Названия полей ниже оставлены "mysql_*" для совместимости /// с уже заполненным config.json — по факту это просто host/port/user/password/database /// для PostgreSQL. /// /// Для каждого из полей mysql_host / mysql_port / mysql_user / mysql_password / mysql_name /// можно указать либо "default", либо своё значение: /// /// "default" -> mysql_host: "localhost", mysql_port: 5432 (порт Postgres по /// умолчанию), mysql_user / mysql_password / mysql_name берутся /// из database_username / database_password / database_name /// в Supercell.Laser.Server/config.json (таблицы бота живут /// в той же базе, что и весь сервер) /// любое другое -> используется как есть (отдельная база данных для бота, /// тоже PostgreSQL) /// /// table_prefix добавляется к именам всех таблиц бота — чтобы не столкнуться /// с таблицами самой игры, если mysql_name оставлен как "default". /// public class Configuration { public static Configuration Instance; [JsonProperty("bot_token")] public readonly string BotToken; [JsonProperty("telegram_admins")] public readonly long[] TelegramAdmins; [JsonProperty("mysql_host")] public readonly string MySqlHost; [JsonProperty("mysql_port")] public readonly string MySqlPort; [JsonProperty("mysql_user")] public readonly string MySqlUser; [JsonProperty("mysql_password")] public readonly string MySqlPassword; [JsonProperty("mysql_name")] public readonly string MySqlName; [JsonProperty("table_prefix")] public readonly string TablePrefix; public bool HasAdmins => TelegramAdmins is { Length: > 0 }; public bool IsAdmin(long telegramUserId) { return TelegramAdmins != null && Array.IndexOf(TelegramAdmins, telegramUserId) >= 0; } public static Configuration LoadFromFile(string filename) { return JsonConvert.DeserializeObject(File.ReadAllText(filename)); } /// /// Находит config.json бота. /// ВАЖНО: рядом со сборкой (bin/.../config.json) лежит config.json СЕРВЕРА /// (одинаковое имя файла) — его нельзя перепутать с конфигом бота, иначе /// bot_token/telegram_admins/mysql_* всегда будут пустыми. Поэтому сначала /// ищем именно "Supercell.Laser.Bot/config.json" (исходную папку проекта, соседнюю /// с Supercell.Laser.Server — как и весь репозиторий), и только в последнюю /// очередь — плоский config.json рядом со сборкой, да и то только если в нём /// реально есть поле "bot_token" (иначе это чужой, серверный конфиг). /// public static string FindOwnConfigPath() { string asmDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); // 1) исходная папка проекта — Bot и Server лежат рядом в репозитории. string dir = asmDir; for (int i = 0; i < 8 && dir != null; i++) { string candidate = Path.Combine(dir, "Supercell.Laser.Bot", "config.json"); if (File.Exists(candidate)) return candidate; dir = Path.GetDirectoryName(dir); } // 2) подпапка Supercell.Laser.Bot рядом со сборкой (публикация без исходников). string subfolder = Path.Combine(asmDir ?? ".", "Supercell.Laser.Bot", "config.json"); if (File.Exists(subfolder)) return subfolder; // 3) последний резерв — плоский config.json рядом со сборкой, но только если // в нём реально есть "bot_token" (иначе это config.json сервера). string flat = Path.Combine(asmDir ?? ".", "config.json"); if (File.Exists(flat)) { try { if (JObject.Parse(File.ReadAllText(flat))["bot_token"] != null) return flat; } catch { // не похоже на конфиг бота — игнорируем и идём в throw ниже. } } throw new FileNotFoundException( "Не найден config.json бота. Ожидался в 'Supercell.Laser.Bot/config.json' рядом с проектом сервера."); } public class ResolvedMySql { public string Host; public int Port; public string User; public string Password; public string Database; } /// /// Разворачивает mysql_* поля конфига в реальные параметры подключения. /// server — данные, переданные из Supercell.Laser.Server.Program при старте /// (см. Start.Run). Если server == null (бот запущен не прицепом к серверу), /// значения "default" для user/password/name читаются напрямую из /// Supercell.Laser.Server/config.json, лежащего рядом. /// public ResolvedMySql ResolveMySql(ServerLink server) { JObject serverConfig = null; bool NeedsFile(string configured) => IsDefault(configured) && server == null; if (NeedsFile(MySqlUser) || NeedsFile(MySqlPassword) || NeedsFile(MySqlName)) serverConfig = TryLoadServerConfig(); return new ResolvedMySql { Host = IsDefault(MySqlHost) ? "localhost" : MySqlHost, Port = IsDefault(MySqlPort) || string.IsNullOrWhiteSpace(MySqlPort) ? 5432 : int.Parse(MySqlPort), User = ResolveCredential(MySqlUser, server?.DatabaseUsername, serverConfig, "database_username"), Password = ResolveCredential(MySqlPassword, server?.DatabasePassword, serverConfig, "database_password"), Database = ResolveCredential(MySqlName, server?.DatabaseName, serverConfig, "database_name") }; } private static bool IsDefault(string value) { return string.IsNullOrWhiteSpace(value) || value.Trim().Equals("default", StringComparison.OrdinalIgnoreCase); } private static string ResolveCredential(string configured, string fromServer, JObject serverConfigJson, string serverConfigKey) { if (!IsDefault(configured)) return configured; if (fromServer != null) return fromServer; if (serverConfigJson?[serverConfigKey] != null) return serverConfigJson[serverConfigKey].ToString(); throw new InvalidOperationException( $"config.json бота: '{serverConfigKey}' указан как \"default\", но Supercell.Laser.Server/config.json " + "не найден рядом, а бот не присоединён к серверу. Укажи значение вручную вместо \"default\"."); } private static JObject TryLoadServerConfig() { string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); for (int i = 0; i < 6 && dir != null; i++) { string candidate = Path.Combine(dir, "Supercell.Laser.Server", "config.json"); if (File.Exists(candidate)) { try { return JObject.Parse(File.ReadAllText(candidate)); } catch { return null; } } dir = Path.GetDirectoryName(dir); } return null; } } }