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;
}
}
}